From b6042ac57ad62d7f73600588061cb194e114ba83 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 21:12:47 -0800 Subject: [PATCH] adding dynamic columns to models (which aren't working yet) working on the QuerySet class adding additional tests around model saving and loading --- cassandraengine/columns.py | 2 +- cassandraengine/models.py | 18 ++++--- cassandraengine/query.py | 54 ++++++++++++++----- .../tests/columns/test_validation.py | 16 ++++++ cassandraengine/tests/model/test_model_io.py | 45 ++++++++++++++-- .../tests/model/test_validation.py | 0 6 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 cassandraengine/tests/columns/test_validation.py create mode 100644 cassandraengine/tests/model/test_validation.py diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index 747cfba9..aa7a4b35 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -14,7 +14,7 @@ class BaseColumn(object): :param primary_key: bool flag, there can be only one primary key per doc :param db_field: the fieldname this field will map to in the database :param default: the default value, can be a value or a callable (no args) - :param null: bool, is the field nullable? + :param null: boolean, is the field nullable? """ self.primary_key = primary_key self.db_field = db_field diff --git a/cassandraengine/models.py b/cassandraengine/models.py index 4e5bd9d3..11500896 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -25,10 +25,6 @@ class BaseModel(object): if k not in values: setattr(self, k, None) - @classmethod - def _column_family_definition(cls): - pass - @classmethod def find(cls, pk): """ Loads a document by it's primary key """ @@ -39,6 +35,16 @@ class BaseModel(object): """ Returns the object's primary key, regardless of it's name """ return getattr(self, self._pk_name) + #dynamic column methods + def __getitem__(self, key): + return self._dynamic_columns[key] + + def __setitem__(self, key, val): + self._dynamic_columns[key] = val + + def __delitem__(self, key): + del self._dynamic_columns[key] + def validate(self): """ Cleans and validates the field values """ for name, col in self._columns.items(): @@ -47,11 +53,9 @@ class BaseModel(object): def as_dict(self): """ Returns a map of column names to cleaned values """ - values = {} + values = self._dynamic_columns or {} for name, col in self._columns.items(): values[name] = col.to_database(getattr(self, name, None)) - - #TODO: merge in dynamic columns return values def save(self): diff --git a/cassandraengine/query.py b/cassandraengine/query.py index bb0532d8..76149f63 100644 --- a/cassandraengine/query.py +++ b/cassandraengine/query.py @@ -1,3 +1,5 @@ +import copy + from cassandraengine.connection import get_connection class QuerySet(object): @@ -5,16 +7,18 @@ class QuerySet(object): #TODO: querysets should be executed lazily #TODO: conflicting filter args should raise exception unless a force kwarg is supplied - def __init__(self, model, query={}): + def __init__(self, model, query_args={}): super(QuerySet, self).__init__() self.model = model + self.query_args = query_args self.column_family_name = self.model.objects.column_family_name self._cursor = None #----query generation / execution---- def _execute_query(self): - pass + conn = get_connection() + self._cursor = conn.cursor() def _generate_querystring(self): pass @@ -27,39 +31,61 @@ class QuerySet(object): #----Reads------ def __iter__(self): - pass + if self._cursor is None: + self._execute_query() + return self + + def _get_next(self): + """ + Gets the next cursor result + Returns a db_field->value dict + """ + cur = self._cursor + values = cur.fetchone() + if values is None: return None + names = [i[0] for i in cur.description] + value_dict = dict(zip(names, values)) + return value_dict def next(self): - pass + values = self._get_next() + if values is None: raise StopIteration + return values def first(self): - conn = get_connection() - cur = conn.cursor() pass def all(self): - pass + return QuerySet(self.model) def filter(self, **kwargs): - pass + qargs = copy.deepcopy(self.query_args) + qargs.update(kwargs) + return QuerySet(self.model, query_args=qargs) def exclude(self, **kwargs): + """ + Need to invert the logic for all kwargs + """ pass + def count(self): + """ + Returns the number of rows matched by this query + """ + def find(self, pk): """ loads one document identified by it's primary key """ + #TODO: make this a convenience wrapper of the filter method qs = 'SELECT * FROM {column_family} WHERE {pk_name}=:{pk_name}' qs = qs.format(column_family=self.column_family_name, pk_name=self.model._pk_name) conn = get_connection() - cur = conn.cursor() - cur.execute(qs, {self.model._pk_name:pk}) - values = cur.fetchone() - names = [i[0] for i in cur.description] - value_dict = dict(zip(names, values)) - return value_dict + self._cursor = conn.cursor() + self._cursor.execute(qs, {self.model._pk_name:pk}) + return self._get_next() #----writes---- diff --git a/cassandraengine/tests/columns/test_validation.py b/cassandraengine/tests/columns/test_validation.py new file mode 100644 index 00000000..e42519cc --- /dev/null +++ b/cassandraengine/tests/columns/test_validation.py @@ -0,0 +1,16 @@ +#tests the behavior of the column classes + +from cassandraengine.tests.base import BaseCassEngTestCase + +from cassandraengine.columns import BaseColumn +from cassandraengine.columns import Bytes +from cassandraengine.columns import Ascii +from cassandraengine.columns import Text +from cassandraengine.columns import Integer +from cassandraengine.columns import DateTime +from cassandraengine.columns import UUID +from cassandraengine.columns import Boolean +from cassandraengine.columns import Float +from cassandraengine.columns import Decimal + + diff --git a/cassandraengine/tests/model/test_model_io.py b/cassandraengine/tests/model/test_model_io.py index 4b73c26e..7680a789 100644 --- a/cassandraengine/tests/model/test_model_io.py +++ b/cassandraengine/tests/model/test_model_io.py @@ -1,3 +1,4 @@ +from unittest import skip from cassandraengine.tests.base import BaseCassEngTestCase from cassandraengine.models import Model @@ -7,20 +8,56 @@ class TestModel(Model): count = columns.Integer() text = columns.Text() +#class TestModel2(Model): + class TestModelIO(BaseCassEngTestCase): def setUp(self): super(TestModelIO, self).setUp() TestModel.objects._create_column_family() - def tearDown(self): - super(TestModelIO, self).tearDown() - TestModel.objects._delete_column_family() - def test_model_save_and_load(self): + """ + Tests that models can be saved and retrieved + """ tm = TestModel.objects.create(count=8, text='123456789') tm2 = TestModel.objects.find(tm.pk) for cname in tm._columns.keys(): self.assertEquals(getattr(tm, cname), getattr(tm2, cname)) + def test_model_updating_works_properly(self): + """ + Tests that subsequent saves after initial model creation work + """ + tm = TestModel.objects.create(count=8, text='123456789') + + tm.count = 100 + tm.save() + + tm2 = TestModel.objects.find(tm.pk) + self.assertEquals(tm.count, tm2.count) + + def test_nullable_columns_are_saved_properly(self): + """ + Tests that nullable columns save without any trouble + """ + + @skip + def test_dynamic_columns(self): + """ + Tests that items put into dynamic columns are saved and retrieved properly + + Note: seems I've misunderstood how arbitrary column names work in Cassandra + skipping for now + """ + #TODO:Fix this + tm = TestModel(count=8, text='123456789') + tm['other'] = 'something' + tm['number'] = 5 + tm.save() + + tm2 = TestModel.objects.find(tm.pk) + self.assertEquals(tm['other'], tm2['other']) + self.assertEquals(tm['number'], tm2['number']) + diff --git a/cassandraengine/tests/model/test_validation.py b/cassandraengine/tests/model/test_validation.py new file mode 100644 index 00000000..e69de29b