This is an automated email from the ASF dual-hosted git repository.
dcapwell pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push:
new c08666567d When updating a multi cell collection element, if the
update is rejected then the shared Row.Builder is not freed causing all future
mutations to be rejected
c08666567d is described below
commit c08666567d95562eeb28f3c49469e564af9a70b8
Author: David Capwell <[email protected]>
AuthorDate: Fri Dec 5 09:41:28 2025 -0800
When updating a multi cell collection element, if the update is rejected
then the shared Row.Builder is not freed causing all future mutations to be
rejected
patch by David Capwell; reviewed by Bernardo Botella Corbi, Caleb
Rackliffe, Dmitry Konstantinov for CASSANDRA-21055
---
CHANGES.txt | 1 +
.../apache/cassandra/cql3/UpdateParameters.java | 19 +-
.../utils/caching/TinyThreadLocalPool.java | 15 ++
.../cassandra/distributed/test/TestBaseImpl.java | 5 +
.../cql3/AccordInteropMultiNodeTableWalkBase.java | 7 +
.../test/cql3/CasMultiNodeTableWalkBase.java | 7 +
.../test/cql3/SingleNodeTableWalkTest.java | 1 +
.../test/cql3/SingleNodeTokenConflictTest.java | 1 +
.../distributed/test/cql3/StatefulASTBase.java | 54 +++-
.../fuzz/topology/AccordTopologyMixupTest.java | 6 +-
.../cassandra/harry/model/ASTSingleTableModel.java | 277 ++++++++++++++++++---
.../simulator/test/SingleTableASTSimulation.java | 4 +-
test/unit/org/apache/cassandra/cql3/CQLTester.java | 6 +
.../cassandra/cql3/ast/CollectionAccess.java | 17 +-
.../org/apache/cassandra/cql3/ast/Conditional.java | 6 +-
.../apache/cassandra/cql3/ast/CreateIndexDDL.java | 9 +-
.../org/apache/cassandra/cql3/ast/Mutation.java | 32 +--
.../org/apache/cassandra/cql3/ast/Reference.java | 9 +
.../cassandra/cql3/ast/ReferenceExpression.java | 9 +
.../unit/org/apache/cassandra/cql3/ast/Symbol.java | 11 +
.../cql3/validation/entities/CollectionsTest.java | 16 ++
.../org/apache/cassandra/utils/ASTGenerators.java | 55 +++-
.../org/apache/cassandra/utils/AssertionUtils.java | 2 +
23 files changed, 476 insertions(+), 93 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 787b6f83aa..6cd56b9ed2 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.1
+ * When updating a multi cell collection element, if the update is rejected
then the shared Row.Builder is not freed causing all future mutations to be
rejected (CASSANDRA-21055)
* Schema annotations escape validation on CREATE and ALTER DDL statements
(CASSANDRA-21046)
* Calculate once and cache the result of ModificationStatement#requiresRead
as a perf optimization (CASSANDRA-21040)
* Update system schema tables with new distributed keyspace on upgrade
(CASSANDRA-20872)
diff --git a/src/java/org/apache/cassandra/cql3/UpdateParameters.java
b/src/java/org/apache/cassandra/cql3/UpdateParameters.java
index e6ad075974..85a97ccc3d 100644
--- a/src/java/org/apache/cassandra/cql3/UpdateParameters.java
+++ b/src/java/org/apache/cassandra/cql3/UpdateParameters.java
@@ -62,9 +62,6 @@ public class UpdateParameters
// Holds data for operations that require a read-before-write. Will be
null otherwise.
private final Map<DecoratedKey, Partition> prefetchedRows;
- private Row.Builder staticBuilder;
- private Row.Builder regularBuilder;
-
// The builder currently in use. Will alias either staticBuilder or
regularBuilder, which are themselves built lazily.
private Row.Builder builder;
@@ -108,20 +105,8 @@ public class UpdateParameters
throw new InvalidRequestException("Invalid empty or null
value for column " + metadata.clusteringColumns().get(0).name);
}
}
-
- if (clustering == Clustering.STATIC_CLUSTERING)
- {
- if (staticBuilder == null)
- staticBuilder = BTreeRow.pooledUnsortedBuilder();
- builder = staticBuilder;
- }
- else
- {
- if (regularBuilder == null)
- regularBuilder = BTreeRow.pooledUnsortedBuilder();
- builder = regularBuilder;
- }
-
+ assert builder == null : "newRow called without building the previous
row";
+ builder = BTreeRow.pooledUnsortedBuilder();
builder.newRow(clustering);
}
diff --git
a/src/java/org/apache/cassandra/utils/caching/TinyThreadLocalPool.java
b/src/java/org/apache/cassandra/utils/caching/TinyThreadLocalPool.java
index 7712c09258..78ce8d0f26 100644
--- a/src/java/org/apache/cassandra/utils/caching/TinyThreadLocalPool.java
+++ b/src/java/org/apache/cassandra/utils/caching/TinyThreadLocalPool.java
@@ -18,10 +18,12 @@
package org.apache.cassandra.utils.caching;
+import accord.utils.Invariants;
import io.netty.util.concurrent.FastThreadLocal;
public class TinyThreadLocalPool<V> extends
FastThreadLocal<TinyThreadLocalPool.TinyPool<V>>
{
+ private static final boolean DEBUG = Invariants.debug();
protected TinyPool<V> initialValue()
{
return new TinyPool<>();
@@ -46,10 +48,23 @@ public class TinyThreadLocalPool<V> extends
FastThreadLocal<TinyThreadLocalPool.
}
private void offerSafe(V value)
{
+ if (DEBUG)
+ checkOfferSafe(value);
if (val1 == null) val1 = value;
else if (val2 == null) val2 = value;
else if (val3 == null) val3 = value;
}
+
+ private void checkOfferSafe(V value)
+ {
+ if (val1 == value)
+ throw new IllegalStateException("Double offer");
+ if (val2 == value)
+ throw new IllegalStateException("Double offer");
+ if (val3 == value)
+ throw new IllegalStateException("Double offer");
+ }
+
public V poll()
{
Object result;
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java
b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java
index 6d91a59237..1f76eda33e 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java
@@ -94,6 +94,11 @@ import static org.assertj.core.api.Assertions.fail;
// checkstyle: suppress below 'blockSystemPropertyUsage'
public class TestBaseImpl extends DistributedTestBase
{
+ static
+ {
+ System.setProperty("accord.debug", "true"); // checkstyle: suppress
nearby 'blockSystemPropertyUsage'
+ }
+
private static final Logger logger =
LoggerFactory.getLogger(TestBaseImpl.class);
public static final Object[][] EMPTY_ROWS = new Object[0][];
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/cql3/AccordInteropMultiNodeTableWalkBase.java
b/test/distributed/org/apache/cassandra/distributed/test/cql3/AccordInteropMultiNodeTableWalkBase.java
index d1a5e0f32e..170d9ea8f6 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/cql3/AccordInteropMultiNodeTableWalkBase.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/cql3/AccordInteropMultiNodeTableWalkBase.java
@@ -117,6 +117,13 @@ Suppressed: java.lang.AssertionError: Unknown keyspace ks12
return super.command(rs, mutation, annotate);
}
+ @Override
+ protected boolean allowListElementAccessForUpdateSet()
+ {
+ // See CASSANDRA-20828
+ return false;
+ }
+
@Override
protected boolean allowUsingTimestamp()
{
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/cql3/CasMultiNodeTableWalkBase.java
b/test/distributed/org/apache/cassandra/distributed/test/cql3/CasMultiNodeTableWalkBase.java
index 9a466b9493..d812eadec1 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/cql3/CasMultiNodeTableWalkBase.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/cql3/CasMultiNodeTableWalkBase.java
@@ -75,6 +75,13 @@ public abstract class CasMultiNodeTableWalkBase extends
MultiNodeTableWalkBase
super(rs, cluster);
}
+ @Override
+ protected boolean allowUsingTimestamp()
+ {
+ // Paxos doesn't allow USING TIMESTAMP
+ return false;
+ }
+
@Override
protected Gen<Mutation> toMutationGen(ASTGenerators.MutationGenBuilder
mutationGenBuilder)
{
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
index a2a526121b..a463e06bdb 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
@@ -447,6 +447,7 @@ public class SingleNodeTableWalkTest extends StatefulASTBase
ASTGenerators.MutationGenBuilder mutationGenBuilder = new
ASTGenerators.MutationGenBuilder(metadata)
.withTxnSafe()
.withColumnExpressions(e ->
e.withOperators(Generators.fromGen(BOOLEAN_DISTRIBUTION.next(rs))))
+
.withListElementAccessForUpdateSet(allowListElementAccessForUpdateSet())
.withIgnoreIssues(IGNORED_ISSUES);
// Run the test with and without bound partitions
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java
b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java
index 703c4e3c93..72da2a8d6c 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java
@@ -386,6 +386,7 @@ public class SingleNodeTokenConflictTest extends
StatefulASTBase
this.mutationGen = toGen(new
ASTGenerators.MutationGenBuilder(metadata)
.withTxnSafe()
.withPartitions(SourceDSL.arbitrary().pick(uniquePartitions))
+
.withListElementAccessForUpdateSet(allowListElementAccessForUpdateSet())
.withIgnoreIssues(IGNORED_ISSUES)
.build());
}
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/cql3/StatefulASTBase.java
b/test/distributed/org/apache/cassandra/distributed/test/cql3/StatefulASTBase.java
index 756095673e..ce43be6373 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/cql3/StatefulASTBase.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/cql3/StatefulASTBase.java
@@ -27,11 +27,13 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableList;
+import org.assertj.core.api.ThrowableAssert;
import org.slf4j.Logger;
import accord.utils.Gen;
@@ -105,6 +107,11 @@ public class StatefulASTBase extends TestBaseImpl
*/
protected static boolean CQL_DEBUG_APPLY_OPERATOR = false;
+ /**
+ * Allows for overriding the CQL format logic, default is none (single
line), but can create custom ones to help with the history
+ */
+ protected static Supplier<CQLFormatter> CQL_FORMATTER = () ->
CQLFormatter.None.instance;
+
protected static final Gen<Gen<Boolean>> BOOL_DISTRIBUTION =
Gens.bools().mixedDistribution();
protected static final Gen<Gen<Conditional.Where.Inequality>>
LESS_THAN_DISTRO =
Gens.mixedDistribution(Stream.of(Conditional.Where.Inequality.values())
.filter(i -> i ==
Conditional.Where.Inequality.LESS_THAN || i ==
Conditional.Where.Inequality.LESS_THAN_EQ)
@@ -196,7 +203,7 @@ public class StatefulASTBase extends TestBaseImpl
protected static <S extends BaseState> Property.Command<S, Void, ?>
compactTable(RandomSource rs, S state)
{
return new Property.SimpleCommand<>("nodetool compact " +
state.metadata.keyspace + ' ' + state.metadata.name, s2 -> {
- state.cluster.forEach(i -> i.nodetoolResult("compact",
s2.metadata.keyspace, s2.metadata.name).asserts().success());
+ s2.cluster.forEach(i -> i.nodetoolResult("compact",
s2.metadata.keyspace, s2.metadata.name).asserts().success());
s2.compact();
});
}
@@ -475,6 +482,12 @@ public class StatefulASTBase extends TestBaseImpl
return command(rs, select, null);
}
+ protected boolean allowListElementAccessForUpdateSet()
+ {
+ // this requires a read at the same CL, but the model is global
level and not per-node level, so can't handle
+ return mutationCl() != ConsistencyLevel.NODE_LOCAL;
+ }
+
protected boolean allowRepair()
{
return false;
@@ -586,18 +599,24 @@ public class StatefulASTBase extends TestBaseImpl
{
var inst = selectInstance(rs);
String postfix = "on " + inst;
- if (mutation.isCas())
- {
+ @Nullable
+ Consumer<ThrowableAssert.ThrowingCallable> shouldRaiseThrowable =
model.shouldReject(mutation);
+ if (shouldRaiseThrowable != null)
+ postfix += ", should reject";
+ else if (mutation.isCas())
postfix += ", would apply " + model.shouldApply(mutation);
- // CAS doesn't allow timestamps
- mutation = mutation.withoutTimestamp();
- }
+
if (annotate == null) annotate = postfix;
else annotate += ", " + postfix;
- Mutation finalMutation = mutation;
return new Property.SimpleCommand<>(humanReadable(mutation,
annotate), s -> {
- var result = s.executeQuery(inst, Integer.MAX_VALUE,
s.mutationCl(), finalMutation);
- s.model.updateAndValidate(result, finalMutation);
+ if (shouldRaiseThrowable != null)
+ {
+ shouldRaiseThrowable.accept(() -> s.executeQuery(inst,
Integer.MAX_VALUE, s.mutationCl(), mutation));
+ s.mutation();
+ return;
+ }
+ var result = s.executeQuery(inst, Integer.MAX_VALUE,
s.mutationCl(), mutation);
+ s.model.updateAndValidate(result, mutation);
s.mutation();
});
}
@@ -611,7 +630,11 @@ public class StatefulASTBase extends TestBaseImpl
{
var inst = selectInstance(rs);
String postfix = "on " + inst;
- if (model.isConditional(txn))
+ @Nullable
+ Consumer<ThrowableAssert.ThrowingCallable> shouldRaiseThrowable =
model.shouldReject(txn);
+ if (shouldRaiseThrowable != null)
+ postfix += ", should reject";
+ else if (model.isConditional(txn))
postfix += ", would apply " + model.shouldApply(txn);
if (annotate == null) annotate = postfix;
else annotate += ", " + postfix;
@@ -619,7 +642,14 @@ public class StatefulASTBase extends TestBaseImpl
return new Property.SimpleCommand<>(humanReadable(txn, annotate),
s -> {
boolean hasMutation = txn.ifBlock.isPresent() ||
!txn.mutations.isEmpty();
ConsistencyLevel cl = hasMutation ? s.mutationCl() :
s.selectCl();
- s.model.updateAndValidate(s.executeQuery(inst,
Integer.MAX_VALUE, cl, txn), txn);
+ if (shouldRaiseThrowable != null)
+ {
+ shouldRaiseThrowable.accept(() -> s.executeQuery(inst,
Integer.MAX_VALUE, cl, txn));
+ }
+ else
+ {
+ s.model.updateAndValidate(s.executeQuery(inst,
Integer.MAX_VALUE, cl, txn), txn);
+ }
if (hasMutation)
s.mutation();
});
@@ -706,7 +736,7 @@ public class StatefulASTBase extends TestBaseImpl
{
// With UTF-8 some chars can cause printing issues leading to
error messages that don't reproduce the original issue.
// To avoid this problem, always escape the CQL so nothing gets
lost
- String cql =
StringUtils.escapeControlChars(stmt.visit(debug).toCQL(CQLFormatter.None.instance));
+ String cql =
StringUtils.escapeControlChars(stmt.visit(debug).toCQL(CQL_FORMATTER.get()));
if (annotate != null)
cql += " -- " + annotate;
return cql;
diff --git
a/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java
b/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java
index 82a444c37d..e12b4860dc 100644
---
a/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java
+++
b/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java
@@ -148,7 +148,11 @@ public class AccordTopologyMixupTest extends
TopologyMixupTestBase<AccordTopolog
private static CommandGen<Spec> cqlOperations(Spec spec)
{
Gen<Statement> select = (Gen<Statement>) (Gen<?>) fromQT(new
ASTGenerators.SelectGenBuilder(spec.metadata).withLimit1().build());
- Gen<Statement> mutation = (Gen<Statement>) (Gen<?>) fromQT(new
ASTGenerators.MutationGenBuilder(spec.metadata).withTxnSafe().disallowUpdateMultiplePartitionKeys().build());
+ Gen<Statement> mutation = (Gen<Statement>) (Gen<?>) fromQT(new
ASTGenerators.MutationGenBuilder(spec.metadata)
+
.withTxnSafe()
+
.disallowUpdateMultiplePartitionKeys() //TODO (coverage): this is something
Accord should support, so should remove and make sure accord is updated
+
.disallowListElementAccessForUpdateSet() //TODO (coverage): CASSANDRA-20828
found an issue with multi cell list type timestamp handling, so make sure
accord doesn't hit this
+ .build());
Gen<Statement> txn = (Gen<Statement>) (Gen<?>) fromQT(new
ASTGenerators.TxnGenBuilder(spec.metadata).build());
Map<Gen<Statement>, Integer> operations = new LinkedHashMap<>();
operations.put(select, 1);
diff --git
a/test/harry/main/org/apache/cassandra/harry/model/ASTSingleTableModel.java
b/test/harry/main/org/apache/cassandra/harry/model/ASTSingleTableModel.java
index 37f4585332..805857ea98 100644
--- a/test/harry/main/org/apache/cassandra/harry/model/ASTSingleTableModel.java
+++ b/test/harry/main/org/apache/cassandra/harry/model/ASTSingleTableModel.java
@@ -35,6 +35,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
@@ -50,6 +51,7 @@ import accord.utils.Invariants;
import org.apache.cassandra.cql3.KnownIssue;
import org.apache.cassandra.cql3.ast.AssignmentOperator;
import org.apache.cassandra.cql3.ast.CasCondition;
+import org.apache.cassandra.cql3.ast.CollectionAccess;
import org.apache.cassandra.cql3.ast.Conditional.Where.Inequality;
import org.apache.cassandra.cql3.ast.Conditional;
import org.apache.cassandra.cql3.ast.Element;
@@ -71,17 +73,24 @@ import org.apache.cassandra.db.BufferClustering;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.BooleanType;
+import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.harry.model.BytesPartitionState.PrimaryKey;
import org.apache.cassandra.harry.util.StringUtils;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
+import org.apache.cassandra.utils.AssertionUtils;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.ImmutableUniqueList;
import org.apache.cassandra.utils.Pair;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.ThrowableAssert;
import static org.apache.cassandra.cql3.ast.Elements.symbols;
import static org.apache.cassandra.harry.MagicConstants.NO_TIMESTAMP;
@@ -268,6 +277,8 @@ public class ASTSingleTableModel
if (!mutations.isEmpty())
numMutations++; // bump here to make sure the last mutation
doesn't have the same ts as the mutations here
long nowTs = numMutations;
+ for (var m : mutations)
+ Invariants.require(shouldReject(m) == null, "Mutation should have
been rejected");
for (var m : mutations)
{
if (m.timestampOrDefault(NO_TIMESTAMP) == NO_TIMESTAMP)
@@ -315,6 +326,7 @@ public class ASTSingleTableModel
public void update(Mutation mutation)
{
if (!shouldApply(mutation)) return;
+ Invariants.require(shouldReject(mutation) == null, "Mutation should
have been rejected");
updateInternal(mutation);
}
@@ -326,6 +338,7 @@ public class ASTSingleTableModel
validateCasNotApplied(actual, mutation);
return;
}
+ Invariants.require(shouldReject(mutation) == null, "Mutation should
have been rejected");
if (mutation.isCas() && validateCass.validate())
validate(CAS_APPLIED_COLUMNS, actual, CAS_SUCCESS_RESULT);
updateInternal(mutation);
@@ -553,12 +566,11 @@ public class ASTSingleTableModel
partition = factory.create(pd);
partitions.put(partition.ref(), partition);
}
- Map<Symbol, Expression> values = insert.values;
+ Map<ReferenceExpression, Expression> values = insert.values;
if (!factory.staticColumns.isEmpty() &&
!Sets.intersection(factory.staticColumns, values.keySet()).isEmpty())
{
- maybeUpdateColumns(Sets.intersection(factory.staticColumns,
values.keySet()),
- partition.staticRow(),
- nowTs, values,
+ maybeUpdateColumns(partition.staticRow(),
+ nowTs, filter(values, factory.staticColumns),
partition::setStaticColumns);
}
// table has clustering but non are in the write, so only pk/static
can be updated
@@ -566,9 +578,8 @@ public class ASTSingleTableModel
return;
BytesPartitionState finalPartition = partition;
var cd = key(insert.values, factory.clusteringColumns);
- maybeUpdateColumns(Sets.intersection(factory.regularColumns,
values.keySet()),
- partition.get(cd),
- nowTs, values,
+ maybeUpdateColumns(partition.get(cd),
+ nowTs, filter(values, factory.regularColumns),
(ts, write) -> finalPartition.setColumns(cd, ts,
write, true));
}
@@ -586,12 +597,11 @@ public class ASTSingleTableModel
partition = factory.create(pd);
partitions.put(partition.ref(), partition);
}
- Map<Symbol, Expression> set = update.set;
- if (!factory.staticColumns.isEmpty() &&
!Sets.intersection(factory.staticColumns, set.keySet()).isEmpty())
+ Map<ReferenceExpression, Expression> set = update.set;
+ if (!factory.staticColumns.isEmpty() && !filter(set,
factory.staticColumns).isEmpty())
{
- maybeUpdateColumns(Sets.intersection(factory.staticColumns,
set.keySet()),
- partition.staticRow(),
- nowTs, set,
+ maybeUpdateColumns(partition.staticRow(),
+ nowTs, filter(set, factory.staticColumns),
partition::setStaticColumns);
}
// table has clustering but non are in the write, so only
pk/static can be updated
@@ -600,9 +610,8 @@ public class ASTSingleTableModel
BytesPartitionState finalPartition = partition;
for (Clustering<ByteBuffer> cd : clustering(remaining))
{
- maybeUpdateColumns(Sets.intersection(factory.regularColumns,
set.keySet()),
- partition.get(cd),
- nowTs, set,
+ maybeUpdateColumns(partition.get(cd),
+ nowTs, filter(set, factory.regularColumns),
(ts, write) ->
finalPartition.setColumns(cd, ts, write, false));
}
}
@@ -665,24 +674,35 @@ public class ASTSingleTableModel
}
}
- private static void maybeUpdateColumns(Set<Symbol> columns,
- @Nullable BytesPartitionState.Row
row,
- long nowTs, Map<Symbol, Expression>
set,
+ private static Map<ReferenceExpression, Expression>
filter(Map<ReferenceExpression, Expression> map, Set<Symbol> columns)
+ {
+ Map<ReferenceExpression, Expression> update = new HashMap<>();
+ for (var e : map.entrySet())
+ {
+ if (columns.contains(e.getKey().column()))
+ update.put(e.getKey(), e.getValue());
+ }
+ return update;
+ }
+
+ private static void maybeUpdateColumns(@Nullable BytesPartitionState.Row
row,
+ long nowTs,
Map<ReferenceExpression, Expression> set,
ColumnUpdate update)
{
- if (columns.isEmpty())
+ if (set.isEmpty())
{
update.update(nowTs, Collections.emptyMap());
return;
}
// static columns to add in. If we are doing something like += to a
row that doesn't exist, we still update statics...
Map<Symbol, ByteBuffer> write = new HashMap<>();
- for (Symbol col : columns)
+ for (var e : set.entrySet())
{
- ByteBuffer current = row == null ? null : row.get(col);
- EvalResult result = eval(col, current, set.get(col));
+ var col = e.getKey();
+ ByteBuffer current = row == null ? null : row.get(col.column());
+ EvalResult result = eval(col, current, e.getValue());
if (result.kind == EvalResult.Kind.SKIP) continue;
- write.put(col, result.value);
+ write.put(col.column(), result.value);
}
if (!write.isEmpty())
update.update(nowTs, write);
@@ -753,11 +773,124 @@ public class ASTSingleTableModel
return process(Who.cas, updatedCondition, lets);
}
+ public Consumer<ThrowableAssert.ThrowingCallable> shouldReject(Txn txn)
+ {
+ if (!shouldApply(txn)) return null;
+ List<Mutation> mutations = null;
+ if (txn.ifBlock.isPresent()) mutations = txn.ifBlock.get().mutations;
+ else if (!txn.mutations.isEmpty()) mutations = txn.mutations;
+
+ if (mutations == null) return null;
+ Set<Consumer<Throwable>> checks = new HashSet<>();
+ for (var m : mutations)
+ {
+ var failures = shouldReject0(m);
+ if (failures != null)
+ checks.addAll(failures);
+ }
+ return toShouldRejectConsumer(checks);
+ }
+
+ public Consumer<ThrowableAssert.ThrowingCallable> shouldReject(Mutation
mutation)
+ {
+ return toShouldRejectConsumer(shouldReject0(mutation));
+ }
+
+ private static Consumer<ThrowableAssert.ThrowingCallable>
toShouldRejectConsumer(@Nullable Set<Consumer<Throwable>> checks)
+ {
+ return checks == null || checks.isEmpty() ? null
+ : t ->
Assertions.assertThatThrownBy(t)
+
.satisfiesAnyOf(checks.toArray(Consumer[]::new));
+ }
+
+ private Set<Consumer<Throwable>> shouldReject0(Mutation mutation)
+ {
+ if (!shouldApply(mutation)) return null;
+ Set<Consumer<Throwable>> checks = null;
+ if (mutation.kind == Mutation.Kind.UPDATE)
+ {
+ Consumer<Throwable> notFound = t ->
+ Assertions.assertThat(t)
+
.is(AssertionUtils.anyOfThrowable(com.datastax.driver.core.exceptions.InvalidQueryException.class,
// result from java driver
+
InvalidRequestException.class)) // result from jvm-dtest execute api
+ .hasMessage("Attempted to
set an element on a list which is null");
+ Mutation.Update update = mutation.asUpdate();
+ for (var e : update.set.entrySet())
+ {
+ if (e.getKey() instanceof CollectionAccess)
+ {
+ CollectionAccess access = (CollectionAccess) e.getKey();
+ if (access.column().type().getClass() == ListType.class)
+ {
+ // if the column doesn't have data reject
+ for (BytesPartitionState.Ref ref :
referencePartitions(mutation))
+ {
+ BytesPartitionState partition =
partitions.get(ref);
+ if (partition == null)
+ {
+ if (checks == null) checks = new HashSet<>();
+ checks.add(notFound);
+ continue;
+ }
+ List<BytesPartitionState.Row> rows;
+ if
(factory.staticColumns.contains(access.column()))
+ {
+ rows = List.of(partition.staticRow());
+ }
+ else
+ {
+ rows =
cds(mutation).stream().map(partition::get).collect(Collectors.toList());
+ }
+ if (rows.isEmpty())
+ {
+ if (checks == null) checks = new HashSet<>();
+ checks.add(notFound);
+ continue;
+ }
+ for (var row : rows)
+ {
+ if (row == null)
+ {
+ if (checks == null) checks = new
HashSet<>();
+ checks.add(notFound);
+ continue;
+ }
+ if (row.get(access.column()) == null)
+ {
+ if (checks == null) checks = new
HashSet<>();
+ checks.add(notFound);
+ continue;
+ }
+ int offset =
Int32Type.instance.compose(access.element.valueEncoded());
+ ByteBuffer columnValue =
row.get(access.column());
+ var values = ((ListType<?>)
access.column().type()).unpack(columnValue);
+ if (offset < 0 || offset >= values.size())
+ {
+ if (checks == null) checks = new
HashSet<>();
+ checks.add(t -> Assertions.assertThat(t)
+
.is(AssertionUtils.anyOfThrowable(com.datastax.driver.core.exceptions.InvalidQueryException.class,
// result from java driver
+
InvalidRequestException.class)) // result from jvm-dtest
execute api
+
.hasMessage(String.format("List index %s out of bound, list has size %s",
offset, values.size())));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return checks;
+ }
+
public BytesPartitionState.Ref referencePartition(Mutation mutation)
{
return factory.createRef(pd(mutation));
}
+ public List<BytesPartitionState.Ref> referencePartitions(Mutation mutation)
+ {
+ return
pds(mutation).stream().map(factory::createRef).collect(Collectors.toList());
+ }
+
private enum Who { cas, accord }
private boolean process(Who who, Conditional condition, Map<String,
SelectResult> lets)
@@ -952,18 +1085,18 @@ public class ASTSingleTableModel
return Pair.create(partitionKeys, other);
}
- private static ImmutableUniqueList<Clustering<ByteBuffer>>
keys(Collection<Symbol> columns, Map<Symbol, List<ByteBuffer>> columnValues)
+ private static ImmutableUniqueList<Clustering<ByteBuffer>>
keys(Collection<Symbol> columns, Map<? extends ReferenceExpression,
List<ByteBuffer>> columnValues)
{
return keys(columns, columnValues, Function.identity());
}
- private static ImmutableUniqueList<Clustering<ByteBuffer>>
keys(Map<Symbol, List<? extends Expression>> values, Collection<Symbol> columns)
+ private static ImmutableUniqueList<Clustering<ByteBuffer>> keys(Map<?
extends ReferenceExpression, List<? extends Expression>> values,
Collection<Symbol> columns)
{
return keys(columns, values, ASTSingleTableModel::eval);
}
private static <T> ImmutableUniqueList<Clustering<ByteBuffer>>
keys(Collection<Symbol> columns,
-
Map<Symbol, ? extends List<? extends T>> columnValues,
+ Map<?
extends ReferenceExpression, ? extends List<? extends T>> columnValues,
Function<T, ByteBuffer> eval)
{
if (columns.isEmpty()) return ImmutableUniqueList.empty();
@@ -1037,16 +1170,51 @@ public class ASTSingleTableModel
return pks.get(0);
}
+ private List<Clustering<ByteBuffer>> pds(Mutation mutation)
+ {
+ switch (mutation.kind)
+ {
+ case INSERT:
+ return pds((Mutation.Insert) mutation);
+ case UPDATE:
+ return pds((Mutation.Update) mutation);
+ case DELETE:
+ return pds((Mutation.Delete) mutation);
+ default:
+ throw new UnsupportedOperationException(mutation.kind.name());
+ }
+ }
+
+ private List<Clustering<ByteBuffer>> pds(Mutation.Insert mutation)
+ {
+ return List.of(key(mutation.values, factory.partitionColumns));
+ }
+
+ private List<Clustering<ByteBuffer>> pds(Mutation.Update mutation)
+ {
+ return pds(mutation.where.simplify());
+ }
+
+ private List<Clustering<ByteBuffer>> pds(Mutation.Delete mutation)
+ {
+ return pds(mutation.where.simplify());
+ }
+
+ private List<Clustering<ByteBuffer>> pds(List<Conditional> conditionals)
+ {
+ return splitOnPartition(conditionals).left;
+ }
+
@Nullable
- private Clustering<ByteBuffer> cdOrNull(Mutation mutation)
+ private List<Clustering<ByteBuffer>> cds(Mutation mutation)
{
- if (factory.clusteringColumns.isEmpty()) return Clustering.EMPTY;
+ if (factory.clusteringColumns.isEmpty()) return
List.of(Clustering.EMPTY);
if (mutation.kind == Mutation.Kind.INSERT)
{
var insert = (Mutation.Insert) mutation;
return
!insert.values.keySet().containsAll(factory.clusteringColumns)
- ? null
- : key(insert.values, factory.clusteringColumns);
+ ? null
+ : List.of(key(insert.values, factory.clusteringColumns));
}
Conditional where;
switch (mutation.kind)
@@ -1056,13 +1224,20 @@ public class ASTSingleTableModel
break;
case DELETE:
where = ((Mutation.Delete) mutation).where;
- break;
+ break;
default:
throw new UnsupportedOperationException("Unexpected mutation:
" + mutation.kind);
}
var partitions = splitOnPartition(where.simplify());
if (partitions.right.isEmpty()) return null;
- var matches = clustering(partitions.right);
+ return clustering(partitions.right);
+ }
+
+ @Nullable
+ private Clustering<ByteBuffer> cdOrNull(Mutation mutation)
+ {
+ var matches = cds(mutation);
+ if (matches == null) return null;
Preconditions.checkArgument(matches.size() == 1);
return matches.get(0);
}
@@ -1654,7 +1829,7 @@ public class ASTSingleTableModel
private List<PrimaryKey> filter(LookupContext ctx, BytesPartitionState
partition)
{
- Map<Symbol, List<? extends Expression>> values = ctx.eq;
+ Map<ReferenceExpression, List<? extends Expression>> values = ctx.eq;
List<PrimaryKey> rows = new ArrayList<>(partition.size());
if (!factory.clusteringColumns.isEmpty() &&
values.keySet().containsAll(factory.clusteringColumns))
{
@@ -1699,10 +1874,11 @@ public class ASTSingleTableModel
return matches;
}
- private Clustering<ByteBuffer> key(Map<Symbol, Expression> values,
ImmutableUniqueList<Symbol> columns)
+ private Clustering<ByteBuffer> key(Map<? extends ReferenceExpression,
Expression> input, ImmutableUniqueList<Symbol> columns)
{
if (columns.isEmpty()) return Clustering.EMPTY;
// same as keys, but only one possible value can happen
+ Map<Symbol, Expression> values = input.entrySet().stream().filter(e ->
e.getKey() instanceof Symbol).map(e -> (Map.Entry<Symbol, Expression>)
e).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
List<Clustering<ByteBuffer>> keys = keys(Maps.transformValues(values,
Collections::singletonList), columns);
Preconditions.checkState(keys.size() == 1, "Expected 1 key, but found
%s", keys.size());
return keys.get(0);
@@ -1729,8 +1905,35 @@ public class ASTSingleTableModel
}
}
- private static EvalResult eval(Symbol col, @Nullable ByteBuffer current,
Expression e)
+ private static EvalResult eval(ReferenceExpression col, @Nullable
ByteBuffer current, Expression e)
{
+ if (col instanceof Reference)
+ throw new IllegalArgumentException("References are not supported
yet; foo.bar.baz style eval is not yet handled; given " + col.toCQL());
+ if (col instanceof CollectionAccess)
+ {
+ CollectionAccess access = (CollectionAccess) col;
+ CollectionType<?> ct = (CollectionType<?>) access.column().type();
+ switch (ct.kind)
+ {
+ case LIST:
+ {
+ int offset =
Int32Type.instance.compose(eval(access.element));
+ var values = ct.unpack(current);
+ values.set(offset, eval(e));
+ return EvalResult.accept(ct.pack(values));
+ }
+ case MAP:
+ {
+ @SuppressWarnings("unchecked") MapType<Object, Object> mt
= (MapType<Object, Object>) ct;
+ Object key =
mt.nameComparator().compose(eval(access.element));
+ Map<Object, Object> values = current == null ? new
HashMap<>() : mt.compose(current);
+ values.put(key, mt.valueComparator().compose(eval(e)));
+ return EvalResult.accept(mt.decompose(values));
+ }
+ case SET:
+ throw new UnsupportedOperationException("Set collection
access not supported");
+ }
+ }
if (!(e instanceof AssignmentOperator)) return
EvalResult.accept(eval(e));
current = col.type().sanitize(current);
// multi cell collections have the property that they do update even
if the current value is null
@@ -1880,8 +2083,8 @@ public class ASTSingleTableModel
private class LookupContext
{
- private final Map<Symbol, List<? extends Expression>> eq = new
HashMap<>();
- private final Map<Symbol, List<ColumnCondition>> ltOrGt = new
HashMap<>();
+ private final Map<ReferenceExpression, List<? extends Expression>> eq
= new HashMap<>();
+ private final Map<ReferenceExpression, List<ColumnCondition>> ltOrGt =
new HashMap<>();
@Nullable
private Token token = null;
@Nullable
diff --git
a/test/simulator/test/org/apache/cassandra/simulator/test/SingleTableASTSimulation.java
b/test/simulator/test/org/apache/cassandra/simulator/test/SingleTableASTSimulation.java
index ef1f60b470..3b963aed88 100644
---
a/test/simulator/test/org/apache/cassandra/simulator/test/SingleTableASTSimulation.java
+++
b/test/simulator/test/org/apache/cassandra/simulator/test/SingleTableASTSimulation.java
@@ -242,7 +242,9 @@ public class SingleTableASTSimulation extends
SimulationTestBase.SimpleSimulatio
.uniqueBestEffort()
.ofSize(rs.nextInt(1, 20))
.next(rs);
- Gen<Action> mutationGen = toGen(ASTGenerators.mutationBuilder(rs,
model, uniquePartitions, i -> null).build())
+ Gen<Action> mutationGen = toGen(ASTGenerators.mutationBuilder(rs,
model, uniquePartitions, i -> null)
+
.disallowListElementAccessForUpdateSet() //TODO (coverage): CASSANDRA-20828
found an issue with multi cell list type timestamp handling, so make sure
accord doesn't hit this
+ .build())
.map(mutation -> query(mutation));
Gen<Action> selectPartitionGen = Gens.pick(uniquePartitions)
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java
b/test/unit/org/apache/cassandra/cql3/CQLTester.java
index 413b1553fc..4ac41a608b 100644
--- a/test/unit/org/apache/cassandra/cql3/CQLTester.java
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -232,6 +232,12 @@ import static
org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
*/
public abstract class CQLTester
{
+
+ static
+ {
+ System.setProperty("accord.debug", "true"); // checkstyle: suppress
nearby 'blockSystemPropertyUsage'
+ }
+
/**
* The super user
*/
diff --git a/test/unit/org/apache/cassandra/cql3/ast/CollectionAccess.java
b/test/unit/org/apache/cassandra/cql3/ast/CollectionAccess.java
index 19c8176f6e..d43d7bf34f 100644
--- a/test/unit/org/apache/cassandra/cql3/ast/CollectionAccess.java
+++ b/test/unit/org/apache/cassandra/cql3/ast/CollectionAccess.java
@@ -25,10 +25,10 @@ import org.apache.cassandra.db.marshal.AbstractType;
public class CollectionAccess implements ReferenceExpression
{
private final ReferenceExpression column;
- private final ReferenceExpression element;
+ public final Value element;
private final AbstractType<?> type;
- public CollectionAccess(ReferenceExpression column, ReferenceExpression
element, AbstractType<?> type)
+ public CollectionAccess(ReferenceExpression column, Value element,
AbstractType<?> type)
{
this.column = column;
this.element = element;
@@ -55,4 +55,17 @@ public class CollectionAccess implements ReferenceExpression
{
return Stream.of(column, element);
}
+
+ @Override
+ public Symbol column()
+ {
+ // could be recursive... foo.bar.baz[42]
+ return column.column();
+ }
+
+ @Override
+ public String toString()
+ {
+ return toCQL();
+ }
}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Conditional.java
b/test/unit/org/apache/cassandra/cql3/ast/Conditional.java
index d2a8e2b125..c7323127f6 100644
--- a/test/unit/org/apache/cassandra/cql3/ast/Conditional.java
+++ b/test/unit/org/apache/cassandra/cql3/ast/Conditional.java
@@ -326,8 +326,8 @@ public interface Conditional extends Expression
interface EqBuilder<T extends EqBuilder<T>>
{
- T value(Symbol symbol, Expression e);
- default T value(Symbol symbol, Object value)
+ T value(ReferenceExpression symbol, Expression e);
+ default T value(ReferenceExpression symbol, Object value)
{
return value(symbol, new Bind(value, symbol.type()));
}
@@ -413,7 +413,7 @@ public interface Conditional extends Expression
T is(ReferenceExpression ref, Is.Kind kind);
@Override
- default T value(Symbol symbol, Expression e)
+ default T value(ReferenceExpression symbol, Expression e)
{
return where(symbol, Where.Inequality.EQUAL, e);
}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/CreateIndexDDL.java
b/test/unit/org/apache/cassandra/cql3/ast/CreateIndexDDL.java
index 91f6167ba5..3e75b9c56b 100644
--- a/test/unit/org/apache/cassandra/cql3/ast/CreateIndexDDL.java
+++ b/test/unit/org/apache/cassandra/cql3/ast/CreateIndexDDL.java
@@ -256,16 +256,21 @@ public class CreateIndexDDL implements Element
public static class CollectionReference implements ReferenceExpression
{
public enum Kind { FULL, KEYS, ENTRIES }
-
public final Kind kind;
- public final ReferenceExpression column;
+ public final ReferenceExpression column;
public CollectionReference(Kind kind, ReferenceExpression column)
{
this.kind = kind;
this.column = column;
}
+ @Override
+ public Symbol column()
+ {
+ return column.column();
+ }
+
@Override
public AbstractType<?> type()
{
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Mutation.java
b/test/unit/org/apache/cassandra/cql3/ast/Mutation.java
index 1031fa0fd2..1a598b6555 100644
--- a/test/unit/org/apache/cassandra/cql3/ast/Mutation.java
+++ b/test/unit/org/apache/cassandra/cql3/ast/Mutation.java
@@ -261,11 +261,11 @@ public abstract class Mutation implements Statement
public static class Insert extends Mutation
{
- public final LinkedHashMap<Symbol, Expression> values;
+ public final LinkedHashMap<ReferenceExpression, Expression> values;
public final boolean ifNotExists;
public final Optional<Using> using;
- public Insert(TableReference table, LinkedHashMap<Symbol, Expression>
values, boolean ifNotExists, Optional<Using> using)
+ public Insert(TableReference table, LinkedHashMap<ReferenceExpression,
Expression> values, boolean ifNotExists, Optional<Using> using)
{
super(Mutation.Kind.INSERT, table);
this.values = values;
@@ -290,7 +290,7 @@ public abstract class Mutation implements Statement
sb.append("INSERT INTO ");
table.toCQL(sb, formatter);
sb.append(" (");
- for (Symbol col : values.keySet())
+ for (ReferenceExpression col : values.keySet())
{
col.toCQL(sb, formatter);
sb.append(", ");
@@ -339,10 +339,10 @@ public abstract class Mutation implements Statement
var u = v.visit(this);
if (u != this) return u;
boolean updated = false;
- LinkedHashMap<Symbol, Expression> copied = new
LinkedHashMap<>(values.size());
+ LinkedHashMap<ReferenceExpression, Expression> copied = new
LinkedHashMap<>(values.size());
for (var e : values.entrySet())
{
- Symbol s = e.getKey();
+ Symbol s = e.getKey().asSymbol();
Symbol s2 = s.visit("INSERT", v);
if (s != s2)
updated = true;
@@ -388,11 +388,11 @@ public abstract class Mutation implements Statement
public static class Update extends Mutation
{
public final Optional<Using> using;
- public final LinkedHashMap<Symbol, Expression> set;
+ public final LinkedHashMap<ReferenceExpression, Expression> set;
public final Conditional where;
public final Optional<? extends CasCondition> casCondition;
- public Update(TableReference table, Optional<Using> using,
LinkedHashMap<Symbol, Expression> set, Conditional where, Optional<? extends
CasCondition> casCondition)
+ public Update(TableReference table, Optional<Using> using,
LinkedHashMap<ReferenceExpression, Expression> set, Conditional where,
Optional<? extends CasCondition> casCondition)
{
super(Mutation.Kind.UPDATE, table);
this.using = using;
@@ -477,10 +477,10 @@ public abstract class Mutation implements Statement
var u = v.visit(this);
if (u != this) return u;
boolean updated = false;
- LinkedHashMap<Symbol, Expression> copied = new
LinkedHashMap<>(set.size());
+ LinkedHashMap<ReferenceExpression, Expression> copied = new
LinkedHashMap<>(set.size());
for (var e : set.entrySet())
{
- Symbol s = e.getKey().visit("UPDATE", v);
+ ReferenceExpression s = e.getKey().visit(v);
if (s != e.getKey())
updated = true;
Expression ex = e.getValue().visit(v);
@@ -736,7 +736,7 @@ WHERE PK_column_conditions
public static class InsertBuilder extends TableBasedBuilder<Insert,
InsertBuilder>
{
- private final LinkedHashMap<Symbol, Expression> values = new
LinkedHashMap<>();
+ private final LinkedHashMap<ReferenceExpression, Expression> values =
new LinkedHashMap<>();
private boolean ifNotExists = false;
private @Nullable TTL ttl;
private @Nullable Timestamp timestamp;
@@ -775,7 +775,7 @@ WHERE PK_column_conditions
}
@Override
- public InsertBuilder value(Symbol ref, Expression e)
+ public InsertBuilder value(ReferenceExpression ref, Expression e)
{
maybePkEq(ref);
values.put(ref, e);
@@ -816,7 +816,7 @@ WHERE PK_column_conditions
Conditional.EqBuilder<B>,
WithTTl<B>, WithTimestamp<B>
{
- B set(Symbol column, Expression value);
+ B set(ReferenceExpression column, Expression value);
default B set(String column, Object value, AbstractType<?> type)
{
return set(new Symbol(column, type), new Literal(value, type));
@@ -837,7 +837,7 @@ WHERE PK_column_conditions
private TableReference table;
private @Nullable TTL ttl;
private @Nullable Timestamp timestamp;
- private final LinkedHashMap<Symbol, Expression> set = new
LinkedHashMap<>();
+ private final LinkedHashMap<ReferenceExpression, Expression> set = new
LinkedHashMap<>();
private final Conditional.Builder where = new Conditional.Builder();
private @Nullable CasCondition casCondition;
@@ -868,7 +868,7 @@ WHERE PK_column_conditions
}
@Override
- public B set(Symbol column, Expression value)
+ public B set(ReferenceExpression column, Expression value)
{
set.put(column, value);
return (B) this;
@@ -967,9 +967,9 @@ WHERE PK_column_conditions
}
@Override
- public TableBasedUpdateBuilder set(Symbol column, Expression value)
+ public TableBasedUpdateBuilder set(ReferenceExpression column,
Expression value)
{
- if (!metadata.regularAndStaticColumns.contains(column))
+ if (!metadata.regularAndStaticColumns.contains(column.column()))
throw new IllegalArgumentException("Attempted to set a non
regular or static column " + column + "; expected " +
metadata.regularAndStaticColumns);
return super.set(column, value);
}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Reference.java
b/test/unit/org/apache/cassandra/cql3/ast/Reference.java
index ee3b73d6fd..905a9b94ce 100644
--- a/test/unit/org/apache/cassandra/cql3/ast/Reference.java
+++ b/test/unit/org/apache/cassandra/cql3/ast/Reference.java
@@ -30,6 +30,9 @@ import com.google.common.collect.ImmutableList;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.BytesType;
+/**
+ * Represents {@code foo.bar.baz} syntax. Mainly used for UDT field access
({@code col_udt.foo}) and Accord LET access ({@code row1.foo}).
+ */
public class Reference implements ReferenceExpression
{
public final List<ReferenceExpression> path;
@@ -136,6 +139,12 @@ public class Reference implements ReferenceExpression
return toCQL();
}
+ @Override
+ public Symbol column()
+ {
+ return path.get(0).column();
+ }
+
public static class Builder
{
private final List<ReferenceExpression> path;
diff --git a/test/unit/org/apache/cassandra/cql3/ast/ReferenceExpression.java
b/test/unit/org/apache/cassandra/cql3/ast/ReferenceExpression.java
index d1727fccdb..5e4c471540 100644
--- a/test/unit/org/apache/cassandra/cql3/ast/ReferenceExpression.java
+++ b/test/unit/org/apache/cassandra/cql3/ast/ReferenceExpression.java
@@ -28,4 +28,13 @@ public interface ReferenceExpression extends Expression
{
return v.visit(this);
}
+
+ default Symbol asSymbol()
+ {
+ if (!(this instanceof Symbol))
+ throw new IllegalStateException("Unable to convert type " +
getClass() + " to Symbol");
+ return (Symbol) this;
+ }
+
+ Symbol column();
}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Symbol.java
b/test/unit/org/apache/cassandra/cql3/ast/Symbol.java
index e492293691..431ce6878a 100644
--- a/test/unit/org/apache/cassandra/cql3/ast/Symbol.java
+++ b/test/unit/org/apache/cassandra/cql3/ast/Symbol.java
@@ -151,6 +151,12 @@ public class Symbol implements ReferenceExpression,
Comparable<Symbol>
return toCQL().compareTo(o.toCQL());
}
+ @Override
+ public Symbol column()
+ {
+ return this;
+ }
+
public static class UnquotedSymbol extends Symbol
{
public UnquotedSymbol(String symbol, AbstractType<?> type)
@@ -158,6 +164,11 @@ public class Symbol implements ReferenceExpression,
Comparable<Symbol>
super(symbol, type);
}
+ public static UnquotedSymbol unknownType(String name)
+ {
+ return new UnquotedSymbol(name, BytesType.instance);
+ }
+
@Override
public void toCQL(StringBuilder sb, CQLFormatter formatter)
{
diff --git
a/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java
b/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java
index 5af7255516..ed9c837bd9 100644
---
a/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java
+++
b/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java
@@ -2107,4 +2107,20 @@ public class CollectionsTest extends CQLTester
assertRows(execute("SELECT c['t3'..] FROM %s"), row(set("t3", "t4")));
assertRows(execute("SELECT c[..'t5'] FROM %s"), row(set("t1", "t2",
"t3", "t4")));
}
+
+ /**
+ * {@link org.apache.cassandra.db.rows.Row.Builder} is cached, and there
was logic to reuse the same builder cross
+ * rows, even if the builder was returned to the pool! This is "fine" in
the happy path, but if the second row fails
+ * for any reason, then the builder is in a partial state and also
referenced by the pool!
+ */
+ @Test
+ public void CASSANDRA_21055()
+ {
+ createTable("CREATE TABLE %s (pk int, ck int, l list<int>, PRIMARY KEY
(pk, ck))");
+
+ execute("INSERT INTO %s (pk, ck, l) VALUES (1, 1, [10, 20, 30])");
+ assertInvalidMessage("Attempted to set an element on a list which is
null",
+ "UPDATE %s SET l[0] = 0 WHERE pk=1 AND ck IN (1,
2)");
+ execute("INSERT INTO %s (pk, ck, l) VALUES (1, 2, [40, 50, 60])");
+ }
}
diff --git a/test/unit/org/apache/cassandra/utils/ASTGenerators.java
b/test/unit/org/apache/cassandra/utils/ASTGenerators.java
index 9cd39abba7..d433cc73fa 100644
--- a/test/unit/org/apache/cassandra/utils/ASTGenerators.java
+++ b/test/unit/org/apache/cassandra/utils/ASTGenerators.java
@@ -50,6 +50,7 @@ import org.apache.cassandra.cql3.KnownIssue;
import org.apache.cassandra.cql3.ast.AssignmentOperator;
import org.apache.cassandra.cql3.ast.Bind;
import org.apache.cassandra.cql3.ast.CasCondition;
+import org.apache.cassandra.cql3.ast.CollectionAccess;
import org.apache.cassandra.cql3.ast.Conditional;
import org.apache.cassandra.cql3.ast.CreateIndexDDL;
import org.apache.cassandra.cql3.ast.Expression;
@@ -64,6 +65,7 @@ import org.apache.cassandra.cql3.ast.Txn;
import org.apache.cassandra.cql3.ast.TypeHint;
import org.apache.cassandra.cql3.ast.Value;
import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.IntegerType;
import org.apache.cassandra.db.marshal.ListType;
@@ -392,6 +394,11 @@ public class ASTGenerators
}
}
+ public interface ValueGen
+ {
+ Value generate(RandomnessSource rnd, Object value, AbstractType<?>
type);
+ }
+
public static class MutationGenBuilder
{
public enum DeleteKind { Partition, Row, Column }
@@ -414,6 +421,9 @@ public class ASTGenerators
private boolean allowUpdateMultiplePartitionKeys = true;
private boolean allowUpdateMultipleClusteringKeys = true;
private EnumSet<KnownIssue> ignoreIssues = IGNORED_ISSUES;
+ private EnumSet<CollectionType.Kind>
allowedCollectionElementAccessForUpdateSet =
EnumSet.of(CollectionType.Kind.LIST, CollectionType.Kind.MAP);
+ private Gen<Boolean> collectionElementAccessForUpdateSet =
SourceDSL.booleans().all();
+ private ValueGen valueGen = (rnd, obj, type) ->
SourceDSL.booleans().all().generate(rnd) ? new Bind(obj, type) : new
Literal(obj, type);
public MutationGenBuilder(TableMetadata metadata)
{
@@ -432,6 +442,18 @@ public class ASTGenerators
columnExpressions.put(symbol, new
ExpressionBuilder(symbol.type()));
}
+ public MutationGenBuilder disallowListElementAccessForUpdateSet()
+ {
+ return withListElementAccessForUpdateSet(false);
+ }
+
+ public MutationGenBuilder withListElementAccessForUpdateSet(boolean
allow)
+ {
+ if (allow)
allowedCollectionElementAccessForUpdateSet.add(CollectionType.Kind.LIST);
+ else
allowedCollectionElementAccessForUpdateSet.remove(CollectionType.Kind.LIST);
+ return this;
+ }
+
public MutationGenBuilder withIgnoreIssues(EnumSet<KnownIssue>
ignoreIssues)
{
this.ignoreIssues = Objects.requireNonNull(ignoreIssues);
@@ -986,6 +1008,33 @@ public class ASTGenerators
{
for (Symbol c : new ArrayList<>(columnsToGenerate))
{
+ if (c.type().isMultiCell()
+ && c.type().isCollection()
+ &&
!allowedCollectionElementAccessForUpdateSet.isEmpty())
+ {
+ CollectionType<?> ct = (CollectionType<?>) c.type();
+ if
(allowedCollectionElementAccessForUpdateSet.contains(ct.kind)
+ &&
collectionElementAccessForUpdateSet.generate(rnd))
+ {
+ //TODO (coverage): nothing stops the following:
c[0] = 42, c[1] = 72, just not impl in generator yet
+ Value key;
+ switch (ct.kind)
+ {
+ case LIST:
+ key = valueGen.generate(rnd, (int)
rnd.next(Constraint.between(0, 10)), Int32Type.instance);
+ break;
+ case MAP:
+ key = valueGen.generate(rnd,
getTypeSupport(ct.nameComparator()).bytesGen().generate(rnd),
ct.nameComparator());
+ break;
+ default:
+ throw new
UnsupportedOperationException(ct.kind.name());
+ }
+ CollectionAccess ref = new CollectionAccess(c,
key, ct.valueComparator());
+ builder.value(ref,
getTypeSupport(ct.valueComparator()).bytesGen().generate(rnd));
+ columnsToGenerate.remove(c);
+ continue;
+ }
+ }
var useOperator = columnExpressions.get(c).useOperator;
EnumSet<AssignmentOperator.Kind> additionOperatorAllowed =
AssignmentOperator.supportsOperators(c.type(), isTransaction);
if (!additionOperatorAllowed.isEmpty() &&
useOperator.generate(rnd))
@@ -1094,7 +1143,8 @@ public class ASTGenerators
}
MutationGenBuilder mutationBuilder = new
MutationGenBuilder(metadata)
.withTxnSafe()
-
.disallowUpdateMultiplePartitionKeys()
+
.disallowUpdateMultiplePartitionKeys() //TODO (coverage): this is something
Accord should support, so should remove and make sure accord is updated
+
.disallowListElementAccessForUpdateSet() //TODO (coverage): CASSANDRA-20828
found an issue with multi cell list type timestamp handling, so make sure
accord doesn't hit this
.withReferences(new
ArrayList<>(builder.allowedReferences()));
if (!allowReferences)
mutationBuilder.withReferences(Collections.emptyList());
@@ -1223,7 +1273,8 @@ public class ASTGenerators
{
MutationGenBuilder builder = mutationBuilder(IGNORED_ISSUES, rs,
model, List.of(pk), indexes)
.withTxnSafe()
-
.disallowUpdateMultiplePartitionKeys();
+
.disallowUpdateMultiplePartitionKeys() //TODO (coverage): this is something
Accord should support, so should remove and make sure accord is updated
+
.disallowListElementAccessForUpdateSet(); //TODO (coverage): CASSANDRA-20828
found an issue with multi cell list type timestamp handling, so make sure
accord doesn't hit this
if (!allowEmpty)
builder.disallowEmpty();
return builder.build();
diff --git a/test/unit/org/apache/cassandra/utils/AssertionUtils.java
b/test/unit/org/apache/cassandra/utils/AssertionUtils.java
index 6a0a1f5fc6..d44fe7f84e 100644
--- a/test/unit/org/apache/cassandra/utils/AssertionUtils.java
+++ b/test/unit/org/apache/cassandra/utils/AssertionUtils.java
@@ -38,6 +38,7 @@ public class AssertionUtils
return Assertions.anyOf(it);
}
+ @SafeVarargs
public static Condition<Throwable> anyOfThrowable(Class<? extends
Throwable>... klasses)
{
return anyOf(Stream.of(klasses).map(AssertionUtils::isThrowable));
@@ -147,6 +148,7 @@ public class AssertionUtils
return hasCause(isThrowable(klass));
}
+ @SafeVarargs
public static Condition<Throwable> hasCauseAnyOf(Class<? extends
Throwable>... matchers)
{
return hasCause(anyOfThrowable(matchers));
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]