Merge pull request #488 from datastax/336
PYTHON-336 - Expose LWT results in cqlengine LWTException
This commit is contained in:
@@ -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
|
||||||
---------
|
---------
|
||||||
|
|||||||
@@ -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]']
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -40,3 +40,4 @@ The methods here are used to filter, order, and constrain results.
|
|||||||
|
|
||||||
.. autoclass:: MultipleObjectsReturned
|
.. autoclass:: MultipleObjectsReturned
|
||||||
|
|
||||||
|
.. autoclass:: LWTException
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user