Skip to content

Commit 2f359cd

Browse files
committed
Merge pull request apache#483 from datastax/478
PYTHON-478 - allow nested collections in cqlengine
2 parents f780f6c + 4dfe894 commit 2f359cd

4 files changed

Lines changed: 108 additions & 75 deletions

File tree

cassandra/cqlengine/columns.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class Column(object):
8080

8181
instance_counter = 0
8282

83+
_python_type_hashable = True
84+
8385
primary_key = False
8486
"""
8587
bool flag, indicates this column is a primary key. The first primary key defined
@@ -611,7 +613,6 @@ class BaseContainerColumn(Column):
611613
612614
https://cassandra.apache.org/doc/cql3/CQL.html#collections
613615
"""
614-
615616
def __init__(self, types, **kwargs):
616617
"""
617618
:param types: a sequence of sub types in this collection
@@ -621,14 +622,14 @@ def __init__(self, types, **kwargs):
621622
inheritance_comparator = issubclass if isinstance(t, type) else isinstance
622623
if not inheritance_comparator(t, Column):
623624
raise ValidationError("%s is not a column class" % (t,))
624-
if inheritance_comparator(t, BaseContainerColumn): # should go away with PYTHON-478
625-
raise ValidationError('container types cannot be nested')
626625
if t.db_type is None:
627626
raise ValidationError("%s is an abstract type" % (t,))
627+
inst = t() if isinstance(t, type) else t
628+
if isinstance(t, BaseContainerColumn):
629+
inst._freeze_db_type()
630+
instances.append(inst)
628631

629-
instances.append(t() if isinstance(t, type) else t)
630632
self.types = instances
631-
632633
super(BaseContainerColumn, self).__init__(**kwargs)
633634

634635
def validate(self, value):
@@ -642,6 +643,10 @@ def validate(self, value):
642643
def _val_is_null(self, val):
643644
return not val
644645

646+
def _freeze_db_type(self):
647+
if not self.db_type.startswith('frozen'):
648+
self.db_type = "frozen<%s>" % (self.db_type,)
649+
645650
@property
646651
def sub_types(self):
647652
return self.types
@@ -653,24 +658,27 @@ class Set(BaseContainerColumn):
653658
654659
http://www.datastax.com/documentation/cql/3.1/cql/cql_using/use_set_t.html
655660
"""
661+
662+
_python_type_hashable = False
663+
656664
def __init__(self, value_type, strict=True, default=set, **kwargs):
657665
"""
658666
:param value_type: a column class indicating the types of the value
659667
:param strict: sets whether non set values will be coerced to set
660668
type on validation, or raise a validation error, defaults to True
661669
"""
662670
self.strict = strict
663-
self.db_type = 'set<{0}>'.format(value_type.db_type)
664-
665671
super(Set, self).__init__((value_type,), default=default, **kwargs)
666-
667672
self.value_col = self.types[0]
673+
if not self.value_col._python_type_hashable:
674+
raise ValidationError("Cannot create a Set with unhashable value type (see PYTHON-494)")
675+
self.db_type = 'set<{0}>'.format(self.value_col.db_type)
668676

669677
def validate(self, value):
670678
val = super(Set, self).validate(value)
671679
if val is None:
672680
return
673-
types = (set,) if self.strict else (set, list, tuple)
681+
types = (set, util.SortedSet) if self.strict else (set, util.SortedSet, list, tuple)
674682
if not isinstance(val, types):
675683
if self.strict:
676684
raise ValidationError('{0} {1} is not a set object'.format(self.column_name, val))
@@ -679,7 +687,8 @@ def validate(self, value):
679687

680688
if None in val:
681689
raise ValidationError("{0} None not allowed in a set".format(self.column_name))
682-
690+
# TODO: stop doing this conversion because it doesn't support non-hashable collections as keys (cassandra does)
691+
# will need to start using the cassandra.util types in the next major rev (PYTHON-494)
683692
return set(self.value_col.validate(v) for v in val)
684693

685694
def to_python(self, value):
@@ -699,15 +708,16 @@ class List(BaseContainerColumn):
699708
700709
http://www.datastax.com/documentation/cql/3.1/cql/cql_using/use_list_t.html
701710
"""
711+
712+
_python_type_hashable = False
713+
702714
def __init__(self, value_type, default=list, **kwargs):
703715
"""
704716
:param value_type: a column class indicating the types of the value
705717
"""
706-
self.db_type = 'list<{0}>'.format(value_type.db_type)
707-
708718
super(List, self).__init__((value_type,), default=default, **kwargs)
709-
710719
self.value_col = self.types[0]
720+
self.db_type = 'list<{0}>'.format(self.value_col.db_type)
711721

712722
def validate(self, value):
713723
val = super(List, self).validate(value)
@@ -736,27 +746,33 @@ class Map(BaseContainerColumn):
736746
737747
http://www.datastax.com/documentation/cql/3.1/cql/cql_using/use_map_t.html
738748
"""
749+
750+
_python_type_hashable = False
751+
739752
def __init__(self, key_type, value_type, default=dict, **kwargs):
740753
"""
741754
:param key_type: a column class indicating the types of the key
742755
:param value_type: a column class indicating the types of the value
743756
"""
744-
745-
self.db_type = 'map<{0}, {1}>'.format(key_type.db_type, value_type.db_type)
746-
747757
super(Map, self).__init__((key_type, value_type), default=default, **kwargs)
748-
749758
self.key_col = self.types[0]
750759
self.value_col = self.types[1]
751760

761+
if not self.key_col._python_type_hashable:
762+
raise ValidationError("Cannot create a Map with unhashable key type (see PYTHON-494)")
763+
764+
self.db_type = 'map<{0}, {1}>'.format(self.key_col.db_type, self.value_col.db_type)
765+
752766
def validate(self, value):
753767
val = super(Map, self).validate(value)
754768
if val is None:
755769
return
756-
if not isinstance(val, dict):
770+
if not isinstance(val, (dict, util.OrderedMap)):
757771
raise ValidationError('{0} {1} is not a dict object'.format(self.column_name, val))
758772
if None in val:
759773
raise ValidationError("{0} None is not allowed in a map".format(self.column_name))
774+
# TODO: stop doing this conversion because it doesn't support non-hashable collections as keys (cassandra does)
775+
# will need to start using the cassandra.util types in the next major rev (PYTHON-494)
760776
return dict((self.key_col.validate(k), self.value_col.validate(v)) for k, v in val.items())
761777

762778
def to_python(self, value):
@@ -771,17 +787,6 @@ def to_database(self, value):
771787
return dict((self.key_col.to_database(k), self.value_col.to_database(v)) for k, v in value.items())
772788

773789

774-
class UDTValueManager(BaseValueManager):
775-
@property
776-
def changed(self):
777-
return self.value != self.previous_value or (self.value is not None and self.value.has_changed_fields())
778-
779-
def reset_previous_value(self):
780-
if self.value is not None:
781-
self.value.reset_changed_fields()
782-
self.previous_value = copy(self.value)
783-
784-
785790
class Tuple(BaseContainerColumn):
786791
"""
787792
Stores a fixed-length set of positional values
@@ -792,13 +797,10 @@ def __init__(self, *args, **kwargs):
792797
"""
793798
:param args: column types representing tuple composition
794799
"""
795-
796-
self.db_type = 'tuple<{0}>'.format(', '.join(typ.db_type for typ in args))
797-
798800
if not args:
799801
raise ValueError("Tuple must specify at least one inner type")
800-
801802
super(Tuple, self).__init__(args, **kwargs)
803+
self.db_type = 'tuple<{0}>'.format(', '.join(typ.db_type for typ in self.types))
802804

803805
def validate(self, value):
804806
val = super(Tuple, self).validate(value)
@@ -820,6 +822,17 @@ def to_database(self, value):
820822
return tuple(t.to_database(v) for t, v in zip(self.types, value))
821823

822824

825+
class UDTValueManager(BaseValueManager):
826+
@property
827+
def changed(self):
828+
return self.value != self.previous_value or (self.value is not None and self.value.has_changed_fields())
829+
830+
def reset_previous_value(self):
831+
if self.value is not None:
832+
self.value.reset_changed_fields()
833+
self.previous_value = copy(self.value)
834+
835+
823836
class UserDefinedType(Column):
824837
"""
825838
User Defined Type column

cassandra/util.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ class OrderedMap(Mapping):
746746
...
747747
)
748748
749-
This class dervies from the (immutable) Mapping API. Objects in these maps
749+
This class derives from the (immutable) Mapping API. Objects in these maps
750750
are not intended be modified.
751751
752752
\* Note: Because of the way Cassandra encodes nested types, when using the
@@ -782,13 +782,24 @@ def _insert(self, key, value):
782782
self._items.append((key, value))
783783
self._index[flat_key] = len(self._items) - 1
784784

785+
__setitem__ = _insert
786+
785787
def __getitem__(self, key):
786788
try:
787789
index = self._index[self._serialize_key(key)]
788790
return self._items[index][1]
789791
except KeyError:
790792
raise KeyError(str(key))
791793

794+
def __delitem__(self, key):
795+
# not efficient -- for convenience only
796+
try:
797+
index = self._index.pop(self._serialize_key(key))
798+
self._index = dict((k, i if i < index else i - 1) for k, i in self._index.items())
799+
self._items.pop(index)
800+
except KeyError:
801+
raise KeyError(str(key))
802+
792803
def __iter__(self):
793804
for i in self._items:
794805
yield i[0]

0 commit comments

Comments
 (0)