@@ -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
0 commit comments