merged in updates branch again, got consistency blind updates working

This commit is contained in:
Jon Haddad
2013-10-25 10:55:22 -07:00
7 changed files with 202 additions and 12 deletions

View File

@@ -2,6 +2,8 @@ CHANGELOG
0.9
* adding update method
* adding BigInt column (thanks @Lifto)
* adding support for timezone aware time uuid functions (thanks @dokai)
* only saving collection fields on insert if they've been modified
0.8.5

View File

@@ -9,7 +9,7 @@ from cqlengine.columns import Counter
from cqlengine.connection import connection_manager, execute, RowResult
from cqlengine.exceptions import CQLEngineException
from cqlengine.exceptions import CQLEngineException, ValidationError
from cqlengine.functions import QueryValue, Token
#CQL 3 reference:
@@ -641,13 +641,6 @@ class AbstractQuerySet(object):
else:
execute(qs, self._where_values())
def update(self, **values):
"""
updates the contents of the query
"""
qs = ['UPDATE {}'.format(self.column_family_name)]
qs += ['SET']
def __eq__(self, q):
return set(self._where) == set(q._where)
@@ -772,6 +765,62 @@ class ModelQuerySet(AbstractQuerySet):
clone._flat_values_list = flat
return clone
def consistency(self, consistency):
self._consistency = consistency
return self
def ttl(self, ttl):
self._ttl = ttl
return self
def update(self, **values):
""" Updates the rows in this queryset """
if not values:
return
set_statements = []
ctx = {}
nulled_columns = set()
for name, val in values.items():
col = self.model._columns.get(name)
# check for nonexistant columns
if col is None:
raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.model.__name__, name))
# check for primary key update attempts
if col.is_primary_key:
raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(name, self.__module__, self.model.__name__))
val = col.validate(val)
if val is None:
nulled_columns.add(name)
continue
# add the update statements
if isinstance(col, (BaseContainerColumn, Counter)):
val_mgr = self.instance._values[name]
set_statements += col.get_update_statement(val, val_mgr.previous_value, ctx)
else:
field_id = uuid4().hex
set_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)]
ctx[field_id] = val
if set_statements:
qs = "UPDATE {} SET {} WHERE {}".format(
self.column_family_name,
', '.join(set_statements),
self._where_clause()
)
ctx.update(self._where_values())
execute(qs, ctx, self._consistency)
if nulled_columns:
qs = "DELETE {} FROM {} WHERE {}".format(
', '.join(nulled_columns),
self.column_family_name,
self._where_clause()
)
execute(qs, self._where_values(), self._consistency)
class DMLQuery(object):
"""

View File

@@ -0,0 +1,114 @@
from uuid import uuid4
from cqlengine.exceptions import ValidationError
from cqlengine.query import QueryException
from cqlengine.tests.base import BaseCassEngTestCase
from cqlengine.models import Model
from cqlengine.management import sync_table, drop_table
from cqlengine import columns
class TestQueryUpdateModel(Model):
partition = columns.UUID(primary_key=True, default=uuid4)
cluster = columns.Integer(primary_key=True)
count = columns.Integer(required=False)
text = columns.Text(required=False, index=True)
class QueryUpdateTests(BaseCassEngTestCase):
@classmethod
def setUpClass(cls):
super(QueryUpdateTests, cls).setUpClass()
sync_table(TestQueryUpdateModel)
@classmethod
def tearDownClass(cls):
super(QueryUpdateTests, cls).tearDownClass()
drop_table(TestQueryUpdateModel)
def test_update_values(self):
""" tests calling udpate on a queryset """
partition = uuid4()
for i in range(5):
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
# sanity check
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
assert row.cluster == i
assert row.count == i
assert row.text == str(i)
# perform update
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6)
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
assert row.cluster == i
assert row.count == (6 if i == 3 else i)
assert row.text == str(i)
def test_update_values_validation(self):
""" tests calling udpate on models with values passed in """
partition = uuid4()
for i in range(5):
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
# sanity check
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
assert row.cluster == i
assert row.count == i
assert row.text == str(i)
# perform update
with self.assertRaises(ValidationError):
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count='asdf')
def test_invalid_update_kwarg(self):
""" tests that passing in a kwarg to the update method that isn't a column will fail """
with self.assertRaises(ValidationError):
TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(bacon=5000)
def test_primary_key_update_failure(self):
""" tests that attempting to update the value of a primary key will fail """
with self.assertRaises(ValidationError):
TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(cluster=5000)
def test_null_update_deletes_column(self):
""" setting a field to null in the update should issue a delete statement """
partition = uuid4()
for i in range(5):
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
# sanity check
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
assert row.cluster == i
assert row.count == i
assert row.text == str(i)
# perform update
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(text=None)
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
assert row.cluster == i
assert row.count == i
assert row.text == (None if i == 3 else str(i))
def test_mixed_value_and_null_update(self):
""" tests that updating a columns value, and removing another works properly """
partition = uuid4()
for i in range(5):
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
# sanity check
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
assert row.cluster == i
assert row.count == i
assert row.text == str(i)
# perform update
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6, text=None)
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
assert row.cluster == i
assert row.count == (6 if i == 3 else i)
assert row.text == (None if i == 3 else str(i))

View File

@@ -72,7 +72,7 @@ class TestConsistency(BaseConsistencyTest):
uid = t.id
with mock.patch.object(ConnectionPool, 'execute') as m:
TestConsistencyModel.objects(id=uid).update(text="grilled cheese")
TestConsistencyModel.objects(id=uid).consistency(ALL).update(text="grilled cheese")
args = m.call_args
self.assertEqual(ALL, args[0][2])

View File

@@ -53,6 +53,7 @@ class TTLModelTests(BaseTTLTest):
self.assertTrue(isinstance(qs, TestTTLModel.__queryset__), type(qs))
class TTLInstanceUpdateTest(BaseTTLTest):
def test_update_includes_ttl(self):
model = TestTTLModel.create(text="goodbye blake")
@@ -87,6 +88,17 @@ class TTLInstanceTest(BaseTTLTest):
self.assertIn("USING TTL", query)
class TTLBlindUpdateTest(BaseTTLTest):
def test_ttl_included_with_blind_update(self):
o = TestTTLModel.create(text="whatever")
tid = o.id
with mock.patch.object(ConnectionPool, 'execute') as m:
TestTTLModel.objects(id=tid).ttl(60).update(text="bacon")
query = m.call_args[0][0]
self.assertIn("USING TTL", query)
class QuerySetTTLFragmentTest(BaseTTLTest):
pass

View File

@@ -149,6 +149,7 @@ Model Methods
-- method:: update(**values)
Performs an update on the model instance. You can pass in values to set on the model
for updating, or you can call without values to execute an update against any modified
fields. If no fields on the model have been modified since loading, no query will be

View File

@@ -358,7 +358,19 @@ QuerySet method reference
Sets the batch object to run the query on. Note that running a select query with a batch object will raise an exception
.. method:: ttl(batch_object)
.. method:: ttl(ttl_in_seconds)
Sets the ttl to run the query query with. Note that running a select query with a ttl value will raise an exception
-- method:: update(**values)
Performs an update on the row selected by the queryset. Include values to update in the
update like so:
.. code-block:: python
Model.objects(key=n).update(value='x')
Passing in updates for columns which are not part of the model will raise a ValidationError.
Per column validation will be performed, but instance level validation will not
(`Model.validate` is not called).