Skip to content

Commit 8d325d5

Browse files
sunil9977maedhroz
authored andcommitted
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
1 parent b50152a commit 8d325d5

19 files changed

Lines changed: 1421 additions & 44 deletions

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
5.1
2+
* Allow value/element indexing on frozen collections in SAI (CASSANDRA-18492)
23
* Add tool to offline dump cluster metadata and the log (CASSANDRA-21129)
34
* Send client warnings when writing to a large partition (CASSANDRA-17258)
45
* Harden the possible range of values for max dictionary size and max total sample size for dictionary training (CASSANDRA-21194)

src/java/org/apache/cassandra/cql3/Relation.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
*/
4545
public final class Relation
4646
{
47+
public static final String FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED = "Map-entry predicates on frozen map column %s are not supported";
48+
4749
/**
4850
* The raw columns'expression.
4951
*/
@@ -204,7 +206,10 @@ public SingleRestriction toRestriction(TableMetadata table, VariableSpecificatio
204206
AbstractType<?> baseType = column.type.unwrap();
205207
checkFalse(baseType instanceof ListType, "Indexes on list entries (%s[index] = value) are not supported.", column.name);
206208
checkTrue(baseType instanceof MapType, "Column %s cannot be used as a map", column.name);
207-
checkTrue(baseType.isMultiCell(), "Map-entry predicates on frozen map column %s are not supported", column.name);
209+
210+
if (column.isClusteringColumn() && baseType.isCollection() && !column.type.isMultiCell())
211+
throw invalidRequest(FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, column.name);
212+
208213
columnsExpression.collectMarkerSpecification(boundNames);
209214
}
210215

src/java/org/apache/cassandra/cql3/restrictions/MergedRestriction.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.apache.cassandra.cql3.Operator;
2929
import org.apache.cassandra.cql3.QueryOptions;
30+
import org.apache.cassandra.cql3.Relation;
3031
import org.apache.cassandra.cql3.functions.Function;
3132
import org.apache.cassandra.db.filter.IndexHints;
3233
import org.apache.cassandra.db.filter.RowFilter;
@@ -126,9 +127,21 @@ private static void validate(SimpleRestriction restriction, SimpleRestriction ot
126127
checkOperator(other);
127128

128129
if (restriction.isContains() != other.isContains())
130+
{
131+
SimpleRestriction mapEntryRestriction = restriction.isContains() ? restriction : other;
132+
if (mapEntryRestriction.isMapElementExpression())
133+
{
134+
ColumnMetadata column = mapEntryRestriction.firstColumn();
135+
if (column.type.isFrozenCollection())
136+
{
137+
throw invalidRequest(Relation.FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, column.name);
138+
}
139+
}
140+
129141
throw invalidRequest("Collection column %s can only be restricted by CONTAINS, CONTAINS KEY, NOT_CONTAINS, NOT_CONTAINS_KEY" +
130142
" or map-entry equality if it already restricted by one of those",
131143
restriction.firstColumn().name);
144+
}
132145

133146
if (restriction.isSlice() && other.isSlice())
134147
{

src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.cassandra.cql3.ColumnsExpression;
3131
import org.apache.cassandra.cql3.Operator;
3232
import org.apache.cassandra.cql3.QueryOptions;
33+
import org.apache.cassandra.cql3.Relation;
3334
import org.apache.cassandra.cql3.functions.Function;
3435
import org.apache.cassandra.cql3.terms.Term;
3536
import org.apache.cassandra.cql3.terms.Terms;
@@ -157,6 +158,14 @@ public boolean isContains()
157158
|| columnsExpression.isMapElementExpression();
158159
}
159160

161+
/**
162+
* Checks if this restriction is a map element expression (e.g., map['key'] = value).
163+
*/
164+
public boolean isMapElementExpression()
165+
{
166+
return columnsExpression.isMapElementExpression();
167+
}
168+
160169
@Override
161170
public boolean needsFilteringOrIndexing()
162171
{
@@ -218,6 +227,18 @@ public boolean isSupportedBy(Index index)
218227
if (isOnToken())
219228
return false;
220229

230+
// For map element expressions, check if the index explicitly supports them.
231+
if (columnsExpression.isMapElementExpression())
232+
{
233+
// If the index directly supports map element expressions, return true
234+
if (index.supportsMapElementExpression())
235+
return true;
236+
237+
// Supports post-filtering only and require ALLOW FILTERING
238+
if (index.supportsFilteringOnMapElementExpression())
239+
return false;
240+
}
241+
221242
for (ColumnMetadata column : columns())
222243
{
223244
if (index.supportsExpression(column, operator))
@@ -415,13 +436,28 @@ else if (isIN())
415436
// TODO only map elements supported for now
416437
if (columnsExpression.isMapElementExpression())
417438
{
439+
// For frozen maps, check if any index on the column can support map entry predicates
440+
// either directly or via filtering. If not, throw an error.
441+
if (column.type.isFrozenCollection())
442+
{
443+
for (Index index : indexRegistry.listIndexes())
444+
{
445+
if (index.dependsOn(column)
446+
&& !index.supportsMapElementExpression()
447+
&& !index.supportsFilteringOnMapElementExpression())
448+
{
449+
throw invalidRequest(Relation.FROZEN_MAP_ENTRY_PREDICATES_NOT_SUPPORTED, column.name);
450+
}
451+
}
452+
}
453+
418454
ByteBuffer key = columnsExpression.element(options);
419455
if (key == null)
420-
throw invalidRequest("Invalid null map key for column %s", firstColumn().name.toCQLString());
456+
throw invalidRequest("Invalid null map key for column %s", column.name.toCQLString());
421457
if (key == ByteBufferUtil.UNSET_BYTE_BUFFER)
422-
throw invalidRequest("Invalid unset map key for column %s", firstColumn().name.toCQLString());
458+
throw invalidRequest("Invalid unset map key for column %s", column.name.toCQLString());
423459
List<ByteBuffer> values = bindAndGet(options);
424-
filter.addMapEquality(firstColumn(), key, operator, values.get(0));
460+
filter.addMapEquality(column, key, operator, values.get(0));
425461
}
426462
break;
427463
default: throw new UnsupportedOperationException();

src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,13 @@ public final class CreateIndexStatement extends AlterSchemaStatement
8181
public static final String ONLY_PARTITION_KEY = "Cannot create secondary index on the only partition key column %s";
8282
public static final String CREATE_ON_FROZEN_COLUMN = "Cannot create %s() index on frozen column %s. Frozen collections are immutable and must be fully " +
8383
"indexed by using the 'full(%s)' modifier";
84-
public static final String FULL_ON_FROZEN_COLLECTIONS = "full() indexes can only be created on frozen collections";
84+
public static final String FULL_ON_FROZEN_COLLECTIONS = "full() non-SAI indexes can only be created on frozen collections";
8585
public static final String NON_COLLECTION_SIMPLE_INDEX = "Cannot create %s() index on %s. Non-collection columns only support simple indexes";
8686
public static final String CREATE_WITH_NON_MAP_TYPE = "Cannot create index on %s of column %s with non-map type";
8787
public static final String CREATE_ON_NON_FROZEN_UDT = "Cannot create index on non-frozen UDT column %s";
88+
public static final String ENTRIES_INDEX_ON_FROZEN_MAP_CLUSTERING_KEY_NOT_SUPPORTED = "Cannot create ENTRIES index on frozen map clustering column '%s'. " +
89+
"Map entry predicates (column[key] = value) are not supported on clustering columns. " +
90+
"Use FULL, KEYS, or VALUES index instead.";
8891
public static final String INDEX_ALREADY_EXISTS = "Index '%s' already exists";
8992
public static final String INDEX_DUPLICATE_OF_EXISTING = "Index %s is a duplicate of existing index %s";
9093
public static final String KEYSPACE_DOES_NOT_MATCH_TABLE = "Keyspace name '%s' doesn't match table name '%s'";
@@ -200,7 +203,7 @@ public Keyspaces apply(ClusterMetadata metadata)
200203

201204
IndexMetadata.Kind kind = attrs.isCustom ? IndexMetadata.Kind.CUSTOM : IndexMetadata.Kind.COMPOSITES;
202205

203-
indexTargets.forEach(t -> validateIndexTarget(table, kind, t));
206+
indexTargets.forEach(t -> validateIndexTarget(table, kind, t, attrs));
204207

205208
String name = null == indexName ? generateIndexName(keyspace, indexTargets) : indexName;
206209

@@ -244,7 +247,7 @@ private void validateCustomIndexColumnName(String name)
244247
throw ire(TOO_LONG_CUSTOM_INDEX_TARGET, name, SchemaConstants.NAME_LENGTH);
245248
}
246249

247-
private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, IndexTarget target)
250+
private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, IndexTarget target, IndexAttributes attrs)
248251
{
249252
ColumnMetadata column = table.getColumn(target.column);
250253

@@ -253,6 +256,8 @@ private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, I
253256

254257
AbstractType<?> baseType = column.type.unwrap();
255258

259+
boolean isNonSAIIndex = !isSAIIndex(attrs);
260+
256261
// TODO: this check needs to be removed with CASSANDRA-20235
257262
if ((kind == IndexMetadata.Kind.CUSTOM))
258263
validateCustomIndexColumnName(target.column.toString());
@@ -283,22 +288,41 @@ private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, I
283288
if (column.isPartitionKey() && table.partitionKeyColumns().size() == 1)
284289
throw ire(ONLY_PARTITION_KEY, column);
285290

286-
if (baseType.isFrozenCollection() && target.type != Type.FULL)
287-
throw ire(CREATE_ON_FROZEN_COLUMN, target.type, column, column.name.toCQLString());
288-
289-
if (!baseType.isFrozenCollection() && target.type == Type.FULL)
291+
if (target.type == Type.FULL && isNonSAIIndex && (!baseType.isCollection() || column.type.isMultiCell()))
290292
throw ire(FULL_ON_FROZEN_COLLECTIONS);
291293

292294
if (!baseType.isCollection() && target.type != Type.SIMPLE)
293295
throw ire(NON_COLLECTION_SIMPLE_INDEX, target.type, column);
294296

295-
if (!(baseType instanceof MapType && baseType.isMultiCell()) && (target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES))
297+
// Frozen collections are only supported with SAI indexes.
298+
if (isNonSAIIndex && baseType.isCollection() && !column.type.isMultiCell())
299+
{
300+
if (target.type == Type.VALUES || target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES)
301+
{
302+
throw ire(CREATE_ON_FROZEN_COLUMN, target.type.toString(), column.name, column.name);
303+
}
304+
}
305+
306+
if (!(baseType instanceof MapType) && (target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES ))
296307
throw ire(CREATE_WITH_NON_MAP_TYPE, target.type, column);
297308

309+
// Can't query map[key]=value on clustering key columns, so ENTRIES index would be not queryable.
310+
if (column.isClusteringColumn() && baseType instanceof MapType && !column.type.isMultiCell()
311+
&& target.type == Type.KEYS_AND_VALUES)
312+
throw ire(ENTRIES_INDEX_ON_FROZEN_MAP_CLUSTERING_KEY_NOT_SUPPORTED, column.name);
313+
298314
if (column.type.isUDT() && column.type.isMultiCell())
299315
throw ire(CREATE_ON_NON_FROZEN_UDT, column);
300316
}
301317

318+
/**
319+
* Checks if the given index attributes represent a Storage Attached Index.
320+
*/
321+
private boolean isSAIIndex(IndexAttributes attrs)
322+
{
323+
return attrs.isCustom && IndexMetadata.isSAIIndex(attrs.customClass);
324+
}
325+
302326
private String generateIndexName(KeyspaceMetadata keyspace, List<IndexTarget> targets)
303327
{
304328
String baseName = targets.size() == 1

src/java/org/apache/cassandra/db/filter/RowFilter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,11 @@ public Operator operator()
527527
return operator;
528528
}
529529

530+
public boolean isMapElementExpression()
531+
{
532+
return kind() == Kind.MAP_ELEMENT;
533+
}
534+
530535
/**
531536
* If this expression is used to query an index, the value to use as
532537
* partition key for that index query.

src/java/org/apache/cassandra/index/Index.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,27 @@ default boolean filtersMultipleContains()
437437
return true;
438438
}
439439

440+
/**
441+
* Return whether this index supports map element expressions on frozen map columns.
442+
*
443+
* @return {@code true} if this index supports map element else {@code false}.
444+
*/
445+
default boolean supportsMapElementExpression()
446+
{
447+
return false;
448+
}
449+
450+
/**
451+
* Returns whether index allows filtering for map element expressions on frozen collections.
452+
* SAI can handle map element predicates via post-filtering.
453+
*
454+
* @return {@code true} if map element expressions can be evaluated via filtering, {@code false} otherwise.
455+
*/
456+
default boolean supportsFilteringOnMapElementExpression()
457+
{
458+
return false;
459+
}
460+
440461
/**
441462
* If the index supports custom search expressions using the
442463
* {@code}SELECT * FROM table WHERE expr(index_name, expression){@code} syntax, this

src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,12 +453,37 @@ public boolean supportsExpression(ColumnMetadata column, Operator operator)
453453
return dependsOn(column) && indexTermType.supports(operator);
454454
}
455455

456+
@Override
457+
public boolean supportsExpression(RowFilter.Expression expression)
458+
{
459+
if (expression.isMapElementExpression() &&
460+
indexTermType.isFrozenCollection() &&
461+
indexTermType.indexTargetType() == IndexTarget.Type.FULL)
462+
463+
return false;
464+
465+
return supportsExpression(expression.column(), expression.operator());
466+
}
467+
456468
@Override
457469
public boolean filtersMultipleContains()
458470
{
459471
return false;
460472
}
461473

474+
@Override
475+
public boolean supportsMapElementExpression()
476+
{
477+
return termType().indexTargetType() == IndexTarget.Type.KEYS_AND_VALUES;
478+
}
479+
480+
@Override
481+
public boolean supportsFilteringOnMapElementExpression()
482+
{
483+
// SAI supports map element expressions via post-filtering on frozen collections
484+
return true;
485+
}
486+
462487
@Override
463488
public AbstractType<?> customExpressionValueType()
464489
{
@@ -784,6 +809,12 @@ public void validateTermSizeForRow(DecoratedKey key, Row row, boolean isClientMu
784809
while (bufferIterator != null && bufferIterator.hasNext())
785810
validateTermSizeForCell(analyzer, key, bufferIterator.next(), isClientMutation, state);
786811
}
812+
else if (indexTermType.isFrozenCollection() && indexTermType.indexTargetType() != IndexTarget.Type.FULL)
813+
{
814+
Iterator<ByteBuffer> bufferIterator = indexTermType.valuesOfFrozenCollection(row, FBUtilities.nowInSeconds());
815+
while (bufferIterator != null && bufferIterator.hasNext())
816+
validateTermSizeForCell(analyzer, key, bufferIterator.next(), isClientMutation, state);
817+
}
787818
else
788819
{
789820
ByteBuffer value = indexTermType.valueOf(key, row, FBUtilities.nowInSeconds());

src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.slf4j.Logger;
3333
import org.slf4j.LoggerFactory;
3434

35+
import org.apache.cassandra.cql3.statements.schema.IndexTarget;
3536
import org.apache.cassandra.db.rows.Row;
3637
import org.apache.cassandra.index.sai.StorageAttachedIndex;
3738
import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer;
@@ -94,6 +95,18 @@ public void addRow(PrimaryKey key, Row row, long sstableRowId) throws IOExceptio
9495
}
9596
}
9697
}
98+
else if (index.termType().isFrozenCollection() && index.termType().indexTargetType() != IndexTarget.Type.FULL)
99+
{
100+
Iterator<ByteBuffer> valueIterator = index.termType().valuesOfFrozenCollection(row, nowInSec);
101+
if (valueIterator != null)
102+
{
103+
while (valueIterator.hasNext())
104+
{
105+
ByteBuffer value = valueIterator.next();
106+
addTerm(index.termType().asIndexBytes(value.duplicate()), key, sstableRowId);
107+
}
108+
}
109+
}
97110
else
98111
{
99112
ByteBuffer value = index.termType().valueOf(key.partitionKey(), row, nowInSec);

src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import com.google.common.annotations.VisibleForTesting;
3333

34+
import org.apache.cassandra.cql3.statements.schema.IndexTarget;
3435
import org.apache.cassandra.db.DecoratedKey;
3536
import org.apache.cassandra.db.lifecycle.ILifecycleTransaction;
3637
import org.apache.cassandra.db.memtable.Memtable;
@@ -76,6 +77,18 @@ public long index(DecoratedKey key, Row row, Memtable mt)
7677
}
7778
}
7879
}
80+
else if (index.termType().isFrozenCollection() && index.termType().indexTargetType() != IndexTarget.Type.FULL)
81+
{
82+
Iterator<ByteBuffer> bufferIterator = index.termType().valuesOfFrozenCollection(row, FBUtilities.nowInSeconds());
83+
if (bufferIterator != null)
84+
{
85+
while (bufferIterator.hasNext())
86+
{
87+
ByteBuffer value = bufferIterator.next();
88+
bytes += target.index(key, row.clustering(), value);
89+
}
90+
}
91+
}
7992
else
8093
{
8194
ByteBuffer value = index.termType().valueOf(key, row, FBUtilities.nowInSeconds());

0 commit comments

Comments
 (0)