Allow value/element indexing on frozen collections in SAI

patch by Sunil Ramchandra Pawar; reviewed by Caleb Rackliffe, David Capwell, and Andres de la Peña for CASSANDRA-18492
This commit is contained in:
Sunil Ramchandra Pawar 2026-01-15 11:21:07 +05:30 committed by Caleb Rackliffe
parent b50152af22
commit 8d325d50ed
19 changed files with 1421 additions and 44 deletions

View File

@ -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)

View File

@ -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);
}

View File

@ -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())
{

View File

@ -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<ByteBuffer> values = bindAndGet(options);
filter.addMapEquality(firstColumn(), key, operator, values.get(0));
filter.addMapEquality(column, key, operator, values.get(0));
}
break;
default: throw new UnsupportedOperationException();

View File

@ -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<IndexTarget> targets)
{
String baseName = targets.size() == 1

View File

@ -527,6 +527,11 @@ public class RowFilter implements Iterable<RowFilter.Expression>
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.

View File

@ -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

View File

@ -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<ByteBuffer> 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());

View File

@ -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<ByteBuffer> 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);

View File

@ -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<ByteBuffer> 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());

View File

@ -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<ByteBuffer> 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<ByteBuffer> 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;

View File

@ -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<ByteBuffer> 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<ByteBuffer> 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<ByteBuffer> extractMapElements(List<ByteBuffer> elements)
{
List<ByteBuffer> 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<ByteBuffer> 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<Capability> 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<ByteBuffer> 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:

View File

@ -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()
{

View File

@ -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));
}

View File

@ -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

View File

@ -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<map<int, int>>, 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);
}

View File

@ -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'");
}
}

View File

@ -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");