From 8c0f49d34715c31aa79122789d828890b7873f7f Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Sat, 31 Jan 2026 11:38:40 -0400 Subject: [PATCH] Add frozen parameter to collection columns with FULL index support Add a `frozen` parameter to List, Set, and Map collection columns in cqlengine. When `frozen=True`, the collection is wrapped as `frozen` and indexes use `FULL(column)` instead of the implicit VALUES indexing. This enables creating frozen collections with FULL indexes: class MyModel(Model): items = columns.List(columns.Text, frozen=True, index=True) Generates: CREATE TABLE mymodel (items frozen>); CREATE INDEX ON mymodel (FULL(items)); Fixes #677 Co-Authored-By: Claude Opus 4.5 --- cassandra/cqlengine/columns.py | 46 +++++++++++++++++++++++----- cassandra/cqlengine/management.py | 6 +++- tests/unit/cqlengine/test_columns.py | 43 +++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 9 deletions(-) diff --git a/cassandra/cqlengine/columns.py b/cassandra/cqlengine/columns.py index 3d85587524..509b606ccc 100644 --- a/cassandra/cqlengine/columns.py +++ b/cassandra/cqlengine/columns.py @@ -837,7 +837,30 @@ def to_database(self, value): class BaseContainerColumn(BaseCollectionColumn): - pass + """ + Base class for container columns (Set, List, Map). + + Supports optional freezing for immutable collections. + """ + + frozen = False + """ + bool flag, indicates this collection should be frozen (immutable). + Frozen collections use FULL indexes instead of VALUES indexes. + """ + + def __init__(self, types, frozen=False, **kwargs): + """ + :param types: a sequence of sub types in this collection + :param frozen: if True, the collection will be frozen (immutable) + """ + self.frozen = frozen + super(BaseContainerColumn, self).__init__(types, **kwargs) + + def _apply_frozen(self): + """Apply frozen wrapper to db_type if frozen=True.""" + if self.frozen: + self._freeze_db_type() class Set(BaseContainerColumn): @@ -849,18 +872,21 @@ class Set(BaseContainerColumn): _python_type_hashable = False - def __init__(self, value_type, strict=True, default=set, **kwargs): + def __init__(self, value_type, strict=True, default=set, frozen=False, **kwargs): """ :param value_type: a column class indicating the types of the value :param strict: sets whether non set values will be coerced to set type on validation, or raise a validation error, defaults to True + :param frozen: if True, the collection will be frozen (immutable) and + use FULL indexes instead of VALUES indexes """ self.strict = strict - super(Set, self).__init__((value_type,), default=default, **kwargs) + super(Set, self).__init__((value_type,), frozen=frozen, default=default, **kwargs) self.value_col = self.types[0] if not self.value_col._python_type_hashable: raise ValidationError("Cannot create a Set with unhashable value type (see PYTHON-494)") self.db_type = 'set<{0}>'.format(self.value_col.db_type) + self._apply_frozen() def validate(self, value): val = super(Set, self).validate(value) @@ -899,13 +925,16 @@ class List(BaseContainerColumn): _python_type_hashable = False - def __init__(self, value_type, default=list, **kwargs): + def __init__(self, value_type, default=list, frozen=False, **kwargs): """ :param value_type: a column class indicating the types of the value + :param frozen: if True, the collection will be frozen (immutable) and + use FULL indexes instead of VALUES indexes """ - super(List, self).__init__((value_type,), default=default, **kwargs) + super(List, self).__init__((value_type,), frozen=frozen, default=default, **kwargs) self.value_col = self.types[0] self.db_type = 'list<{0}>'.format(self.value_col.db_type) + self._apply_frozen() def validate(self, value): val = super(List, self).validate(value) @@ -937,12 +966,14 @@ class Map(BaseContainerColumn): _python_type_hashable = False - def __init__(self, key_type, value_type, default=dict, **kwargs): + def __init__(self, key_type, value_type, default=dict, frozen=False, **kwargs): """ :param key_type: a column class indicating the types of the key :param value_type: a column class indicating the types of the value + :param frozen: if True, the collection will be frozen (immutable) and + use FULL indexes instead of VALUES indexes """ - super(Map, self).__init__((key_type, value_type), default=default, **kwargs) + super(Map, self).__init__((key_type, value_type), frozen=frozen, default=default, **kwargs) self.key_col = self.types[0] self.value_col = self.types[1] @@ -950,6 +981,7 @@ def __init__(self, key_type, value_type, default=dict, **kwargs): raise ValidationError("Cannot create a Map with unhashable key type (see PYTHON-494)") self.db_type = 'map<{0}, {1}>'.format(self.key_col.db_type, self.value_col.db_type) + self._apply_frozen() def validate(self, value): val = super(Map, self).validate(value) diff --git a/cassandra/cqlengine/management.py b/cassandra/cqlengine/management.py index 4ac4192a80..d6dc44119a 100644 --- a/cassandra/cqlengine/management.py +++ b/cassandra/cqlengine/management.py @@ -282,7 +282,11 @@ def _sync_table(model, connection=None): qs = ['CREATE INDEX'] qs += ['ON {0}'.format(cf_name)] - qs += ['("{0}")'.format(column.db_field_name)] + # Use FULL index for frozen collections, VALUES index (implicit) for non-frozen + if isinstance(column, columns.BaseContainerColumn) and column.frozen: + qs += ['(FULL("{0}"))'.format(column.db_field_name)] + else: + qs += ['("{0}")'.format(column.db_field_name)] qs = ' '.join(qs) execute(qs, connection=connection) diff --git a/tests/unit/cqlengine/test_columns.py b/tests/unit/cqlengine/test_columns.py index cba57e88a6..136e8ba339 100644 --- a/tests/unit/cqlengine/test_columns.py +++ b/tests/unit/cqlengine/test_columns.py @@ -14,7 +14,7 @@ import unittest -from cassandra.cqlengine.columns import Column +from cassandra.cqlengine.columns import Column, List, Set, Map, Text, Integer class ColumnTest(unittest.TestCase): @@ -66,3 +66,44 @@ def test_hash(self): c0 = Column() assert id(c0) == c0.__hash__() + +class FrozenCollectionTest(unittest.TestCase): + """Test frozen parameter for collection columns (List, Set, Map).""" + + def test_list_default_not_frozen(self): + col = List(Text) + assert col.frozen is False + assert col.db_type == 'list' + + def test_list_frozen_true(self): + col = List(Text, frozen=True) + assert col.frozen is True + assert col.db_type == 'frozen>' + + def test_set_default_not_frozen(self): + col = Set(Text) + assert col.frozen is False + assert col.db_type == 'set' + + def test_set_frozen_true(self): + col = Set(Text, frozen=True) + assert col.frozen is True + assert col.db_type == 'frozen>' + + def test_map_default_not_frozen(self): + col = Map(Text, Integer) + assert col.frozen is False + assert col.db_type == 'map' + + def test_map_frozen_true(self): + col = Map(Text, Integer, frozen=True) + assert col.frozen is True + assert col.db_type == 'frozen>' + + def test_frozen_with_index(self): + """Test that frozen collections can be created with index=True.""" + col = List(Text, frozen=True, index=True) + assert col.frozen is True + assert col.index is True + assert col.db_type == 'frozen>' +