Skip to content

Commit a71cd19

Browse files
committed
Merge pull request apache#497 from datastax/py432
PYTHON-432: Add IF EXISTS support to UPDATE and DELETE statements of CQLEngine
2 parents fd61382 + bfcde9e commit a71cd19

7 files changed

Lines changed: 340 additions & 56 deletions

File tree

cassandra/cqlengine/models.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class IfNotExistsDescriptor(object):
178178
def __get__(self, instance, model):
179179
if instance:
180180
# instance method
181-
def ifnotexists_setter(ife):
181+
def ifnotexists_setter(ife=True):
182182
instance._if_not_exists = ife
183183
return instance
184184
return ifnotexists_setter
@@ -189,6 +189,24 @@ def __call__(self, *args, **kwargs):
189189
raise NotImplementedError
190190

191191

192+
class IfExistsDescriptor(object):
193+
"""
194+
return a query set descriptor with a if_exists flag specified
195+
"""
196+
def __get__(self, instance, model):
197+
if instance:
198+
# instance method
199+
def ifexists_setter(ife=True):
200+
instance._if_exists = ife
201+
return instance
202+
return ifexists_setter
203+
204+
return model.objects.if_exists
205+
206+
def __call__(self, *args, **kwargs):
207+
raise NotImplementedError
208+
209+
192210
class ConsistencyDescriptor(object):
193211
"""
194212
returns a query set descriptor if called on Class, instance if it was an instance call
@@ -303,6 +321,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned):
303321

304322
if_not_exists = IfNotExistsDescriptor()
305323

324+
if_exists = IfExistsDescriptor()
325+
306326
# _len is lazily created by __len__
307327

308328
__table_name__ = None
@@ -323,6 +343,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned):
323343

324344
_if_not_exists = False # optional if_not_exists flag to check existence before insertion
325345

346+
_if_exists = False # optional if_exists flag to check existence before update
347+
326348
_table_name = None # used internally to cache a derived table name
327349

328350
def __init__(self, **values):
@@ -654,7 +676,8 @@ def save(self):
654676
consistency=self.__consistency__,
655677
if_not_exists=self._if_not_exists,
656678
transaction=self._transaction,
657-
timeout=self._timeout).save()
679+
timeout=self._timeout,
680+
if_exists=self._if_exists).save()
658681

659682
self._set_persisted()
660683

@@ -700,7 +723,8 @@ def update(self, **values):
700723
timestamp=self._timestamp,
701724
consistency=self.__consistency__,
702725
transaction=self._transaction,
703-
timeout=self._timeout).update()
726+
timeout=self._timeout,
727+
if_exists=self._if_exists).update()
704728

705729
self._set_persisted()
706730

@@ -717,7 +741,8 @@ def delete(self):
717741
batch=self._batch,
718742
timestamp=self._timestamp,
719743
consistency=self.__consistency__,
720-
timeout=self._timeout).delete()
744+
timeout=self._timeout,
745+
if_exists=self._if_exists).delete()
721746

722747
def get_changed_columns(self):
723748
"""

cassandra/cqlengine/query.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ class QueryException(CQLEngineException):
3838
class IfNotExistsWithCounterColumn(CQLEngineException):
3939
pass
4040

41+
class IfExistsWithCounterColumn(CQLEngineException):
42+
pass
43+
4144

4245
class LWTException(CQLEngineException):
4346
"""Lightweight transaction exception.
@@ -294,6 +297,7 @@ def __init__(self, model):
294297
self._timestamp = None
295298
self._if_not_exists = False
296299
self._timeout = connection.NOT_SET
300+
self._if_exists = False
297301

298302
@property
299303
def column_family_name(self):
@@ -304,7 +308,7 @@ def _execute(self, q):
304308
return self._batch.add_query(q)
305309
else:
306310
result = connection.execute(q, consistency_level=self._consistency, timeout=self._timeout)
307-
if self._transaction:
311+
if self._if_not_exists or self._if_exists or self._transaction:
308312
check_applied(result)
309313
return result
310314

@@ -780,9 +784,14 @@ def defer(self, fields):
780784
return self._only_or_defer('defer', fields)
781785

782786
def create(self, **kwargs):
783-
return self.model(**kwargs).batch(self._batch).ttl(self._ttl).\
784-
consistency(self._consistency).if_not_exists(self._if_not_exists).\
785-
timestamp(self._timestamp).save()
787+
return self.model(**kwargs) \
788+
.batch(self._batch) \
789+
.ttl(self._ttl) \
790+
.consistency(self._consistency) \
791+
.if_not_exists(self._if_not_exists) \
792+
.timestamp(self._timestamp) \
793+
.if_exists(self._if_exists) \
794+
.save()
786795

787796
def delete(self):
788797
"""
@@ -796,7 +805,8 @@ def delete(self):
796805
dq = DeleteStatement(
797806
self.column_family_name,
798807
where=self._where,
799-
timestamp=self._timestamp
808+
timestamp=self._timestamp,
809+
if_exists=self._if_exists
800810
)
801811
self._execute(dq)
802812

@@ -938,12 +948,29 @@ def timestamp(self, timestamp):
938948
return clone
939949

940950
def if_not_exists(self):
951+
"""
952+
Check the existence of an object before insertion.
953+
954+
If the insertion isn't applied, a LWTException is raised.
955+
"""
941956
if self.model._has_counter:
942957
raise IfNotExistsWithCounterColumn('if_not_exists cannot be used with tables containing counter columns')
943958
clone = copy.deepcopy(self)
944959
clone._if_not_exists = True
945960
return clone
946961

962+
def if_exists(self):
963+
"""
964+
Check the existence of an object before an update or delete.
965+
966+
If the update or delete isn't applied, a LWTException is raised.
967+
"""
968+
if self.model._has_counter:
969+
raise IfExistsWithCounterColumn('if_exists cannot be used with tables containing counter columns')
970+
clone = copy.deepcopy(self)
971+
clone._if_exists = True
972+
return clone
973+
947974
def update(self, **values):
948975
"""
949976
Performs an update on the row selected by the queryset. Include values to update in the
@@ -1036,7 +1063,7 @@ class Row(Model):
10361063

10371064
nulled_columns = set()
10381065
us = UpdateStatement(self.column_family_name, where=self._where, ttl=self._ttl,
1039-
timestamp=self._timestamp, transactions=self._transaction)
1066+
timestamp=self._timestamp, transactions=self._transaction, if_exists=self._if_exists)
10401067
for name, val in values.items():
10411068
col_name, col_op = self._parse_filter_arg(name)
10421069
col = self.model._columns.get(col_name)
@@ -1076,7 +1103,8 @@ class Row(Model):
10761103
self._execute(us)
10771104

10781105
if nulled_columns:
1079-
ds = DeleteStatement(self.column_family_name, fields=nulled_columns, where=self._where)
1106+
ds = DeleteStatement(self.column_family_name, fields=nulled_columns,
1107+
where=self._where, if_exists=self._if_exists)
10801108
self._execute(ds)
10811109

10821110

@@ -1092,9 +1120,10 @@ class DMLQuery(object):
10921120
_consistency = None
10931121
_timestamp = None
10941122
_if_not_exists = False
1123+
_if_exists = False
10951124

10961125
def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None,
1097-
if_not_exists=False, transaction=None, timeout=connection.NOT_SET):
1126+
if_not_exists=False, transaction=None, timeout=connection.NOT_SET, if_exists=False):
10981127
self.model = model
10991128
self.column_family_name = self.model.column_family_name()
11001129
self.instance = instance
@@ -1103,6 +1132,7 @@ def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None,
11031132
self._consistency = consistency
11041133
self._timestamp = timestamp
11051134
self._if_not_exists = if_not_exists
1135+
self._if_exists = if_exists
11061136
self._transaction = transaction
11071137
self._timeout = timeout
11081138

@@ -1111,7 +1141,7 @@ def _execute(self, q):
11111141
return self._batch.add_query(q)
11121142
else:
11131143
tmp = connection.execute(q, consistency_level=self._consistency, timeout=self._timeout)
1114-
if self._if_not_exists or self._transaction:
1144+
if self._if_not_exists or self._if_exists or self._transaction:
11151145
check_applied(tmp)
11161146
return tmp
11171147

@@ -1125,7 +1155,7 @@ def _delete_null_columns(self):
11251155
"""
11261156
executes a delete query to remove columns that have changed to null
11271157
"""
1128-
ds = DeleteStatement(self.column_family_name)
1158+
ds = DeleteStatement(self.column_family_name, if_exists=self._if_exists)
11291159
deleted_fields = False
11301160
for _, v in self.instance._values.items():
11311161
col = v.column
@@ -1159,8 +1189,8 @@ def update(self):
11591189
assert type(self.instance) == self.model
11601190
null_clustering_key = False if len(self.instance._clustering_keys) == 0 else True
11611191
static_changed_only = True
1162-
statement = UpdateStatement(self.column_family_name, ttl=self._ttl,
1163-
timestamp=self._timestamp, transactions=self._transaction)
1192+
statement = UpdateStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp,
1193+
transactions=self._transaction, if_exists=self._if_exists)
11641194
for name, col in self.instance._clustering_keys.items():
11651195
null_clustering_key = null_clustering_key and col._val_is_null(getattr(self.instance, name, None))
11661196
# get defined fields and their column names
@@ -1264,7 +1294,7 @@ def delete(self):
12641294
if self.instance is None:
12651295
raise CQLEngineException("DML Query instance attribute is None")
12661296

1267-
ds = DeleteStatement(self.column_family_name, timestamp=self._timestamp)
1297+
ds = DeleteStatement(self.column_family_name, timestamp=self._timestamp, if_exists=self._if_exists)
12681298
for name, col in self.model._primary_keys.items():
12691299
if (not col.partition_key) and (getattr(self.instance, name) is None):
12701300
continue

cassandra/cqlengine/statements.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,8 @@ def __init__(self,
708708
where=None,
709709
ttl=None,
710710
timestamp=None,
711-
transactions=None):
711+
transactions=None,
712+
if_exists=False):
712713
super(UpdateStatement, self). __init__(table,
713714
assignments=assignments,
714715
consistency=consistency,
@@ -721,6 +722,8 @@ def __init__(self,
721722
for transaction in transactions or []:
722723
self.add_transaction_clause(transaction)
723724

725+
self.if_exists = if_exists
726+
724727
def __unicode__(self):
725728
qs = ['UPDATE', self.table]
726729

@@ -744,6 +747,9 @@ def __unicode__(self):
744747
if len(self.transactions) > 0:
745748
qs += [self._get_transactions()]
746749

750+
if self.if_exists:
751+
qs += ["IF EXISTS"]
752+
747753
return ' '.join(qs)
748754

749755
def add_transaction_clause(self, clause):
@@ -778,7 +784,7 @@ def update_context_id(self, i):
778784
class DeleteStatement(BaseCQLStatement):
779785
""" a cql delete statement """
780786

781-
def __init__(self, table, fields=None, consistency=None, where=None, timestamp=None):
787+
def __init__(self, table, fields=None, consistency=None, where=None, timestamp=None, if_exists=False):
782788
super(DeleteStatement, self).__init__(
783789
table,
784790
consistency=consistency,
@@ -790,6 +796,7 @@ def __init__(self, table, fields=None, consistency=None, where=None, timestamp=N
790796
fields = [fields]
791797
for field in fields or []:
792798
self.add_field(field)
799+
self.if_exists = if_exists
793800

794801
def update_context_id(self, i):
795802
super(DeleteStatement, self).update_context_id(i)
@@ -829,4 +836,7 @@ def __unicode__(self):
829836
if self.where_clauses:
830837
qs += [self._where]
831838

839+
if self.if_exists:
840+
qs += ["IF EXISTS"]
841+
832842
return ' '.join(qs)

docs/api/cassandra/cqlengine/models.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ Model
100100
101101
This method is supported on Cassandra 2.0 or later.
102102

103+
.. method:: if_exists()
104+
105+
Check the existence of an object before an update or delete. The existence of an
106+
object is determined by its primary key(s). And please note using this flag
107+
would incur performance cost.
108+
109+
If the update or delete isn't applied, a :class:`~cassandra.cqlengine.query.LWTException` is raised.
110+
111+
.. code-block:: python
112+
113+
try:
114+
TestIfExistsModel.objects(id=id).if_exists().update(count=9, text='111111111111')
115+
except LWTException as e:
116+
# handle failure case
117+
pass
118+
119+
This method is supported on Cassandra 2.0 or later.
120+
103121
.. automethod:: save
104122

105123
.. automethod:: update

docs/api/cassandra/cqlengine/query.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ The methods here are used to filter, order, and constrain results.
2626

2727
.. automethod:: limit
2828

29+
.. automethod:: if_not_exists
30+
31+
.. automethod:: if_exists
32+
2933
.. automethod:: order_by
3034

3135
.. automethod:: allow_filtering

0 commit comments

Comments
 (0)