This is an automated email from the ASF dual-hosted git repository. dcapwell pushed a commit to branch cassandra-5.0 in repository https://gitbox.apache.org/repos/asf/cassandra.git
commit 8125a694800df01f1d1117626981d48dbe52eb6d Merge: 3cdd540bfe 4da92af589 Author: David Capwell <[email protected]> AuthorDate: Tue Jan 7 17:45:06 2025 -0800 Merge branch 'cassandra-4.1' into cassandra-5.0 CHANGES.txt | 1 + .../org/apache/cassandra/db/filter/RowFilter.java | 32 ++++++++++++---------- .../cql3/validation/operations/SelectTest.java | 30 ++++++++++++++++++++ .../cassandra/index/sai/plan/OperationTest.java | 2 +- .../cassandra/index/sasi/plan/OperationTest.java | 2 +- 5 files changed, 50 insertions(+), 17 deletions(-) diff --cc CHANGES.txt index b76e5bed8a,3e5597558d..8eb1fba300 --- a/CHANGES.txt +++ b/CHANGES.txt @@@ -1,16 -1,10 +1,17 @@@ -4.1.8 +5.0.3 + * Avoid memory allocation in offheap_object's NativeCell.valueSize() and NativeClustering.dataSize() (CASSANDRA-20162) + * Add flag to avoid invalidating key cache on sstable deletions (CASSANDRA-20068) + * Interpret inet, bigint, varint, and decimal as non-reversed types for query construction and post-filtering (CASSANDRA-20100) + * Fix delayed gossip shutdown messages clobbering startup states that leave restarted nodes appearing down (CASSANDRA-20033) + * Streamline the serialized format for index status gossip messages (CASSANDRA-20058) + * Batch clusterings into single SAI partition post-filtering reads (CASSANDRA-19497) + * Ban the usage of "var" instead of full types in the production code (CASSANDRA-20038) + * Suppress CVE-2024-45772 from lucene-core-9.7.0.jar (CASSANDRA-20024) +Merged from 4.1: * Add nodetool checktokenmetadata command that checks TokenMetadata is insync with Gossip endpointState (CASSANDRA-18758) - * Backport Java 11 support for Simulator (CASSANDRA-17178/CASSANDRA-19935) * Equality check for Paxos.Electorate should not depend on collection types (CASSANDRA-19935) - * Fix race condition in DecayingEstimatedHistogramReservoir during rescale (CASSANDRA-19365) Merged from 4.0: + * IndexOutOfBoundsException when accessing partition where the column was deleted (CASSANDRA-20108) * Enhance CQLSSTableWriter to notify clients on sstable production (CASSANDRA-19800) * Fix gossip issue with gossip-only and bootstrapping nodes missing DC/Rack/Host ID endpoint state (CASSANDRA-19983) * Change the resolution of AbstractCommitLogService#lastSyncedAt to nanos to be aligned with later comparisons (CASSANDRA-20074) diff --cc src/java/org/apache/cassandra/db/filter/RowFilter.java index beb3edfcd7,4730656263..0742f4ee9f --- a/src/java/org/apache/cassandra/db/filter/RowFilter.java +++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java @@@ -191,74 -130,7 +191,74 @@@ public class RowFilter implements Itera return false; } - protected abstract Transformation<BaseRowIterator<?>> filter(TableMetadata metadata, int nowInSec); + /** + * Note that the application of this transformation does not yet take {@link #isStrict()} into account. This means + * that even when strict filtering is not safe, expressions will be applied as intersections rather than unions. + * The filter will always be evaluated strictly in conjunction with replica filtering protection at the + * coordinator, however, even after CASSANDRA-19007 is addressed. + * + * @see <a href="https://issues.apache.org/jira/browse/CASSANDRA-190007">CASSANDRA-19007</a> + */ + protected Transformation<BaseRowIterator<?>> filter(TableMetadata metadata, long nowInSec) + { + List<Expression> partitionLevelExpressions = new ArrayList<>(); + List<Expression> rowLevelExpressions = new ArrayList<>(); + for (Expression e: expressions) + { + if (e.column.isStatic() || e.column.isPartitionKey()) + partitionLevelExpressions.add(e); + else + rowLevelExpressions.add(e); + } + + long numberOfRegularColumnExpressions = rowLevelExpressions.size(); + final boolean filterNonStaticColumns = numberOfRegularColumnExpressions > 0; + + return new Transformation<>() + { + DecoratedKey pk; + + @Override + protected BaseRowIterator<?> applyToPartition(BaseRowIterator<?> partition) + { + pk = partition.partitionKey(); + + // Short-circuit all partitions that won't match based on static and partition keys + for (Expression e : partitionLevelExpressions) - if (!e.isSatisfiedBy(metadata, partition.partitionKey(), partition.staticRow())) ++ if (!e.isSatisfiedBy(metadata, partition.partitionKey(), partition.staticRow(), nowInSec)) + { + partition.close(); + return null; + } + + BaseRowIterator<?> iterator = partition instanceof UnfilteredRowIterator + ? Transformation.apply((UnfilteredRowIterator) partition, this) + : Transformation.apply((RowIterator) partition, this); + + if (filterNonStaticColumns && !iterator.hasNext()) + { + iterator.close(); + return null; + } + + return iterator; + } + + @Override + public Row applyToRow(Row row) + { + Row purged = row.purge(DeletionPurger.PURGE_ALL, nowInSec, metadata.enforceStrictLiveness()); + if (purged == null) + return null; + + for (Expression e : rowLevelExpressions) - if (!e.isSatisfiedBy(metadata, pk, purged)) ++ if (!e.isSatisfiedBy(metadata, pk, purged, nowInSec)) + return null; + + return row; + } + }; + } /** * Filters the provided iterator so that only the row satisfying the expression of this filter @@@ -529,9 -470,9 +529,9 @@@ * (i.e. it should come from a RowIterator). * @return whether the row is satisfied by this expression. */ - public abstract boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row); - public abstract boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, int nowInSec); ++ public abstract boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec); - protected ByteBuffer getValue(TableMetadata metadata, DecoratedKey partitionKey, Row row) - protected ByteBuffer getValue(TableMetadata metadata, DecoratedKey partitionKey, Row row, int nowInSec) ++ protected ByteBuffer getValue(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { switch (column.kind) { @@@ -704,7 -645,7 +704,7 @@@ super(column, operator, value); } - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row) - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, int nowInSec) ++ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { // We support null conditions for LWT (in ColumnCondition) but not for RowFilter. // TODO: we should try to merge both code someday. @@@ -744,10 -685,9 +744,10 @@@ case LIKE_SUFFIX: case LIKE_CONTAINS: case LIKE_MATCHES: + case ANN: { - assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for 'complex' types"; + assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for collection types"; - ByteBuffer foundValue = getValue(metadata, partitionKey, row); + ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); // Note that CQL expression are always of the form 'x < 4', i.e. the tested value is on the left. return foundValue != null && operator.isSatisfiedBy(column.type, foundValue, value); } @@@ -874,7 -814,8 +874,8 @@@ return CompositeType.build(ByteBufferAccessor.instance, key, value); } - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row) + @Override - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, int nowInSec) ++ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { assert key != null; // We support null conditions for LWT (in ColumnCondition) but not for RowFilter. @@@ -993,7 -934,8 +994,8 @@@ } // Filtering by custom expressions isn't supported yet, so just accept any row - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row) + @Override - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, int nowInSec) ++ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { return true; } diff --cc test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java index 639f6a5145,cdedec01a8..4c7a7c1c2a --- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java @@@ -2326,6 -2302,36 +2326,36 @@@ public class SelectTest extends CQLTest }); } + @Test + public void filteringOnDeletedStaticColumnValue() throws Throwable + { + // Create table with int-only columns + createTable("CREATE TABLE %s (pk0 int, pk1 int, ck0 int, ck1 int, s0 tinyint static, v0 int, v1 int, PRIMARY KEY ((pk0, pk1), ck0, ck1))"); + + // Insert rows + execute("INSERT INTO %s (pk0, pk1, s0, ck0, ck1, v0, v1) VALUES (?, ?, ?, ?, ?, ?, ?)", 1000, 2000, (byte) 126, 100, 1, 20, 30); + execute("INSERT INTO %s (pk0, pk1, s0, ck0, ck1, v0, v1) VALUES (?, ?, ?, ?, ?, ?, ?)", 1000, 2000, (byte) 125, 200, 2, 40, 50); + execute("INSERT INTO %s (pk0, pk1, s0, ck0, ck1, v0, v1) VALUES (?, ?, ?, ?, ?, ?, ?)", 1000, 3000, (byte) 122, 300, 3, 60, 70); + execute("DELETE s0,v0,v1 FROM %s WHERE pk0=1000 AND pk1=2000 and ck0=100 and ck1=1"); + + beforeAndAfterFlush(() -> { + // Verify the columns are deleted + assertRows(execute("SELECT pk0, pk1, s0, ck0, ck1, v0, v1 FROM %s WHERE s0=? ALLOW FILTERING", (byte) 122), - row(1000, 3000, (byte) 122, 300, 3, 60, 70)); ++ row(1000, 3000, (byte) 122, 300, 3, 60, 70)); + }); + + execute("DELETE v0 FROM %s WHERE pk0=1000 AND pk1=3000 AND ck0=300 AND ck1=3"); + + beforeAndAfterFlush(() -> { + assertRows(execute("SELECT pk0, pk1, s0, ck0, ck1, v0, v1 FROM %s WHERE s0=? ALLOW FILTERING", (byte) 122), - row(1000, 3000, (byte) 122, 300, 3, null, 70)); ++ row(1000, 3000, (byte) 122, 300, 3, null, 70)); + + assertRows(execute("SELECT pk0, pk1, s0, ck0, ck1, v0, v1 FROM %s WHERE pk0=1000 AND pk1=3000 AND ck0=300 AND ck1=3"), - row(1000, 3000, (byte) 122, 300, 3, null, 70)); ++ row(1000, 3000, (byte) 122, 300, 3, null, 70)); + }); + + } + @Test public void containsFilteringOnNonClusteringColumn() throws Throwable { createTable("CREATE TABLE %s (a int, b int, c int, d list<int>, PRIMARY KEY (a, b, c))"); diff --cc test/unit/org/apache/cassandra/index/sai/plan/OperationTest.java index 8b0acaaf2b,0000000000..81292cbda0 mode 100644,000000..100644 --- a/test/unit/org/apache/cassandra/index/sai/plan/OperationTest.java +++ b/test/unit/org/apache/cassandra/index/sai/plan/OperationTest.java @@@ -1,549 -1,0 +1,549 @@@ +/* + * 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.index.sai.plan; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.DoubleType; +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.db.marshal.UTF8Type; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.BufferCell; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.Indexes; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; +import static org.apache.cassandra.db.marshal.Int32Type.instance; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class OperationTest +{ + private static final String KS_NAME = "sai"; + private static final String CF_NAME = "test_cf"; + private static final String CLUSTERING_CF_NAME = "clustering_test_cf"; + private static final String STATIC_CF_NAME = "static_sai_test_cf"; + + private static ColumnFamilyStore BACKEND; + private static ColumnFamilyStore CLUSTERING_BACKEND; + private static ColumnFamilyStore STATIC_BACKEND; + + private QueryController controller; + private QueryController controllerClustering; + private QueryController controllerStatic; + + @BeforeClass + public static void loadSchema() throws ConfigurationException + { + CASSANDRA_CONFIG.setString("cassandra-murmur.yaml"); + + SchemaLoader.loadSchema(); + + SchemaLoader.createKeyspace(KS_NAME, + KeyspaceParams.simpleTransient(1), + skinnySAITableMetadata(KS_NAME, CF_NAME), + clusteringSAITableMetadata(KS_NAME, CLUSTERING_CF_NAME), + staticSAITableMetadata(KS_NAME, STATIC_CF_NAME)); + + BACKEND = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME); + CLUSTERING_BACKEND = Keyspace.open(KS_NAME).getColumnFamilyStore(CLUSTERING_CF_NAME); + STATIC_BACKEND = Keyspace.open(KS_NAME).getColumnFamilyStore(STATIC_CF_NAME); + } + + @Before + public void beforeTest() + { + ReadCommand command = PartitionRangeReadCommand.allDataRead(BACKEND.metadata(), FBUtilities.nowInSeconds()); + controller = new QueryController(BACKEND, command, null, contextWithUnrepairedMatches(command)); + + command = PartitionRangeReadCommand.allDataRead(CLUSTERING_BACKEND.metadata(), FBUtilities.nowInSeconds()); + controllerClustering = new QueryController(CLUSTERING_BACKEND, command, null, contextWithUnrepairedMatches(command)); + + command = PartitionRangeReadCommand.allDataRead(STATIC_BACKEND.metadata(), FBUtilities.nowInSeconds()); + controllerStatic = new QueryController(STATIC_BACKEND, command, null, contextWithUnrepairedMatches(command)); + } + + private static QueryContext contextWithUnrepairedMatches(ReadCommand command) + { + QueryContext context = new QueryContext(command, DatabaseDescriptor.getRangeRpcTimeout(TimeUnit.MILLISECONDS)); + context.hasUnrepairedMatches = true; + return context; + } + + @Test + public void testAnalyze() + { + final ColumnMetadata age = getColumn(UTF8Type.instance.decompose("age")); + + // age > 1 AND age < 7 + Map<Expression.IndexOperator, Expression> expressions = convert(Operation.buildIndexExpressions(controller, + Arrays.asList(new SimpleExpression(age, Operator.GT, Int32Type.instance.decompose(1)), + new SimpleExpression(age, Operator.LT, Int32Type.instance.decompose(7))))); + + assertEquals(1, expressions.size()); + + Expression rangeExpression = expressions.get(Expression.IndexOperator.RANGE); + + assertExpression(rangeExpression, Expression.IndexOperator.RANGE, Int32Type.instance.decompose(1), false, Int32Type.instance.decompose(7), false); + } + + @Test + public void testSatisfiedBy() + { + final ColumnMetadata timestamp = getColumn(UTF8Type.instance.decompose("timestamp")); + final ColumnMetadata age = getColumn(UTF8Type.instance.decompose("age")); + + Operation.Node node = new Operation.ExpressionNode(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(5))); + FilterTree filterTree = node.buildFilter(controller, true); + + DecoratedKey key = buildKey("0"); + Unfiltered row = buildRow(buildCell(age, instance.decompose(6), System.currentTimeMillis())); + Row staticRow = buildRow(Clustering.STATIC_CLUSTERING); + + assertFalse(filterTree.isSatisfiedBy(key, (Row) row, staticRow)); + + row = buildRow(buildCell(age, instance.decompose(5), System.currentTimeMillis())); + + assertTrue(filterTree.isSatisfiedBy(key, (Row) row, staticRow)); + + row = buildRow(buildCell(age, instance.decompose(6), System.currentTimeMillis())); + + assertFalse(filterTree.isSatisfiedBy(key, (Row) row, staticRow)); + + // range with exclusions - age > 1 AND age <= 10 + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.GT, Int32Type.instance.decompose(1)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.LTE, Int32Type.instance.decompose(10)))); + filterTree = node.buildFilter(controller, true); + + Set<Integer> exclusions = Sets.newHashSet(0, 1, 11); + for (int i = 0; i <= 11; i++) + { + row = buildRow(buildCell(age, instance.decompose(i), System.currentTimeMillis())); + + boolean result = filterTree.isSatisfiedBy(key, (Row) row, staticRow); + assertTrue(exclusions.contains(i) != result); + } + + // now let's test aggregated AND commands + node = new Operation.AndNode(); + + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.GTE, Int32Type.instance.decompose(0)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.LT, Int32Type.instance.decompose(10)))); + + filterTree = node.buildFilter(controller, true); + + for (int i = 0; i < 10; i++) + { + row = buildRow(buildCell(age, instance.decompose(i), System.currentTimeMillis())); + + boolean result = filterTree.isSatisfiedBy(key, (Row) row, staticRow); + assertTrue(result); + } + + // multiple analyzed expressions in the Operation timestamp >= 10 AND age = 5 + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(timestamp, Operator.GTE, LongType.instance.decompose(10L)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(5)))); + + FilterTree filterTreeStrict = node.buildFilter(controller, true); + FilterTree filterTreeNonStrict = node.buildFilter(controller, false); + + long startTime = System.currentTimeMillis(); + row = buildRow(buildCell(age, instance.decompose(6), startTime), + buildCell(timestamp, LongType.instance.decompose(11L), startTime + 1)); + + assertFalse(filterTreeStrict.isSatisfiedBy(key, (Row) row, staticRow)); + assertTrue(filterTreeNonStrict.isSatisfiedBy(key, (Row) row, staticRow)); // matches on timestamp >= 10 + + row = buildRow(buildCell(age, instance.decompose(5), startTime + 2), + buildCell(timestamp, LongType.instance.decompose(22L), startTime + 3)); + + assertTrue(filterTreeStrict.isSatisfiedBy(key, (Row) row, staticRow)); + assertTrue(filterTreeNonStrict.isSatisfiedBy(key, (Row) row, staticRow)); + + row = buildRow(buildCell(age, instance.decompose(5), startTime + 4), + buildCell(timestamp, LongType.instance.decompose(9L), startTime + 5)); + + assertFalse(filterTreeStrict.isSatisfiedBy(key, (Row) row, staticRow)); + assertTrue(filterTreeNonStrict.isSatisfiedBy(key, (Row) row, staticRow)); // matches on age = 5 + } + + @Test + public void testAnalyzeNotIndexedButDefinedColumn() + { + final ColumnMetadata firstName = getColumn(UTF8Type.instance.decompose("first_name")); + final ColumnMetadata height = getColumn(UTF8Type.instance.decompose("height")); + + // first_name = 'a' AND height > 5 + Map<Expression.IndexOperator, Expression> expressions; + expressions = convert(Operation.buildIndexExpressions(controller, + Arrays.asList(new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("a")), + new SimpleExpression(height, Operator.GT, Int32Type.instance.decompose(5))))); + + assertEquals(2, expressions.size()); + + expressions = convert(Operation.buildIndexExpressions(controller, + Arrays.asList(new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("a")), + new SimpleExpression(height, Operator.GT, Int32Type.instance.decompose(0)), + new SimpleExpression(height, Operator.EQ, Int32Type.instance.decompose(5))))); + + assertEquals(2, expressions.size()); + + Expression rangeExpression = expressions.get(Expression.IndexOperator.RANGE); + + assertExpression(rangeExpression, Expression.IndexOperator.RANGE, Int32Type.instance.decompose(0), false, Int32Type.instance.decompose(5), true); + + expressions = convert(Operation.buildIndexExpressions(controller, + Arrays.asList(new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("a")), + new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(0)), + new SimpleExpression(height, Operator.LT, Int32Type.instance.decompose(10))))); + + assertEquals(2, expressions.size()); + + rangeExpression = expressions.get(Expression.IndexOperator.RANGE); + + assertExpression(rangeExpression, Expression.IndexOperator.RANGE, Int32Type.instance.decompose(0), true, Int32Type.instance.decompose(10), false); + } + + @Test + public void testSatisfiedByWithClustering() + { + ColumnMetadata location = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("location")); + ColumnMetadata age = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("age")); + ColumnMetadata height = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("height")); + ColumnMetadata score = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("score")); + + DecoratedKey key = buildKey(CLUSTERING_BACKEND, "0"); + Row row = buildRow(Clustering.make(UTF8Type.instance.fromString("US"), Int32Type.instance.decompose(27)), + buildCell(height, instance.decompose(182), System.currentTimeMillis()), + buildCell(score, DoubleType.instance.decompose(1.0d), System.currentTimeMillis())); + Row staticRow = buildRow(Clustering.STATIC_CLUSTERING); + + Operation.Node node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(27)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(height, Operator.EQ, Int32Type.instance.decompose(182)))); + + assertTrue(node.buildFilter(controllerClustering, true).isSatisfiedBy(key, row, staticRow)); + + node = new Operation.AndNode(); + + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(28)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(height, Operator.EQ, Int32Type.instance.decompose(182)))); + + assertFalse(node.buildFilter(controllerClustering, true).isSatisfiedBy(key, row, staticRow)); + + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("US")))); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.GTE, Int32Type.instance.decompose(27)))); + + assertTrue(node.buildFilter(controllerClustering, true).isSatisfiedBy(key, row, staticRow)); + + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("BY")))); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.GTE, Int32Type.instance.decompose(28)))); + + assertFalse(node.buildFilter(controllerClustering, true).isSatisfiedBy(key, row, staticRow)); + + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("US")))); + node.add(new Operation.ExpressionNode(new SimpleExpression(age, Operator.LTE, Int32Type.instance.decompose(27)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(182)))); + + assertTrue(node.buildFilter(controllerClustering, true).isSatisfiedBy(key, row, staticRow)); + + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("US")))); + node.add(new Operation.ExpressionNode(new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(182)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(score, Operator.EQ, DoubleType.instance.decompose(1.0d)))); + + assertTrue(node.buildFilter(controllerClustering, true).isSatisfiedBy(key, row, staticRow)); + + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(182)))); + node.add(new Operation.ExpressionNode(new SimpleExpression(score, Operator.EQ, DoubleType.instance.decompose(1.0d)))); + + assertTrue(node.buildFilter(controllerClustering, true).isSatisfiedBy(key, row, staticRow)); + } + + private Map<Expression.IndexOperator, Expression> convert(Multimap<ColumnMetadata, Expression> expressions) + { + Map<Expression.IndexOperator, Expression> converted = new EnumMap<>(Expression.IndexOperator.class); + for (Expression expression : expressions.values()) + { + Expression column = converted.get(expression.getIndexOperator()); + assert column == null; // sanity check + converted.put(expression.getIndexOperator(), expression); + } + + return converted; + } + + @Test + public void testSatisfiedByWithStatic() + { + final ColumnMetadata sensorType = getColumn(STATIC_BACKEND, UTF8Type.instance.decompose("sensor_type")); + final ColumnMetadata value = getColumn(STATIC_BACKEND, UTF8Type.instance.decompose("value")); + + DecoratedKey key = buildKey(STATIC_BACKEND, 0); + Row row = buildRow(Clustering.make(UTF8Type.instance.fromString("date"), LongType.instance.decompose(20160401L)), + buildCell(value, DoubleType.instance.decompose(24.56), System.currentTimeMillis())); + Row staticRow = buildRow(Clustering.STATIC_CLUSTERING, + buildCell(sensorType, UTF8Type.instance.decompose("TEMPERATURE"), System.currentTimeMillis())); + + // sensor_type ='TEMPERATURE' AND value = 24.56 + Operation.Node node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("TEMPERATURE")))); + node.add(new Operation.ExpressionNode(new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(24.56)))); + + assertTrue(node.buildFilter(controllerStatic, true).isSatisfiedBy(key, row, staticRow)); + + // sensor_type ='TEMPERATURE' AND value = 30 + node = new Operation.AndNode(); + node.add(new Operation.ExpressionNode(new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("TEMPERATURE")))); + node.add(new Operation.ExpressionNode(new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(30.00)))); + + assertFalse(node.buildFilter(controllerStatic, true).isSatisfiedBy(key, row, staticRow)); + } + + public static TableMetadata.Builder skinnySAITableMetadata(String keyspace, String table) + { + TableMetadata.Builder builder = + TableMetadata.builder(keyspace, table) + .addPartitionKeyColumn("id", UTF8Type.instance) + .addRegularColumn("first_name", UTF8Type.instance) + .addRegularColumn("last_name", UTF8Type.instance) + .addRegularColumn("age", Int32Type.instance) + .addRegularColumn("height", Int32Type.instance) + .addRegularColumn("timestamp", LongType.instance) + .addRegularColumn("address", UTF8Type.instance) + .addRegularColumn("score", DoubleType.instance); + + Indexes.Builder indexes = Indexes.builder(); + addIndex(indexes, table, "first_name"); + addIndex(indexes, table, "last_name"); + addIndex(indexes, table, "age"); + addIndex(indexes, table, "timestamp"); + addIndex(indexes, table, "address"); + addIndex(indexes, table, "score"); + + return builder.indexes(indexes.build()); + } + + public static TableMetadata.Builder clusteringSAITableMetadata(String keyspace, String table) + { + return clusteringSAITableMetadata(keyspace, table, "location", "age", "height", "score"); + } + + public static TableMetadata.Builder clusteringSAITableMetadata(String keyspace, String table, String...indexedColumns) + { + Indexes.Builder indexes = Indexes.builder(); + for (String indexedColumn : indexedColumns) + { + addIndex(indexes, table, indexedColumn); + } + + return TableMetadata.builder(keyspace, table) + .addPartitionKeyColumn("name", UTF8Type.instance) + .addClusteringColumn("location", UTF8Type.instance) + .addClusteringColumn("age", Int32Type.instance) + .addRegularColumn("height", Int32Type.instance) + .addRegularColumn("score", DoubleType.instance) + .indexes(indexes.build()); + } + + public static TableMetadata.Builder staticSAITableMetadata(String keyspace, String table) + { + TableMetadata.Builder builder = + TableMetadata.builder(keyspace, table) + .addPartitionKeyColumn("sensor_id", Int32Type.instance) + .addStaticColumn("sensor_type", UTF8Type.instance) + .addClusteringColumn("date", LongType.instance) + .addRegularColumn("value", DoubleType.instance) + .addRegularColumn("variance", Int32Type.instance); + + Indexes.Builder indexes = Indexes.builder(); + + addIndex(indexes, table, "sensor_type"); + addIndex(indexes, table, "value"); + addIndex(indexes, table, "variance"); + + return builder.indexes(indexes.build()); + } + + private void assertExpression(Expression expression, Expression.IndexOperator indexOperator, ByteBuffer lower, + boolean lowerInclusive, ByteBuffer upper, boolean upperInclusive) + { + assertEquals(indexOperator, expression.getIndexOperator()); + assertEquals(lower, expression.lower().value.raw); + assertEquals(lowerInclusive, expression.lower().inclusive); + assertEquals(upper, expression.upper().value.raw); + assertEquals(upperInclusive, expression.upper().inclusive); + } + + private static void addIndex(Indexes.Builder indexes, String table, String column) + { + String indexName = table + '_' + column; + indexes.add(IndexMetadata.fromSchemaMetadata(indexName, IndexMetadata.Kind.CUSTOM, new HashMap<String, String>() + {{ + put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, StorageAttachedIndex.class.getName()); + put(IndexTarget.TARGET_OPTION_NAME, column); + }})); + } + + private static DecoratedKey buildKey(Object... key) + { + return buildKey(BACKEND, key); + } + + private static DecoratedKey buildKey(ColumnFamilyStore cfs, Object... key) + { + AbstractType<?> type = cfs.metadata().partitionKeyType; + ByteBuffer decomposed; + if(type instanceof CompositeType) + { + Preconditions.checkArgument(key.length == type.subTypes().size()); + decomposed = ((CompositeType) type).decompose(key); + } + else + { + Preconditions.checkArgument(key.length == 1); + decomposed = ((AbstractType) type).decompose(key[0]); + } + return Murmur3Partitioner.instance.decorateKey(decomposed); + } + + private static Unfiltered buildRow(Cell<?>... cells) + { + return buildRow(Clustering.EMPTY, cells); + } + + private static Row buildRow(Clustering<?> clustering, Cell<?>... cells) + { + Row.Builder rowBuilder = BTreeRow.sortedBuilder(); + rowBuilder.newRow(clustering); + for (Cell<?> c : cells) + rowBuilder.addCell(c); + + return rowBuilder.build(); + } + + private static Cell<?> buildCell(ColumnMetadata column, ByteBuffer value, long timestamp) + { + return BufferCell.live(column, timestamp, value); + } + + private static ColumnMetadata getColumn(ByteBuffer name) + { + return getColumn(BACKEND, name); + } + + private static ColumnMetadata getColumn(ColumnFamilyStore cfs, ByteBuffer name) + { + return cfs.metadata().getColumn(name); + } + + private static class SimpleExpression extends RowFilter.Expression + { + SimpleExpression(ColumnMetadata column, Operator operator, ByteBuffer value) + { + super(column, operator, value); + } + + @Override + public Kind kind() + { + return Kind.SIMPLE; + } + + @Override - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row) ++ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) + { + throw new UnsupportedOperationException(); + } + + @Override + protected String toString(boolean cql) + { + AbstractType<?> type = column.type; + switch (operator) + { + case CONTAINS: + assert type instanceof CollectionType; + CollectionType<?> ct = (CollectionType<?>)type; + type = ct.kind == CollectionType.Kind.SET ? ct.nameComparator() : ct.valueComparator(); + break; + case CONTAINS_KEY: + assert type instanceof MapType; + type = ((MapType<?, ?>)type).nameComparator(); + break; + case IN: + type = ListType.getInstance(type, false); + break; + default: + break; + } + return cql + ? String.format("%s %s %s", column.name.toCQLString(), operator, type.toCQLString(value) ) + : String.format("%s %s %s", column.name.toString(), operator, type.getString(value)); + } + } +} diff --cc test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java index 0cef902be0,1d732aa59b..58a8243a4c --- a/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java +++ b/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java @@@ -650,7 -648,7 +650,7 @@@ public class OperationTest extends Sche } @Override - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row) - public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, int nowInSec) ++ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { throw new UnsupportedOperationException(); } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
