apache/cassandra · error · InvalidRequestException

Cannot create index on non-frozen UDT column

Error message

Cannot create index on non-frozen UDT column %s

What it means

Cassandra rejects CREATE INDEX when the target column is a user-defined type (UDT) that is not frozen (i.e. isMultiCell). Non-frozen UDTs store each field in separate cells, so a plain index over the whole column cannot be built; only frozen UDT columns (or indexing specific non-frozen UDT sub-fields) are supported.

Solutions

  1. Declare the column frozen in the table schema: `address frozen<address_udt>`, then create the index.
  2. Index a specific non-frozen UDT field instead of the whole column: CREATE INDEX ON t(keys(addr)) or ON t(addr.zip) style UDT-field targets if supported.
  3. Use Storage Attached Index (SAI) via `CREATE CUSTOM INDEX ... USING 'StorageAttachedIndex'` if the field-level indexing needed is supported.
  4. Re-model the data: split the UDT fields into regular columns that can each be indexed.

Example fix

// before
CREATE TABLE t (id int PRIMARY KEY, addr address_udt);
CREATE INDEX ON t(addr);

// after
CREATE TABLE t (id int PRIMARY KEY, addr frozen<address_udt>);
CREATE INDEX ON t(addr);
Defensive patterns

Strategy: validation

Validate before calling

if (columnType.startsWith("frozen<")) { /* indexable */ } else if (isUdt(columnType)) throw new IllegalArgumentException("Declare column as frozen<udt> before indexing: " + columnName);

Type guard

boolean isIndexableUdt(String colType) { return colType != null && colType.matches("frozen<.*>"); }

Try / catch

try { session.execute(createIndexCql); } catch (InvalidQueryException e) { if (e.getMessage().contains("non-frozen UDT")) { /* alter schema to frozen<> or index a UDT field */ } else throw e; }

Prevention

When it happens

Trigger: CREATE INDEX ... ON table(udt_column) where udt_column is declared as a multi-cell (non-frozen) UDT, e.g. a column typed `address <address_udt>` without the frozen<> wrapper. Raised from validateIndexTarget during CREATE INDEX statement validation.

Common situations: Developers migrate a schema from frozen to non-frozen UDTs to get partial updates, then forget existing secondary indexes are no longer valid; or they model a UDT column and try to index it directly like a scalar column.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of apache/cassandra@88fd0f6a0e (2026-09-10). Data as JSON: /api/errors/57c8f6a6576db2af. Report an issue: GitHub.

Appendix: source

Thrown at src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java:315

        // 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
                        ? IndexMetadata.generateDefaultIndexName(tableName, targets.get(0).column)
                        : IndexMetadata.generateDefaultIndexName(tableName);
        return keyspace.findAvailableIndexName(baseName);
    }

View on GitHub (pinned to 88fd0f6a0e)