Skip to content

Commit e2f7ab3

Browse files
author
bjmb
committed
Merged python-657 branch
2 parents 0578dbb + fba89ce commit e2f7ab3

6 files changed

Lines changed: 210 additions & 45 deletions

File tree

cassandra/cqlengine/columns.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,19 @@ def changed(self):
4747
:rtype: boolean
4848
4949
"""
50-
return self.value != self.previous_value
50+
if self.explicit:
51+
return self.value != self.previous_value
52+
53+
if isinstance(self.column, BaseContainerColumn):
54+
default_value = self.column.get_default()
55+
if self.column._val_is_null(default_value):
56+
return not self.column._val_is_null(self.value) and self.value != self.previous_value
57+
elif self.previous_value is None:
58+
return self.value != default_value
59+
60+
return self.value != self.previous_value
61+
62+
return False
5163

5264
def reset_previous_value(self):
5365
self.previous_value = deepcopy(self.value)
@@ -57,6 +69,7 @@ def getval(self):
5769

5870
def setval(self, val):
5971
self.value = val
72+
self.explicit = True
6073

6174
def delval(self):
6275
self.value = None

cassandra/cqlengine/models.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,6 @@ def __init__(self, **values):
407407
value = column.to_python(value)
408408
value_mngr = column.value_manager(self, column, value)
409409
value_mngr.explicit = name in values
410-
if not value_mngr.explicit and column.has_default:
411-
value_mngr.previous_value = value
412410
self._values[name] = value_mngr
413411

414412
def __repr__(self):
@@ -486,11 +484,12 @@ def _construct_instance(cls, values):
486484
klass = cls
487485

488486
instance = klass(**values)
489-
instance._set_persisted()
487+
instance._set_persisted(force=True)
490488
return instance
491489

492-
def _set_persisted(self):
493-
for v in self._values.values():
490+
def _set_persisted(self, force=False):
491+
# ensure we don't modify to any values not affected by the last save/update
492+
for v in [v for v in self._values.values() if v.changed or force]:
494493
v.reset_previous_value()
495494
v.explicit = False
496495
self._is_persisted = True
@@ -591,6 +590,10 @@ def _raw_column_family_name(cls):
591590

592591
return cls._table_name
593592

593+
def _set_column_value(self, name, value):
594+
"""Function to change a column value without changing the value manager states"""
595+
self._values[name].value = value # internal assignement, skip the main setter
596+
594597
def validate(self):
595598
"""
596599
Cleans and validates the field values
@@ -600,7 +603,7 @@ def validate(self):
600603
if v is None and not self._values[name].explicit and col.has_default:
601604
v = col.get_default()
602605
val = col.validate(v)
603-
setattr(self, name, val)
606+
self._set_column_value(name, val)
604607

605608
# Let an instance be used like a dict of its columns keys/values
606609
def __iter__(self):

cassandra/cqlengine/query.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,9 @@ def save(self):
14571457
if self.instance._values[name].changed:
14581458
nulled_fields.add(col.db_field_name)
14591459
continue
1460+
if col.has_default and not self.instance._values[name].changed:
1461+
# Ensure default columns included in a save() are marked as explicit, to get them *persisted* properly
1462+
self.instance._values[name].explicit = True
14601463
insert.add_assignment(col, getattr(self.instance, name, None))
14611464

14621465
# skip query execution if it's empty

tests/integration/cqlengine/model/test_model_io.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from cassandra.cqlengine.models import Model
3131
from cassandra.query import SimpleStatement
3232
from cassandra.util import Date, Time
33-
from cassandra.cqltypes import Int32Type
3433
from cassandra.cqlengine.statements import SelectStatement, DeleteStatement, WhereClause
3534
from cassandra.cqlengine.operators import EqualsOperator
3635

@@ -483,37 +482,6 @@ def test_previous_value_tracking_on_instantiation(self):
483482
self.assertTrue(self.instance._values['count'].previous_value is None)
484483
self.assertTrue(self.instance.count is None)
485484

486-
def test_value_override_with_default(self):
487-
"""
488-
Updating a row with a new Model instance shouldn't set columns to defaults
489-
490-
@since 3.9
491-
@jira_ticket PYTHON-657
492-
@expected_result column value should not change
493-
494-
@test_category object_mapper
495-
"""
496-
class ModelWithDefault(Model):
497-
id = columns.Integer(primary_key=True)
498-
mf = columns.Map(columns.Integer, columns.Integer)
499-
dummy = columns.Integer(default=42)
500-
sync_table(ModelWithDefault)
501-
502-
initial = ModelWithDefault(id=1, mf={0: 0}, dummy=0)
503-
initial.save()
504-
505-
session = cassandra.cluster.Cluster().connect()
506-
session.execute('USE ' + DEFAULT_KEYSPACE)
507-
self.assertEqual(
508-
list(session.execute('SELECT * from model_with_default'))[0].dummy, 0
509-
)
510-
511-
second = ModelWithDefault(id=1)
512-
second.update(mf={0: 1})
513-
self.assertEqual(
514-
list(session.execute('SELECT * from model_with_default'))[0].dummy, 0
515-
)
516-
517485
def test_previous_value_tracking_on_instantiation_with_default(self):
518486

519487
class TestDefaultValueTracking(Model):
@@ -546,16 +514,12 @@ class TestDefaultValueTracking(Model):
546514
# yet.
547515
self.assertTrue(instance._values['id'].previous_value is None)
548516
self.assertTrue(instance._values['int1'].previous_value is None)
517+
self.assertTrue(instance._values['int2'].previous_value is None)
549518
self.assertTrue(instance._values['int3'].previous_value is None)
519+
self.assertTrue(instance._values['int4'].previous_value is None)
550520
self.assertTrue(instance._values['int5'].previous_value is None)
551521
self.assertTrue(instance._values['int6'].previous_value is None)
552522

553-
# When a column has a default value and that field has no explicit value specified at
554-
# the instance creation, the previous_value should be set to the default value to
555-
# avoid any undesired update
556-
self.assertEqual(instance._values['int2'].previous_value, 456)
557-
self.assertIsNotNone(instance._values['int4'])
558-
559523
# All explicitely set columns, and those with default values are
560524
# flagged has changed.
561525
self.assertTrue(set(instance.get_changed_columns()) == set([

tests/integration/cqlengine/model/test_updates.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,157 @@ def test_primary_key_update_failure(self):
134134
m0 = TestUpdateModel.create(count=5, text='monkey')
135135
with self.assertRaises(ValidationError):
136136
m0.update(partition=uuid4())
137+
138+
139+
class ModelWithDefault(Model):
140+
id = columns.Integer(primary_key=True)
141+
mf = columns.Map(columns.Integer, columns.Integer)
142+
dummy = columns.Integer(default=42)
143+
144+
145+
class ModelWithDefaultCollection(Model):
146+
id = columns.Integer(primary_key=True)
147+
mf = columns.Map(columns.Integer, columns.Integer, default={2:2})
148+
dummy = columns.Integer(default=42)
149+
150+
151+
class ModelWithDefaultTests(BaseCassEngTestCase):
152+
def setUp(self):
153+
sync_table(ModelWithDefault)
154+
155+
def tearDown(self):
156+
drop_table(ModelWithDefault)
157+
158+
def test_value_override_with_default(self):
159+
"""
160+
Updating a row with a new Model instance shouldn't set columns to defaults
161+
162+
@since 3.9
163+
@jira_ticket PYTHON-657
164+
@expected_result column value should not change
165+
166+
@test_category object_mapper
167+
"""
168+
initial = ModelWithDefault(id=1, mf={0: 0}, dummy=0)
169+
initial.save()
170+
171+
self.assertEqual(ModelWithDefault.objects().all().get()._as_dict(),
172+
{'id': 1, 'dummy': 0, 'mf': {0: 0}})
173+
174+
second = ModelWithDefault(id=1)
175+
second.update(mf={0: 1})
176+
177+
self.assertEqual(ModelWithDefault.objects().all().get()._as_dict(),
178+
{'id': 1, 'dummy': 0, 'mf': {0: 1}})
179+
180+
def test_value_is_written_if_is_default(self):
181+
"""
182+
Check if the we try to update with the default value, the update
183+
happens correctly
184+
@since 3.9
185+
@jira_ticket PYTHON-657
186+
@expected_result column value should be updated
187+
188+
@test_category object_mapper
189+
:return:
190+
"""
191+
initial = ModelWithDefault(id=1)
192+
initial.mf = {0: 0}
193+
initial.dummy = 42
194+
initial.update()
195+
196+
self.assertEqual(ModelWithDefault.objects().all().get()._as_dict(),
197+
{'id': 1, 'dummy': 42, 'mf': {0: 0}})
198+
199+
def test_null_update_is_respected(self):
200+
"""
201+
Check if the we try to update with None under particular
202+
circumstances, it works correctly
203+
@since 3.9
204+
@jira_ticket PYTHON-657
205+
@expected_result column value should be updated to None
206+
207+
@test_category object_mapper
208+
:return:
209+
"""
210+
ModelWithDefault.create(id=1, mf={0: 0}).save()
211+
212+
q = ModelWithDefault.objects.all().allow_filtering()
213+
obj = q.filter(id=1).get()
214+
215+
obj.update(dummy=None)
216+
217+
self.assertEqual(ModelWithDefault.objects().all().get()._as_dict(),
218+
{'id': 1, 'dummy': None, 'mf': {0: 0}})
219+
220+
def test_only_set_values_is_updated(self):
221+
"""
222+
Test the updates work as expected when an object is deleted
223+
@since 3.9
224+
@jira_ticket PYTHON-657
225+
@expected_result the non updated column is None and the
226+
updated column has the set value
227+
228+
@test_category object_mapper
229+
"""
230+
231+
ModelWithDefault.create(id=1, mf={1: 1}, dummy=1).save()
232+
233+
item = ModelWithDefault.filter(id=1).first()
234+
ModelWithDefault.objects(id=1).delete()
235+
item.mf = {1: 2}
236+
237+
item.save()
238+
239+
self.assertEqual(ModelWithDefault.objects().all().get()._as_dict(),
240+
{'id': 1, 'dummy': None, 'mf': {1: 2}})
241+
242+
def test_collections(self):
243+
"""
244+
Test the updates work as expected when an object is deleted
245+
@since 3.9
246+
@jira_ticket PYTHON-657
247+
@expected_result the non updated column is None and the
248+
updated column has the set value
249+
250+
@test_category object_mapper
251+
"""
252+
ModelWithDefault.create(id=1, mf={1: 1, 2: 1}, dummy=1).save()
253+
item = ModelWithDefault.filter(id=1).first()
254+
255+
item.update(mf={2:1})
256+
self.assertEqual(ModelWithDefault.objects().all().get()._as_dict(),
257+
{'id': 1, 'dummy': 1, 'mf': {2: 1}})
258+
259+
def test_collection_with_default(self):
260+
"""
261+
Test the updates work as expected when an object is deleted
262+
@since 3.9
263+
@jira_ticket PYTHON-657
264+
@expected_result the non updated column is None and the
265+
updated column has the set value
266+
267+
@test_category object_mapper
268+
"""
269+
sync_table(ModelWithDefaultCollection)
270+
item = ModelWithDefaultCollection.create(id=1, mf={1: 1}, dummy=1).save()
271+
self.assertEqual(ModelWithDefaultCollection.objects().all().get()._as_dict(),
272+
{'id': 1, 'dummy': 1, 'mf': {1: 1}})
273+
274+
item.update(mf={2: 2})
275+
self.assertEqual(ModelWithDefaultCollection.objects().all().get()._as_dict(),
276+
{'id': 1, 'dummy': 1, 'mf': {2: 2}})
277+
278+
item.update(mf=None)
279+
self.assertEqual(ModelWithDefaultCollection.objects().all().get()._as_dict(),
280+
{'id': 1, 'dummy': 1, 'mf': {}})
281+
282+
item = ModelWithDefaultCollection.create(id=2, dummy=2).save()
283+
self.assertEqual(ModelWithDefaultCollection.objects().all().get(id=2)._as_dict(),
284+
{'id': 2, 'dummy': 2, 'mf': {2: 2}})
285+
286+
item.update(mf={1: 1, 4: 4})
287+
self.assertEqual(ModelWithDefaultCollection.objects().all().get(id=2)._as_dict(),
288+
{'id': 2, 'dummy': 2, 'mf': {1: 1, 4: 4}})
289+
290+
drop_table(ModelWithDefaultCollection)

tests/integration/cqlengine/query/test_updates.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,34 @@ def test_map_remove_rejects_non_sets(self):
290290
text_map__remove=["bar"]
291291
)
292292

293+
@execute_count(3)
294+
def test_an_extra_delete_is_not_sent(self):
295+
"""
296+
Test to ensure that an extra DELETE is not sent if an object is read
297+
from the DB with a None value
298+
299+
@since 3.9
300+
@jira_ticket PYTHON-719
301+
@expected_result only three queries are executed, the first one for
302+
inserting the object, the second one for reading it, and the third
303+
one for updating it
304+
305+
@test_category object_mapper
306+
"""
307+
partition = uuid4()
308+
cluster = 1
309+
310+
TestQueryUpdateModel.objects.create(
311+
partition=partition, cluster=cluster)
312+
313+
obj = TestQueryUpdateModel.objects(
314+
partition=partition, cluster=cluster).first()
315+
316+
self.assertFalse(any([obj._values[column].deleted for column in obj._values]))
317+
318+
obj.text = 'foo'
319+
obj.save()
320+
293321

294322
class StaticDeleteModel(Model):
295323
example_id = columns.Integer(partition_key=True, primary_key=True, default=uuid4)

0 commit comments

Comments
 (0)