Skip to content

Commit e606593

Browse files
aboudreaultmambocab
authored andcommitted
cqlengine: Remove two-phase query when deleting values
1 parent c21c5e4 commit e606593

6 files changed

Lines changed: 83 additions & 127 deletions

File tree

CHANGELOG.rst

Lines changed: 8 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,81 +3,24 @@
33

44
Features
55
--------
6-
* Make protocol messages pluggable (PYTHON-956)
7-
* cqlengine: Remove default limit on QuerySets (PYTHON-517)
86
* cqlengine: asynchronous execution support (PYTHON-605)
9-
* cqlengine: disallow Counter create, save operations (PYTHON-497)
107
* cqlengine: Makes Model._table_name_ case sensitive (PYTHON-855)
11-
12-
13-
Other
14-
-----
15-
* Deprecate ResultSet indexing (PYTHON-945)
16-
* Cassandra 2.0 support removal (PYTHON-716)
17-
* cqlengine: remove the negative indices slicing support in ModelQuerySet (PYTHON-875)
18-
* Remove legacy execution parameters (PYTHON-876)
19-
* PreparedStatement.column_metadata should be renamed to bind_metadata (PYTHON-884)
20-
* Support cassandra.query.BatchType with cqlengine BatchQuery (PYTHON-888)
21-
* cqlengine: Remove Model.__default_ttl__ (PYTHON-889)
22-
* Remove Cluster.set_meta_refresh_enabled (PYTHON-890)
23-
24-
25-
3.14.0
26-
======
27-
28-
Features
29-
--------
30-
* Add one() function to the ResultSet API (PYTHON-947)
31-
* Create an utility function to fetch concurrently many keys from the same replica (PYTHON-647)
32-
* Allow filter queries with fields that have an index managed outside of cqlengine (PYTHON-966)
33-
* Twisted SSL Support (PYTHON-343)
34-
* Support IS NOT NULL operator in cqlengine (PYTHON-968)
35-
36-
Other
37-
-----
38-
* Fix Broken Links in Docs (PYTHON-916)
39-
* Reevaluate MONKEY_PATCH_LOOP in test codebase (PYTHON-903)
40-
* Remove CASS_SERVER_VERSION and replace it for CASSANDRA_VERSION in tests (PYTHON-910)
41-
* Refactor CASSANDRA_VERSION to a some kind of version object (PYTHON-915)
42-
* Log warning when driver configures an authenticator, but server does not request authentication (PYTHON-940)
43-
* Warn users when using the deprecated Session.default_consistency_level (PYTHON-953)
44-
* Add DSE smoke test to OSS driver tests (PYTHON-894)
45-
* Document long compilation times and workarounds (PYTHON-868)
46-
* Improve error for batch WriteTimeouts (PYTHON-941)
47-
48-
3.13.0
49-
======
50-
January 30, 2018
51-
52-
Features
53-
--------
548
* cqlengine: LIKE filter operator (PYTHON-512)
559

5610
Bug Fixes
5711
---------
58-
* Calling next() on a ResultSet throws an exception (PYTHON-463)
5912
* Support retry_policy in PreparedStatement (PYTHON-861)
60-
* cqlengine: Remove cassandra.cqlengine.query.BatchType in favor of cassandra.query.BatchType (PYTHON-888)
61-
* AttributeError: 'NoneType' object has no attribute 'add_timer' (PYTHON-862)
62-
* Support retry_policy in PreparedStatement (PYTHON-861)
63-
* __del__ method in Session is throwing an exception (PYTHON-813)
64-
* LZ4 import issue with recent versions (PYTHON-897)
65-
* ResponseFuture._connection can be None when returning request_id (PYTHON-853)
66-
* ResultSet.was_applied doesn't support batch with LWT statements (PYTHON-848)
6713

6814
Other
6915
-----
70-
* Remove basic equality and indexing support of ResultSet (PYTHON-945)
71-
* cqlengine: avoid warning when unregistering connection on shutdown (PYTHON-865)
72-
* Fix DeprecationWarning of log.warn (PYTHON-846)
73-
* Fix example_mapper.py for python3 (PYTHON-860)
74-
* Possible deadlock on cassandra.concurrent.execute_concurrent (PYTHON-768)
75-
* Add some known deprecated warnings for 4.x (PYTHON-877)
76-
* Remove copyright dates from copyright notices (PYTHON-863)
77-
* Remove "Experimental" tag from execution profiles documentation (PYTHON-840)
78-
* request_timer metrics descriptions are slightly incorrect (PYTHON-885)
79-
* Remove "Experimental" tag from cqlengine connections documentation (PYTHON-892)
80-
* Set in documentation default consistency for operations is LOCAL_ONE (PYTHON-901)
16+
* Cassandra 2.0 support removal (PYTHON-716)
17+
* PreparedStatement.column_metadata should be renamed to bind_metadata (PYTHON-884)
18+
* Remove Cluster.set_meta_refresh_enabled (PYTHON-890)
19+
* Remove legacy execution parameters (PYTHON-876)
20+
* cqlengine: disallow Counter create, save operations (PYTHON-497)
21+
* cqlengine: remove the negative indices slicing support in ModelQuerySet (PYTHON-875)
22+
* cqlengine: Remove Model.__default_ttl__ (PYTHON-889)
23+
* cqlengine: Remove two-phase query when deleting values (PYTHON-506)
8124

8225
3.12.0
8326
======
@@ -93,8 +36,6 @@ Features
9336
* Include hash of result set metadata in prepared stmt id (PYTHON-808)
9437
* Add NO_COMPACT startup option (PYTHON-839)
9538
* Add new exception type for CDC (PYTHON-837)
96-
* Allow 0ms in ConstantSpeculativeExecutionPolicy (PYTHON-836)
97-
* Add asyncio reactor (PYTHON-507)
9839

9940
Bug Fixes
10041
---------

cassandra/cqlengine/query.py

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,6 @@ def update_async(self, **values):
12911291
if not values:
12921292
return
12931293

1294-
nulled_columns = set()
12951294
updated_columns = set()
12961295
us = UpdateStatement(self.column_family_name, where=self._where, ttl=self._ttl,
12971296
timestamp=self._timestamp, conditionals=self._conditional, if_exists=self._if_exists)
@@ -1314,25 +1313,13 @@ def update_async(self, **values):
13141313
# we should not provide default values in this use case.
13151314
val = col.validate(val)
13161315

1317-
if val is None:
1318-
nulled_columns.add(col_name)
1319-
continue
1320-
13211316
us.add_update(col, val, operation=col_op)
13221317
updated_columns.add(col_name)
13231318

1324-
futures = []
1325-
if us.assignments:
1326-
futures.append(self._execute_async(us))
1327-
1328-
if nulled_columns:
1329-
delete_conditional = [condition for condition in self._conditional
1330-
if condition.field not in updated_columns] if self._conditional else None
1331-
ds = DeleteStatement(self.column_family_name, fields=nulled_columns,
1332-
where=self._where, conditionals=delete_conditional, if_exists=self._if_exists)
1333-
futures.append(self._execute_async(ds))
1319+
if not us.assignments:
1320+
return CQLEngineFuture()
13341321

1335-
return CQLEngineFutureWaiter(futures)
1322+
return self._execute_async(us)
13361323

13371324
def update(self, **values):
13381325
"""
@@ -1481,20 +1468,20 @@ def batch(self, batch_obj):
14811468
self._batch = batch_obj
14821469
return self
14831470

1484-
def _delete_null_columns_async(self, conditionals=None):
1471+
def _delete_map_keys_async(self, conditionals=None):
14851472
"""
1486-
executes a delete query to remove columns that have changed to null
1473+
executes a delete query to remove map keys that have changed to null.
1474+
1475+
Doing map keys deletion in two phases allow us to update a map collection
1476+
without creating a tombstone.
14871477
"""
14881478
ds = DeleteStatement(self.column_family_name, conditionals=conditionals, if_exists=self._if_exists)
14891479
deleted_fields = False
14901480
static_only = True
1481+
14911482
for _, v in self.instance._values.items():
14921483
col = v.column
1493-
if v.deleted:
1494-
ds.add_field(col.db_field_name)
1495-
deleted_fields = True
1496-
static_only &= col.static
1497-
elif isinstance(col, columns.Map):
1484+
if isinstance(col, columns.Map):
14981485
uc = MapDeleteClause(col.db_field_name, v.value, v.previous_value)
14991486
if uc.get_context_size() > 0:
15001487
ds.add_field(uc)
@@ -1509,11 +1496,11 @@ def _delete_null_columns_async(self, conditionals=None):
15091496
else:
15101497
return CQLEngineFuture()
15111498

1512-
def _delete_null_columns(self, conditionals=None):
1499+
def _delete_map_keys(self, conditionals=None):
15131500
"""
15141501
executes a delete query to remove columns that have changed to null
15151502
"""
1516-
return self._delete_null_columns_async(conditionals=conditionals).result()
1503+
return self._delete_map_keys_async(conditionals=conditionals).result()
15171504

15181505
def update_async(self):
15191506
"""
@@ -1541,18 +1528,14 @@ def update_async(self):
15411528
val = getattr(self.instance, name, None)
15421529
val_mgr = self.instance._values[name]
15431530

1544-
if val is None:
1545-
continue
1546-
1547-
if not val_mgr.changed and not isinstance(col, columns.Counter):
1531+
if not (val_mgr.changed or val_mgr.deleted) and not isinstance(col, columns.Counter):
15481532
continue
15491533

15501534
static_changed_only = static_changed_only and col.static
15511535
statement.add_update(col, val, previous=val_mgr.previous_value)
15521536
updated_columns.add(col.db_field_name)
15531537

15541538
futures = []
1555-
15561539
if statement.assignments:
15571540
for name, col in self.model._primary_keys.items():
15581541
# only include clustering key if clustering key is not null, and non static columns are changed to avoid cql error
@@ -1565,7 +1548,7 @@ def update_async(self):
15651548
# remove conditions on fields that have been updated
15661549
delete_conditionals = [condition for condition in self._conditional
15671550
if condition.field not in updated_columns] if self._conditional else None
1568-
futures.append(self._delete_null_columns_async(delete_conditionals))
1551+
futures.append(self._delete_map_keys_async(delete_conditionals))
15691552

15701553
return CQLEngineFutureWaiter(futures)
15711554

@@ -1591,7 +1574,6 @@ def save_async(self):
15911574
if self.instance._has_counter:
15921575
raise CQLEngineException("Counters can only be updated. Use the 'update' mechanism instead.")
15931576

1594-
nulled_fields = set()
15951577
if self.instance._can_update():
15961578
return self.update_async()
15971579
else:
@@ -1603,9 +1585,8 @@ def save_async(self):
16031585
if static_save_only and not col.static and not col.partition_key:
16041586
continue
16051587
val = getattr(self.instance, name, None)
1606-
if col._val_is_null(val):
1607-
if self.instance._values[name].changed:
1608-
nulled_fields.add(col.db_field_name)
1588+
if (col._val_is_null(val) and
1589+
not (self.instance._values[name].changed or self.instance._values[name].deleted)):
16091590
continue
16101591
if col.has_default and not self.instance._values[name].changed:
16111592
# Ensure default columns included in a save() are marked as explicit, to get them *persisted* properly
@@ -1614,17 +1595,10 @@ def save_async(self):
16141595

16151596
# skip query execution if it's empty
16161597
# caused by pointless update queries
1617-
if insert.is_empty and static_save_only:
1598+
if insert.is_empty:
16181599
return CQLEngineFuture()
16191600

1620-
futures = []
1621-
if not insert.is_empty:
1622-
futures.append(self._execute_async(insert))
1623-
# delete any nulled columns
1624-
if not static_save_only:
1625-
futures.append(self._delete_null_columns_async())
1626-
1627-
return CQLEngineFutureWaiter(futures)
1601+
return self._execute_async(insert)
16281602

16291603
def save(self):
16301604
"""

cassandra/cqlengine/statements.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ class SetUpdateClause(ContainerUpdateClause):
214214
_additions = None
215215
_removals = None
216216

217+
@property
218+
def _is_assignment(self):
219+
return not (self._additions or self._removals or
220+
self._operation or self.value == self.previous)
221+
217222
def __unicode__(self):
218223
qs = []
219224
ctx_id = self.context_id
@@ -222,7 +227,7 @@ def __unicode__(self):
222227
self._additions is None and
223228
self._removals is None):
224229
qs += ['"{0}" = %({1})s'.format(self.field, ctx_id)]
225-
if self._assignments is not None:
230+
if self._is_assignment:
226231
qs += ['"{0}" = %({1})s'.format(self.field, ctx_id)]
227232
ctx_id += 1
228233
if self._additions is not None:
@@ -253,11 +258,11 @@ def get_context_size(self):
253258
if not self._analyzed:
254259
self._analyze()
255260
if (self.previous is None and
256-
not self._assignments and
261+
not self._is_assignment and
257262
self._additions is None and
258263
self._removals is None):
259264
return 1
260-
return int(bool(self._assignments)) + int(bool(self._additions)) + int(bool(self._removals))
265+
return int(bool(self._is_assignment)) + int(bool(self._additions)) + int(bool(self._removals))
261266

262267
def update_context(self, ctx):
263268
if not self._analyzed:
@@ -268,7 +273,7 @@ def update_context(self, ctx):
268273
self._additions is None and
269274
self._removals is None):
270275
ctx[str(ctx_id)] = set()
271-
if self._assignments is not None:
276+
if self._is_assignment:
272277
ctx[str(ctx_id)] = self._assignments
273278
ctx_id += 1
274279
if self._additions is not None:
@@ -286,12 +291,17 @@ class ListUpdateClause(ContainerUpdateClause):
286291
_append = None
287292
_prepend = None
288293

294+
@property
295+
def _is_assignment(self):
296+
return not (self._append or self._prepend or
297+
self._operation or self.value == self.previous)
298+
289299
def __unicode__(self):
290300
if not self._analyzed:
291301
self._analyze()
292302
qs = []
293303
ctx_id = self.context_id
294-
if self._assignments is not None:
304+
if self._is_assignment:
295305
qs += ['"{0}" = %({1})s'.format(self.field, ctx_id)]
296306
ctx_id += 1
297307

@@ -307,13 +317,13 @@ def __unicode__(self):
307317
def get_context_size(self):
308318
if not self._analyzed:
309319
self._analyze()
310-
return int(self._assignments is not None) + int(bool(self._append)) + int(bool(self._prepend))
320+
return int(self._is_assignment) + int(bool(self._append)) + int(bool(self._prepend))
311321

312322
def update_context(self, ctx):
313323
if not self._analyzed:
314324
self._analyze()
315325
ctx_id = self.context_id
316-
if self._assignments is not None:
326+
if self._is_assignment:
317327
ctx[str(ctx_id)] = self._assignments
318328
ctx_id += 1
319329
if self._prepend is not None:
@@ -388,7 +398,9 @@ def _analyze(self):
388398
if self.previous is None:
389399
self._updates = sorted([k for k, v in self.value.items()])
390400
else:
391-
self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None
401+
if self.value:
402+
self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None
403+
392404
self._analyzed = True
393405

394406
def get_context_size(self):
@@ -399,7 +411,7 @@ def get_context_size(self):
399411
def update_context(self, ctx):
400412
ctx_id = self.context_id
401413
if self.is_assignment:
402-
ctx[str(ctx_id)] = {}
414+
ctx[str(ctx_id)] = {} if self.value is None else self.value
403415
elif self._removals is not None:
404416
ctx[str(ctx_id)] = self._removals
405417
else:
@@ -413,7 +425,7 @@ def update_context(self, ctx):
413425
def is_assignment(self):
414426
if not self._analyzed:
415427
self._analyze()
416-
return self.previous is None and not self._updates and not self._removals
428+
return not self._updates and not self._removals
417429

418430
def __unicode__(self):
419431
qs = []
@@ -831,7 +843,8 @@ def add_update(self, column, value, operation=None, previous=None):
831843
clause = CounterUpdateClause(column.db_field_name, value, previous)
832844
else:
833845
clause = AssignmentClause(column.db_field_name, value)
834-
if clause.get_context_size(): # this is to exclude map removals from updates. Can go away if we drop support for C* < 1.2.4 and remove two-phase updates
846+
847+
if clause.get_context_size():
835848
self._add_assignment_clause(clause)
836849

837850

tests/integration/cqlengine/query/test_updates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_null_update_deletes_column(self):
106106
self.assertEqual(row.count, i)
107107
self.assertEqual(row.text, None if i == 3 else str(i))
108108

109-
@execute_count(9)
109+
@execute_count(8)
110110
def test_mixed_value_and_null_update(self):
111111
""" tests that updating a columns value, and removing another works properly """
112112
partition = uuid4()

0 commit comments

Comments
 (0)