From 4d21458264e239501c4aa284584050a8dc79cb58 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:22:12 -0700 Subject: [PATCH 01/11] expanding descriptor documentation --- cqlengine/models.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 4875d0b3..cf070cd6 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -29,7 +29,9 @@ class hybrid_classmethod(object): return self.instmethod.__get__(instance, owner) def __call__(self, *args, **kwargs): - """ Just a hint to IDEs that it's ok to call this """ + """ + Just a hint to IDEs that it's ok to call this + """ raise NotImplementedError class QuerySetDescriptor(object): @@ -39,12 +41,18 @@ class QuerySetDescriptor(object): """ def __get__(self, obj, model): + """ :rtype: QuerySet """ if model.__abstract__: raise CQLEngineException('cannot execute queries against abstract models') return QuerySet(model) def __call__(self, *args, **kwargs): - """ Just a hint to IDEs that it's ok to call this """ + """ + Just a hint to IDEs that it's ok to call this + + :rtype: QuerySet + """ + raise NotImplementedError raise NotImplementedError class ColumnDescriptor(AbstractColumnDescriptor): From a9c19d3120d2752985aad4ae7f83e6e8dcbc1829 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:26:27 -0700 Subject: [PATCH 02/11] implementing query method, and mode query method descriptor --- cqlengine/models.py | 19 +++++++++++++++++++ cqlengine/query.py | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index cf070cd6..bfa66b87 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -53,6 +53,25 @@ class QuerySetDescriptor(object): :rtype: QuerySet """ raise NotImplementedError + +class QueryExpressionDescriptor(object): + """ + returns a fresh queryset /query method for the given model + it's declared on everytime it's accessed + """ + + def __get__(self, obj, model): + """ :rtype: QuerySet """ + if model.__abstract__: + raise CQLEngineException('cannot execute queries against abstract models') + return QuerySet(model).query + + def __call__(self, *args, **kwargs): + """ + Just a hint to IDEs that it's ok to call this + + :rtype: QuerySet + """ raise NotImplementedError class ColumnDescriptor(AbstractColumnDescriptor): diff --git a/cqlengine/query.py b/cqlengine/query.py index 88d16aa4..95b6fbc3 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -530,6 +530,13 @@ class QuerySet(object): raise QueryException("Can't parse '{}'".format(arg)) def filter(self, **kwargs): + """ + Adds WHERE arguments to the queryset, returning a new queryset + + #TODO: show examples + + :rtype: QuerySet + """ #add arguments to the where clause filters clone = copy.deepcopy(self) for arg, val in kwargs.items(): @@ -555,7 +562,16 @@ class QuerySet(object): """ Same end result as filter, but uses the new comparator style args ie: Model.column == val + + #TODO: show examples + + :rtype: QuerySet """ + clone = copy.deepcopy(self) + for operator in args: + clone._where.append(operator) + + return clone def get(self, **kwargs): """ From 1e6531afda1ba055de09388e0a2a0bfb725dd57d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:27:36 -0700 Subject: [PATCH 03/11] moving column query expression evaluator out of column descriptor --- cqlengine/models.py | 14 ++++++++++++-- cqlengine/tests/model/test_class_construction.py | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index bfa66b87..808938e0 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -74,7 +74,15 @@ class QueryExpressionDescriptor(object): """ raise NotImplementedError -class ColumnDescriptor(AbstractColumnDescriptor): +class ColumnQueryEvaluator(AbstractColumnDescriptor): + + def __init__(self, column): + self.column = column + + def _get_column(self): + return self.column + +class ColumnDescriptor(object): """ Handles the reading and writing of column values to and from a model instance's value manager, as well as creating @@ -88,6 +96,7 @@ class ColumnDescriptor(AbstractColumnDescriptor): :return: """ self.column = column + self.query_evaluator = ColumnQueryEvaluator(self.column) def __get__(self, instance, owner): """ @@ -101,7 +110,7 @@ class ColumnDescriptor(AbstractColumnDescriptor): if instance: return instance._values[self.column.column_name].getval() else: - return self.column + return self.query_evaluator def __set__(self, instance, value): """ @@ -133,6 +142,7 @@ class BaseModel(object): class MultipleObjectsReturned(_MultipleObjectsReturned): pass objects = QuerySetDescriptor() + query = QueryExpressionDescriptor() #table names will be generated automatically from it's model and package name #however, you can also define them manually here diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 155fc17e..25cd5906 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -3,7 +3,7 @@ from cqlengine.query import QueryException from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException, CQLEngineException -from cqlengine.models import Model, ModelDefinitionException +from cqlengine.models import Model, ModelDefinitionException, ColumnQueryEvaluator from cqlengine import columns import cqlengine @@ -270,7 +270,7 @@ class TestAbstractModelClasses(BaseCassEngTestCase): def test_abstract_columns_are_inherited(self): """ Tests that columns defined in the abstract class are inherited into the concrete class """ assert hasattr(ConcreteModelWithCol, 'pkey') - assert isinstance(ConcreteModelWithCol.pkey, columns.Column) + assert isinstance(ConcreteModelWithCol.pkey, ColumnQueryEvaluator) assert isinstance(ConcreteModelWithCol._columns['pkey'], columns.Column) def test_concrete_class_table_creation_cycle(self): From c3c21fa1d4b5459617292710dfb083ac38584cda Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:27:56 -0700 Subject: [PATCH 04/11] adding test around query expressions --- cqlengine/tests/query/test_queryset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 17954d18..549eec68 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -52,6 +52,22 @@ class TestQuerySetOperation(BaseCassEngTestCase): assert isinstance(op, query.GreaterThanOrEqualOperator) assert op.value == 1 + def test_query_expression_parsing(self): + """ Tests that query experessions are evaluated properly """ + query1 = TestModel.query(TestModel.test_id == 5) + assert len(query1._where) == 1 + + op = query1._where[0] + assert isinstance(op, query.EqualsOperator) + assert op.value == 5 + + query2 = query1.query(TestModel.expected_result >= 1) + assert len(query2._where) == 2 + + op = query2._where[1] + assert isinstance(op, query.GreaterThanOrEqualOperator) + assert op.value == 1 + def test_using_invalid_column_names_in_filter_kwargs_raises_error(self): """ Tests that using invalid or nonexistant column names for filter args raises an error From 0d251d170a1a2be5d8d76a0746602601de3f938a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:33:02 -0700 Subject: [PATCH 05/11] adding query method argument validation and supporting tests --- cqlengine/query.py | 2 ++ cqlengine/tests/query/test_queryset.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 95b6fbc3..efeb40e0 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -569,6 +569,8 @@ class QuerySet(object): """ clone = copy.deepcopy(self) for operator in args: + if not isinstance(operator, QueryOperator): + raise QueryException('{} is not a valid query operator'.format(operator)) clone._where.append(operator) return clone diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 549eec68..7208075e 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -73,7 +73,21 @@ class TestQuerySetOperation(BaseCassEngTestCase): Tests that using invalid or nonexistant column names for filter args raises an error """ with self.assertRaises(query.QueryException): - query0 = TestModel.objects(nonsense=5) + TestModel.objects(nonsense=5) + + def test_using_nonexistant_column_names_in_query_args_raises_error(self): + """ + Tests that using invalid or nonexistant columns for query args raises an error + """ + with self.assertRaises(AttributeError): + TestModel.query(TestModel.nonsense == 5) + + def test_using_non_query_operators_in_query_args_raises_error(self): + """ + Tests that providing query args that are not query operator instances raises an error + """ + with self.assertRaises(query.QueryException): + TestModel.query(5) def test_where_clause_generation(self): """ From 286da25d9aa8d7528217a4c7497cc3dbcd4111d1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:36:43 -0700 Subject: [PATCH 06/11] adding more tests around query method behavior --- cqlengine/tests/query/test_queryset.py | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 7208075e..3d9cca8a 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -89,7 +89,7 @@ class TestQuerySetOperation(BaseCassEngTestCase): with self.assertRaises(query.QueryException): TestModel.query(5) - def test_where_clause_generation(self): + def test_filter_method_where_clause_generation(self): """ Tests the where clause creation """ @@ -103,6 +103,19 @@ class TestQuerySetOperation(BaseCassEngTestCase): where = query2._where_clause() assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) + def test_query_method_where_clause_generation(self): + """ + Tests the where clause creation + """ + query1 = TestModel.query(TestModel.test_id == 5) + ids = [o.query_value.identifier for o in query1._where] + where = query1._where_clause() + assert where == '"test_id" = :{}'.format(*ids) + + query2 = query1.query(TestModel.expected_result >= 1) + ids = [o.query_value.identifier for o in query2._where] + where = query2._where_clause() + assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) def test_querystring_generation(self): """ @@ -118,6 +131,18 @@ class TestQuerySetOperation(BaseCassEngTestCase): query2 = query1.filter(expected_result__gte=1) assert len(query2._where) == 2 + assert len(query1._where) == 1 + + def test_querymethod_queryset_is_immutable(self): + """ + Tests that calling a queryset function that changes it's state returns a new queryset + """ + query1 = TestModel.query(TestModel.test_id == 5) + assert len(query1._where) == 1 + + query2 = query1.query(TestModel.expected_result >= 1) + assert len(query2._where) == 2 + assert len(query1._where) == 1 def test_the_all_method_duplicates_queryset(self): """ From 83d904f58f76dd628f1c97392dd353bd3c7cdc22 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:45:11 -0700 Subject: [PATCH 07/11] adding additional tests around query method --- cqlengine/tests/query/test_queryset.py | 67 +++++++++++++++++--------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 3d9cca8a..230c1dd0 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -218,12 +218,21 @@ class BaseQuerySetUsage(BaseCassEngTestCase): class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): def test_count(self): + """ Tests that adding filtering statements affects the count query as expected """ assert TestModel.objects.count() == 12 q = TestModel.objects(test_id=0) assert q.count() == 4 + def test_query_method_count(self): + """ Tests that adding query statements affects the count query as expected """ + assert TestModel.objects.count() == 12 + + q = TestModel.query(TestModel.test_id == 0) + assert q.count() == 4 + def test_iteration(self): + """ Tests that iterating over a query set pulls back all of the expected results """ q = TestModel.objects(test_id=0) #tuple of expected attempt_id, expected_result values compare_set = set([(0,5), (1,10), (2,15), (3,20)]) @@ -233,6 +242,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): compare_set.remove(val) assert len(compare_set) == 0 + # test with regular filtering q = TestModel.objects(attempt_id=3).allow_filtering() assert len(q) == 3 #tuple of expected test_id, expected_result values @@ -243,36 +253,49 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): compare_set.remove(val) assert len(compare_set) == 0 + # test with query method + q = TestModel.query(TestModel.attempt_id == 3).allow_filtering() + assert len(q) == 3 + #tuple of expected test_id, expected_result values + compare_set = set([(0,20), (1,20), (2,75)]) + for t in q: + val = t.test_id, t.expected_result + assert val in compare_set + compare_set.remove(val) + assert len(compare_set) == 0 + def test_multiple_iterations_work_properly(self): """ Tests that iterating over a query set more than once works """ - q = TestModel.objects(test_id=0) - #tuple of expected attempt_id, expected_result values - compare_set = set([(0,5), (1,10), (2,15), (3,20)]) - for t in q: - val = t.attempt_id, t.expected_result - assert val in compare_set - compare_set.remove(val) - assert len(compare_set) == 0 + # test with both the filtering method and the query method + for q in (TestModel.objects(test_id=0), TestModel.query(TestModel.test_id==0)): + #tuple of expected attempt_id, expected_result values + compare_set = set([(0,5), (1,10), (2,15), (3,20)]) + for t in q: + val = t.attempt_id, t.expected_result + assert val in compare_set + compare_set.remove(val) + assert len(compare_set) == 0 - #try it again - compare_set = set([(0,5), (1,10), (2,15), (3,20)]) - for t in q: - val = t.attempt_id, t.expected_result - assert val in compare_set - compare_set.remove(val) - assert len(compare_set) == 0 + #try it again + compare_set = set([(0,5), (1,10), (2,15), (3,20)]) + for t in q: + val = t.attempt_id, t.expected_result + assert val in compare_set + compare_set.remove(val) + assert len(compare_set) == 0 def test_multiple_iterators_are_isolated(self): """ tests that the use of one iterator does not affect the behavior of another """ - q = TestModel.objects(test_id=0).order_by('attempt_id') - expected_order = [0,1,2,3] - iter1 = iter(q) - iter2 = iter(q) - for attempt_id in expected_order: - assert iter1.next().attempt_id == attempt_id - assert iter2.next().attempt_id == attempt_id + for q in (TestModel.objects(test_id=0), TestModel.query(TestModel.test_id==0)): + q = q.order_by('attempt_id') + expected_order = [0,1,2,3] + iter1 = iter(q) + iter2 = iter(q) + for attempt_id in expected_order: + assert iter1.next().attempt_id == attempt_id + assert iter2.next().attempt_id == attempt_id def test_get_success_case(self): """ From b6701db99ed17e15c3e26dcd82fb061ca3dfbf29 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:54:23 -0700 Subject: [PATCH 08/11] rolling all query types into the filter method and removing the query method --- cqlengine/models.py | 29 ++++------------------- cqlengine/query.py | 32 ++++++++------------------ cqlengine/tests/query/test_queryset.py | 24 +++++++++---------- 3 files changed, 27 insertions(+), 58 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 808938e0..afc2678d 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -34,6 +34,7 @@ class hybrid_classmethod(object): """ raise NotImplementedError + class QuerySetDescriptor(object): """ returns a fresh queryset for the given model @@ -54,25 +55,6 @@ class QuerySetDescriptor(object): """ raise NotImplementedError -class QueryExpressionDescriptor(object): - """ - returns a fresh queryset /query method for the given model - it's declared on everytime it's accessed - """ - - def __get__(self, obj, model): - """ :rtype: QuerySet """ - if model.__abstract__: - raise CQLEngineException('cannot execute queries against abstract models') - return QuerySet(model).query - - def __call__(self, *args, **kwargs): - """ - Just a hint to IDEs that it's ok to call this - - :rtype: QuerySet - """ - raise NotImplementedError class ColumnQueryEvaluator(AbstractColumnDescriptor): @@ -142,7 +124,6 @@ class BaseModel(object): class MultipleObjectsReturned(_MultipleObjectsReturned): pass objects = QuerySetDescriptor() - query = QueryExpressionDescriptor() #table names will be generated automatically from it's model and package name #however, you can also define them manually here @@ -239,12 +220,12 @@ class BaseModel(object): return cls.objects.all() @classmethod - def filter(cls, **kwargs): - return cls.objects.filter(**kwargs) + def filter(cls, *args, **kwargs): + return cls.objects.filter(*args, **kwargs) @classmethod - def get(cls, **kwargs): - return cls.objects.get(**kwargs) + def get(cls, *args, **kwargs): + return cls.objects.get(*args, **kwargs) def save(self): is_new = self.pk is None diff --git a/cqlengine/query.py b/cqlengine/query.py index efeb40e0..47eabf9a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -316,8 +316,8 @@ class QuerySet(object): def __str__(self): return str(self.__unicode__()) - def __call__(self, **kwargs): - return self.filter(**kwargs) + def __call__(self, *args, **kwargs): + return self.filter(*args, **kwargs) def __deepcopy__(self, memo): clone = self.__class__(self.model) @@ -529,7 +529,7 @@ class QuerySet(object): else: raise QueryException("Can't parse '{}'".format(arg)) - def filter(self, **kwargs): + def filter(self, *args, **kwargs): """ Adds WHERE arguments to the queryset, returning a new queryset @@ -539,6 +539,11 @@ class QuerySet(object): """ #add arguments to the where clause filters clone = copy.deepcopy(self) + for operator in args: + if not isinstance(operator, QueryOperator): + raise QueryException('{} is not a valid query operator'.format(operator)) + clone._where.append(operator) + for arg, val in kwargs.items(): col_name, col_op = self._parse_filter_arg(arg) #resolve column and operator @@ -558,31 +563,14 @@ class QuerySet(object): return clone - def query(self, *args): - """ - Same end result as filter, but uses the new comparator style args - ie: Model.column == val - - #TODO: show examples - - :rtype: QuerySet - """ - clone = copy.deepcopy(self) - for operator in args: - if not isinstance(operator, QueryOperator): - raise QueryException('{} is not a valid query operator'.format(operator)) - clone._where.append(operator) - - return clone - - def get(self, **kwargs): + def get(self, *args, **kwargs): """ Returns a single instance matching this query, optionally with additional filter kwargs. A DoesNotExistError will be raised if there are no rows matching the query A MultipleObjectsFoundError will be raised if there is more than one row matching the queyr """ - if kwargs: return self.filter(**kwargs).get() + if kwargs: return self.filter(*args, **kwargs).get() self._execute_query() if len(self._result_cache) == 0: raise self.model.DoesNotExist diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 230c1dd0..0b686178 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -54,14 +54,14 @@ class TestQuerySetOperation(BaseCassEngTestCase): def test_query_expression_parsing(self): """ Tests that query experessions are evaluated properly """ - query1 = TestModel.query(TestModel.test_id == 5) + query1 = TestModel.filter(TestModel.test_id == 5) assert len(query1._where) == 1 op = query1._where[0] assert isinstance(op, query.EqualsOperator) assert op.value == 5 - query2 = query1.query(TestModel.expected_result >= 1) + query2 = query1.filter(TestModel.expected_result >= 1) assert len(query2._where) == 2 op = query2._where[1] @@ -80,14 +80,14 @@ class TestQuerySetOperation(BaseCassEngTestCase): Tests that using invalid or nonexistant columns for query args raises an error """ with self.assertRaises(AttributeError): - TestModel.query(TestModel.nonsense == 5) + TestModel.objects(TestModel.nonsense == 5) def test_using_non_query_operators_in_query_args_raises_error(self): """ Tests that providing query args that are not query operator instances raises an error """ with self.assertRaises(query.QueryException): - TestModel.query(5) + TestModel.objects(5) def test_filter_method_where_clause_generation(self): """ @@ -107,12 +107,12 @@ class TestQuerySetOperation(BaseCassEngTestCase): """ Tests the where clause creation """ - query1 = TestModel.query(TestModel.test_id == 5) + query1 = TestModel.objects(TestModel.test_id == 5) ids = [o.query_value.identifier for o in query1._where] where = query1._where_clause() assert where == '"test_id" = :{}'.format(*ids) - query2 = query1.query(TestModel.expected_result >= 1) + query2 = query1.filter(TestModel.expected_result >= 1) ids = [o.query_value.identifier for o in query2._where] where = query2._where_clause() assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) @@ -137,10 +137,10 @@ class TestQuerySetOperation(BaseCassEngTestCase): """ Tests that calling a queryset function that changes it's state returns a new queryset """ - query1 = TestModel.query(TestModel.test_id == 5) + query1 = TestModel.objects(TestModel.test_id == 5) assert len(query1._where) == 1 - query2 = query1.query(TestModel.expected_result >= 1) + query2 = query1.filter(TestModel.expected_result >= 1) assert len(query2._where) == 2 assert len(query1._where) == 1 @@ -228,7 +228,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): """ Tests that adding query statements affects the count query as expected """ assert TestModel.objects.count() == 12 - q = TestModel.query(TestModel.test_id == 0) + q = TestModel.objects(TestModel.test_id == 0) assert q.count() == 4 def test_iteration(self): @@ -254,7 +254,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): assert len(compare_set) == 0 # test with query method - q = TestModel.query(TestModel.attempt_id == 3).allow_filtering() + q = TestModel.objects(TestModel.attempt_id == 3).allow_filtering() assert len(q) == 3 #tuple of expected test_id, expected_result values compare_set = set([(0,20), (1,20), (2,75)]) @@ -267,7 +267,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): def test_multiple_iterations_work_properly(self): """ Tests that iterating over a query set more than once works """ # test with both the filtering method and the query method - for q in (TestModel.objects(test_id=0), TestModel.query(TestModel.test_id==0)): + for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id==0)): #tuple of expected attempt_id, expected_result values compare_set = set([(0,5), (1,10), (2,15), (3,20)]) for t in q: @@ -288,7 +288,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): """ tests that the use of one iterator does not affect the behavior of another """ - for q in (TestModel.objects(test_id=0), TestModel.query(TestModel.test_id==0)): + for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id==0)): q = q.order_by('attempt_id') expected_order = [0,1,2,3] iter1 = iter(q) From 264fcaeb7b277573fb6e057cb40c3bf1ba8a9416 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 12:37:36 -0700 Subject: [PATCH 09/11] updating the get methods to work with query expressions --- cqlengine/query.py | 4 ++- cqlengine/tests/query/test_queryset.py | 40 ++++++++++++++++---------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 47eabf9a..a56a8a33 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -570,7 +570,9 @@ class QuerySet(object): A DoesNotExistError will be raised if there are no rows matching the query A MultipleObjectsFoundError will be raised if there is more than one row matching the queyr """ - if kwargs: return self.filter(*args, **kwargs).get() + if args or kwargs: + return self.filter(*args, **kwargs).get() + self._execute_query() if len(self._result_cache) == 0: raise self.model.DoesNotExist diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 0b686178..921da1b5 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -103,7 +103,7 @@ class TestQuerySetOperation(BaseCassEngTestCase): where = query2._where_clause() assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) - def test_query_method_where_clause_generation(self): + def test_query_expression_where_clause_generation(self): """ Tests the where clause creation """ @@ -133,17 +133,6 @@ class TestQuerySetOperation(BaseCassEngTestCase): assert len(query2._where) == 2 assert len(query1._where) == 1 - def test_querymethod_queryset_is_immutable(self): - """ - Tests that calling a queryset function that changes it's state returns a new queryset - """ - query1 = TestModel.objects(TestModel.test_id == 5) - assert len(query1._where) == 1 - - query2 = query1.filter(TestModel.expected_result >= 1) - assert len(query2._where) == 2 - assert len(query1._where) == 1 - def test_the_all_method_duplicates_queryset(self): """ Tests that calling all on a queryset with previously defined filters duplicates queryset @@ -224,7 +213,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): q = TestModel.objects(test_id=0) assert q.count() == 4 - def test_query_method_count(self): + def test_query_expression_count(self): """ Tests that adding query statements affects the count query as expected """ assert TestModel.objects.count() == 12 @@ -267,7 +256,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): def test_multiple_iterations_work_properly(self): """ Tests that iterating over a query set more than once works """ # test with both the filtering method and the query method - for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id==0)): + for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id == 0)): #tuple of expected attempt_id, expected_result values compare_set = set([(0,5), (1,10), (2,15), (3,20)]) for t in q: @@ -288,7 +277,7 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): """ tests that the use of one iterator does not affect the behavior of another """ - for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id==0)): + for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id == 0)): q = q.order_by('attempt_id') expected_order = [0,1,2,3] iter1 = iter(q) @@ -318,6 +307,27 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): assert m.test_id == 0 assert m.attempt_id == 0 + def test_query_expression_get_success_case(self): + """ + Tests that the .get() method works on new and existing querysets + """ + m = TestModel.get(TestModel.test_id == 0, TestModel.attempt_id == 0) + assert isinstance(m, TestModel) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = TestModel.objects(TestModel.test_id == 0, TestModel.attempt_id == 0) + m = q.get() + assert isinstance(m, TestModel) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = TestModel.objects(TestModel.test_id == 0) + m = q.get(TestModel.attempt_id == 0) + assert isinstance(m, TestModel) + assert m.test_id == 0 + assert m.attempt_id == 0 + def test_get_doesnotexist_exception(self): """ Tests that get calls that don't return a result raises a DoesNotExist error From f8c4317098ebb710570f79207044067c2fd1afe6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 12:45:36 -0700 Subject: [PATCH 10/11] adding additional tests around the query expression queries --- cqlengine/tests/query/test_queryset.py | 40 ++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 921da1b5..943efb20 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -361,10 +361,14 @@ class TestQuerySetOrdering(BaseQuerySetUsage): assert model.attempt_id == expect def test_ordering_by_non_second_primary_keys_fail(self): - + # kwarg filtering with self.assertRaises(query.QueryException): q = TestModel.objects(test_id=0).order_by('test_id') + # kwarg filtering + with self.assertRaises(query.QueryException): + q = TestModel.objects(TestModel.test_id == 0).order_by('test_id') + def test_ordering_by_non_primary_keys_fails(self): with self.assertRaises(query.QueryException): q = TestModel.objects(test_id=0).order_by('description') @@ -431,7 +435,7 @@ class TestQuerySetValidation(BaseQuerySetUsage): """ with self.assertRaises(query.QueryException): q = TestModel.objects(test_result=25) - [i for i in q] + list([i for i in q]) def test_primary_key_or_index_must_have_equal_relation_filter(self): """ @@ -439,7 +443,7 @@ class TestQuerySetValidation(BaseQuerySetUsage): """ with self.assertRaises(query.QueryException): q = TestModel.objects(test_id__gt=0) - [i for i in q] + list([i for i in q]) def test_indexed_field_can_be_queried(self): @@ -447,7 +451,6 @@ class TestQuerySetValidation(BaseQuerySetUsage): Tests that queries on an indexed field will work without any primary key relations specified """ q = IndexedTestModel.objects(test_result=25) - count = q.count() assert q.count() == 4 class TestQuerySetDelete(BaseQuerySetUsage): @@ -526,6 +529,7 @@ class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase): TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='4') time.sleep(0.2) + # test kwarg filtering q = TimeUUIDQueryModel.filter(partition=pk, time__lte=functions.MaxTimeUUID(midpoint)) q = [d for d in q] assert len(q) == 2 @@ -539,13 +543,39 @@ class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase): assert '3' in datas assert '4' in datas + # test query expression filtering + q = TimeUUIDQueryModel.filter( + TimeUUIDQueryModel.partition == pk, + TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint) + ) + q = [d for d in q] + assert len(q) == 2 + datas = [d.data for d in q] + assert '1' in datas + assert '2' in datas + + q = TimeUUIDQueryModel.filter( + TimeUUIDQueryModel.partition == pk, + TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint) + ) + assert len(q) == 2 + datas = [d.data for d in q] + assert '3' in datas + assert '4' in datas + class TestInOperator(BaseQuerySetUsage): - def test_success_case(self): + def test_kwarg_success_case(self): + """ Tests the in operator works with the kwarg query method """ q = TestModel.filter(test_id__in=[0,1]) assert q.count() == 8 + def test_query_expression_success_case(self): + """ Tests the in operator works with the query expression query method """ + q = TestModel.filter(TestModel.test_id in [0, 1]) + assert q.count() == 8 + class TestValuesList(BaseQuerySetUsage): def test_values_list(self): From 09883d6b0408424e32b4190da41271e6673d8f85 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 16:46:34 -0700 Subject: [PATCH 11/11] changed __contains__ to in_() --- cqlengine/query.py | 14 ++++++++------ cqlengine/tests/query/test_queryset.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a56a8a33..8d7a6392 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -163,12 +163,17 @@ class AbstractColumnDescriptor(object): def _get_column(self): raise NotImplementedError + def in_(self, item): + """ + Returns an in operator + + used in where you'd typically want to use python's `in` operator + """ + return InOperator(self._get_column(), item) + def __eq__(self, other): return EqualsOperator(self._get_column(), other) - def __contains__(self, item): - return InOperator(self._get_column(), item) - def __gt__(self, other): return GreaterThanOperator(self._get_column(), other) @@ -195,7 +200,6 @@ class NamedColumnDescriptor(AbstractColumnDescriptor): def to_database(self, val): return val -C = NamedColumnDescriptor class TableDescriptor(object): """ describes a cql table """ @@ -204,7 +208,6 @@ class TableDescriptor(object): self.keyspace = keyspace self.name = name -T = TableDescriptor class KeyspaceDescriptor(object): """ Describes a cql keyspace """ @@ -219,7 +222,6 @@ class KeyspaceDescriptor(object): """ return TableDescriptor(self.name, name) -K = KeyspaceDescriptor class BatchType(object): Unlogged = 'UNLOGGED' diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 943efb20..117c87c1 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -573,7 +573,7 @@ class TestInOperator(BaseQuerySetUsage): def test_query_expression_success_case(self): """ Tests the in operator works with the query expression query method """ - q = TestModel.filter(TestModel.test_id in [0, 1]) + q = TestModel.filter(TestModel.test_id.in_([0, 1])) assert q.count() == 8