Merge pull request #488 from datastax/336

PYTHON-336 - Expose LWT results in cqlengine LWTException
This commit is contained in:
Adam Holmberg
2016-02-24 08:26:57 -06:00
8 changed files with 117 additions and 19 deletions

View File

@@ -6,6 +6,7 @@ Features
* Pass name of server auth class to AuthProvider (PYTHON-454) * Pass name of server auth class to AuthProvider (PYTHON-454)
* Surface schema agreed flag for DDL statements (PYTHON-458) * Surface schema agreed flag for DDL statements (PYTHON-458)
* Automatically convert float and int to Decimal on serialization (PYTHON-468) * Automatically convert float and int to Decimal on serialization (PYTHON-468)
* Expose prior state information via cqlengine LWTException (github #343)
Bug Fixes Bug Fixes
--------- ---------

View File

@@ -70,8 +70,8 @@ from cassandra.pool import (Host, _ReconnectionHandler, _HostReconnectionHandler
HostConnectionPool, HostConnection, HostConnectionPool, HostConnection,
NoConnectionsAvailable) NoConnectionsAvailable)
from cassandra.query import (SimpleStatement, PreparedStatement, BoundStatement, from cassandra.query import (SimpleStatement, PreparedStatement, BoundStatement,
BatchStatement, bind_params, QueryTrace, Statement, BatchStatement, bind_params, QueryTrace,
named_tuple_factory, dict_factory, FETCH_SIZE_UNSET) named_tuple_factory, dict_factory, tuple_factory, FETCH_SIZE_UNSET)
def _is_eventlet_monkey_patched(): def _is_eventlet_monkey_patched():
@@ -3409,3 +3409,24 @@ class ResultSet(object):
See :meth:`.ResponseFuture.get_all_query_traces` for details. See :meth:`.ResponseFuture.get_all_query_traces` for details.
""" """
return self.response_future.get_all_query_traces(max_wait_sec_per) return self.response_future.get_all_query_traces(max_wait_sec_per)
@property
def was_applied(self):
"""
For LWT results, returns whether the transaction was applied.
Result is indeterminate if called on a result that was not an LWT request.
Only valid when one of tne of the internal row factories is in use.
"""
if self.response_future.row_factory not in (named_tuple_factory, dict_factory, tuple_factory):
raise RuntimeError("Cannot determine LWT result with row factory %s" % (self.response_future.row_factsory,))
if len(self.current_rows) != 1:
raise RuntimeError("LWT result should have exactly one row. This has %d." % (len(self.current_rows)))
row = self.current_rows[0]
if isinstance(row, tuple):
return row[0]
else:
return row['[applied]']

View File

@@ -40,7 +40,17 @@ class IfNotExistsWithCounterColumn(CQLEngineException):
class LWTException(CQLEngineException): class LWTException(CQLEngineException):
pass """Lightweight transaction exception.
This exception will be raised when a write using an `IF` clause could not be
applied due to existing data violating the condition. The existing data is
available through the ``existing`` attribute.
:param existing: The current state of the data which prevented the write.
"""
def __init__(self, existing):
super(LWTException, self).__init__(self)
self.existing = existing
class DoesNotExist(QueryException): class DoesNotExist(QueryException):
@@ -53,12 +63,14 @@ class MultipleObjectsReturned(QueryException):
def check_applied(result): def check_applied(result):
""" """
check if result contains some column '[applied]' with false value, Raises LWTException if it looks like a failed LWT request.
if that value is false, it means our light-weight transaction didn't
applied to database.
""" """
if result and '[applied]' in result[0] and not result[0]['[applied]']: try:
raise LWTException('') applied = result.was_applied
except Exception:
applied = True # result was not LWT form
if not applied:
raise LWTException(result[0])
class AbstractQueryableColumn(UnicodeMixin): class AbstractQueryableColumn(UnicodeMixin):

View File

@@ -48,7 +48,6 @@ Model
See the `list of supported table properties for more information See the `list of supported table properties for more information
<http://www.datastax.com/documentation/cql/3.1/cql/cql_reference/tabProp.html>`_. <http://www.datastax.com/documentation/cql/3.1/cql/cql_reference/tabProp.html>`_.
.. attribute:: __options__ .. attribute:: __options__
For example: For example:
@@ -89,7 +88,7 @@ Model
object is determined by its primary key(s). And please note using this flag object is determined by its primary key(s). And please note using this flag
would incur performance cost. would incur performance cost.
if the insertion didn't applied, a LWTException exception would be raised. If the insertion isn't applied, a :class:`~cassandra.cqlengine.query.LWTException` is raised.
.. code-block:: python .. code-block:: python
@@ -97,7 +96,7 @@ Model
TestIfNotExistsModel.if_not_exists().create(id=id, count=9, text='111111111111') TestIfNotExistsModel.if_not_exists().create(id=id, count=9, text='111111111111')
except LWTException as e: except LWTException as e:
# handle failure case # handle failure case
print e.existing # existing object print e.existing # dict containing LWT result fields
This method is supported on Cassandra 2.0 or later. This method is supported on Cassandra 2.0 or later.
@@ -111,7 +110,7 @@ Model
Simply specify the column(s) and the expected value(s). As with if_not_exists, Simply specify the column(s) and the expected value(s). As with if_not_exists,
this incurs a performance cost. this incurs a performance cost.
If the insertion isn't applied, a LWTException is raised If the insertion isn't applied, a :class:`~cassandra.cqlengine.query.LWTException` is raised.
.. code-block:: python .. code-block:: python
@@ -119,7 +118,8 @@ Model
try: try:
t.iff(count=5).update('other text') t.iff(count=5).update('other text')
except LWTException as e: except LWTException as e:
# handle failure # handle failure case
print e.existing # existing object
.. automethod:: get .. automethod:: get

View File

@@ -40,3 +40,4 @@ The methods here are used to filter, order, and constrain results.
.. autoclass:: MultipleObjectsReturned .. autoclass:: MultipleObjectsReturned
.. autoclass:: LWTException

View File

@@ -81,9 +81,17 @@ class IfNotExistsInsertTests(BaseIfNotExistsTest):
id = uuid4() id = uuid4()
TestIfNotExistsModel.create(id=id, count=8, text='123456789') TestIfNotExistsModel.create(id=id, count=8, text='123456789')
with self.assertRaises(LWTException):
with self.assertRaises(LWTException) as assertion:
TestIfNotExistsModel.if_not_exists().create(id=id, count=9, text='111111111111') TestIfNotExistsModel.if_not_exists().create(id=id, count=9, text='111111111111')
self.assertEqual(assertion.exception.existing, {
'count': 8,
'id': id,
'text': '123456789',
'[applied]': False,
})
q = TestIfNotExistsModel.objects(id=id) q = TestIfNotExistsModel.objects(id=id)
self.assertEqual(len(q), 1) self.assertEqual(len(q), 1)
@@ -117,9 +125,16 @@ class IfNotExistsInsertTests(BaseIfNotExistsTest):
b = BatchQuery() b = BatchQuery()
TestIfNotExistsModel.batch(b).if_not_exists().create(id=id, count=9, text='111111111111') TestIfNotExistsModel.batch(b).if_not_exists().create(id=id, count=9, text='111111111111')
with self.assertRaises(LWTException): with self.assertRaises(LWTException) as assertion:
b.execute() b.execute()
self.assertEqual(assertion.exception.existing, {
'count': 8,
'id': id,
'text': '123456789',
'[applied]': False,
})
q = TestIfNotExistsModel.objects(id=id) q = TestIfNotExistsModel.objects(id=id)
self.assertEqual(len(q), 1) self.assertEqual(len(q), 1)

View File

@@ -29,8 +29,9 @@ from cassandra.cqlengine.statements import TransactionClause
from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration.cqlengine.base import BaseCassEngTestCase
from tests.integration import CASSANDRA_VERSION from tests.integration import CASSANDRA_VERSION
class TestTransactionModel(Model): class TestTransactionModel(Model):
id = columns.UUID(primary_key=True, default=lambda:uuid4()) id = columns.UUID(primary_key=True, default=uuid4)
count = columns.Integer() count = columns.Integer()
text = columns.Text(required=False) text = columns.Text(required=False)
@@ -71,7 +72,14 @@ class TestTransaction(BaseCassEngTestCase):
t = TestTransactionModel.create(text='blah blah') t = TestTransactionModel.create(text='blah blah')
t.text = 'new blah' t.text = 'new blah'
t = t.iff(text='something wrong') t = t.iff(text='something wrong')
self.assertRaises(LWTException, t.save)
with self.assertRaises(LWTException) as assertion:
t.save()
self.assertEqual(assertion.exception.existing, {
'text': 'blah blah',
'[applied]': False,
})
def test_blind_update(self): def test_blind_update(self):
t = TestTransactionModel.create(text='blah blah') t = TestTransactionModel.create(text='blah blah')
@@ -89,7 +97,13 @@ class TestTransaction(BaseCassEngTestCase):
t.text = 'something else' t.text = 'something else'
uid = t.id uid = t.id
qs = TestTransactionModel.objects(id=uid).iff(text='Not dis!') qs = TestTransactionModel.objects(id=uid).iff(text='Not dis!')
self.assertRaises(LWTException, qs.update, text='this will never work') with self.assertRaises(LWTException) as assertion:
qs.update(text='this will never work')
self.assertEqual(assertion.exception.existing, {
'text': 'blah blah',
'[applied]': False,
})
def test_transaction_clause(self): def test_transaction_clause(self):
tc = TransactionClause('some_value', 23) tc = TransactionClause('some_value', 23)
@@ -109,7 +123,14 @@ class TestTransaction(BaseCassEngTestCase):
b = BatchQuery() b = BatchQuery()
updated.batch(b).iff(count=6).update(text='and another thing') updated.batch(b).iff(count=6).update(text='and another thing')
self.assertRaises(LWTException, b.execute) with self.assertRaises(LWTException) as assertion:
b.execute()
self.assertEqual(assertion.exception.existing, {
'id': id,
'count': 5,
'[applied]': False,
})
updated = TestTransactionModel.objects(id=id).first() updated = TestTransactionModel.objects(id=id).first()
self.assertEqual(updated.text, 'something else') self.assertEqual(updated.text, 'something else')

View File

@@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from cassandra.query import named_tuple_factory, dict_factory, tuple_factory
try: try:
import unittest2 as unittest import unittest2 as unittest
@@ -161,3 +162,29 @@ class ResultSetTests(unittest.TestCase):
def test_bool(self): def test_bool(self):
self.assertFalse(ResultSet(Mock(has_more_pages=False), [])) self.assertFalse(ResultSet(Mock(has_more_pages=False), []))
self.assertTrue(ResultSet(Mock(has_more_pages=False), [1])) self.assertTrue(ResultSet(Mock(has_more_pages=False), [1]))
def test_was_applied(self):
# unknown row factory raises
with self.assertRaises(RuntimeError):
ResultSet(Mock(), []).was_applied
response_future = Mock(row_factory=named_tuple_factory)
# no row
with self.assertRaises(RuntimeError):
ResultSet(response_future, []).was_applied
# too many rows
with self.assertRaises(RuntimeError):
ResultSet(response_future, [tuple(), tuple()]).was_applied
# various internal row factories
for row_factory in (named_tuple_factory, tuple_factory):
for applied in (True, False):
rs = ResultSet(Mock(row_factory=row_factory), [(applied,)])
self.assertEqual(rs.was_applied, applied)
row_factory = dict_factory
for applied in (True, False):
rs = ResultSet(Mock(row_factory=row_factory), [{'[applied]': applied}])
self.assertEqual(rs.was_applied, applied)