diff --git a/CHANGES.txt b/CHANGES.txt index 8c90013fcc..ec7d803a9f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Allow value/element indexing on frozen collections in SAI (CASSANDRA-18492) * Add tool to offline dump cluster metadata and the log (CASSANDRA-21129) * Send client warnings when writing to a large partition (CASSANDRA-17258) * Harden the possible range of values for max dictionary size and max total sample size for dictionary training (CASSANDRA-21194) diff --git a/src/java/org/apache/cassandra/cql3/Relation.java b/src/java/org/apache/cassandra/cql3/Relation.java index 804b6e79e8..5de031102c 100644 --- a/src/java/org/apache/cassandra/cql3/Relation.java +++ b/src/java/org/apache/cassandra/cql3/Relation.java @@ -44,6 +44,8 @@ import static org.apache.cassandra.cql3.statements.RequestValidations.invalidReq */ public final class Relation { + public static final String FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED = "Map-entry predicates on frozen map column %s are not supported"; + /** * The raw columns'expression. */ @@ -204,7 +206,10 @@ public final class Relation AbstractType baseType = column.type.unwrap(); checkFalse(baseType instanceof ListType, "Indexes on list entries (%s[index] = value) are not supported.", column.name); checkTrue(baseType instanceof MapType, "Column %s cannot be used as a map", column.name); - checkTrue(baseType.isMultiCell(), "Map-entry predicates on frozen map column %s are not supported", column.name); + + if (column.isClusteringColumn() && baseType.isCollection() && !column.type.isMultiCell()) + throw invalidRequest(FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, column.name); + columnsExpression.collectMarkerSpecification(boundNames); } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/MergedRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/MergedRestriction.java index bb352cb076..447a1ac6dc 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/MergedRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/MergedRestriction.java @@ -27,6 +27,7 @@ import com.google.common.collect.RangeSet; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.Relation; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; @@ -126,9 +127,21 @@ public final class MergedRestriction implements SingleRestriction checkOperator(other); if (restriction.isContains() != other.isContains()) + { + SimpleRestriction mapEntryRestriction = restriction.isContains() ? restriction : other; + if (mapEntryRestriction.isMapElementExpression()) + { + ColumnMetadata column = mapEntryRestriction.firstColumn(); + if (column.type.isFrozenCollection()) + { + throw invalidRequest(Relation.FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, column.name); + } + } + throw invalidRequest("Collection column %s can only be restricted by CONTAINS, CONTAINS KEY, NOT_CONTAINS, NOT_CONTAINS_KEY" + " or map-entry equality if it already restricted by one of those", restriction.firstColumn().name); + } if (restriction.isSlice() && other.isSlice()) { diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java index c5e05560a2..e8db9daa54 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java @@ -30,6 +30,7 @@ import com.google.common.collect.RangeSet; import org.apache.cassandra.cql3.ColumnsExpression; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.Relation; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.cql3.terms.Terms; @@ -157,6 +158,14 @@ public final class SimpleRestriction implements SingleRestriction || columnsExpression.isMapElementExpression(); } + /** + * Checks if this restriction is a map element expression (e.g., map['key'] = value). + */ + public boolean isMapElementExpression() + { + return columnsExpression.isMapElementExpression(); + } + @Override public boolean needsFilteringOrIndexing() { @@ -218,6 +227,18 @@ public final class SimpleRestriction implements SingleRestriction if (isOnToken()) return false; + // For map element expressions, check if the index explicitly supports them. + if (columnsExpression.isMapElementExpression()) + { + // If the index directly supports map element expressions, return true + if (index.supportsMapElementExpression()) + return true; + + // Supports post-filtering only and require ALLOW FILTERING + if (index.supportsFilteringOnMapElementExpression()) + return false; + } + for (ColumnMetadata column : columns()) { if (index.supportsExpression(column, operator)) @@ -415,13 +436,28 @@ public final class SimpleRestriction implements SingleRestriction // TODO only map elements supported for now if (columnsExpression.isMapElementExpression()) { + // For frozen maps, check if any index on the column can support map entry predicates + // either directly or via filtering. If not, throw an error. + if (column.type.isFrozenCollection()) + { + for (Index index : indexRegistry.listIndexes()) + { + if (index.dependsOn(column) + && !index.supportsMapElementExpression() + && !index.supportsFilteringOnMapElementExpression()) + { + throw invalidRequest(Relation.FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, column.name); + } + } + } + ByteBuffer key = columnsExpression.element(options); if (key == null) - throw invalidRequest("Invalid null map key for column %s", firstColumn().name.toCQLString()); + throw invalidRequest("Invalid null map key for column %s", column.name.toCQLString()); if (key == ByteBufferUtil.UNSET_BYTE_BUFFER) - throw invalidRequest("Invalid unset map key for column %s", firstColumn().name.toCQLString()); + throw invalidRequest("Invalid unset map key for column %s", column.name.toCQLString()); List values = bindAndGet(options); - filter.addMapEquality(firstColumn(), key, operator, values.get(0)); + filter.addMapEquality(column, key, operator, values.get(0)); } break; default: throw new UnsupportedOperationException(); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java index 332857a943..d3e08dfee5 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java @@ -81,10 +81,13 @@ public final class CreateIndexStatement extends AlterSchemaStatement public static final String ONLY_PARTITION_KEY = "Cannot create secondary index on the only partition key column %s"; public static final String CREATE_ON_FROZEN_COLUMN = "Cannot create %s() index on frozen column %s. Frozen collections are immutable and must be fully " + "indexed by using the 'full(%s)' modifier"; - public static final String FULL_ON_FROZEN_COLLECTIONS = "full() indexes can only be created on frozen collections"; + public static final String FULL_ON_FROZEN_COLLECTIONS = "full() non-SAI indexes can only be created on frozen collections"; public static final String NON_COLLECTION_SIMPLE_INDEX = "Cannot create %s() index on %s. Non-collection columns only support simple indexes"; public static final String CREATE_WITH_NON_MAP_TYPE = "Cannot create index on %s of column %s with non-map type"; public static final String CREATE_ON_NON_FROZEN_UDT = "Cannot create index on non-frozen UDT column %s"; + public static final String ENTRIES_INDEX_ON_FROZEN_MAP_CLUSTERING_KEY_NOT_SUPPORTED = "Cannot create ENTRIES index on frozen map clustering column '%s'. " + + "Map entry predicates (column[key] = value) are not supported on clustering columns. " + + "Use FULL, KEYS, or VALUES index instead."; public static final String INDEX_ALREADY_EXISTS = "Index '%s' already exists"; public static final String INDEX_DUPLICATE_OF_EXISTING = "Index %s is a duplicate of existing index %s"; public static final String KEYSPACE_DOES_NOT_MATCH_TABLE = "Keyspace name '%s' doesn't match table name '%s'"; @@ -200,7 +203,7 @@ public final class CreateIndexStatement extends AlterSchemaStatement IndexMetadata.Kind kind = attrs.isCustom ? IndexMetadata.Kind.CUSTOM : IndexMetadata.Kind.COMPOSITES; - indexTargets.forEach(t -> validateIndexTarget(table, kind, t)); + indexTargets.forEach(t -> validateIndexTarget(table, kind, t, attrs)); String name = null == indexName ? generateIndexName(keyspace, indexTargets) : indexName; @@ -244,7 +247,7 @@ public final class CreateIndexStatement extends AlterSchemaStatement throw ire(TOO_LONG_CUSTOM_INDEX_TARGET, name, SchemaConstants.NAME_LENGTH); } - private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, IndexTarget target) + private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, IndexTarget target, IndexAttributes attrs) { ColumnMetadata column = table.getColumn(target.column); @@ -253,6 +256,8 @@ public final class CreateIndexStatement extends AlterSchemaStatement AbstractType baseType = column.type.unwrap(); + boolean isNonSAIIndex = !isSAIIndex(attrs); + // TODO: this check needs to be removed with CASSANDRA-20235 if ((kind == IndexMetadata.Kind.CUSTOM)) validateCustomIndexColumnName(target.column.toString()); @@ -283,22 +288,41 @@ public final class CreateIndexStatement extends AlterSchemaStatement if (column.isPartitionKey() && table.partitionKeyColumns().size() == 1) throw ire(ONLY_PARTITION_KEY, column); - if (baseType.isFrozenCollection() && target.type != Type.FULL) - throw ire(CREATE_ON_FROZEN_COLUMN, target.type, column, column.name.toCQLString()); - - if (!baseType.isFrozenCollection() && target.type == Type.FULL) + if (target.type == Type.FULL && isNonSAIIndex && (!baseType.isCollection() || column.type.isMultiCell())) throw ire(FULL_ON_FROZEN_COLLECTIONS); if (!baseType.isCollection() && target.type != Type.SIMPLE) throw ire(NON_COLLECTION_SIMPLE_INDEX, target.type, column); - if (!(baseType instanceof MapType && baseType.isMultiCell()) && (target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES)) + // Frozen collections are only supported with SAI indexes. + if (isNonSAIIndex && baseType.isCollection() && !column.type.isMultiCell()) + { + if (target.type == Type.VALUES || target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES) + { + throw ire(CREATE_ON_FROZEN_COLUMN, target.type.toString(), column.name, column.name); + } + } + + if (!(baseType instanceof MapType) && (target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES )) throw ire(CREATE_WITH_NON_MAP_TYPE, target.type, column); + // Can't query map[key]=value on clustering key columns, so ENTRIES index would be not queryable. + if (column.isClusteringColumn() && baseType instanceof MapType && !column.type.isMultiCell() + && target.type == Type.KEYS_AND_VALUES) + throw ire(ENTRIES_INDEX_ON_FROZEN_MAP_CLUSTERING_KEY_NOT_SUPPORTED, column.name); + if (column.type.isUDT() && column.type.isMultiCell()) throw ire(CREATE_ON_NON_FROZEN_UDT, column); } + /** + * Checks if the given index attributes represent a Storage Attached Index. + */ + private boolean isSAIIndex(IndexAttributes attrs) + { + return attrs.isCustom && IndexMetadata.isSAIIndex(attrs.customClass); + } + private String generateIndexName(KeyspaceMetadata keyspace, List targets) { String baseName = targets.size() == 1 diff --git a/src/java/org/apache/cassandra/db/filter/RowFilter.java b/src/java/org/apache/cassandra/db/filter/RowFilter.java index 53a3ce940e..ef9f1a7e2c 100644 --- a/src/java/org/apache/cassandra/db/filter/RowFilter.java +++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java @@ -527,6 +527,11 @@ public class RowFilter implements Iterable return operator; } + public boolean isMapElementExpression() + { + return kind() == Kind.MAP_ELEMENT; + } + /** * If this expression is used to query an index, the value to use as * partition key for that index query. diff --git a/src/java/org/apache/cassandra/index/Index.java b/src/java/org/apache/cassandra/index/Index.java index 61127407bb..73c4ffe480 100644 --- a/src/java/org/apache/cassandra/index/Index.java +++ b/src/java/org/apache/cassandra/index/Index.java @@ -437,6 +437,27 @@ public interface Index return true; } + /** + * Return whether this index supports map element expressions on frozen map columns. + * + * @return {@code true} if this index supports map element else {@code false}. + */ + default boolean supportsMapElementExpression() + { + return false; + } + + /** + * Returns whether index allows filtering for map element expressions on frozen collections. + * SAI can handle map element predicates via post-filtering. + * + * @return {@code true} if map element expressions can be evaluated via filtering, {@code false} otherwise. + */ + default boolean supportsFilteringOnMapElementExpression() + { + return false; + } + /** * If the index supports custom search expressions using the * {@code}SELECT * FROM table WHERE expr(index_name, expression){@code} syntax, this diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java index 69217a6825..1dba97d3ea 100644 --- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java +++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java @@ -453,12 +453,37 @@ public class StorageAttachedIndex implements Index return dependsOn(column) && indexTermType.supports(operator); } + @Override + public boolean supportsExpression(RowFilter.Expression expression) + { + if (expression.isMapElementExpression() && + indexTermType.isFrozenCollection() && + indexTermType.indexTargetType() == IndexTarget.Type.FULL) + + return false; + + return supportsExpression(expression.column(), expression.operator()); + } + @Override public boolean filtersMultipleContains() { return false; } + @Override + public boolean supportsMapElementExpression() + { + return termType().indexTargetType() == IndexTarget.Type.KEYS_AND_VALUES; + } + + @Override + public boolean supportsFilteringOnMapElementExpression() + { + // SAI supports map element expressions via post-filtering on frozen collections + return true; + } + @Override public AbstractType customExpressionValueType() { @@ -784,6 +809,12 @@ public class StorageAttachedIndex implements Index while (bufferIterator != null && bufferIterator.hasNext()) validateTermSizeForCell(analyzer, key, bufferIterator.next(), isClientMutation, state); } + else if (indexTermType.isFrozenCollection() && indexTermType.indexTargetType() != IndexTarget.Type.FULL) + { + Iterator bufferIterator = indexTermType.valuesOfFrozenCollection(row, FBUtilities.nowInSeconds()); + while (bufferIterator != null && bufferIterator.hasNext()) + validateTermSizeForCell(analyzer, key, bufferIterator.next(), isClientMutation, state); + } else { ByteBuffer value = indexTermType.valueOf(key, row, FBUtilities.nowInSeconds()); diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java index 65480f0985..6f995730d1 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java @@ -32,6 +32,7 @@ import com.google.common.base.Stopwatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; @@ -94,6 +95,18 @@ public class SSTableIndexWriter implements PerColumnIndexWriter } } } + else if (index.termType().isFrozenCollection() && index.termType().indexTargetType() != IndexTarget.Type.FULL) + { + Iterator valueIterator = index.termType().valuesOfFrozenCollection(row, nowInSec); + if (valueIterator != null) + { + while (valueIterator.hasNext()) + { + ByteBuffer value = valueIterator.next(); + addTerm(index.termType().asIndexBytes(value.duplicate()), key, sstableRowId); + } + } + } else { ByteBuffer value = index.termType().valueOf(key.partitionKey(), row, nowInSec); diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java index 99bff5a1e1..686eb100b1 100644 --- a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java +++ b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java @@ -31,6 +31,7 @@ import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.db.memtable.Memtable; @@ -76,6 +77,18 @@ public class MemtableIndexManager } } } + else if (index.termType().isFrozenCollection() && index.termType().indexTargetType() != IndexTarget.Type.FULL) + { + Iterator bufferIterator = index.termType().valuesOfFrozenCollection(row, FBUtilities.nowInSeconds()); + if (bufferIterator != null) + { + while (bufferIterator.hasNext()) + { + ByteBuffer value = bufferIterator.next(); + bytes += target.index(key, row.clustering(), value); + } + } + } else { ByteBuffer value = index.termType().valueOf(key, row, FBUtilities.nowInSeconds()); diff --git a/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java b/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java index 15ea273145..03ce9daf3b 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java +++ b/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.index.sai.QueryContext; @@ -161,6 +162,12 @@ public class FilterTree Iterator valueIterator = expression.getIndexTermType().valuesOf(row, now); return operator.apply(result, collectionMatch(valueIterator, expression)); } + else if (expression.getIndexTermType().isFrozenCollection() && expression.getIndexTermType().indexTargetType() != IndexTarget.Type.FULL) + { + Iterator valueIterator = expression.getIndexTermType().valuesOfFrozenCollection(row, now); + boolean matchResult = collectionMatch(valueIterator, expression); + return operator.apply(result, matchResult); + } else { ByteBuffer value = expression.getIndexTermType().valueOf(key, row, now); @@ -181,6 +188,7 @@ public class FilterTree while (valueIterator.hasNext()) { ByteBuffer value = valueIterator.next(); + if (value == null) continue; diff --git a/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java b/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java index 05abe93c38..e5c75a53a2 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java +++ b/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java @@ -34,6 +34,8 @@ import java.util.Set; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import javax.annotation.Nullable; + import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableSet; import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; @@ -309,6 +311,11 @@ public class IndexTermType return columnMetadata.name.toString(); } + public IndexTarget.Type indexTargetType() + { + return indexTargetType; + } + public AbstractType vectorElementType() { assert isVector(); @@ -453,10 +460,80 @@ public class IndexTermType } } + @Nullable + public Iterator valuesOfFrozenCollection(Row row, long nowInSecs) + { + if (row == null) + return null; + + ByteBuffer buffer; + + if (columnMetadata.kind == ColumnMetadata.Kind.CLUSTERING) + buffer = row.clustering().bufferAt(columnMetadata.position()); + else + { + Cell cell = row.getCell(columnMetadata); + if (cell == null || !cell.isLive(nowInSecs)) + return null; + buffer = cell.buffer(); + } + + if (buffer == null || buffer.remaining() == 0) + return null; + + CollectionType collectionType = (CollectionType) columnMetadata.type.unwrap(); + List elements = collectionType.unpack(buffer); + + switch (collectionType.kind) + { + case LIST: + case SET: + break; + case MAP: + elements = extractMapElements(elements); + break; + default: + throw new IllegalArgumentException("Unsupported type of collection - " + collectionType.kind); + } + + if (isInetAddress()) + elements.sort((c1, c2) -> compareInet(encodeInetAddress(c1), encodeInetAddress(c2))); + + return elements.iterator(); + } + + private List extractMapElements(List elements) + { + List result = new ArrayList<>(elements.size()); + + for (int i = 0; i < elements.size(); i += 2) + { + ByteBuffer key = elements.get(i); + ByteBuffer value = i + 1 < elements.size() ? elements.get(i + 1) : null; + + switch (indexTargetType) + { + case KEYS: + result.add(key); + break; + case VALUES: + if (value != null) + result.add(value); + break; + case KEYS_AND_VALUES: + if (value != null) + result.add(CompositeType.build(ByteBufferAccessor.instance, key, value)); + break; + } + } + + return result; + } + public Comparator comparator() { - // Override the comparator for BigInteger, frozen collections and composite types - if (isBigInteger() || isBigDecimal() || isComposite() || isFrozen()) + // Override the comparator for BigInteger, BigDecimal and composite types + if (isBigInteger() || isBigDecimal() || isComposite()) return FastByteOperations::compareUnsigned; return indexType; @@ -474,9 +551,9 @@ public class IndexTermType return compareInet(b1, b2); else if (isLong()) return indexType.unwrap().compare(b1, b2); - // BigInteger values, frozen types and composite types (map entries) use compareUnsigned to maintain + // BigInteger, BigDecimal and composite types (map entries) use compareUnsigned to maintain // a consistent order between the in-memory index and the on-disk index. - else if (isBigInteger() || isBigDecimal() || isComposite() || isFrozen()) + else if (isBigInteger() || isBigDecimal() || isComposite()) return FastByteOperations.compareUnsigned(b1, b2); return indexType.compare(b1, b2); @@ -612,6 +689,23 @@ public class IndexTermType return indexTargetType == IndexTarget.Type.KEYS_AND_VALUES && indexOperator == Expression.IndexOperator.EQ; } + if (isFrozenCollection()) + { + if (indexTargetType == IndexTarget.Type.VALUES) + return indexOperator == Expression.IndexOperator.CONTAINS_VALUE; + + if (indexTargetType == IndexTarget.Type.KEYS) + return indexOperator == Expression.IndexOperator.CONTAINS_KEY; + + if (indexTargetType == IndexTarget.Type.KEYS_AND_VALUES) + return indexOperator == Expression.IndexOperator.EQ; + + if (indexTargetType == IndexTarget.Type.FULL) + return indexOperator == Expression.IndexOperator.EQ; + + return false; + } + if (indexTargetType == IndexTarget.Type.FULL) return indexOperator == Expression.IndexOperator.EQ; @@ -725,7 +819,9 @@ public class IndexTermType private AbstractType calculateIndexType(AbstractType baseType, EnumSet capabilities, IndexTarget.Type indexTargetType) { - return capabilities.contains(Capability.NON_FROZEN_COLLECTION) ? collectionCellValueType(baseType, indexTargetType) : baseType; + if (IndexTarget.Type.FULL == indexTargetType) + return baseType; + return capabilities.contains(Capability.COLLECTION) ? collectionCellValueType(baseType, indexTargetType) : baseType; } private Iterator collectionIterator(ComplexColumnData cellData, long nowInSecs) @@ -747,7 +843,7 @@ public class IndexTermType { if (isNonFrozenCollection()) { - switch (((CollectionType) columnMetadata.type).kind) + switch (((CollectionType) columnMetadata.type.unwrap()).kind) { case LIST: return cell.buffer(); @@ -770,7 +866,7 @@ public class IndexTermType private AbstractType collectionCellValueType(AbstractType type, IndexTarget.Type indexType) { - CollectionType collection = ((CollectionType) type); + CollectionType collection = ((CollectionType) type.unwrap()); switch (collection.kind) { case LIST: diff --git a/src/java/org/apache/cassandra/schema/IndexMetadata.java b/src/java/org/apache/cassandra/schema/IndexMetadata.java index 585f51e7df..15a0670e3a 100644 --- a/src/java/org/apache/cassandra/schema/IndexMetadata.java +++ b/src/java/org/apache/cassandra/schema/IndexMetadata.java @@ -225,6 +225,17 @@ public final class IndexMetadata return kind == Kind.COMPOSITES; } + /** + * Checks if the given custom index class name represents a SAI. + */ + public static boolean isSAIIndex(String customClass) + { + if (customClass == null) + return false; + String resolved = indexNameAliases.getOrDefault(toLowerCaseLocalized(customClass), customClass); + return StorageAttachedIndex.class.getName().equals(resolved); + } + @Override public int hashCode() { diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexOnMapEntriesTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexOnMapEntriesTest.java index 0ef0bbc4d6..7e6aef938b 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexOnMapEntriesTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexOnMapEntriesTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.Relation; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.dht.ByteOrderedPartitioner; import org.apache.cassandra.exceptions.InvalidRequestException; @@ -242,7 +243,7 @@ public class SecondaryIndexOnMapEntriesTest extends CQLTester } catch (InvalidRequestException e) { - String expectedMessage = "Map-entry predicates on frozen map column v are not supported"; + String expectedMessage = String.format(Relation.FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, "v"); assertTrue("Expected error message to contain '" + expectedMessage + "' but got '" + e.getMessage() + "'", e.getMessage().contains(expectedMessage)); } diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java index ca5049ef55..6ed67520f8 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java @@ -1560,7 +1560,7 @@ public class SecondaryIndexTest extends CQLTester execute("INSERT INTO %s (k, v) VALUES (?, ?)", 1, set(udt1)); assertInvalidMessage("Cannot create index on keys of column v with non-map type", "CREATE INDEX ON %s (keys(v))"); - assertInvalidMessage("full() indexes can only be created on frozen collections", + assertInvalidMessage("full() non-SAI indexes can only be created on frozen collections", "CREATE INDEX ON %s (full(v))"); String indexName = createIndex("CREATE INDEX ON %s (values(v))"); @@ -1589,7 +1589,7 @@ public class SecondaryIndexTest extends CQLTester assertInvalidMessage("Cannot create index on non-frozen UDT column v", "CREATE INDEX ON %s (v)"); assertInvalidMessage("Cannot create keys() index on v. Non-collection columns only support simple indexes", "CREATE INDEX ON %s (keys(v))"); assertInvalidMessage("Cannot create values() index on v. Non-collection columns only support simple indexes", "CREATE INDEX ON %s (values(v))"); - assertInvalidMessage("full() indexes can only be created on frozen collections", "CREATE INDEX ON %s (full(v))"); + assertInvalidMessage("full() non-SAI indexes can only be created on frozen collections", "CREATE INDEX ON %s (full(v))"); } @Test diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java index 1ebeec495e..187cba1d82 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.apache.cassandra.Util; import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.Relation; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; public class SelectSingleColumnRelationTest extends CQLTester @@ -33,7 +34,7 @@ public class SelectSingleColumnRelationTest extends CQLTester public void textInvalidMapEntryPredicate() throws Throwable { createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY(pk, ck)) WITH CLUSTERING ORDER BY (ck DESC)"); - assertInvalidMessage("Map-entry predicates on frozen map column ck are not supported", + assertInvalidMessage(String.format(Relation.FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, "ck"), "SELECT * FROM %s WHERE pk=? AND ck[0] = ?", 0, 0); } diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java index 4fe87a5832..c511c72d2c 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java @@ -27,6 +27,7 @@ import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.Duration; +import org.apache.cassandra.cql3.Relation; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.exceptions.InvalidRequestException; @@ -1646,11 +1647,12 @@ public class SelectTest extends CQLTester row(1, 2, list(1, 6), set(2, 12), map(1, 6)), row(1, 4, list(1, 2), set(2, 4), map(1, 2))); - assertInvalidMessage("Map-entry predicates on frozen map column e are not supported", - "SELECT * FROM %s WHERE e[1] = 6 ALLOW FILTERING"); + // CASSANDRA-18492: Allow filtering works with frozen map[key] + assertRows(execute("SELECT * FROM %s WHERE e[1] = 6 ALLOW FILTERING"), + row(1, 2, list(1, 6), set(2, 12), map(1, 6))); - assertInvalidMessage("Map-entry predicates on frozen map column e are not supported", - "SELECT * FROM %s WHERE e[1] != 6 ALLOW FILTERING"); + assertRows(execute("SELECT * FROM %s WHERE e[1] != 6 ALLOW FILTERING"), + row(1, 4, list(1, 2), set(2, 4), map(1, 2))); assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 AND e CONTAINS 2 ALLOW FILTERING"), row(1, 4, list(1, 2), set(2, 4), map(1, 2))); @@ -1674,9 +1676,9 @@ public class SelectTest extends CQLTester "SELECT * FROM %s WHERE e CONTAINS null ALLOW FILTERING"); assertInvalidMessage("Invalid null value for column e", "SELECT * FROM %s WHERE e CONTAINS KEY null ALLOW FILTERING"); - assertInvalidMessage("Map-entry predicates on frozen map column e are not supported", + assertInvalidMessage("Invalid null map key for column e", "SELECT * FROM %s WHERE e[null] = 2 ALLOW FILTERING"); - assertInvalidMessage("Map-entry predicates on frozen map column e are not supported", + assertInvalidMessage("Invalid null value for e[1]", "SELECT * FROM %s WHERE e[1] = null ALLOW FILTERING"); // Checks filtering with unset @@ -1701,10 +1703,10 @@ public class SelectTest extends CQLTester assertInvalidMessage("Invalid unset value for column e", "SELECT * FROM %s WHERE e CONTAINS KEY ? ALLOW FILTERING", unset()); - assertInvalidMessage("Map-entry predicates on frozen map column e are not supported", + assertInvalidMessage("Invalid unset map key for column e", "SELECT * FROM %s WHERE e[?] = 2 ALLOW FILTERING", unset()); - assertInvalidMessage("Map-entry predicates on frozen map column e are not supported", + assertInvalidMessage("Invalid unset value for e[1]", "SELECT * FROM %s WHERE e[1] = ? ALLOW FILTERING", unset()); } @@ -3556,7 +3558,7 @@ public class SelectTest extends CQLTester "NOT_CONTAINS_KEY or map-entry equality if it already restricted by one of those", "SELECT * FROM %s WHERE fm > {'lmn' : 'f'} AND fm CONTAINS KEY 'lmn'"); - assertInvalidMessage("Map-entry predicates on frozen map column fm are not supported", + assertInvalidMessage(String.format(Relation.FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, "fm"), "SELECT * FROM %s WHERE fm > {'lmn' : 'f'} AND fm['lmn'] = 'foo2'"); } } diff --git a/test/unit/org/apache/cassandra/db/AbstractReadQueryToCQLStringTest.java b/test/unit/org/apache/cassandra/db/AbstractReadQueryToCQLStringTest.java index c7aac0c296..8fe1bfbc4c 100644 --- a/test/unit/org/apache/cassandra/db/AbstractReadQueryToCQLStringTest.java +++ b/test/unit/org/apache/cassandra/db/AbstractReadQueryToCQLStringTest.java @@ -657,7 +657,8 @@ public class AbstractReadQueryToCQLStringTest extends CQLTester test("SELECT * FROM %s WHERE u = {a: 'a', b: 1} ALLOW FILTERING"); testInvalid("SELECT * FROM %s WHERE l['a'] = 'a' ALLOW FILTERING"); testInvalid("SELECT * FROM %s WHERE s['a'] = 'a' ALLOW FILTERING"); - testInvalid("SELECT * FROM %s WHERE m['a'] = 'a' ALLOW FILTERING"); + // CASSANDRA-18492 Allow filtering works with map[key] + test("SELECT * FROM %s WHERE m['a'] = 'a' ALLOW FILTERING"); testInvalid("SELECT * FROM %s WHERE u.a = 'a' ALLOW FILTERING"); testInvalid("SELECT * FROM %s WHERE u.b = 0 ALLOW FILTERING"); testInvalid("SELECT * FROM %s WHERE u.a = 'a' ANd u.b = 0 ALLOW FILTERING"); diff --git a/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java b/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java index 296840616f..3c1b78533b 100644 --- a/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java +++ b/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java @@ -17,8 +17,15 @@ */ package org.apache.cassandra.index.sai.cql; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +import com.datastax.driver.core.ResultSet; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.junit.Before; import org.junit.Test; @@ -26,6 +33,7 @@ import org.junit.Test; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.index.sai.SAITester; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; /** @@ -71,43 +79,51 @@ public class CollectionIndexingTest extends SAITester } @Test - public void indexFrozenList() + public void indexFrozenList() throws Throwable { createPopulatedFrozenList(createIndexDDL("FULL(value)")); - assertEquals(2, execute("SELECT * FROM %s WHERE value = ?", Arrays.asList(1, 2, 3)).size()); + beforeAndAfterFlush(() -> { + assertEquals(2, execute("SELECT * FROM %s WHERE value = ?", Arrays.asList(1, 2, 3)).size()); + }); } @Test public void indexFrozenMap() throws Throwable { createPopulatedFrozenMap(createIndexDDL("FULL(value)")); - assertEquals(1, execute("SELECT * FROM %s WHERE value = ?", new HashMap() {{ - put(1, "v1"); - put(2, "v2"); - }}).size()); - + beforeAndAfterFlush(() -> { + assertEquals(1, execute("SELECT * FROM %s WHERE value = ?", new HashMap() {{ + put(1, "v1"); + put(2, "v2"); + }}).size()); + }); } @Test public void indexFrozenMapQueryKeys() throws Throwable { createPopulatedFrozenMap(createIndexDDL("FULL(value)")); - assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains key 1"); + beforeAndAfterFlush(() -> { + assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains key 1"); + }); } @Test public void indexFrozenMapQueryValues() throws Throwable { createPopulatedFrozenMap(createIndexDDL("FULL(value)")); - assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains 'v1'"); + beforeAndAfterFlush(() -> { + assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains 'v1'"); + }); } @Test public void indexFrozenMapQueryEntries() throws Throwable { createPopulatedFrozenMap(createIndexDDL("FULL(value)")); - assertInvalidMessage("Map-entry predicates on frozen map column value are not supported", - "SELECT * FROM %s WHERE value[1] = 'v1'"); + beforeAndAfterFlush(() -> { + assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value[1] = 'v1'"); + }); } @Test @@ -204,6 +220,1085 @@ public class CollectionIndexingTest extends SAITester assertRows(execute("SELECT k, v, m FROM %s WHERE v = 1 AND m CONTAINS KEY 2 AND m CONTAINS 3"), row); } + @Test + public void testFrozenListFullIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_list frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list) VALUES (1, [1, 2, 3])"); + execute("INSERT INTO %s (pk, frozen_list) VALUES (2, [1, 2, 3])"); + execute("INSERT INTO %s (pk, frozen_list) VALUES (3, [4, 5, 6])"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list = ? ALLOW FILTERING", Arrays.asList(1, 2, 3)).size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_list)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list = ?", Arrays.asList(1, 2, 3)); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenListValuesIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_list frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list) VALUES (1, [1, 2, 3])"); + execute("INSERT INTO %s (pk, frozen_list) VALUES (2, [3, 4, 5])"); + execute("INSERT INTO %s (pk, frozen_list) VALUES (3, [4, 5, 6])"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list CONTAINS 3 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(frozen_list)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list CONTAINS 3"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenSetFullIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_set frozen>)"); + + execute("INSERT INTO %s (pk, frozen_set) VALUES (1, {'apple', 'banana'})"); + execute("INSERT INTO %s (pk, frozen_set) VALUES (2, {'apple', 'banana'})"); + execute("INSERT INTO %s (pk, frozen_set) VALUES (3, {'cherry', 'date'})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_set = ? ALLOW FILTERING", Arrays.asList("apple", "banana")).size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_set)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_set = ?", Arrays.asList("apple", "banana")); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenSetValuesIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_set frozen>)"); + + execute("INSERT INTO %s (pk, frozen_set) VALUES (1, {'apple', 'banana'})"); + execute("INSERT INTO %s (pk, frozen_set) VALUES (2, {'banana', 'cherry'})"); + execute("INSERT INTO %s (pk, frozen_set) VALUES (3, {'cherry', 'date'})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_set CONTAINS 'banana' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(frozen_set)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_set CONTAINS 'banana'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapFullIndexEquality() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, {'k1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, {'k1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, {'k3': 'v3', 'k4': 'v4'})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_map = ? ALLOW FILTERING", ImmutableMap.of("k1", "v1", "k2", "v2")).size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_map)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map = ?", ImmutableMap.of("k1", "v1", "k2", "v2")); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapFullIndexMapEntryRequiresFiltering() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of(1, "v1", 2, "v2")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, ?)", ImmutableMap.of(1, "v1", 3, "v3")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, ?)", ImmutableMap.of(3, "v3", 4, "v4")); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_map[1] = 'v1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_map)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT pk FROM %s WHERE frozen_map[1] = 'v1'"); + + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map[1] = 'v1' ALLOW FILTERING"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapEntriesIndexMapEntry() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of(1, "v1", 2, "v2")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, ?)", ImmutableMap.of(1, "v1", 3, "v3")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, ?)", ImmutableMap.of(3, "v3", 4, "v4")); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_map[1] = 'v1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map[1] = 'v1'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapValuesIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, {'k1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, {'k3': 'v1', 'k4': 'v3'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, {'k5': 'v3', 'k6': 'v4'})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_map CONTAINS 'v1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(frozen_map)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map CONTAINS 'v1'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapKeysIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, {'k1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, {'k1': 'v3', 'k4': 'v4'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, {'k5': 'v5', 'k6': 'v6'})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_map CONTAINS KEY 'k1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(KEYS(frozen_map)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map CONTAINS KEY 'k1'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testDifferentIndexTypesOnDifferentColumns() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, " + + "map_full frozen>, " + + "map_values frozen>, " + + "map_keys frozen>, " + + "map_entries frozen>)"); + + + execute("INSERT INTO %s (pk, map_full, map_values, map_keys, map_entries) " + + "VALUES (1, {'k1': 'v1', 'k2': 'v2'}, {'k1': 'v1', 'k2': 'v2'}, {'k1': 'v1', 'k2': 'v2'}, {'k1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (pk, map_full, map_values, map_keys, map_entries) " + + "VALUES (2, {'k1': 'v1', 'k2': 'v2'}, {'k3': 'v1', 'k4': 'v3'}, {'k1': 'v3', 'k5': 'v5'}, {'k1': 'v1', 'k6': 'v6'})"); + execute("INSERT INTO %s (pk, map_full, map_values, map_keys, map_entries) " + + "VALUES (3, {'k3': 'v3', 'k4': 'v4'}, {'k5': 'v5', 'k6': 'v6'}, {'k7': 'v7', 'k8': 'v8'}, {'k9': 'v9', 'k10': 'v10'})"); + + assertEquals(2,execute("SELECT pk FROM %s WHERE map_full = ? ALLOW FILTERING", ImmutableMap.of("k1", "v1", "k2", "v2")).size()); + assertEquals(2,execute("SELECT pk FROM %s WHERE map_values CONTAINS 'v1' ALLOW FILTERING").size()); + assertEquals(2,execute("SELECT pk FROM %s WHERE map_keys CONTAINS KEY 'k1' ALLOW FILTERING").size()); + assertEquals(2,execute("SELECT pk FROM %s WHERE map_entries['k1'] = 'v1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX idx_full ON %s(FULL(map_full)) USING 'sai'"); + createIndex("CREATE INDEX idx_values ON %s(VALUES(map_values)) USING 'sai'"); + createIndex("CREATE INDEX idx_keys ON %s(KEYS(map_keys)) USING 'sai'"); + createIndex("CREATE INDEX idx_entries ON %s(ENTRIES(map_entries)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE map_full = ?", ImmutableMap.of("k1", "v1", "k2", "v2")); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE map_values CONTAINS 'v1'"); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE map_keys CONTAINS KEY 'k1'"); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE map_entries['k1'] = 'v1'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFullIndexDoesNotSupportContainsOperations() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_list frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list) VALUES (1, [1, 2, 3])"); + + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_list CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_list)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT pk FROM %s WHERE frozen_list CONTAINS 1"); + }); + } + + @Test + public void testValuesIndexDoesNotSupportEquality() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_list frozen>)"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_list)) USING 'sai'"); + + execute("INSERT INTO %s (pk, frozen_list) VALUES (1, [1, 2, 3])"); + + beforeAndAfterFlush(() -> { + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT pk FROM %s WHERE frozen_list = ?", Arrays.asList(1, 2, 3)); + }); + } + + @Test + public void testMapEntryWithAllowFilteringDifferentKeyTypes() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of(1, "v1", 2, "v2")); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map[1] = 'v1' ALLOW FILTERING"); + assertEquals(1, rows.all().size()); + }); + + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of("k1", "v1", "k2", "v2")); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map['k1'] = 'v1' ALLOW FILTERING"); + assertEquals(1, rows.all().size()); + }); + } + + @Test + public void testFrozenMapValuesIndexWithMapEntryQuery() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_map)) USING 'sai'"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of(1, "v1", 2, "v2")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, ?)", ImmutableMap.of(1, "v1", 3, "v3")); + + beforeAndAfterFlush(() -> { + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT pk FROM %s WHERE frozen_map[1] = 'v1'"); + + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map[1] = 'v1' ALLOW FILTERING"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapKeysIndexWithMapEntryQuery() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + createIndex("CREATE INDEX ON %s(KEYS(frozen_map)) USING 'sai'"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of(1, "v1", 2, "v2")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, ?)", ImmutableMap.of(1, "v1", 3, "v3")); + + beforeAndAfterFlush(() -> { + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT pk FROM %s WHERE frozen_map[1] = 'v1'"); + + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map[1] = 'v1' ALLOW FILTERING"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapDefaultIndexType() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + createIndex("CREATE INDEX ON %s(frozen_map) USING 'sai'"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of(1, "v1", 2, "v2")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, ?)", ImmutableMap.of(1, "v1", 3, "v3")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, ?)", ImmutableMap.of(4, "v4", 5, "v5")); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map CONTAINS 'v1'"); + assertEquals(2, rows.all().size()); + + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT pk FROM %s WHERE frozen_map = ?", ImmutableMap.of(1, "v1", 2, "v2")); + + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT pk FROM %s WHERE frozen_map[1] = 'v1'"); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_map[1] = 'v1' ALLOW FILTERING"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testMapEntryQueryWithNullAndEmptyCollections() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map)) USING 'sai'"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, ?)", ImmutableMap.of(1, "v1", 2, "v2")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, ?)", ImmutableMap.of(3, "v3", 4, "v4")); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, NULL)"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (4, ?)", ImmutableMap.of()); + execute("INSERT INTO %s (pk, frozen_map) VALUES (5, ?)", ImmutableMap.of(1, "different_value")); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map[1] = 'v1'"); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s"); + assertEquals(5, rows.all().size()); + }); + } + + @Test + public void testFullIndexWithNullAndEmptyCollections() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_list frozen>)"); + createIndex("CREATE INDEX ON %s(FULL(frozen_list)) USING 'sai'"); + + execute("INSERT INTO %s (pk, frozen_list) VALUES (1, ?)", Arrays.asList(1, 2, 3)); + execute("INSERT INTO %s (pk, frozen_list) VALUES (2, ?)", Arrays.asList(1, 2, 3)); + execute("INSERT INTO %s (pk, frozen_list) VALUES (3, ?)", Arrays.asList(4, 5, 6)); + execute("INSERT INTO %s (pk, frozen_list) VALUES (4, NULL)"); + execute("INSERT INTO %s (pk, frozen_list) VALUES (5, ?)", List.of()); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list = ?", Arrays.asList(1, 2, 3)); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_list = ?", List.of()); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s"); + assertEquals(5, rows.all().size()); + }); + } + + @Test + public void testFrozenCollectionsWithNumericTypes() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, " + + "frozen_list_bigint frozen>, " + + "frozen_set_smallint frozen>, " + + "frozen_map_tinyint frozen>, " + + "frozen_list_varint frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list_bigint, frozen_set_smallint, frozen_map_tinyint, frozen_list_varint) " + + "VALUES (1, [1, 2, 3], {10, 20}, {1: 'v1', 2: 'v2'}, [100, 200])"); + execute("INSERT INTO %s (pk, frozen_list_bigint, frozen_set_smallint, frozen_map_tinyint, frozen_list_varint) " + + "VALUES (2, [1, 2, 3], {30, 40}, {3: 'v3'}, [300])"); + execute("INSERT INTO %s (pk, frozen_list_bigint, frozen_set_smallint, frozen_map_tinyint, frozen_list_varint) " + + "VALUES (3, [], {}, {}, [])"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list_bigint = ? ALLOW FILTERING", Arrays.asList(1L, 2L, 3L)).size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_set_smallint CONTAINS ? ALLOW FILTERING", (short)10).size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map_tinyint[?] = 'v1' ALLOW FILTERING", (byte)1).size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_list_varint CONTAINS 100 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_list_bigint)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_set_smallint)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map_tinyint)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_list_varint)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list_bigint = ?", Arrays.asList(1L, 2L, 3L)); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_set_smallint CONTAINS ?", (short)10); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_map_tinyint[?] = 'v1'", (byte)1); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_list_varint CONTAINS 100"); + assertEquals(1, rows.all().size()); + }); + } + + @Test + public void testFrozenCollectionsWithDecimalTypes() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, " + + "frozen_list_float frozen>, " + + "frozen_set_double frozen>, " + + "frozen_map_decimal frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list_float, frozen_set_double, frozen_map_decimal) " + + "VALUES (1, [1.1, 2.2], {10.5, 20.5}, {1.0: 'v1', 2.0: 'v2'})"); + execute("INSERT INTO %s (pk, frozen_list_float, frozen_set_double, frozen_map_decimal) " + + "VALUES (2, [1.1, 2.2], {30.5}, {3.0: 'v3'})"); + execute("INSERT INTO %s (pk, frozen_list_float, frozen_set_double, frozen_map_decimal) " + + "VALUES (3, [], {}, {})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list_float = ? ALLOW FILTERING", Arrays.asList(1.1f, 2.2f)).size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_set_double CONTAINS 10.5 ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map_decimal[1.0] = 'v1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_list_float)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_set_double)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map_decimal)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list_float = ?", Arrays.asList(1.1f, 2.2f)); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_set_double CONTAINS 10.5"); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_map_decimal[1.0] = 'v1'"); + assertEquals(1, rows.all().size()); + }); + } + + @Test + public void testFrozenCollectionsWithTextVariants() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, " + + "frozen_list_varchar frozen>, " + + "frozen_set_ascii frozen>, " + + "frozen_map_text frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list_varchar, frozen_set_ascii, frozen_map_text) " + + "VALUES (1, ['apple', 'banana'], {'hello', 'world'}, {'key1': 'value1', 'key2': 'value2'})"); + execute("INSERT INTO %s (pk, frozen_list_varchar, frozen_set_ascii, frozen_map_text) " + + "VALUES (2, ['apple', 'banana'], {'test'}, {'key3': 'value3'})"); + execute("INSERT INTO %s (pk, frozen_list_varchar, frozen_set_ascii, frozen_map_text) " + + "VALUES (3, [], {}, {})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list_varchar = ? ALLOW FILTERING", Arrays.asList("apple", "banana")).size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_set_ascii CONTAINS 'hello' ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map_text['key1'] = 'value1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_list_varchar)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_set_ascii)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map_text)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list_varchar = ?", Arrays.asList("apple", "banana")); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_set_ascii CONTAINS 'hello'"); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_map_text['key1'] = 'value1'"); + assertEquals(1, rows.all().size()); + }); + } + + @Test + public void testFrozenCollectionsWithTimeTypes() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, " + + "frozen_list_timestamp frozen>, " + + "frozen_set_date frozen>, " + + "frozen_map_time frozen>, " + + "frozen_list_timeuuid frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list_timestamp, frozen_set_date, frozen_map_time, frozen_list_timeuuid) " + + "VALUES (1, ['2023-01-01 00:00:00+0000', '2023-01-02 00:00:00+0000'], {'2023-01-01', '2023-01-02'}, " + + "{'12:00:00': 'noon', '18:00:00': 'evening'}, [50554d6e-29bb-11e5-b345-feff819cdc9f])"); + execute("INSERT INTO %s (pk, frozen_list_timestamp, frozen_set_date, frozen_map_time, frozen_list_timeuuid) " + + "VALUES (2, ['2023-01-01 00:00:00+0000', '2023-01-02 00:00:00+0000'], {'2023-01-03'}, " + + "{'06:00:00': 'morning'}, [50554d6e-29bb-11e5-b345-feff819cdc9f])"); + execute("INSERT INTO %s (pk, frozen_list_timestamp, frozen_set_date, frozen_map_time, frozen_list_timeuuid) " + + "VALUES (3, [], {}, {}, [])"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list_timestamp = ['2023-01-01 00:00:00+0000', '2023-01-02 00:00:00+0000'] ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_set_date CONTAINS '2023-01-01' ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map_time['12:00:00'] = 'noon' ALLOW FILTERING").size()); + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list_timeuuid CONTAINS 50554d6e-29bb-11e5-b345-feff819cdc9f ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_list_timestamp)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_set_date)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map_time)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_list_timeuuid)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list_timestamp = ['2023-01-01 00:00:00+0000', '2023-01-02 00:00:00+0000']"); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_set_date CONTAINS '2023-01-01'"); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_map_time['12:00:00'] = 'noon'"); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_list_timeuuid CONTAINS 50554d6e-29bb-11e5-b345-feff819cdc9f"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenCollectionsWithOtherTypes() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, " + + "frozen_list_uuid frozen>, " + + "frozen_set_boolean frozen>, " + + "frozen_map_inet frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list_uuid, frozen_set_boolean, frozen_map_inet) " + + "VALUES (1, [550e8400-e29b-41d4-a716-446655440000, 550e8400-e29b-41d4-a716-446655440001], " + + "{true, false}, {'127.0.0.1': 'localhost', '192.168.1.1': 'router'})"); + execute("INSERT INTO %s (pk, frozen_list_uuid, frozen_set_boolean, frozen_map_inet) " + + "VALUES (2, [550e8400-e29b-41d4-a716-446655440000, 550e8400-e29b-41d4-a716-446655440001], " + + "{true}, {'10.0.0.1': 'server'})"); + execute("INSERT INTO %s (pk, frozen_list_uuid, frozen_map_inet) " + + "VALUES (3, [], {})"); + + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_list_uuid = ? ALLOW FILTERING", + Arrays.asList(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), + UUID.fromString("550e8400-e29b-41d4-a716-446655440001"))).size()); + assertEquals(2, execute("SELECT pk FROM %s WHERE frozen_set_boolean CONTAINS true ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map_inet['127.0.0.1'] = 'localhost' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(FULL(frozen_list_uuid)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(frozen_set_boolean)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map_inet)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list_uuid = ?", + Arrays.asList(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), + UUID.fromString("550e8400-e29b-41d4-a716-446655440001"))); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_set_boolean CONTAINS true"); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE frozen_map_inet['127.0.0.1'] = 'localhost'"); + assertEquals(1, rows.all().size()); + }); + } + + @Test + public void testNonFrozenCollectionsIndexes() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, list_col list)"); + createIndex("CREATE INDEX ON %s(VALUES(list_col)) USING 'sai'"); + + execute("INSERT INTO %s (id, list_col) VALUES (1, ['apple', 'banana'])"); + execute("INSERT INTO %s (id, list_col) VALUES (2, ['cherry', 'date'])"); + execute("INSERT INTO %s (id, list_col) VALUES (3, ['apple', 'banana', 'cherry'])"); + flush(); + + ResultSet rows = executeNet("SELECT id FROM %s WHERE list_col CONTAINS 'apple'"); + assertEquals(2, rows.all().size()); + + createTable("CREATE TABLE %s (id int PRIMARY KEY, set_col set)"); + createIndex("CREATE INDEX ON %s(VALUES(set_col)) USING 'sai'"); + + execute("INSERT INTO %s (id, set_col) VALUES (1, {'apple', 'banana'})"); + execute("INSERT INTO %s (id, set_col) VALUES (2, {'cherry', 'date'})"); + execute("INSERT INTO %s (id, set_col) VALUES (3, {'apple', 'banana', 'cherry'})"); + flush(); + + rows = executeNet("SELECT id FROM %s WHERE set_col CONTAINS 'apple'"); + assertEquals(2, rows.all().size()); + + createTable("CREATE TABLE %s (id int PRIMARY KEY, map_col map)"); + createIndex("CREATE INDEX ON %s(VALUES(map_col)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(KEYS(map_col)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(ENTRIES(map_col)) USING 'sai'"); + + execute("INSERT INTO %s (id, map_col) VALUES (1, {'k1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (id, map_col) VALUES (2, {'k3': 'v3', 'k4': 'v4'})"); + execute("INSERT INTO %s (id, map_col) VALUES (3, {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'})"); + flush(); + + rows = executeNet("SELECT id FROM %s WHERE map_col CONTAINS 'v1'"); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT id FROM %s WHERE map_col CONTAINS key 'k1'"); + assertEquals(2, rows.all().size()); + + rows = executeNet("SELECT id FROM %s WHERE map_col['k1'] = 'v1'"); + assertEquals(2, rows.all().size()); + + execute("UPDATE %s SET map_col = map_col + {'k1': 'v1_updated'} WHERE id = 1"); + flush(); + + rows = executeNet("SELECT id FROM %s WHERE map_col['k1'] = 'v1_updated'"); + assertEquals(1, rows.all().size()); + + rows = executeNet("SELECT id FROM %s WHERE map_col['k1'] = 'v1'"); + assertEquals(1, rows.all().size()); + + execute("DELETE map_col['k1'] FROM %s WHERE id = 3"); + flush(); + + rows = executeNet("SELECT id FROM %s WHERE map_col CONTAINS key 'k1'"); + assertEquals(1, rows.all().size()); + + assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE map_col = {'k1': 'v1'} ALLOW FILTERING")) + .isInstanceOf(Exception.class); + } + + @Test + public void testSaiNonFrozenMap() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, frozen_map map)"); + createIndex("CREATE INDEX ON %s(ENTRIES(frozen_map)) USING 'sai'"); + + execute("INSERT INTO %s (id, frozen_map) VALUES (1, {'k1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (id, frozen_map) VALUES (2, {'k3': 'v3', 'k4': 'v4'})"); + execute("INSERT INTO %s (id, frozen_map) VALUES (3, {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'})"); + execute("INSERT INTO %s (id, frozen_map) VALUES (4, {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'})"); + flush(); + + ResultSet rows = executeNet("SELECT id FROM %s WHERE frozen_map['k1']='v1';"); + assertEquals(3, rows.all().size()); + } + + @Test + public void testFrozenListClusteringKeyWithValuesIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, [1, 2, 3], 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (1, [4, 5, 6], 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, [1, 7, 8], 300)"); + + assertEquals("Should find 2 rows containing value 1", 2, + execute("SELECT pk, v FROM %s WHERE ck CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk, v FROM %s WHERE ck CONTAINS 1"); + assertEquals("Should find 2 rows containing value 1", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenSetClusteringKeyWithValuesIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {'a', 'b'}, 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {'c', 'd'}, 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, {'a', 'e'}, 300)"); + + assertEquals("Should find 2 rows containing 'a'", 2, + execute("SELECT pk, v FROM %s WHERE ck CONTAINS 'a' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk, v FROM %s WHERE ck CONTAINS 'a'"); + assertEquals("Should find 2 rows containing 'a'", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapClusteringKeyWithKeysIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {'x': 1, 'y': 2}, 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {'z': 3}, 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, {'x': 4}, 300)"); + + assertEquals("Should find 2 rows with key 'x'", 2, + execute("SELECT pk, v FROM %s WHERE ck CONTAINS KEY 'x' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(KEYS(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk, v FROM %s WHERE ck CONTAINS KEY 'x'"); + assertEquals("Should find 2 rows with key 'x'", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapClusteringKeyWithValuesIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {'x': 1, 'y': 2}, 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {'z': 3}, 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, {'x': 1}, 300)"); + + assertEquals("Should find 2 rows with value 1", 2, + execute("SELECT pk, v FROM %s WHERE ck CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk, v FROM %s WHERE ck CONTAINS 1"); + assertEquals("Should find 2 rows with value 1", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapClusteringKeyWithEntriesIndexNotAllowed() throws Throwable + { + // ENTRIES index on frozen map clustering column should not be allowed because + // map entry predicates (ck[key] = value) are not supported on clustering columns, + // making the index unqueryable. + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + assertInvalidMessage("Cannot create ENTRIES index on frozen map clustering column", + "CREATE INDEX ON %s(ENTRIES(ck)) USING 'sai'"); + } + + @Test + public void testFrozenListAsSecondClusteringKey() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int, ck2 frozen>, v int, PRIMARY KEY (pk, ck1, ck2))"); + + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 1, [1, 2, 3], 100)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, [4, 5, 6], 200)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (2, 1, [1, 7, 8], 300)"); + + assertEquals("Should find 2 rows containing value 1", 2, + execute("SELECT pk, ck1, v FROM %s WHERE ck2 CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck2)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk, ck1, v FROM %s WHERE ck2 CONTAINS 1"); + assertEquals("Should find 2 rows containing value 1", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyWithEmptyCollections() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, [], 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, [1, 2], 200)"); + + assertEquals("Should find 1 row containing value 1", 1, + execute("SELECT pk FROM %s WHERE ck CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck CONTAINS 1"); + assertEquals("Should find 1 row containing value 1", 1, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s"); + assertEquals("Should have 2 rows total", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyWithFullIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, [1, 2, 3], 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, [1, 2, 3], 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (3, [4, 5, 6], 300)"); + + // Equality on clustering column works without index + assertEquals("Should find 2 rows with list [1, 2, 3]", 2, + execute("SELECT pk FROM %s WHERE ck = ? ALLOW FILTERING", Arrays.asList(1, 2, 3)).size()); + + createIndex("CREATE INDEX ON %s(FULL(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck = ?", Arrays.asList(1, 2, 3)); + assertEquals("Should find 2 rows with list [1, 2, 3]", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenSetClusteringKeyWithFullIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {1, 2, 3}, 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, {1, 2, 3}, 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (3, {4, 5}, 300)"); + + // Equality on clustering column works without index + assertEquals("Should find 2 rows with set {1, 2, 3}", 2, + execute("SELECT pk FROM %s WHERE ck = ? ALLOW FILTERING", ImmutableSet.of(1, 2, 3)).size()); + + createIndex("CREATE INDEX ON %s(FULL(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck = ?", ImmutableSet.of(1, 2, 3)); + assertEquals("Should find 2 rows with set {1, 2, 3}", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapClusteringKeyWithFullIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, {'a': 1, 'b': 2}, 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, {'a': 1, 'b': 2}, 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (3, {'c': 3}, 300)"); + + // Equality on clustering column works without index + assertEquals("Should find 2 rows with map {'a': 1, 'b': 2}", 2, + execute("SELECT pk FROM %s WHERE ck = ? ALLOW FILTERING", ImmutableMap.of("a", 1, "b", 2)).size()); + + createIndex("CREATE INDEX ON %s(FULL(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck = ?", ImmutableMap.of("a", 1, "b", 2)); + assertEquals("Should find 2 rows with map {'a': 1, 'b': 2}", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyDelete() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, [1, 2, 3], 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, [1, 4, 5], 200)"); + + assertEquals("Should have 2 rows containing value 1", 2, + execute("SELECT pk FROM %s WHERE ck CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck CONTAINS 1"); + assertEquals("Should have 2 rows containing value 1", 2, rows.all().size()); + + execute("DELETE FROM %s WHERE pk = 1 AND ck = [1, 2, 3]"); + + beforeAndAfterFlush(() -> { + ResultSet rows2 = executeNet("SELECT pk FROM %s WHERE ck CONTAINS 1"); + assertEquals("After delete, should have 1 row containing value 1", 1, rows2.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyMultiplePartitions() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, [1, 2], 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, [1, 3], 200)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (3, [1, 4], 300)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (4, [5, 6], 400)"); + + assertEquals("Should have 3 rows containing value 1 across different partitions", 3, + execute("SELECT pk FROM %s WHERE ck CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck CONTAINS 1"); + assertEquals("Should have 3 rows containing value 1 across different partitions", 3, rows.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyWithDuplicateValues() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, [1, 1, 1, 2], 100)"); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, [1, 3], 200)"); + + assertEquals("Should find 2 rows containing value 1 (even with duplicates)", 2, + execute("SELECT pk FROM %s WHERE ck CONTAINS 1 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck CONTAINS 1"); + assertEquals("Should find 2 rows containing value 1 (even with duplicates)", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyWithLargeCollection() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + List largeList = new ArrayList<>(); + for (int i = 0; i < 100; i++) + { + largeList.add(i); + } + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, ?, 100)", largeList); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, [99, 200], 200)"); + + assertEquals("Should find 2 rows containing value 99", 2, + execute("SELECT pk FROM %s WHERE ck CONTAINS 99 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck CONTAINS 99"); + assertEquals("Should find 2 rows containing value 99", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyWithNumericTypes() throws Throwable + { + createTable("CREATE TABLE %s (pk int, " + + "ck1 frozen>, " + + "ck2 frozen>, " + + "ck3 frozen>, " + + "v int, " + + "PRIMARY KEY (pk, ck1, ck2, ck3))"); + + execute("INSERT INTO %s (pk, ck1, ck2, ck3, v) VALUES (1, [1, 2], [10, 20], [100, 200], 100)"); + execute("INSERT INTO %s (pk, ck1, ck2, ck3, v) VALUES (2, [1, 3], [10, 30], [100, 300], 200)"); + + assertEquals("Should find 2 rows with bigint value 1", 2, + execute("SELECT pk FROM %s WHERE ck1 CONTAINS ? ALLOW FILTERING", 1L).size()); + assertEquals("Should find 2 rows with smallint value 10", 2, + execute("SELECT pk FROM %s WHERE ck2 CONTAINS ? ALLOW FILTERING", (short)10).size()); + assertEquals("Should find 2 rows with varint value 100", 2, + execute("SELECT pk FROM %s WHERE ck3 CONTAINS 100 ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck1)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(ck2)) USING 'sai'"); + createIndex("CREATE INDEX ON %s(VALUES(ck3)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck1 CONTAINS ?", 1L); + assertEquals("Should find 2 rows with bigint value 1", 2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE ck2 CONTAINS ?", (short)10); + assertEquals("Should find 2 rows with smallint value 10", 2, rows.all().size()); + + rows = executeNet("SELECT pk FROM %s WHERE ck3 CONTAINS 100"); + assertEquals("Should find 2 rows with varint value 100", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenListClusteringKeyWithUUIDType() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck frozen>, v int, PRIMARY KEY (pk, ck))"); + + UUID uuid1 = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + UUID uuid2 = UUID.fromString("550e8400-e29b-41d4-a716-446655440001"); + + execute("INSERT INTO %s (pk, ck, v) VALUES (1, ?, 100)", Arrays.asList(uuid1, uuid2)); + execute("INSERT INTO %s (pk, ck, v) VALUES (2, ?, 200)", Arrays.asList(uuid1)); + + assertEquals("Should find 2 rows containing UUID", 2, + execute("SELECT pk FROM %s WHERE ck CONTAINS ? ALLOW FILTERING", uuid1).size()); + + createIndex("CREATE INDEX ON %s(VALUES(ck)) USING 'sai'"); + + beforeAndAfterFlush(() -> { + ResultSet rows = executeNet("SELECT pk FROM %s WHERE ck CONTAINS ?", uuid1); + assertEquals("Should find 2 rows containing UUID", 2, rows.all().size()); + }); + } + + @Test + public void testFrozenListValuesIndexCaseInsensitive() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_list frozen>)"); + + execute("INSERT INTO %s (pk, frozen_list) VALUES (1, ['Apple', 'Banana'])"); + execute("INSERT INTO %s (pk, frozen_list) VALUES (2, ['banana', 'cherry'])"); + execute("INSERT INTO %s (pk, frozen_list) VALUES (3, ['cherry', 'date'])"); + + // Without index, case-sensitive CONTAINS finds only exact matches + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_list CONTAINS 'Banana' ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_list CONTAINS 'banana' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(frozen_list)) USING 'sai' WITH OPTIONS = { 'case_sensitive' : false }"); + + beforeAndAfterFlush(() -> { + // With case-insensitive index, 'banana' matches both 'Banana' and 'banana' + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_list CONTAINS 'banana'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenSetValuesIndexCaseInsensitive() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_set frozen>)"); + + execute("INSERT INTO %s (pk, frozen_set) VALUES (1, {'apple', 'Banana'})"); + execute("INSERT INTO %s (pk, frozen_set) VALUES (2, {'banana', 'cherry'})"); + execute("INSERT INTO %s (pk, frozen_set) VALUES (3, {'cherry', 'date'})"); + + // Without index, case-sensitive CONTAINS finds only exact matches + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_set CONTAINS 'Banana' ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_set CONTAINS 'banana' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(frozen_set)) USING 'sai' WITH OPTIONS = { 'case_sensitive' : false }"); + + beforeAndAfterFlush(() -> { + // With case-insensitive index, 'banana' matches both 'Banana' and 'banana' + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_set CONTAINS 'banana'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapValuesIndexCaseInsensitive() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, {'k1': 'Value1', 'k2': 'v2'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, {'k3': 'value1', 'k4': 'v3'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, {'k5': 'v3', 'k6': 'v4'})"); + + // Without index, case-sensitive CONTAINS finds only exact matches + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map CONTAINS 'Value1' ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map CONTAINS 'value1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(VALUES(frozen_map)) USING 'sai' WITH OPTIONS = { 'case_sensitive' : false }"); + + beforeAndAfterFlush(() -> { + // With case-insensitive index, 'value1' matches both 'Value1' and 'value1' + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map CONTAINS 'value1'"); + assertEquals(2, rows.all().size()); + }); + } + + @Test + public void testFrozenMapKeysIndexCaseInsensitive() throws Throwable + { + createTable("CREATE TABLE %s (pk int PRIMARY KEY, frozen_map frozen>)"); + + execute("INSERT INTO %s (pk, frozen_map) VALUES (1, {'Key1': 'v1', 'k2': 'v2'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (2, {'key1': 'v3', 'k4': 'v4'})"); + execute("INSERT INTO %s (pk, frozen_map) VALUES (3, {'k5': 'v5', 'k6': 'v6'})"); + + // Without index, case-sensitive CONTAINS KEY finds only exact matches + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map CONTAINS KEY 'Key1' ALLOW FILTERING").size()); + assertEquals(1, execute("SELECT pk FROM %s WHERE frozen_map CONTAINS KEY 'key1' ALLOW FILTERING").size()); + + createIndex("CREATE INDEX ON %s(KEYS(frozen_map)) USING 'sai' WITH OPTIONS = { 'case_sensitive' : false }"); + + beforeAndAfterFlush(() -> { + // With case-insensitive index, 'key1' matches both 'Key1' and 'key1' + ResultSet rows = executeNet("SELECT pk FROM %s WHERE frozen_map CONTAINS KEY 'key1'"); + assertEquals(2, rows.all().size()); + }); + } + private void createPopulatedMap(String createIndex) { createTable("CREATE TABLE %s (pk int primary key, value map)");