Add range delete support to AST-based fuzz tests

patch by Sunil Ramchandra Pawar; reviewed by Caleb Rackliffe and David Capwell for CASSANDRA-20949
This commit is contained in:
Sunil Ramchandra Pawar 2026-04-01 10:06:37 +05:30 committed by Caleb Rackliffe
parent 2184d5cc6b
commit 157e8808f9
4 changed files with 381 additions and 23 deletions

View File

@ -392,23 +392,15 @@ public class SingleNodeTableWalkTest extends StatefulASTBase
String annotation;
if (rs.nextBoolean())
{
builder.between(ckSymbol, state.value(rs, low, ckSymbol.type()), state.value(rs, high, ckSymbol.type()));
annotation = "clustering BETWEEN";
}
else if (rs.nextBoolean())
{
builder.where(ckSymbol, Inequality.GREATER_THAN_EQ, low);
builder.where(ckSymbol, Inequality.LESS_THAN_EQ, high);
annotation = "clustering >= AND <=";
}
else
{
builder.where(ckSymbol, Inequality.GREATER_THAN, low);
builder.where(ckSymbol, Inequality.LESS_THAN, high);
annotation = "clustering > AND <";
}
ASTGenerators.RangeType rangeType = rs.pick(ASTGenerators.RangeType.BOUND, ASTGenerators.RangeType.BETWEEN);
ASTGenerators.applyRangeCondition(builder, rangeType, ckSymbol,
state.greaterThanGen.next(rs),
state.lessThanGen.next(rs),
state.value(rs, low, ckSymbol.type()),
state.value(rs, high, ckSymbol.type()));
annotation = "clustering " + rangeType.name();
if (rs.nextBoolean())
builder.orderByColumn(ckSymbol, rs.pick(Select.OrderBy.Ordering.values()));

View File

@ -76,6 +76,9 @@ import org.apache.cassandra.cql3.ast.Value;
import org.apache.cassandra.cql3.ast.Visitor;
import org.apache.cassandra.db.BufferClustering;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ClusteringBound;
import org.apache.cassandra.db.Slice;
import org.apache.cassandra.db.Slices;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.BooleanType;
import org.apache.cassandra.db.marshal.CollectionType;
@ -621,14 +624,13 @@ public class ASTSingleTableModel
}
private enum DeleteKind
{PARTITION, ROW, COLUMN}
{PARTITION, ROW, COLUMN, RANGE}
private void update(Mutation.Delete delete)
{
long nowTs = delete.timestampOrDefault(numMutations);
//TODO (coverage): range deletes
var split = splitOnPartition(delete.where.simplify());
List<Clustering<ByteBuffer>> pks = split.left;
List<Clustering<ByteBuffer>> clusterings = split.right.isEmpty() ? Collections.emptyList() : clustering(split.right);
List<Clustering<ByteBuffer>> clusterings = Collections.emptyList();
HashSet<Symbol> columns = delete.columns.isEmpty() ? null : new HashSet<>(delete.columns);
for (Clustering<ByteBuffer> pd : pks)
{
@ -638,8 +640,15 @@ public class ASTSingleTableModel
DeleteKind kind = DeleteKind.PARTITION;
if (!delete.columns.isEmpty())
kind = DeleteKind.COLUMN;
else if (!clusterings.isEmpty())
kind = DeleteKind.ROW;
else if (containsRangeConditionOnClustering(split.right))
kind = DeleteKind.RANGE;
if (kind != DeleteKind.RANGE)
{
clusterings = split.right.isEmpty() ? Collections.emptyList() : clustering(split.right);
if (kind == DeleteKind.PARTITION && !clusterings.isEmpty())
kind = DeleteKind.ROW;
}
switch (kind)
{
@ -671,6 +680,19 @@ public class ASTSingleTableModel
}
}
break;
case RANGE:
assert clusterings.isEmpty();
LookupContext ctx = new LookupContext(delete);
for (BytesPartitionState.Row value : partition.rows())
{
if (ctx.include(value))
{
partition.deleteRow(value.clustering, nowTs);
if (partition.shouldDelete())
partitions.remove(partition.ref());
}
}
break;
default:
throw new UnsupportedOperationException();
}
@ -1016,6 +1038,10 @@ public class ASTSingleTableModel
if (factory.clusteringColumns.isEmpty()) return Collections.singletonList(Clustering.EMPTY);
throw new IllegalArgumentException("No clustering columns defined in the WHERE clause, but clustering columns exist; expected " + factory.clusteringColumns);
}
if (containsRangeConditionOnClustering(conditionals))
return extractClusteringsFromSlices(conditionals);
var split = splitOnClustering(conditionals);
var clusterings = split.left;
var remaining = split.right;
@ -1088,6 +1114,131 @@ public class ASTSingleTableModel
return Pair.create(partitionKeys, other);
}
private boolean containsRangeConditionOnClustering(List<Conditional> conditionals)
{
for (Conditional cond : conditionals)
{
if (cond instanceof Conditional.Where)
{
Conditional.Where where = (Conditional.Where) cond;
if (factory.clusteringColumns.contains(where.lhs))
{
switch (where.kind)
{
case GREATER_THAN:
case GREATER_THAN_EQ:
case LESS_THAN:
case LESS_THAN_EQ:
return true;
}
}
}
else if (cond instanceof Conditional.Between)
{
Conditional.Between between = (Conditional.Between) cond;
if (between.ref instanceof Symbol && factory.clusteringColumns.contains((Symbol) between.ref))
return true;
}
}
return false;
}
private List<Clustering<ByteBuffer>> extractClusteringsFromSlices(List<Conditional> conditionals)
{
Map<Symbol, List<Conditional.Where>> rangeConditions = new HashMap<>();
// Build slices
Slices.Builder builder = new Slices.Builder(factory.clusteringComparator);
for (Conditional cond : conditionals)
{
if (cond instanceof Conditional.Where)
{
Conditional.Where where = (Conditional.Where) cond;
if (factory.clusteringColumns.contains(where.lhs)
&& (where.kind != Inequality.EQUAL)) // skip equality
{
Symbol col = (Symbol) where.lhs;
rangeConditions.computeIfAbsent(col, __ -> new ArrayList<>()).add(where);
}
}
else if (cond instanceof Conditional.Between)
{
Conditional.Between between = (Conditional.Between) cond;
ByteBuffer start = eval(between.start);
ByteBuffer end = eval(between.end);
ClusteringBound<ByteBuffer> first = ClusteringBound.inclusiveStartOf(Clustering.make(start));
ClusteringBound<ByteBuffer> last = ClusteringBound.inclusiveEndOf(Clustering.make(end));
Slice slice = Slice.make(first, last);
// To avoid adding empty slices
if (!slice.isEmpty(factory.clusteringComparator))
builder.add(slice);
}
}
for (Map.Entry<Symbol, List<Conditional.Where>> entry : rangeConditions.entrySet())
{
ByteBuffer lower = null;
ByteBuffer upper = null;
boolean includeLower = false;
boolean includeUpper = false;
for (Conditional.Where cond : entry.getValue())
{
ByteBuffer val = eval(cond.rhs);
switch (cond.kind)
{
case GREATER_THAN: lower = val; includeLower = false; break;
case GREATER_THAN_EQ: lower = val; includeLower = true; break;
case LESS_THAN: upper = val; includeUpper = false; break;
case LESS_THAN_EQ: upper = val; includeUpper = true; break;
}
}
ClusteringBound<?> start = (lower == null)
? ClusteringBound.BOTTOM
: (includeLower
? ClusteringBound.inclusiveStartOf(Clustering.make(lower))
: ClusteringBound.exclusiveStartOf(Clustering.make(lower)));
ClusteringBound<?> end = (upper == null)
? ClusteringBound.TOP
: (includeUpper
? ClusteringBound.inclusiveEndOf(Clustering.make(upper))
: ClusteringBound.exclusiveEndOf(Clustering.make(upper)));
Slice slice = Slice.make(start, end);
// To avoid adding empty slices
if (!slice.isEmpty(factory.clusteringComparator))
builder.add(slice);
}
List<Clustering<ByteBuffer>> clusterings = new ArrayList<>();
for (Slice slice : builder.build())
{
if (!slice.start().isBottom())
clusterings.add(createClusteringBound(slice.start()));
if (!slice.end().isTop())
clusterings.add(createClusteringBound(slice.end()));
}
return clusterings;
}
private static Clustering<ByteBuffer> createClusteringBound(ClusteringBound<?> bound)
{
if (bound == null)
throw new IllegalArgumentException("ClusteringBound should not be null.");
int size = bound.size();
ByteBuffer[] values = new ByteBuffer[size];
for (int i = 0; i < size; i++)
values[i] = (ByteBuffer) bound.get(i);
return Clustering.make(values);
}
private static ImmutableUniqueList<Clustering<ByteBuffer>> keys(Collection<Symbol> columns, Map<? extends ReferenceExpression, List<ByteBuffer>> columnValues)
{
return keys(columns, columnValues, Function.identity());

View File

@ -65,6 +65,7 @@ import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
public class ASTSingleTableModelTest
{
public static final ByteBuffer ZERO = ByteBufferUtil.bytes(0);
@ -557,6 +558,116 @@ public class ASTSingleTableModelTest
.build());
}
@Test
public void testClusteringRangeDelete()
{
TableMetadata metadata = new Builder().pk(1).ck(1).statics(0).regular(1).build();
ASTSingleTableModel model = new ASTSingleTableModel(metadata);
for (int i = 0; i < 10; i++)
{
model.update(Mutation.insert(metadata)
.value("pk", 0)
.value("ck", i)
.value("v", i)
.build());
}
model.update(Mutation.delete(metadata)
.value("pk", 0)
.where("ck", Inequality.GREATER_THAN_EQ, 0)
.where("ck", Inequality.LESS_THAN, 7)
.build());
model.validate(rows(row(metadata, 0, 7, 7), row(metadata, 0, 8, 8), row(metadata,0,9,9)),
Select.builder(metadata).value("pk", 0).build());
}
@Test
public void testRangeDeleteWithMultipleClusteringKeys()
{
TableMetadata metadata = new Builder().pk(1).ck(2).statics(0).regular(1).build();
ASTSingleTableModel model = new ASTSingleTableModel(metadata);
for (int i = 0; i < 10; i++)
{
model.update(Mutation.insert(metadata)
.value("pk", 0)
.value("ck0", i)
.value("ck1", i+1)
.value("v", i)
.build());
}
model.update(Mutation.delete(metadata)
.value("pk", 0)
.between("ck0", Literal.of(3), Literal.of(7))
.build());
model.validate(rows(row(metadata, 0,0,1,0),
row(metadata, 0,1,2,1),
row(metadata,0,2,3,2),
row(metadata,0,8,9,8),
row(metadata,0,9,10,9)),
Select.builder(metadata).build());
}
@Test
public void testRangeDeleteWithStaticColumn()
{
TableMetadata metadata = new Builder().pk(1).ck(1).statics(1).regular(1).build();
ASTSingleTableModel model = new ASTSingleTableModel(metadata);
for (int i = 0; i < 10; i++)
{
model.update(Mutation.insert(metadata)
.value("pk", 0)
.value("ck", i)
.value("s", 0)
.value("v", i)
.build());
}
model.update(Mutation.delete(metadata)
.value("pk", 0)
.between("ck", Literal.of(3), Literal.of(7))
.build());
model.validate(rows(row(metadata, 0,0,0,0),
row(metadata, 0,1,0,1),
row(metadata,0,2,0,2),
row(metadata,0,8,0,8),
row(metadata,0,9,0,9)),
Select.builder(metadata).build());
}
@Test
public void testClusteringRangeDeleteBetween()
{
TableMetadata metadata = new Builder().pk(1).ck(1).statics(0).regular(1).build();
ASTSingleTableModel model = new ASTSingleTableModel(metadata);
for (int i = 0; i < 10; i++)
{
model.update(Mutation.insert(metadata)
.value("pk", 0)
.value("ck", i)
.value("v", i)
.build());
}
model.update(Mutation.delete(metadata)
.value("pk", 0)
.between("ck", Bind.of(0), Literal.of(6))
.build());
model.validate(rows(row(metadata, 0, 7, 7), row(metadata, 0, 8, 8), row(metadata,0,9,9)),
Select.builder(metadata).value("pk", 0).build());
}
@Test
public void tokenEqIncludesEmptyPartition()
{
@ -753,6 +864,35 @@ public class ASTSingleTableModelTest
model.validate(rows(row(metadata, 0, 1, null, List.of(1))), Select.builder(metadata).build());
}
@Test
public void testRangeDeleteGeneratorWithBetweenClause()
{
TableMetadata metadata = new Builder().pk(1).ck(1).statics(0).regular(1).build();
ASTSingleTableModel model = new ASTSingleTableModel(metadata);
for (int i = 0; i < 10; i++)
{
model.update(Mutation.insert(metadata)
.value("pk", 0)
.value("ck", i)
.value("v", i)
.build());
}
model.update(Mutation.delete(metadata)
.value("pk", 0)
.between("ck", Literal.of(3), Literal.of(7))
.build());
model.validate(rows(row(metadata, 0, 0,0),
row(metadata, 0, 1,1),
row(metadata,0,2,2),
row(metadata,0,8,8),
row(metadata,0,9,9)),
Select.builder(metadata).build());
}
private interface SimpleWrite<T>
{
void write(String name, T value, long ts);

View File

@ -91,6 +91,36 @@ public class ASTGenerators
{
public static final EnumSet<KnownIssue> IGNORED_ISSUES = KnownIssue.ignoreAll();
public enum RangeType {UNBOUND, BOUND , BETWEEN}
/**
* Applies a range condition to a builder for a single column.
*/
public static void applyRangeCondition(Conditional.ConditionalBuilder<?> builder,
RangeType rangeType,
Symbol column,
Conditional.Where.Inequality lowerKind,
Conditional.Where.Inequality upperKind,
Expression lowerValue,
Expression upperValue)
{
switch (rangeType)
{
case UNBOUND:
builder.where(column, lowerKind, lowerValue);
break;
case BOUND:
builder.where(column, lowerKind, lowerValue);
builder.where(column, upperKind, upperValue);
break;
case BETWEEN:
builder.between(column, lowerValue, upperValue);
break;
default:
throw new UnsupportedOperationException("Unsupported RangeType: " + rangeType);
}
}
public static Gen<LinkedHashMap<Symbol, Object>> columnValues(List<Symbol> columns)
{
List<Gen<?>> gens = new ArrayList<>(columns.size());
@ -403,7 +433,7 @@ public class ASTGenerators
public static class MutationGenBuilder
{
public enum DeleteKind { Partition, Row, Column }
public enum DeleteKind { Partition, Row, Column, Range }
private final TableMetadata metadata;
private final LinkedHashSet<Symbol> allColumns;
private final LinkedHashSet<Symbol> partitionColumns, clusteringColumns;
@ -845,6 +875,8 @@ public class ASTGenerators
// if there are no columns to delete, fallback to row
if (deleteKind == DeleteKind.Column && regularAndStaticColumns.isEmpty())
deleteKind = DeleteKind.Row;
if (deleteKind == DeleteKind.Range && clusteringColumns.isEmpty())
deleteKind = DeleteKind.Partition;
if (deleteKind == DeleteKind.Row && clusteringColumns.isEmpty())
deleteKind = DeleteKind.Partition;
@ -907,6 +939,9 @@ public class ASTGenerators
}
}
break;
case Range:
valueRange(rnd, columnExpressions, builder, clusteringColumns);
break;
default:
throw new UnsupportedOperationException();
}
@ -958,6 +993,12 @@ public class ASTGenerators
columns = new ArrayList<>(uniq);
}
break;
case Range:
{
columns = Collections.emptyList();
existAllowed = false;
}
break;
default:
throw new UnsupportedOperationException(deleteKind.name());
}
@ -982,6 +1023,40 @@ public class ASTGenerators
};
}
private static final Gen<Conditional.Where.Inequality> RANGE_INEQUALITY_GEN = SourceDSL.arbitrary().pick(Conditional.Where.Inequality.GREATER_THAN_EQ,
Conditional.Where.Inequality.GREATER_THAN,
Conditional.Where.Inequality.LESS_THAN_EQ,
Conditional.Where.Inequality.LESS_THAN);
private static final Gen<Conditional.Where.Inequality> LOWER_BOUND_GEN = SourceDSL.arbitrary().pick(Conditional.Where.Inequality.GREATER_THAN,
Conditional.Where.Inequality.GREATER_THAN_EQ);
private static final Gen<Conditional.Where.Inequality> UPPER_BOUND_GEN = SourceDSL.arbitrary().pick(Conditional.Where.Inequality.LESS_THAN,
Conditional.Where.Inequality.LESS_THAN_EQ);
private void valueRange(RandomnessSource rnd,
Map<Symbol, ExpressionBuilder> columnExpressions,
Conditional.ConditionalBuilder<?> builder,
LinkedHashSet<Symbol> columns)
{
List<Symbol> columnList = new ArrayList<>(columns);
assert !columnList.isEmpty();
Symbol s = columnList.get(0);
Gen<Expression> expressionGen = columnExpressions.get(s).build();
RangeType type = SourceDSL.arbitrary().enumValues(RangeType.class).generate(rnd);
applyRangeCondition(builder, type, s,
type == RangeType.UNBOUND ? RANGE_INEQUALITY_GEN.generate(rnd) : LOWER_BOUND_GEN.generate(rnd),
UPPER_BOUND_GEN.generate(rnd),
expressionGen.generate(rnd),
expressionGen.generate(rnd));
}
private void generateRemaining(RandomnessSource rnd,
Gen<Boolean> bool,
Mutation.Kind kind,