Skip to content

Commit 4827711

Browse files
authored
Merge pull request datastax#942 from datastax/python-968-2
PYTHON-968: Support IS NOT NULL operator in cqlengine
2 parents 160faa7 + ca0f26f commit 4827711

File tree

6 files changed

+119
-16
lines changed

6 files changed

+119
-16
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Features
66
* Add one() function to the ResultSet API (PYTHON-947)
77
* Create an utility function to fetch concurrently many keys from the same replica (PYTHON-647)
88
* Allow filter queries with fields that have an index managed outside of cqlengine (PYTHON-966)
9+
* Support IS NOT NULL operator in cqlengine (PYTHON-968)
910

1011
Other
1112
-----

cassandra/cqlengine/operators.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,8 @@ class ContainsOperator(EqualsOperator):
9898
class LikeOperator(EqualsOperator):
9999
symbol = "LIKE"
100100
cql_symbol = 'LIKE'
101+
102+
103+
class IsNotNullOperator(EqualsOperator):
104+
symbol = "IS NOT NULL"
105+
cql_symbol = 'IS NOT NULL'

cassandra/cqlengine/statements.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from cassandra.cqlengine import columns
2222
from cassandra.cqlengine import UnicodeMixin
2323
from cassandra.cqlengine.functions import QueryValue
24-
from cassandra.cqlengine.operators import BaseWhereOperator, InOperator, EqualsOperator
24+
from cassandra.cqlengine.operators import BaseWhereOperator, InOperator, EqualsOperator, IsNotNullOperator
2525

2626

2727
class StatementException(Exception):
@@ -138,6 +138,24 @@ def update_context(self, ctx):
138138
self.query_value.update_context(ctx)
139139

140140

141+
class IsNotNullClause(WhereClause):
142+
def __init__(self, field):
143+
super(IsNotNullClause, self).__init__(field, IsNotNullOperator(), '')
144+
145+
def __unicode__(self):
146+
field = ('"{0}"' if self.quote_field else '{0}').format(self.field)
147+
return u'{0} {1}'.format(field, self.operator)
148+
149+
def update_context(self, ctx):
150+
pass
151+
152+
def get_context_size(self):
153+
return 0
154+
155+
# alias for convenience
156+
IsNotNull = IsNotNullClause
157+
158+
141159
class AssignmentClause(BaseClause):
142160
""" a single variable st statement """
143161

docs/cqlengine/queryset.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ In addition to simple equal to queries, cqlengine also supports querying with ot
199199
200200
q = Automobile.objects.filter(model__like='%Civic%').allow_filtering()
201201
202+
:attr:`IS NOT NULL (IsNotNull(column_name)) <query.QueryOperator.IsNotNullOperator>`
203+
204+
The IS NOT NULL operator is not yet supported for C*.
205+
206+
.. code-block:: python
207+
208+
q = Automobile.objects.filter(IsNotNull('model'))
209+
202210
Limitations:
203211
- Currently, cqlengine does not support SASI index creation. To use this feature,
204212
you need to create the SASI index using the core driver.

tests/integration/cqlengine/operators/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,10 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
15+
from cassandra.cqlengine.operators import BaseWhereOperator
16+
17+
18+
def check_lookup(test_case, symbol, expected):
19+
op = BaseWhereOperator.get_operator(symbol)
20+
test_case.assertEqual(op, expected)

tests/integration/cqlengine/operators/test_where_operators.py

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,41 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from unittest import TestCase
15+
try:
16+
import unittest2 as unittest
17+
except ImportError:
18+
import unittest # noqa
19+
1620
from cassandra.cqlengine.operators import *
1721

22+
from uuid import uuid4
23+
24+
from cassandra.cqlengine.management import sync_table, drop_table
25+
from cassandra.cqlengine.operators import IsNotNullOperator
26+
from cassandra.cqlengine.statements import IsNotNull
27+
from cassandra import InvalidRequest
28+
29+
from tests.integration.cqlengine.base import TestQueryUpdateModel, BaseCassEngTestCase
30+
from tests.integration.cqlengine.operators import check_lookup
31+
from tests.integration import greaterthanorequalcass30
32+
1833
import six
1934

2035

21-
class TestWhereOperators(TestCase):
36+
class TestWhereOperators(unittest.TestCase):
2237

2338
def test_symbol_lookup(self):
2439
""" tests where symbols are looked up properly """
2540

26-
def check_lookup(symbol, expected):
27-
op = BaseWhereOperator.get_operator(symbol)
28-
self.assertEqual(op, expected)
29-
30-
check_lookup('EQ', EqualsOperator)
31-
check_lookup('NE', NotEqualsOperator)
32-
check_lookup('IN', InOperator)
33-
check_lookup('GT', GreaterThanOperator)
34-
check_lookup('GTE', GreaterThanOrEqualOperator)
35-
check_lookup('LT', LessThanOperator)
36-
check_lookup('LTE', LessThanOrEqualOperator)
37-
check_lookup('CONTAINS', ContainsOperator)
38-
check_lookup('LIKE', LikeOperator)
41+
check_lookup(self, 'EQ', EqualsOperator)
42+
check_lookup(self, 'NE', NotEqualsOperator)
43+
check_lookup(self, 'IN', InOperator)
44+
check_lookup(self, 'GT', GreaterThanOperator)
45+
check_lookup(self, 'GTE', GreaterThanOrEqualOperator)
46+
check_lookup(self, 'LT', LessThanOperator)
47+
check_lookup(self, 'LTE', LessThanOrEqualOperator)
48+
check_lookup(self, 'CONTAINS', ContainsOperator)
49+
check_lookup(self, 'LIKE', LikeOperator)
3950

4051
def test_operator_rendering(self):
4152
""" tests symbols are rendered properly """
@@ -48,3 +59,56 @@ def test_operator_rendering(self):
4859
self.assertEqual("<=", six.text_type(LessThanOrEqualOperator()))
4960
self.assertEqual("CONTAINS", six.text_type(ContainsOperator()))
5061
self.assertEqual("LIKE", six.text_type(LikeOperator()))
62+
63+
64+
class TestIsNotNull(BaseCassEngTestCase):
65+
def test_is_not_null_to_cql(self):
66+
"""
67+
Verify that IsNotNull is converted correctly to CQL
68+
69+
@since 2.5
70+
@jira_ticket PYTHON-968
71+
@expected_result the strings match
72+
73+
@test_category cqlengine
74+
"""
75+
76+
check_lookup(self, 'IS NOT NULL', IsNotNullOperator)
77+
78+
# The * is not expanded because there are no referred fields
79+
self.assertEqual(
80+
str(TestQueryUpdateModel.filter(IsNotNull("text")).limit(2)),
81+
'SELECT * FROM cqlengine_test.test_query_update_model WHERE "text" IS NOT NULL LIMIT 2'
82+
)
83+
84+
# We already know partition so cqlengine doesn't query for it
85+
self.assertEqual(
86+
str(TestQueryUpdateModel.filter(IsNotNull("text"), partition=uuid4())),
87+
('SELECT "cluster", "count", "text", "text_set", '
88+
'"text_list", "text_map" FROM cqlengine_test.test_query_update_model '
89+
'WHERE "text" IS NOT NULL AND "partition" = %(0)s LIMIT 10000')
90+
)
91+
92+
@greaterthanorequalcass30
93+
def test_is_not_null_execution(self):
94+
"""
95+
Verify that CQL statements have correct syntax when executed
96+
If we wanted them to return something meaningful and not a InvalidRequest
97+
we'd have to create an index in search for the column we are using
98+
IsNotNull
99+
100+
@since 2.5
101+
@jira_ticket PYTHON-968
102+
@expected_result InvalidRequest is arisen
103+
104+
@test_category cqlengine
105+
"""
106+
sync_table(TestQueryUpdateModel)
107+
self.addCleanup(drop_table, TestQueryUpdateModel)
108+
109+
# Raises InvalidRequest instead of dse.protocol.SyntaxException
110+
with self.assertRaises(InvalidRequest):
111+
list(TestQueryUpdateModel.filter(IsNotNull("text")))
112+
113+
with self.assertRaises(InvalidRequest):
114+
list(TestQueryUpdateModel.filter(IsNotNull("text"), partition=uuid4()))

0 commit comments

Comments
 (0)