Skip to content

Commit fa8f616

Browse files
dkropachevclaude
andcommitted
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<collection>` 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<list<text>>); CREATE INDEX ON mymodel (FULL(items)); Fixes #677 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3050cf6 commit fa8f616

3 files changed

Lines changed: 86 additions & 9 deletions

File tree

cassandra/cqlengine/columns.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,30 @@ def to_database(self, value):
837837

838838

839839
class BaseContainerColumn(BaseCollectionColumn):
840-
pass
840+
"""
841+
Base class for container columns (Set, List, Map).
842+
843+
Supports optional freezing for immutable collections.
844+
"""
845+
846+
frozen = False
847+
"""
848+
bool flag, indicates this collection should be frozen (immutable).
849+
Frozen collections use FULL indexes instead of VALUES indexes.
850+
"""
851+
852+
def __init__(self, types, frozen=False, **kwargs):
853+
"""
854+
:param types: a sequence of sub types in this collection
855+
:param frozen: if True, the collection will be frozen (immutable)
856+
"""
857+
self.frozen = frozen
858+
super(BaseContainerColumn, self).__init__(types, **kwargs)
859+
860+
def _apply_frozen(self):
861+
"""Apply frozen wrapper to db_type if frozen=True."""
862+
if self.frozen:
863+
self._freeze_db_type()
841864

842865

843866
class Set(BaseContainerColumn):
@@ -849,18 +872,21 @@ class Set(BaseContainerColumn):
849872

850873
_python_type_hashable = False
851874

852-
def __init__(self, value_type, strict=True, default=set, **kwargs):
875+
def __init__(self, value_type, strict=True, default=set, frozen=False, **kwargs):
853876
"""
854877
:param value_type: a column class indicating the types of the value
855878
:param strict: sets whether non set values will be coerced to set
856879
type on validation, or raise a validation error, defaults to True
880+
:param frozen: if True, the collection will be frozen (immutable) and
881+
use FULL indexes instead of VALUES indexes
857882
"""
858883
self.strict = strict
859-
super(Set, self).__init__((value_type,), default=default, **kwargs)
884+
super(Set, self).__init__((value_type,), frozen=frozen, default=default, **kwargs)
860885
self.value_col = self.types[0]
861886
if not self.value_col._python_type_hashable:
862887
raise ValidationError("Cannot create a Set with unhashable value type (see PYTHON-494)")
863888
self.db_type = 'set<{0}>'.format(self.value_col.db_type)
889+
self._apply_frozen()
864890

865891
def validate(self, value):
866892
val = super(Set, self).validate(value)
@@ -899,13 +925,16 @@ class List(BaseContainerColumn):
899925

900926
_python_type_hashable = False
901927

902-
def __init__(self, value_type, default=list, **kwargs):
928+
def __init__(self, value_type, default=list, frozen=False, **kwargs):
903929
"""
904930
:param value_type: a column class indicating the types of the value
931+
:param frozen: if True, the collection will be frozen (immutable) and
932+
use FULL indexes instead of VALUES indexes
905933
"""
906-
super(List, self).__init__((value_type,), default=default, **kwargs)
934+
super(List, self).__init__((value_type,), frozen=frozen, default=default, **kwargs)
907935
self.value_col = self.types[0]
908936
self.db_type = 'list<{0}>'.format(self.value_col.db_type)
937+
self._apply_frozen()
909938

910939
def validate(self, value):
911940
val = super(List, self).validate(value)
@@ -937,19 +966,22 @@ class Map(BaseContainerColumn):
937966

938967
_python_type_hashable = False
939968

940-
def __init__(self, key_type, value_type, default=dict, **kwargs):
969+
def __init__(self, key_type, value_type, default=dict, frozen=False, **kwargs):
941970
"""
942971
:param key_type: a column class indicating the types of the key
943972
:param value_type: a column class indicating the types of the value
973+
:param frozen: if True, the collection will be frozen (immutable) and
974+
use FULL indexes instead of VALUES indexes
944975
"""
945-
super(Map, self).__init__((key_type, value_type), default=default, **kwargs)
976+
super(Map, self).__init__((key_type, value_type), frozen=frozen, default=default, **kwargs)
946977
self.key_col = self.types[0]
947978
self.value_col = self.types[1]
948979

949980
if not self.key_col._python_type_hashable:
950981
raise ValidationError("Cannot create a Map with unhashable key type (see PYTHON-494)")
951982

952983
self.db_type = 'map<{0}, {1}>'.format(self.key_col.db_type, self.value_col.db_type)
984+
self._apply_frozen()
953985

954986
def validate(self, value):
955987
val = super(Map, self).validate(value)

cassandra/cqlengine/management.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,11 @@ def _sync_table(model, connection=None):
282282

283283
qs = ['CREATE INDEX']
284284
qs += ['ON {0}'.format(cf_name)]
285-
qs += ['("{0}")'.format(column.db_field_name)]
285+
# Use FULL index for frozen collections, VALUES index (implicit) for non-frozen
286+
if isinstance(column, columns.BaseContainerColumn) and column.frozen:
287+
qs += ['(FULL("{0}"))'.format(column.db_field_name)]
288+
else:
289+
qs += ['("{0}")'.format(column.db_field_name)]
286290
qs = ' '.join(qs)
287291
execute(qs, connection=connection)
288292

tests/unit/cqlengine/test_columns.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import unittest
1616

17-
from cassandra.cqlengine.columns import Column
17+
from cassandra.cqlengine.columns import Column, List, Set, Map, Text, Integer
1818

1919

2020
class ColumnTest(unittest.TestCase):
@@ -66,3 +66,44 @@ def test_hash(self):
6666
c0 = Column()
6767
assert id(c0) == c0.__hash__()
6868

69+
70+
class FrozenCollectionTest(unittest.TestCase):
71+
"""Test frozen parameter for collection columns (List, Set, Map)."""
72+
73+
def test_list_default_not_frozen(self):
74+
col = List(Text)
75+
assert col.frozen is False
76+
assert col.db_type == 'list<text>'
77+
78+
def test_list_frozen_true(self):
79+
col = List(Text, frozen=True)
80+
assert col.frozen is True
81+
assert col.db_type == 'frozen<list<text>>'
82+
83+
def test_set_default_not_frozen(self):
84+
col = Set(Text)
85+
assert col.frozen is False
86+
assert col.db_type == 'set<text>'
87+
88+
def test_set_frozen_true(self):
89+
col = Set(Text, frozen=True)
90+
assert col.frozen is True
91+
assert col.db_type == 'frozen<set<text>>'
92+
93+
def test_map_default_not_frozen(self):
94+
col = Map(Text, Integer)
95+
assert col.frozen is False
96+
assert col.db_type == 'map<text, int>'
97+
98+
def test_map_frozen_true(self):
99+
col = Map(Text, Integer, frozen=True)
100+
assert col.frozen is True
101+
assert col.db_type == 'frozen<map<text, int>>'
102+
103+
def test_frozen_with_index(self):
104+
"""Test that frozen collections can be created with index=True."""
105+
col = List(Text, frozen=True, index=True)
106+
assert col.frozen is True
107+
assert col.index is True
108+
assert col.db_type == 'frozen<list<text>>'
109+

0 commit comments

Comments
 (0)