cassandra/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java

314 lines
14 KiB
Java

/*
* 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.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.marshal.UserType;
import org.apache.cassandra.io.sstable.format.SSTableFormat;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
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;
public class RandomSchemaTest extends CQLTester.InMemory
{
private static final Logger logger = LoggerFactory.getLogger(RandomSchemaTest.class);
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);
// make sure ordering is determanstic, else repeatability breaks
NavigableMap<String, SSTableFormat<?, ?>> formats = new TreeMap<>(DatabaseDescriptor.getSSTableFormats());
Gen<SSTableFormat<?, ?>> ssTableFormatGen = SourceDSL.arbitrary().pick(new ArrayList<>(formats.values()));
qt().checkAssert(random -> {
resetSchema();
// TODO : when table level override of sstable format is allowed, migrate to that
SSTableFormat<?, ?> sstableFormat = ssTableFormatGen.generate(random);
DatabaseDescriptor.setSelectedSSTableFormat(sstableFormat);
Gen<String> udtName = Generators.unique(IDENTIFIER_GEN);
TypeGenBuilder withoutUnsafeEquality = AbstractTypeGenerators.withoutUnsafeEquality()
.withUserTypeKeyspace(KEYSPACE)
.withUDTNames(udtName);
TableMetadata metadata = new TableMetadataBuilder()
.withKeyspaceName(KEYSPACE)
.withTableKinds(TableMetadata.Kind.REGULAR)
.withKnownMemtables()
.withDefaultTypeGen(AbstractTypeGenerators.builder()
.withoutEmpty()
.withUserTypeKeyspace(KEYSPACE)
.withMaxDepth(2)
.withDefaultSetKey(withoutUnsafeEquality)
.withoutTypeKinds(AbstractTypeGenerators.TypeKind.COUNTER)
.withUDTNames(udtName)
.build())
.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)
.build())
.withClusteringColumnsBetween(1, 2)
.withRegularColumnsBetween(1, 5)
.withStaticColumnsBetween(0, 2)
.build(random);
maybeCreateUDTs(metadata);
String createTable = metadata.toCqlString(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);
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, expected);
// check memtable
assertRows(execute(selectStmt, rowKey), expected);
assertRows(execute(tokenStmt, partitionKeys), partitionKeys);
assertRowsNet(executeNet(selectStmt, rowKey), expected);
// check sstable
flush(KEYSPACE, metadata.name);
compact(KEYSPACE, metadata.name);
assertRows(execute(selectStmt, rowKey), expected);
assertRows(execute(tokenStmt, partitionKeys), partitionKeys);
assertRowsNet(executeNet(selectStmt, 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 maybeCreateUDTs(TableMetadata metadata)
{
Set<UserType> udts = CassandraGenerators.extractUDTs(metadata);
if (!udts.isEmpty())
{
Deque<UserType> pending = new ArrayDeque<>(udts);
Set<ByteBuffer> created = new HashSet<>();
while (!pending.isEmpty())
{
UserType next = pending.poll();
Set<UserType> subTypes = AbstractTypeGenerators.extractUDTs(next);
subTypes.remove(next); // it includes self
if (subTypes.isEmpty() || subTypes.stream().allMatch(t -> created.contains(t.name)))
{
String cql = next.toCqlString(false, false);
logger.warn("Creating UDT {}", cql);
schemaChange(cql);
created.add(next.name);
}
else
{
logger.warn("Unable to create UDT {}; following sub-types still not created: {}",
next.getCqlTypeName(),
subTypes.stream().filter(t -> !created.contains(t.name)).collect(Collectors.toSet()));
pending.add(next);
}
}
}
}
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;
}
}
}