starting to write the QuerySet class
This commit is contained in:
		| @@ -1,4 +1,6 @@ | |||||||
| #cqlengine exceptions | #cqlengine exceptions | ||||||
| class ModelException(BaseException): pass | class CQLEngineException(BaseException): pass | ||||||
| class ValidationError(BaseException): pass | class ModelException(CQLEngineException): pass | ||||||
|  | class ValidationError(CQLEngineException): pass | ||||||
|  |  | ||||||
|  | class QueryException(CQLEngineException): pass | ||||||
|   | |||||||
| @@ -1,18 +1,58 @@ | |||||||
|  | from collections import namedtuple | ||||||
| import copy | import copy | ||||||
|  |  | ||||||
| from cqlengine.connection import get_connection | from cqlengine.connection import get_connection | ||||||
|  | from cqlengine.exceptions import QueryException | ||||||
|  |  | ||||||
| #CQL 3 reference: | #CQL 3 reference: | ||||||
| #http://www.datastax.com/docs/1.1/references/cql/index | #http://www.datastax.com/docs/1.1/references/cql/index | ||||||
|  |  | ||||||
| class Query(object): | WhereFilter = namedtuple('WhereFilter', ['column', 'operator', 'value']) | ||||||
|  |  | ||||||
|  | class QueryOperatorException(QueryException): pass | ||||||
|  |  | ||||||
|  | class QueryOperator(object): | ||||||
|  |     symbol = None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_operator(cls, symbol): | ||||||
|  |         if not hasattr(cls, 'opmap'): | ||||||
|  |             QueryOperator.opmap = {} | ||||||
|  |             def _recurse(klass): | ||||||
|  |                 if klass.symbol: | ||||||
|  |                     QueryOperator.opmap[klass.symbol.upper()] = klass | ||||||
|  |                 for subklass in klass.__subclasses__(): | ||||||
|  |                     _recurse(subklass) | ||||||
|                 pass |                 pass | ||||||
|  |             _recurse(QueryOperator) | ||||||
|  |         try: | ||||||
|  |             return QueryOperator.opmap[symbol.upper()] | ||||||
|  |         except KeyError: | ||||||
|  |             raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) | ||||||
|  |  | ||||||
|  | class EqualsOperator(QueryOperator): | ||||||
|  |     symbol = 'EQ' | ||||||
|  |  | ||||||
|  | class InOperator(QueryOperator): | ||||||
|  |     symbol = 'IN' | ||||||
|  |  | ||||||
|  | class GreaterThanOperator(QueryOperator): | ||||||
|  |     symbol = "GT" | ||||||
|  |  | ||||||
|  | class GreaterThanOrEqualOperator(QueryOperator): | ||||||
|  |     symbol = "GTE" | ||||||
|  |  | ||||||
|  | class LessThanOperator(QueryOperator): | ||||||
|  |     symbol = "LT" | ||||||
|  |  | ||||||
|  | class LessThanOrEqualOperator(QueryOperator): | ||||||
|  |     symbol = "LTE" | ||||||
|  |  | ||||||
| class QuerySet(object): | class QuerySet(object): | ||||||
|     #TODO: querysets should be immutable |     #TODO: querysets should be immutable | ||||||
|     #TODO: querysets should be executed lazily |     #TODO: querysets should be executed lazily | ||||||
|     #TODO: conflicting filter args should raise exception unless a force kwarg is supplied |     #TODO: support specifying offset and limit (use slice) (maybe return a mutated queryset) | ||||||
|  |     #TODO: support specifying columns to exclude or select only | ||||||
|  |  | ||||||
|     #CQL supports ==, >, >=, <, <=, IN (a,b,c,..n) |     #CQL supports ==, >, >=, <, <=, IN (a,b,c,..n) | ||||||
|     #REVERSE, LIMIT |     #REVERSE, LIMIT | ||||||
| @@ -21,9 +61,22 @@ class QuerySet(object): | |||||||
|     def __init__(self, model, query_args={}): |     def __init__(self, model, query_args={}): | ||||||
|         super(QuerySet, self).__init__() |         super(QuerySet, self).__init__() | ||||||
|         self.model = model |         self.model = model | ||||||
|         self.query_args = query_args |  | ||||||
|         self.column_family_name = self.model.objects.column_family_name |         self.column_family_name = self.model.objects.column_family_name | ||||||
|  |  | ||||||
|  |         #Where clause filters | ||||||
|  |         self._where = [] | ||||||
|  |  | ||||||
|  |         #ordering arguments | ||||||
|  |         self._order = [] | ||||||
|  |  | ||||||
|  |         #subset selection | ||||||
|  |         self._limit = None | ||||||
|  |         self._start = None | ||||||
|  |  | ||||||
|  |         #see the defer and only methods | ||||||
|  |         self._defer_fields = [] | ||||||
|  |         self._only_fields = [] | ||||||
|  |  | ||||||
|         self._cursor = None |         self._cursor = None | ||||||
|  |  | ||||||
|     #----query generation / execution---- |     #----query generation / execution---- | ||||||
| @@ -31,7 +84,16 @@ class QuerySet(object): | |||||||
|         conn = get_connection() |         conn = get_connection() | ||||||
|         self._cursor = conn.cursor() |         self._cursor = conn.cursor() | ||||||
|  |  | ||||||
|     def _generate_querystring(self): |     def _where_clause(self): | ||||||
|  |         """ | ||||||
|  |         Returns a where clause based on the given filter args | ||||||
|  |         """ | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def _select_query(self): | ||||||
|  |         """ | ||||||
|  |         Returns a select clause based on the given filter args | ||||||
|  |         """ | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -67,16 +129,36 @@ class QuerySet(object): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     def all(self): |     def all(self): | ||||||
|         return QuerySet(self.model) |         clone = copy.deepcopy(self) | ||||||
|  |         clone._where = [] | ||||||
|  |         return clone | ||||||
|  |  | ||||||
|  |     def _parse_filter_arg(self, arg, val): | ||||||
|  |         statement = arg.split('__') | ||||||
|  |         if len(statement) == 1: | ||||||
|  |             return WhereFilter(arg, None, val) | ||||||
|  |         elif len(statement) == 2: | ||||||
|  |             return WhereFilter(statement[0], statement[1], val) | ||||||
|  |         else: | ||||||
|  |             raise QueryException("Can't parse '{}'".format(arg)) | ||||||
|  |  | ||||||
|     def filter(self, **kwargs): |     def filter(self, **kwargs): | ||||||
|         qargs = copy.deepcopy(self.query_args) |         #add arguments to the where clause filters | ||||||
|         qargs.update(kwargs) |         clone = copy.deepcopy(self) | ||||||
|         return QuerySet(self.model, query_args=qargs) |         for arg, val in kwargs.items(): | ||||||
|  |             raw_statement = self._parse_filter_arg(arg, val) | ||||||
|  |             #resolve column and operator | ||||||
|  |             try: | ||||||
|  |                 column = self.model._columns[raw_statement.column] | ||||||
|  |             except KeyError: | ||||||
|  |                 raise QueryException("Can't resolve column name: '{}'".format(raw_statement.column)) | ||||||
|  |  | ||||||
|     def exclude(self, **kwargs): |             operator = QueryOperator.get_operator(raw_statement.operator) | ||||||
|         """ Need to invert the logic for all kwargs """ |  | ||||||
|         pass |             statement = WhereFilter(column, operator, val) | ||||||
|  |             clone._where.append(statement) | ||||||
|  |  | ||||||
|  |         return clone | ||||||
|  |  | ||||||
|     def count(self): |     def count(self): | ||||||
|         """ Returns the number of rows matched by this query """ |         """ Returns the number of rows matched by this query """ | ||||||
| @@ -95,6 +177,32 @@ class QuerySet(object): | |||||||
|         self._cursor.execute(qs, {self.model._pk_name:pk}) |         self._cursor.execute(qs, {self.model._pk_name:pk}) | ||||||
|         return self._get_next() |         return self._get_next() | ||||||
|  |  | ||||||
|  |     def _only_or_defer(self, action, fields): | ||||||
|  |         clone = copy.deepcopy(self) | ||||||
|  |         if clone._defer_fields or clone._only_fields: | ||||||
|  |             raise QueryException("QuerySet alread has only or defer fields defined") | ||||||
|  |  | ||||||
|  |         #check for strange fields | ||||||
|  |         missing_fields = [f for f in fields if f not in self.model._columns.keys()] | ||||||
|  |         if missing_fields: | ||||||
|  |             raise QueryException("Can't resolve fields {} in {}".format(', '.join(missing_fields), self.model.__name__)) | ||||||
|  |  | ||||||
|  |         if action == 'defer': | ||||||
|  |             clone._defer_fields = fields | ||||||
|  |         elif action == 'only': | ||||||
|  |             clone._only_fields = fields | ||||||
|  |         else: | ||||||
|  |             raise ValueError | ||||||
|  |  | ||||||
|  |         return clone | ||||||
|  |  | ||||||
|  |     def only(self, fields): | ||||||
|  |         """ Load only these fields for the returned query """ | ||||||
|  |         return self._only_or_defer('only', fields) | ||||||
|  |  | ||||||
|  |     def defer(self, fields): | ||||||
|  |         """ Don't load these fields for the returned query """ | ||||||
|  |         return self._only_or_defer('defer', fields) | ||||||
|  |  | ||||||
|     #----writes---- |     #----writes---- | ||||||
|     def save(self, instance): |     def save(self, instance): | ||||||
| @@ -137,7 +245,7 @@ class QuerySet(object): | |||||||
|         cur.execute(qs, field_values) |         cur.execute(qs, field_values) | ||||||
|  |  | ||||||
|     #----delete--- |     #----delete--- | ||||||
|     def delete(self): |     def delete(self, columns=[]): | ||||||
|         """ |         """ | ||||||
|         Deletes the contents of a query |         Deletes the contents of a query | ||||||
|         """ |         """ | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								cqlengine/tests/query/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cqlengine/tests/query/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										52
									
								
								cqlengine/tests/query/test_queryset.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								cqlengine/tests/query/test_queryset.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | from cqlengine.tests.base import BaseCassEngTestCase | ||||||
|  |  | ||||||
|  | from cqlengine.exceptions import ModelException | ||||||
|  | from cqlengine.models import Model | ||||||
|  | from cqlengine import columns | ||||||
|  |  | ||||||
|  | class TestQuerySet(BaseCassEngTestCase): | ||||||
|  |  | ||||||
|  |     def test_query_filter_parsing(self): | ||||||
|  |         """ | ||||||
|  |         Tests the queryset filter method | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_where_clause_generation(self): | ||||||
|  |         """ | ||||||
|  |         Tests the where clause creation | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_querystring_generation(self): | ||||||
|  |         """ | ||||||
|  |         Tests the select querystring creation | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_queryset_is_immutable(self): | ||||||
|  |         """ | ||||||
|  |         Tests that calling a queryset function that changes it's state returns a new queryset | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_queryset_slicing(self): | ||||||
|  |         """ | ||||||
|  |         Check that the limit and start is implemented as iterator slices | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_proper_delete_behavior(self): | ||||||
|  |         """ | ||||||
|  |         Tests that deleting the contents of a queryset works properly | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_the_all_method_clears_where_filter(self): | ||||||
|  |         """ | ||||||
|  |         Tests that calling all on a queryset with previously defined filters returns a queryset with no filters | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_defining_only_and_defer_fails(self): | ||||||
|  |         """ | ||||||
|  |         Tests that trying to add fields to either only or defer, or doing so more than once fails | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def test_defining_only_or_defer_fields_fails(self): | ||||||
|  |         """ | ||||||
|  |         Tests that setting only or defer fields that don't exist raises an exception | ||||||
|  |         """ | ||||||
		Reference in New Issue
	
	Block a user
	 Blake Eggleston
					Blake Eggleston