content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package org.apache.cassandra.cql3;
  
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Iterator;
  import java.util.List;
  import java.util.stream.Collectors;
  
  import com.google.common.collect.ImmutableList;
  import org.junit.Assert;
  import org.junit.Test;
  
  import org.apache.cassandra.config.CassandraRelevantProperties;
  import org.apache.cassandra.config.DatabaseDescriptor;
  import org.apache.cassandra.io.sstable.format.SSTableFormat;
  import org.apache.cassandra.io.util.DataInputBuffer;
  import org.apache.cassandra.io.util.DataOutputBuffer;
  import org.apache.cassandra.schema.ColumnMetadata;
  import org.apache.cassandra.schema.TableMetadata;
  import org.apache.cassandra.tcm.ClusterMetadata;
  import org.apache.cassandra.tcm.membership.NodeVersion;
  import org.apache.cassandra.utils.AbstractTypeGenerators;
  import org.apache.cassandra.utils.AbstractTypeGenerators.TypeGenBuilder;
  import org.apache.cassandra.utils.CassandraGenerators;
  import org.apache.cassandra.utils.CassandraGenerators.TableMetadataBuilder;
  import org.apache.cassandra.utils.FailingConsumer;
  import org.apache.cassandra.utils.Generators;
  import org.quicktheories.core.Gen;
  import org.quicktheories.core.RandomnessSource;
  import org.quicktheories.generators.SourceDSL;
  import org.quicktheories.impl.JavaRandom;
  
  import static org.apache.cassandra.utils.Generators.IDENTIFIER_GEN;
  import static org.junit.Assert.assertNotNull;
  import static org.junit.Assert.assertTrue;
  
  public class RandomSchemaTest extends CQLTester.InMemory
  {
      static
      {
          // make sure blob is always the same
          CassandraRelevantProperties.TEST_BLOB_SHARED_SEED.setInt(42);
  
          requireNetwork();
      }
  
      @Test
      public void test()
      {
          // in accord branch there is a much cleaner api for this pattern...
          Gen<AbstractTypeGenerators.ValueDomain> domainGen = SourceDSL.integers().between(1, 100).map(i -> i < 2 ? AbstractTypeGenerators.ValueDomain.NULL : i < 4 ? AbstractTypeGenerators.ValueDomain.EMPTY_BYTES : AbstractTypeGenerators.ValueDomain.NORMAL);
  
          Gen<SSTableFormat<?, ?>> sstableFormatGen = CassandraGenerators.sstableFormat();
          qt().checkAssert(random -> {
              resetSchema();
  
              // TODO : when table level override of sstable format is allowed, migrate to that
              DatabaseDescriptor.setSelectedSSTableFormat(sstableFormatGen.generate(random));
  
              Gen<String> udtName = Generators.unique(IDENTIFIER_GEN);
  
              TypeGenBuilder withoutUnsafeEquality = AbstractTypeGenerators.withoutUnsafeEquality()
                                                                           .withUDTNames(udtName);
              TableMetadata metadata = new TableMetadataBuilder()
                                       .withKeyspaceName(KEYSPACE)
                                       .withTableKinds(TableMetadata.Kind.REGULAR)
                                       .withKnownMemtables()
                                       .withDefaultTypeGen(AbstractTypeGenerators.builder()
                                                                                 .withoutEmpty()
                                                                                 .withMaxDepth(2)
                                                                                 .withDefaultSetKey(withoutUnsafeEquality)
                                                                                 .withoutTypeKinds(AbstractTypeGenerators.TypeKind.COUNTER)
                                                                                 .withUDTNames(udtName))
                                       .withPartitionColumnsCount(1)
                                       .withPrimaryColumnTypeGen(new TypeGenBuilder(withoutUnsafeEquality)
                                                                 // map of vector of map crossed the size cut-off for one of the tests, so changed max depth from 2 to 1, so we can't have the second map
                                                                 .withMaxDepth(1))
                                       .withClusteringColumnsBetween(1, 2)
                                       .withRegularColumnsBetween(1, 5)
                                       .withStaticColumnsBetween(0, 2)
                                       .build(random);
              maybeCreateUDTs(metadata);
              String createTable = metadata.toCqlString(true, false, false);
              // just to make the CREATE TABLE stmt easier to read for CUSTOM types
              createTable = createTable.replaceAll("org.apache.cassandra.db.marshal.", "");
              createTable(KEYSPACE, createTable);
              serde(ClusterMetadata.current(), metadata);
  
              Gen<ByteBuffer[]> dataGen = CassandraGenerators.data(metadata, domainGen);
              String insertStmt = insertStmt(metadata);
              int partitionColumnCount = metadata.partitionKeyColumns().size();
              int primaryColumnCount = primaryColumnCount(metadata);
              String selectStmt = selectStmt(metadata);
              String tokenStmt = tokenStmt(metadata);
  
              for (int i = 0; i < 100; i++)
              {
                  ByteBuffer[] expected = dataGen.generate(random);
                  try
                  {
                      ByteBuffer[] partitionKeys = Arrays.copyOf(expected, partitionColumnCount);
                      ByteBuffer[] rowKey = Arrays.copyOf(expected, primaryColumnCount);
                      execute(insertStmt, (Object[]) expected);
                      // check memtable
                      assertRows(execute(selectStmt, (Object[]) rowKey), expected);
                      assertRows(execute(tokenStmt, (Object[]) partitionKeys), partitionKeys);
                      assertRowsNet(executeNet(selectStmt, (Object[]) rowKey), expected);
  
                      // check sstable
                      flush(KEYSPACE, metadata.name);
                      compact(KEYSPACE, metadata.name);
                      assertRows(execute(selectStmt, (Object[]) rowKey), expected);
                      assertRows(execute(tokenStmt, (Object[]) partitionKeys), partitionKeys);
                      assertRowsNet(executeNet(selectStmt, (Object[]) rowKey), expected);
  
                      execute("TRUNCATE " + metadata);
                  }
                  catch (Throwable t)
                  {
                      Iterator<ColumnMetadata> it = metadata.allColumnsInSelectOrder();
                      List<String> literals = new ArrayList<>(expected.length);
                      for (int idx = 0; idx < expected.length; idx++)
                      {
                          assert it.hasNext();
                          ColumnMetadata meta = it.next();
                          ByteBuffer expct = expected[idx];
                          literals.add(expct == null ? "null" : !expct.hasRemaining() ? "empty" : meta.type.asCQL3Type().toCQLLiteral(expct));
                      }
                      throw new AssertionError(String.format("Failure at attempt %d with schema\n%s\nAnd SSTable Format %s\nfor values %s", i, createTable, DatabaseDescriptor.getSelectedSSTableFormat(), literals), t);
                  }
              }
          });
      }
  
      private void serde(ClusterMetadata metadata, TableMetadata tableMetadata) throws IOException
      {
          assertTrue(metadata.schema.getKeyspaces().containsKeyspace(tableMetadata.keyspace));
          assertNotNull(metadata.schema.getKeyspace(tableMetadata.keyspace).getColumnFamilyStore(tableMetadata.name));
  
          DataOutputBuffer dop = new DataOutputBuffer((int) ClusterMetadata.serializer.serializedSize(metadata, NodeVersion.CURRENT_METADATA_VERSION));
          ClusterMetadata.serializer.serialize(metadata, dop, NodeVersion.CURRENT_METADATA_VERSION);
  
          try (DataInputBuffer dip = new DataInputBuffer(dop.buffer(), true))
          {
              ClusterMetadata deserCm = ClusterMetadata.serializer.deserialize(dip, NodeVersion.CURRENT_METADATA_VERSION);
              if (!metadata.equals(deserCm))
              {
                  metadata.dumpDiff(deserCm);
                  Assert.fail("Metadata mismatch");
              }
  
          }
      }
  
      private static int primaryColumnCount(TableMetadata metadata)
      {
          return metadata.partitionKeyColumns().size() + metadata.clusteringColumns().size();
      }
  
      private static String tokenStmt(TableMetadata metadata)
      {
          StringBuilder sb = new StringBuilder();
          List<String> columns = metadata.partitionKeyColumns().stream().map(column -> column.name.toCQLString()).collect(Collectors.toList());
          List<String> binds = columns.stream().map(ignore -> "?").collect(Collectors.toList());
          sb.append("SELECT ").append(String.join(", ", columns));
          sb.append(" FROM ").append(metadata);
          sb.append(" WHERE token(").append(String.join(", ", columns)).append(") = token(").append(String.join(", ", binds)).append(')');
          return sb.toString();
      }
  
      private String selectStmt(TableMetadata metadata)
      {
          StringBuilder sb = new StringBuilder();
          sb.append("SELECT * FROM ").append(metadata).append(" WHERE ");
          for (ColumnMetadata column : ImmutableList.<ColumnMetadata>builder()
                                                    .addAll(metadata.partitionKeyColumns())
                                                    .addAll(metadata.clusteringColumns())
                                                    .build())
          {
              sb.append(column.name.toCQLString()).append(" = ? AND ");
          }
          sb.setLength(sb.length() - " AND ".length());
          return sb.toString();
      }
  
      private String insertStmt(TableMetadata metadata)
      {
          StringBuilder sb = new StringBuilder();
          sb.append("INSERT INTO ").append(metadata.toString()).append(" (");
          Iterator<ColumnMetadata> cols = metadata.allColumnsInSelectOrder();
          while (cols.hasNext())
              sb.append(cols.next().name.toCQLString()).append(", ");
          sb.setLength(sb.length() - 2); // remove last ", "
          sb.append(") VALUES (");
          for (int i = 0; i < metadata.columns().size(); i++)
          {
              if (i > 0)
                  sb.append(", ");
              sb.append('?');
          }
          sb.append(')');
          return sb.toString();
      }
  
      private static Builder qt()
      {
          return new Builder();
      }
  
      public static class Builder
      {
          private long seed = System.currentTimeMillis();
          private int examples = 10;
  
          /**
           * Used to reproduce the test with a fixed seed; main use case is to rerun the same test that failed in CI.
           *
           * This function is dead code, it exists to help authors debug when a failing test is found.
           */
          @SuppressWarnings("unused")
          public Builder withFixedSeed(long seed)
          {
              this.seed = seed;
              return this;
          }
  
          /**
           * Used to override how many examples to run with.  This is dead code, but exists to allow authors to run the test
           * many times to make sure its stable before committing.
           */
          @SuppressWarnings("unused")
          public Builder withExamples(int examples)
          {
              this.examples = examples;
              return this;
          }
  
          // copied from java.util.Random
          private static final long multiplier = 0x5DEECE66DL;
          private static final long addend = 0xBL;
          private static final long mask = (1L << 48) - 1;
  
          public void checkAssert(FailingConsumer<RandomnessSource> test)
          {
              JavaRandom random = new JavaRandom(seed);
              for (int i = 0; i < examples; i++)
              {
                  if (i > 0)
                      seed = (seed * multiplier + addend) & mask;
                  random.setSeed(seed);
                  try
                  {
                      test.doAccept(random);
                  }
                  catch (Throwable e)
                  {
                      throw new PropertyError(seed, e);
                  }
              }
          }
      }
  
      public static class PropertyError extends AssertionError
      {
          public PropertyError(long seed, Throwable cause)
          {
              super(message(seed), cause);
          }
  
          private static String message(long seed)
          {
              return "Failure for seed " + seed;
          }
      }
  }
  