From 9f6fcb11ebe2948167e982aa9ca9713b54a0e47d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 8 Nov 2012 21:28:44 -0800 Subject: [PATCH 0001/3961] Initial commit --- .gitignore | 27 +++++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..f24cd9952d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg diff --git a/README.md b/README.md new file mode 100644 index 0000000000..869495c22b --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +cassandraengine +=============== \ No newline at end of file From 4890726ad57ee388b205acf6749e93b4f8679132 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 8 Nov 2012 22:26:06 -0800 Subject: [PATCH 0002/3961] initial research --- cassandraengine/__init__.py | 0 cassandraengine/columns.py | 0 cassandraengine/connection.py | 19 +++++++++++++++++++ cassandraengine/document.py | 0 cassandraengine/tests/__init__.py | 0 requirements.txt | 0 6 files changed, 19 insertions(+) create mode 100644 cassandraengine/__init__.py create mode 100644 cassandraengine/columns.py create mode 100644 cassandraengine/connection.py create mode 100644 cassandraengine/document.py create mode 100644 cassandraengine/tests/__init__.py create mode 100644 requirements.txt diff --git a/cassandraengine/__init__.py b/cassandraengine/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cassandraengine/connection.py b/cassandraengine/connection.py new file mode 100644 index 0000000000..f4d6c36c62 --- /dev/null +++ b/cassandraengine/connection.py @@ -0,0 +1,19 @@ +#http://pypi.python.org/pypi/cql/1.0.4 +#http://code.google.com/a/apache-extras.org/p/cassandra-dbapi2/ + +import cql + +_keyspace = 'cse_test' +_col_fam = 'colfam' + +conn = cql.connect('127.0.0.1', 9160) +cur = conn.cursor() + +#cli examples here: +#http://wiki.apache.org/cassandra/CassandraCli +cur.execute('create keyspace {};'.format(_keyspace)) + +#http://stackoverflow.com/questions/10871595/how-do-you-create-a-counter-columnfamily-with-cql3-in-cassandra +cur.execute('create column family {};'.format(_col_fam)) + +cur.execute('insert into colfam (obj_id, content) values (:obj_id, :content)', dict(obj_id=str(uuid.uuid4()), content='yo!')) diff --git a/cassandraengine/document.py b/cassandraengine/document.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cassandraengine/tests/__init__.py b/cassandraengine/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..e69de29bb2 From bc3c26a1ef73d64d9a7835a1cd3d3dcd4f76dffd Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 9 Nov 2012 07:17:08 -0800 Subject: [PATCH 0003/3961] adding example cql and crud method stubs --- cassandraengine/connection.py | 56 ++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/cassandraengine/connection.py b/cassandraengine/connection.py index f4d6c36c62..27482829ff 100644 --- a/cassandraengine/connection.py +++ b/cassandraengine/connection.py @@ -1,5 +1,6 @@ #http://pypi.python.org/pypi/cql/1.0.4 #http://code.google.com/a/apache-extras.org/p/cassandra-dbapi2/ +#http://cassandra.apache.org/doc/cql/CQL.html import cql @@ -7,13 +8,60 @@ _col_fam = 'colfam' conn = cql.connect('127.0.0.1', 9160) -cur = conn.cursor() #cli examples here: #http://wiki.apache.org/cassandra/CassandraCli -cur.execute('create keyspace {};'.format(_keyspace)) +try: + conn.set_initial_keyspace(_keyspace) +except cql.ProgrammingError: + #http://www.datastax.com/docs/1.0/references/cql/CREATE_KEYSPACE + cur = conn.cursor() + cur.execute("create keyspace {} with strategy_class = 'SimpleStrategy' and strategy_options:replication_factor=1;".format(_keyspace)) + conn.set_initial_keyspace(_keyspace) +cur = conn.cursor() +#http://www.datastax.com/docs/1.0/dml/using_cql #http://stackoverflow.com/questions/10871595/how-do-you-create-a-counter-columnfamily-with-cql3-in-cassandra -cur.execute('create column family {};'.format(_col_fam)) +try: + cur.execute("""create table colfam (id int PRIMARY KEY);""") +except cql.ProgrammingError: + #cur.execute('drop table colfam;') + pass + +#http://www.datastax.com/docs/1.0/references/cql/INSERT +#updates/inserts do the same thing +cur.execute('insert into colfam (id, content) values (:id, :content)', dict(id=1, content='yo!')) + +cur.execute('select * from colfam WHERE id=1;') +cur.fetchone() + +cur.execute("update colfam set content='hey' where id=1;") +cur.execute('select * from colfam WHERE id=1;') +cur.fetchone() + +cur.execute('delete from colfam WHERE id=1;') +cur.execute('delete from colfam WHERE id in (1);') + +#TODO: Add alter altering existing schema functionality +#http://www.datastax.com/docs/1.0/references/cql/ALTER_COLUMNFAMILY + +def create_column_family(name): + """ + """ + +def select_row(column_family, **kwargs): + """ + """ + +def create_row(column_family, **kwargs): + """ + """ + +def update_row(column_family, **kwargs): + """ + """ + +def delete_row(column_family, **kwargs): + """ + """ -cur.execute('insert into colfam (obj_id, content) values (:obj_id, :content)', dict(obj_id=str(uuid.uuid4()), content='yo!')) From fb36faa0a6b8188ec6bb2d929fd91ae28349e4aa Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 10:43:28 -0800 Subject: [PATCH 0004/3961] adding initial column classes, document class stubs and an exception file --- cassandraengine/columns.py | 142 ++++++++++++++++++++++++++++++++++ cassandraengine/document.py | 18 +++++ cassandraengine/exceptions.py | 2 + 3 files changed, 162 insertions(+) create mode 100644 cassandraengine/exceptions.py diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index e69de29bb2..6bdda146ea 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -0,0 +1,142 @@ +#column field types +import re +from uuid import uuid1, uuid4 + +class BaseColumn(object): + """ + The base column + """ + + #the cassandra type this column maps to + db_type = None + + def __init__(self, primary_key=False, db_field=None, default=None, null=False): + """ + :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? + """ + self.primary_key = primary_key + self.db_field = db_field + self.default = default + self.null = null + + def validate(self, value): + if not self.has_default: + if not self.null and value is None: + raise ValidationError('null values are not allowed') + + def to_python(self, value): + """ + Converts data from the database into python values + raises a ValidationError if the value can't be converted + """ + return value + + @property + def has_default(self): + return bool(self.default) + + def get_default(self): + if self.has_default: + if callable(self.default): + return self.default() + else: + return self.default + + def to_database(self, value): + """ + Converts python value into database value + """ + if value is None and self.has_default: + return self.get_default() + return value + + def get_column_def(self): + """ + Returns a column definition for CQL table definition + """ + dterms = [self.db_field, self.db_type] + if self.primary_key: + dterms.append('PRIMARY KEY') + return ' '.join(dterms) + + def set_db_name(self, name): + """ + Sets the column name during document class construction + This value will be ignored if db_field is set in __init__ + """ + self.db_field = self.db_field or name + +class Bytes(BaseColumn): + db_type = 'blob' + +class Ascii(BaseColumn): + db_type = 'ascii' + +class Text(BaseColumn): + db_type = 'text' + +class Integer(BaseColumn): + db_type = 'int' + + def validate(self, value): + super(Integer, self).validate(value) + try: + long(value) + except (TypeError, ValueError): + raise ValidationError("{} can't be converted to integral value".format(value)) + + def to_python(self, value): + self.validate(value) + return long(value) + + def to_database(self, value): + self.validate() + return value + +class DateTime(BaseColumn): + db_type = 'timestamp' + +class UUID(BaseColumn): + """ + Type 1 or 4 UUID + """ + db_type = 'uuid' + + re_uuid = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') + + def __init__(self, **kwargs): + super(UUID, self).__init__(**kwargs) + if 'default' not in kwargs: + self.default = uuid4 + + def validate(self, value): + super(UUID, self).validate(value) + if not self.re_uuid.match(value): + raise ValidationError("{} is not a valid uuid".format(value)) + return value + +class Boolean(BaseColumn): + db_type = 'boolean' + + def to_python(self, value): + return bool(value) + + def to_database(self, value): + return bool(value) + +class Float(BaseColumn): + db_type = 'double' + + def to_python(self, value): + return float(value) + + def to_database(self, value): + return float(value) + +class Decimal(BaseColumn): + db_type = 'decimal' + #TODO: this + diff --git a/cassandraengine/document.py b/cassandraengine/document.py index e69de29bb2..d8d72f04b7 100644 --- a/cassandraengine/document.py +++ b/cassandraengine/document.py @@ -0,0 +1,18 @@ + +class BaseDocument(object): + pass + +class DocumentMetaClass(type): + + def __new__(cls, name, bases, attrs): + """ + """ + #TODO:assert primary key exists + #TODO:get column family name + #TODO:check that all resolved column family names are unique + return super(DocumentMetaClass, cls).__new__(cls, name, bases, attrs) + +class Document(BaseDocument): + """ + """ + __metaclass__ = DocumentMetaClass diff --git a/cassandraengine/exceptions.py b/cassandraengine/exceptions.py new file mode 100644 index 0000000000..8265b0ceab --- /dev/null +++ b/cassandraengine/exceptions.py @@ -0,0 +1,2 @@ + +class ValidationError(BaseException): pass From f3ca4843b0fb8495a81c9d26d897aea579120e4f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 17:26:27 -0800 Subject: [PATCH 0005/3961] Renamed document.py to models.py defined a base model class, as well as it's metaclass --- cassandraengine/document.py | 18 ----- cassandraengine/models.py | 127 ++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 18 deletions(-) delete mode 100644 cassandraengine/document.py create mode 100644 cassandraengine/models.py diff --git a/cassandraengine/document.py b/cassandraengine/document.py deleted file mode 100644 index d8d72f04b7..0000000000 --- a/cassandraengine/document.py +++ /dev/null @@ -1,18 +0,0 @@ - -class BaseDocument(object): - pass - -class DocumentMetaClass(type): - - def __new__(cls, name, bases, attrs): - """ - """ - #TODO:assert primary key exists - #TODO:get column family name - #TODO:check that all resolved column family names are unique - return super(DocumentMetaClass, cls).__new__(cls, name, bases, attrs) - -class Document(BaseDocument): - """ - """ - __metaclass__ = DocumentMetaClass diff --git a/cassandraengine/models.py b/cassandraengine/models.py new file mode 100644 index 0000000000..c3fce738f1 --- /dev/null +++ b/cassandraengine/models.py @@ -0,0 +1,127 @@ + +from cassandraengine import columns +from cassandraengine.exceptions import ColumnFamilyException +from cassandraengine.manager import Manager + +class BaseModel(object): + """ + The base model class, don't inherit from this, inherit from Model, defined below + """ + + db_name = None + + def __init__(self, **values): + #set columns from values + for k,v in values.items(): + if k in self._columns: + setattr(self, k, v) + else: + self._dynamic_columns[k] = v + + #set excluded columns to None + for k in self._columns.keys(): + 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 """ + cls.objects.find(pk) + + @property + def pk(self): + """ Returns the object's primary key, regardless of it's name """ + return getattr(self, self._pk_name) + + def validate(self): + """ Cleans and validates the field values """ + for name, col in self._columns.items(): + val = col.validate(getattr(self, name)) + setattr(self, name, val) + + def as_dict(self): + """ Returns a map of column names to cleaned values """ + values = {} + 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): + is_new = self.pk is None + self.validate() + self.objects._save_instance(self) + return self + + def delete(self): + pass + + +class ModelMetaClass(type): + + def __new__(cls, name, bases, attrs): + """ + """ + #move column definitions into _columns dict + #and set default column names + _columns = {} + pk_name = None + for k,v in attrs.items(): + if isinstance(v, columns.BaseColumn): + if v.is_primary_key: + if pk_name: + raise ColumnFamilyException("More than one primary key defined for {}".format(name)) + pk_name = k + _columns[k] = attrs.pop(k) + _columns[k].set_db_name(k) + + #set primary key if it's not already defined + if not pk_name: + _columns['id'] = columns.UUID(primary_key=True) + _columns['id'].set_db_name('id') + pk_name = 'id' + + #setup pk shortcut + if pk_name != 'pk': + pk_get = lambda self: getattr(self, pk_name) + pk_set = lambda self, val: setattr(self, pk_name, val) + attrs['pk'] = property(pk_get, pk_set) + + #check for duplicate column names + col_names = set() + for k,v in _columns.items(): + if v.db_field in col_names: + raise ColumnFamilyException("{} defines the column {} more than once".format(name, v.db_field)) + col_names.add(k) + + #get column family name + cf_name = attrs.pop('db_name', None) or name + + #create db_name -> model name map for loading + db_map = {} + for name, col in _columns.items(): + db_map[col.db_field] = name + + attrs['_columns'] = _columns + attrs['_db_map'] = db_map + attrs['_pk_name'] = pk_name + attrs['_dynamic_columns'] = {} + + klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) + klass.objects = Manager(klass) + return klass + + +class Model(BaseModel): + """ + the db name for the column family can be set as the attribute db_name, or + it will be genertaed from the class name + """ + __metaclass__ = ModelMetaClass + + From 44c1232f8ad8567b94e8c4217cd8474b4ebe010b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 17:27:10 -0800 Subject: [PATCH 0006/3961] added validation to columns starting to clean up connection.py adding exceptions --- cassandraengine/columns.py | 51 +++++++++++++++++++---------- cassandraengine/connection.py | 60 +++++++++++++++-------------------- cassandraengine/exceptions.py | 4 ++- 3 files changed, 62 insertions(+), 53 deletions(-) diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index 6bdda146ea..7c90f5584c 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -2,6 +2,8 @@ import re from uuid import uuid1, uuid4 +from cassandraengine.exceptions import ValidationError + class BaseColumn(object): """ The base column @@ -23,9 +25,16 @@ def __init__(self, primary_key=False, db_field=None, default=None, null=False): self.null = null def validate(self, value): - if not self.has_default: - if not self.null and value is None: + """ + Returns a cleaned and validated value. Raises a ValidationError + if there's a problem + """ + if value is None: + if self.has_default: + return self.get_default() + elif not self.null: raise ValidationError('null values are not allowed') + return value def to_python(self, value): """ @@ -38,6 +47,10 @@ def to_python(self, value): def has_default(self): return bool(self.default) + @property + def is_primary_key(self): + return self.primary_key + def get_default(self): if self.has_default: if callable(self.default): @@ -82,19 +95,17 @@ class Integer(BaseColumn): db_type = 'int' def validate(self, value): - super(Integer, self).validate(value) + val = super(Integer, self).validate(value) try: - long(value) + return long(val) except (TypeError, ValueError): raise ValidationError("{} can't be converted to integral value".format(value)) def to_python(self, value): - self.validate(value) - return long(value) + return self.validate(value) def to_database(self, value): - self.validate() - return value + return self.validate(value) class DateTime(BaseColumn): db_type = 'timestamp' @@ -107,16 +118,16 @@ class UUID(BaseColumn): re_uuid = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') - def __init__(self, **kwargs): - super(UUID, self).__init__(**kwargs) - if 'default' not in kwargs: - self.default = uuid4 + def __init__(self, default=lambda:uuid4(), **kwargs): + super(UUID, self).__init__(default=default, **kwargs) def validate(self, value): - super(UUID, self).validate(value) - if not self.re_uuid.match(value): + val = super(UUID, self).validate(value) + from uuid import UUID as _UUID + if isinstance(val, _UUID): return val + if not self.re_uuid.match(val): raise ValidationError("{} is not a valid uuid".format(value)) - return value + return _UUID(val) class Boolean(BaseColumn): db_type = 'boolean' @@ -130,11 +141,17 @@ def to_database(self, value): class Float(BaseColumn): db_type = 'double' + def validate(self, value): + try: + return float(value) + except (TypeError, ValueError): + raise ValidationError("{} is not a valid float".format(value)) + def to_python(self, value): - return float(value) + return self.validate(value) def to_database(self, value): - return float(value) + return self.validate(value) class Decimal(BaseColumn): db_type = 'decimal' diff --git a/cassandraengine/connection.py b/cassandraengine/connection.py index 27482829ff..3cebc3acbf 100644 --- a/cassandraengine/connection.py +++ b/cassandraengine/connection.py @@ -1,35 +1,44 @@ #http://pypi.python.org/pypi/cql/1.0.4 -#http://code.google.com/a/apache-extras.org/p/cassandra-dbapi2/ +#http://code.google.com/a/apache-extras.org/p/cassandra-dbapi2 / #http://cassandra.apache.org/doc/cql/CQL.html import cql -_keyspace = 'cse_test' -_col_fam = 'colfam' - -conn = cql.connect('127.0.0.1', 9160) +_keyspace = 'cassengine_test' + +_conn = None +def get_connection(): + global _conn + if _conn is None: + _conn = cql.connect('127.0.0.1', 9160) + _conn.set_cql_version('3.0.0') + try: + _conn.set_initial_keyspace(_keyspace) + except cql.ProgrammingError, e: + #http://www.datastax.com/docs/1.0/references/cql/CREATE_KEYSPACE + cur = _conn.cursor() + cur.execute("""create keyspace {} + with strategy_class = 'SimpleStrategy' + and strategy_options:replication_factor=1;""".format(_keyspace)) + _conn.set_initial_keyspace(_keyspace) + return _conn #cli examples here: #http://wiki.apache.org/cassandra/CassandraCli -try: - conn.set_initial_keyspace(_keyspace) -except cql.ProgrammingError: - #http://www.datastax.com/docs/1.0/references/cql/CREATE_KEYSPACE - cur = conn.cursor() - cur.execute("create keyspace {} with strategy_class = 'SimpleStrategy' and strategy_options:replication_factor=1;".format(_keyspace)) - conn.set_initial_keyspace(_keyspace) -cur = conn.cursor() - #http://www.datastax.com/docs/1.0/dml/using_cql #http://stackoverflow.com/questions/10871595/how-do-you-create-a-counter-columnfamily-with-cql3-in-cassandra +""" try: - cur.execute("""create table colfam (id int PRIMARY KEY);""") + cur.execute("create table colfam (id int PRIMARY KEY);") except cql.ProgrammingError: #cur.execute('drop table colfam;') pass +""" #http://www.datastax.com/docs/1.0/references/cql/INSERT #updates/inserts do the same thing + +""" cur.execute('insert into colfam (id, content) values (:id, :content)', dict(id=1, content='yo!')) cur.execute('select * from colfam WHERE id=1;') @@ -41,27 +50,8 @@ cur.execute('delete from colfam WHERE id=1;') cur.execute('delete from colfam WHERE id in (1);') +""" #TODO: Add alter altering existing schema functionality #http://www.datastax.com/docs/1.0/references/cql/ALTER_COLUMNFAMILY -def create_column_family(name): - """ - """ - -def select_row(column_family, **kwargs): - """ - """ - -def create_row(column_family, **kwargs): - """ - """ - -def update_row(column_family, **kwargs): - """ - """ - -def delete_row(column_family, **kwargs): - """ - """ - diff --git a/cassandraengine/exceptions.py b/cassandraengine/exceptions.py index 8265b0ceab..a68518875e 100644 --- a/cassandraengine/exceptions.py +++ b/cassandraengine/exceptions.py @@ -1,2 +1,4 @@ - +#cassandraengine exceptions +class ColumnFamilyException(BaseException): pass class ValidationError(BaseException): pass + From ec1a3b02f4482f674fb91faf3a5359249ed2113a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 17:27:54 -0800 Subject: [PATCH 0007/3961] adding object manager adding queryset class with insert, select, and table create and drop --- cassandraengine/manager.py | 80 ++++++++++++++++++++++ cassandraengine/query.py | 137 +++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 cassandraengine/manager.py create mode 100644 cassandraengine/query.py diff --git a/cassandraengine/manager.py b/cassandraengine/manager.py new file mode 100644 index 0000000000..9dc501b244 --- /dev/null +++ b/cassandraengine/manager.py @@ -0,0 +1,80 @@ +#manager class + +from cassandraengine.query import QuerySet + +class Manager(object): + + def __init__(self, model): + super(Manager, self).__init__() + self.model = model + + @property + def column_family_name(self): + """ + Returns the column family name if it's been defined + otherwise, it creates it from the module and class name + """ + if self.model.db_name: + return self.model.db_name + cf_name = self.model.__module__ + '.' + self.model.__name__ + cf_name = cf_name.replace('.', '_') + #trim to less than 48 characters or cassandra will complain + cf_name = cf_name[-48:] + return cf_name + + def column_family_definition(self): + """ + Generates a definition used for tale creation + """ + + def find(self, pk): + """ + Returns the row corresponding to the primary key value given + """ + values = QuerySet(self.model).find(pk) + #change the column names to model names + #in case they are different + field_dict = {} + db_map = self.model._db_map + for key, val in values.items(): + if key in db_map: + field_dict[db_map[key]] = val + else: + field_dict[key] = val + return self.model(**field_dict) + + def all(self): + return QuerySet(self.model).all() + + def filter(self, **kwargs): + return QuerySet(self.model).filter(**kwargs) + + def exclude(self, **kwargs): + return QuerySet(self.model).exclude(**kwargs) + + def create(self, **kwargs): + return self.model(**kwargs).save() + + def _save_instance(self, instance): + """ + The business end of save, this is called by the models + save method and calls the Query save method. This should + only be called by the model saving itself + """ + QuerySet(self.model).save(instance) + + def _create_column_family(self): + QuerySet(self.model)._create_column_family() + + def _delete_column_family(self): + QuerySet(self.model)._delete_column_family() + + def delete(self, **kwargs): + pass + + def __call__(self, **kwargs): + """ + filter shortcut + """ + return self.filter(**kwargs) + diff --git a/cassandraengine/query.py b/cassandraengine/query.py new file mode 100644 index 0000000000..bb0532d86b --- /dev/null +++ b/cassandraengine/query.py @@ -0,0 +1,137 @@ +from cassandraengine.connection import get_connection + +class QuerySet(object): + #TODO: querysets should be immutable + #TODO: querysets should be executed lazily + #TODO: conflicting filter args should raise exception unless a force kwarg is supplied + + def __init__(self, model, query={}): + super(QuerySet, self).__init__() + self.model = model + self.column_family_name = self.model.objects.column_family_name + + self._cursor = None + + #----query generation / execution---- + def _execute_query(self): + pass + + def _generate_querystring(self): + pass + + @property + def cursor(self): + if self._cursor is None: + self._cursor = self._execute_query() + return self._cursor + + #----Reads------ + def __iter__(self): + pass + + def next(self): + pass + + def first(self): + conn = get_connection() + cur = conn.cursor() + pass + + def all(self): + pass + + def filter(self, **kwargs): + pass + + def exclude(self, **kwargs): + pass + + def find(self, pk): + """ + loads one document identified by it's primary key + """ + 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 + + + #----writes---- + def save(self, instance): + """ + Creates / updates a row. + This is a blind insert call. + All validation and cleaning needs to happen + prior to calling this. + """ + assert type(instance) == self.model + #organize data + value_pairs = [] + + #get pk + col = self.model._columns[self.model._pk_name] + values = instance.as_dict() + value_pairs += [(col.db_field, values.get(self.model._pk_name))] + + #get defined fields and their column names + for name, col in self.model._columns.items(): + if col.is_primary_key: continue + value_pairs += [(col.db_field, values.get(name))] + + #add dynamic fields + for key, val in values.items(): + if key in self.model._columns: continue + value_pairs += [(key, val)] + + #construct query string + field_names = zip(*value_pairs)[0] + field_values = dict(value_pairs) + qs = ["INSERT INTO {}".format(self.column_family_name)] + qs += ["({})".format(', '.join(field_names))] + qs += ['VALUES'] + qs += ["({})".format(', '.join([':'+f for f in field_names]))] + qs = ' '.join(qs) + + conn = get_connection() + cur = conn.cursor() + cur.execute(qs, field_values) + + def _create_column_family(self): + #construct query string + qs = ['CREATE TABLE {}'.format(self.column_family_name)] + + #add column types + qtypes = [] + def add_column(col): + s = '{} {}'.format(col.db_field, col.db_type) + if col.primary_key: s += ' PRIMARY KEY' + qtypes.append(s) + add_column(self.model._columns[self.model._pk_name]) + for name, col in self.model._columns.items(): + if col.primary_key: continue + add_column(col) + + qs += ['({})'.format(', '.join(qtypes))] + qs = ' '.join(qs) + + #add primary key + conn = get_connection() + cur = conn.cursor() + try: + cur.execute(qs) + except BaseException, e: + if 'Cannot add already existing column family' not in e.message: + raise + + def _delete_column_family(self): + conn = get_connection() + cur = conn.cursor() + cur.execute('drop table {};'.format(self.column_family_name)) + + From 2aaf1650f32a6e4511c5b0cdcf95e25ebcf0b175 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 17:28:55 -0800 Subject: [PATCH 0008/3961] added some unit tests, updated the readme and added requirements.txt --- README.md | 16 ++++++++- cassandraengine/tests/base.py | 11 +++++++ cassandraengine/tests/columns/__init__.py | 0 cassandraengine/tests/model/__init__.py | 0 .../tests/model/test_class_construction.py | 33 +++++++++++++++++++ cassandraengine/tests/model/test_model_io.py | 26 +++++++++++++++ requirements.txt | 1 + 7 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 cassandraengine/tests/base.py create mode 100644 cassandraengine/tests/columns/__init__.py create mode 100644 cassandraengine/tests/model/__init__.py create mode 100644 cassandraengine/tests/model/test_class_construction.py create mode 100644 cassandraengine/tests/model/test_model_io.py diff --git a/README.md b/README.md index 869495c22b..0e174ce9ad 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ cassandraengine -=============== \ No newline at end of file +=============== + +Django ORM / Mongoengine style ORM for Cassandra + +In it's current state you can define column families, create and delete column families +based on your model definiteions, save models and retrieve models by their primary keys. + +That's about it. Also, there are only 2 tests and the CQL stuff is very simplistic at this point. + +##TODO +* dynamic column support +* return None when row isn't found in find() +* tests +* query functionality +* nice column and model class __repr__ diff --git a/cassandraengine/tests/base.py b/cassandraengine/tests/base.py new file mode 100644 index 0000000000..ac1f31d8f1 --- /dev/null +++ b/cassandraengine/tests/base.py @@ -0,0 +1,11 @@ +from unittest import TestCase + +class BaseCassEngTestCase(TestCase): + + def assertHasAttr(self, obj, attr): + self.assertTrue(hasattr(obj, attr), + "{} doesn't have attribute: {}".format(obj, attr)) + + def assertNotHasAttr(self, obj, attr): + self.assertFalse(hasattr(obj, attr), + "{} shouldn't have the attribute: {}".format(obj, attr)) diff --git a/cassandraengine/tests/columns/__init__.py b/cassandraengine/tests/columns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cassandraengine/tests/model/__init__.py b/cassandraengine/tests/model/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cassandraengine/tests/model/test_class_construction.py b/cassandraengine/tests/model/test_class_construction.py new file mode 100644 index 0000000000..c02fc4e2bd --- /dev/null +++ b/cassandraengine/tests/model/test_class_construction.py @@ -0,0 +1,33 @@ +from cassandraengine.tests.base import BaseCassEngTestCase + +from cassandraengine.models import Model +from cassandraengine import columns + +class TestModelClassFunction(BaseCassEngTestCase): + + def test_column_attributes_handled_correctly(self): + """ + Tests that column attributes are moved to a _columns dict + and replaced with simple value attributes + """ + + class TestModel(Model): + text = columns.Text() + + self.assertHasAttr(TestModel, '_columns') + self.assertNotHasAttr(TestModel, 'id') + self.assertNotHasAttr(TestModel, 'text') + + inst = TestModel() + self.assertHasAttr(inst, 'id') + self.assertHasAttr(inst, 'text') + self.assertIsNone(inst.id) + self.assertIsNone(inst.text) + + +class TestModelValidation(BaseCassEngTestCase): + pass + +class TestModelSerialization(BaseCassEngTestCase): + pass + diff --git a/cassandraengine/tests/model/test_model_io.py b/cassandraengine/tests/model/test_model_io.py new file mode 100644 index 0000000000..4b73c26e0c --- /dev/null +++ b/cassandraengine/tests/model/test_model_io.py @@ -0,0 +1,26 @@ +from cassandraengine.tests.base import BaseCassEngTestCase + +from cassandraengine.models import Model +from cassandraengine import columns + +class TestModel(Model): + count = columns.Integer() + text = columns.Text() + +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): + 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)) + diff --git a/requirements.txt b/requirements.txt index e69de29bb2..b6734c8844 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +cql==1.2.0 From 18f52886e7b570ee7c1fe0dbc6782d73abe7664a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 19:33:22 -0800 Subject: [PATCH 0009/3961] renamed some exceptions --- cassandraengine/__init__.py | 3 +++ cassandraengine/columns.py | 3 --- cassandraengine/exceptions.py | 2 +- cassandraengine/models.py | 10 ++++++---- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cassandraengine/__init__.py b/cassandraengine/__init__.py index e69de29bb2..af95d0b003 100644 --- a/cassandraengine/__init__.py +++ b/cassandraengine/__init__.py @@ -0,0 +1,3 @@ +from cassandraengine.columns import * +from cassandraengine.models import Model + diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index 7c90f5584c..747cfba97c 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -5,9 +5,6 @@ from cassandraengine.exceptions import ValidationError class BaseColumn(object): - """ - The base column - """ #the cassandra type this column maps to db_type = None diff --git a/cassandraengine/exceptions.py b/cassandraengine/exceptions.py index a68518875e..4b20f7a334 100644 --- a/cassandraengine/exceptions.py +++ b/cassandraengine/exceptions.py @@ -1,4 +1,4 @@ #cassandraengine exceptions -class ColumnFamilyException(BaseException): pass +class ModelException(BaseException): pass class ValidationError(BaseException): pass diff --git a/cassandraengine/models.py b/cassandraengine/models.py index c3fce738f1..4e5bd9d3ec 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -1,6 +1,6 @@ from cassandraengine import columns -from cassandraengine.exceptions import ColumnFamilyException +from cassandraengine.exceptions import ModelException from cassandraengine.manager import Manager class BaseModel(object): @@ -8,6 +8,8 @@ class BaseModel(object): The base model class, don't inherit from this, inherit from Model, defined below """ + #table names will be generated automatically from it's model name and package + #however, you can alse define them manually here db_name = None def __init__(self, **values): @@ -75,7 +77,7 @@ def __new__(cls, name, bases, attrs): if isinstance(v, columns.BaseColumn): if v.is_primary_key: if pk_name: - raise ColumnFamilyException("More than one primary key defined for {}".format(name)) + raise ModelException("More than one primary key defined for {}".format(name)) pk_name = k _columns[k] = attrs.pop(k) _columns[k].set_db_name(k) @@ -96,8 +98,8 @@ def __new__(cls, name, bases, attrs): col_names = set() for k,v in _columns.items(): if v.db_field in col_names: - raise ColumnFamilyException("{} defines the column {} more than once".format(name, v.db_field)) - col_names.add(k) + raise ModelException("{} defines the column {} more than once".format(name, v.db_field)) + col_names.add(v.db_field) #get column family name cf_name = attrs.pop('db_name', None) or name From cfebd5a32d3b0628c461d96e09c13d297c42da2d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 19:34:16 -0800 Subject: [PATCH 0010/3961] added more tests around the metaclass behavior --- .../tests/model/test_class_construction.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/cassandraengine/tests/model/test_class_construction.py b/cassandraengine/tests/model/test_class_construction.py index c02fc4e2bd..cc24a8c08b 100644 --- a/cassandraengine/tests/model/test_class_construction.py +++ b/cassandraengine/tests/model/test_class_construction.py @@ -1,9 +1,13 @@ from cassandraengine.tests.base import BaseCassEngTestCase +from cassandraengine.exceptions import ModelException from cassandraengine.models import Model from cassandraengine import columns class TestModelClassFunction(BaseCassEngTestCase): + """ + Tests verifying the behavior of the Model metaclass + """ def test_column_attributes_handled_correctly(self): """ @@ -24,10 +28,33 @@ class TestModel(Model): self.assertIsNone(inst.id) self.assertIsNone(inst.text) + def test_multiple_primary_keys_fail(self): + """Test attempting to define multiple primary keys fails""" + with self.assertRaises(ModelException): + class MultiPK(Model): + id1 = columns.Integer(primary_key=True) + id2 = columns.Integer(primary_key=True) -class TestModelValidation(BaseCassEngTestCase): - pass + def test_db_map(self): + """ + Tests that the db_map is properly defined + -the db_map allows columns + """ + class WildDBNames(Model): + content = columns.Text(db_field='words_and_whatnot') + numbers = columns.Integer(db_field='integers_etc') + + db_map = WildDBNames._db_map + self.assertEquals(db_map['words_and_whatnot'], 'content') + self.assertEquals(db_map['integers_etc'], 'numbers') + + def test_attempting_to_make_duplicate_column_names_fails(self): + """ + Tests that trying to create conflicting db column names will fail + """ -class TestModelSerialization(BaseCassEngTestCase): - pass + with self.assertRaises(ModelException): + class BadNames(Model): + words = columns.Text() + content = columns.Text(db_field='words') From b6042ac57ad62d7f73600588061cb194e114ba83 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 10 Nov 2012 21:12:47 -0800 Subject: [PATCH 0011/3961] 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 747cfba97c..aa7a4b355b 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -14,7 +14,7 @@ def __init__(self, primary_key=False, db_field=None, default=None, null=False): :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 4e5bd9d3ec..115008960f 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -25,10 +25,6 @@ def __init__(self, **values): 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 @@ def pk(self): """ 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 @@ def validate(self): 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 bb0532d86b..76149f633a 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 @@ def cursor(self): #----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 0000000000..e42519cc3b --- /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 4b73c26e0c..7680a7897d 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 0000000000..e69de29bb2 From 4764b492eb44b1a2c3bcdc38b80b71704c39bb58 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 11 Nov 2012 11:43:05 -0800 Subject: [PATCH 0012/3961] adding single instance delete method and supporting unit test adding column type stubs updating readme --- README.md | 12 ++++++-- cassandraengine/columns.py | 24 +++++++++++++++- cassandraengine/manager.py | 30 +++++++++++--------- cassandraengine/models.py | 3 +- cassandraengine/query.py | 28 +++++++++++++----- cassandraengine/tests/model/test_model_io.py | 9 ++++++ 6 files changed, 81 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0e174ce9ad..136523416c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@ cassandraengine =============== -Django ORM / Mongoengine style ORM for Cassandra +Python Cassandra ORM in the style of django / mongoengine In it's current state you can define column families, create and delete column families based on your model definiteions, save models and retrieve models by their primary keys. -That's about it. Also, there are only 2 tests and the CQL stuff is very simplistic at this point. +That's about it. Also, the CQL stuff is pretty simple at this point. ##TODO +* Complex queries (class Q(object)) +* Match column names to mongoengine field names? +* mongoengine fields? URLField, EmbeddedDocument, ListField, DictField +* column ttl? +* ForeignKey/DBRef fields? * dynamic column support -* return None when row isn't found in find() * tests * query functionality * nice column and model class __repr__ + + diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index aa7a4b355b..67f697ea25 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -106,6 +106,9 @@ def to_database(self, value): class DateTime(BaseColumn): db_type = 'timestamp' + def __init__(self, **kwargs): + super(DateTime, self).__init__(**kwargs) + raise NotImplementedError class UUID(BaseColumn): """ @@ -153,4 +156,23 @@ def to_database(self, value): class Decimal(BaseColumn): db_type = 'decimal' #TODO: this - + def __init__(self, **kwargs): + super(DateTime, self).__init__(**kwargs) + raise NotImplementedError + +class Counter(BaseColumn): + def __init__(self, **kwargs): + super(DateTime, self).__init__(**kwargs) + raise NotImplementedError + +#TODO: research supercolumns +#http://wiki.apache.org/cassandra/DataModel +class List(BaseColumn): + def __init__(self, **kwargs): + super(DateTime, self).__init__(**kwargs) + raise NotImplementedError + +class Dict(BaseColumn): + def __init__(self, **kwargs): + super(DateTime, self).__init__(**kwargs) + raise NotImplementedError diff --git a/cassandraengine/manager.py b/cassandraengine/manager.py index 9dc501b244..3f7ec7f66e 100644 --- a/cassandraengine/manager.py +++ b/cassandraengine/manager.py @@ -21,17 +21,20 @@ def column_family_name(self): #trim to less than 48 characters or cassandra will complain cf_name = cf_name[-48:] return cf_name - - def column_family_definition(self): + + def __call__(self, **kwargs): """ - Generates a definition used for tale creation + filter shortcut """ + return self.filter(**kwargs) def find(self, pk): """ Returns the row corresponding to the primary key value given """ values = QuerySet(self.model).find(pk) + if values is None: return + #change the column names to model names #in case they are different field_dict = {} @@ -55,6 +58,10 @@ def exclude(self, **kwargs): def create(self, **kwargs): return self.model(**kwargs).save() + def delete(self, **kwargs): + pass + + #----single instance methods---- def _save_instance(self, instance): """ The business end of save, this is called by the models @@ -63,18 +70,15 @@ def _save_instance(self, instance): """ QuerySet(self.model).save(instance) + def _delete_instance(self, instance): + """ + Deletes a single instance + """ + QuerySet(self.model).delete_instance(instance) + + #----column family create/delete---- def _create_column_family(self): QuerySet(self.model)._create_column_family() def _delete_column_family(self): QuerySet(self.model)._delete_column_family() - - def delete(self, **kwargs): - pass - - def __call__(self, **kwargs): - """ - filter shortcut - """ - return self.filter(**kwargs) - diff --git a/cassandraengine/models.py b/cassandraengine/models.py index 115008960f..040682d4f8 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -65,7 +65,8 @@ def save(self): return self def delete(self): - pass + """ Deletes this instance """ + self.objects._delete_instance(self) class ModelMetaClass(type): diff --git a/cassandraengine/query.py b/cassandraengine/query.py index 76149f633a..025d135ab6 100644 --- a/cassandraengine/query.py +++ b/cassandraengine/query.py @@ -42,7 +42,7 @@ def _get_next(self): """ cur = self._cursor values = cur.fetchone() - if values is None: return None + if values is None: return names = [i[0] for i in cur.description] value_dict = dict(zip(names, values)) return value_dict @@ -64,15 +64,12 @@ def filter(self, **kwargs): return QuerySet(self.model, query_args=qargs) def exclude(self, **kwargs): - """ - Need to invert the logic for all kwargs - """ + """ Need to invert the logic for all kwargs """ pass def count(self): - """ - Returns the number of rows matched by this query - """ + """ Returns the number of rows matched by this query """ + qs = 'SELECT COUNT(*) FROM {}'.format(self.column_family_name) def find(self, pk): """ @@ -128,6 +125,23 @@ def save(self, instance): cur = conn.cursor() cur.execute(qs, field_values) + #----delete--- + def delete(self): + """ + Deletes the contents of a query + """ + + def delete_instance(self, instance): + """ Deletes one instance """ + pk_name = self.model._pk_name + qs = ['DELETE FROM {}'.format(self.column_family_name)] + qs += ['WHERE {0}=:{0}'.format(pk_name)] + qs = ' '.join(qs) + + conn = get_connection() + cur = conn.cursor() + cur.execute(qs, {pk_name:instance.pk}) + def _create_column_family(self): #construct query string qs = ['CREATE TABLE {}'.format(self.column_family_name)] diff --git a/cassandraengine/tests/model/test_model_io.py b/cassandraengine/tests/model/test_model_io.py index 7680a7897d..8adf46913c 100644 --- a/cassandraengine/tests/model/test_model_io.py +++ b/cassandraengine/tests/model/test_model_io.py @@ -38,6 +38,15 @@ def test_model_updating_works_properly(self): tm2 = TestModel.objects.find(tm.pk) self.assertEquals(tm.count, tm2.count) + def test_model_deleting_works_properly(self): + """ + Tests that an instance's delete method deletes the instance + """ + tm = TestModel.objects.create(count=8, text='123456789') + tm.delete() + tm2 = TestModel.objects.find(tm.pk) + self.assertIsNone(tm2) + def test_nullable_columns_are_saved_properly(self): """ Tests that nullable columns save without any trouble From 142ceb184cbcea853182e6a20511a8c8b17cc6aa Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 11 Nov 2012 16:37:26 -0800 Subject: [PATCH 0013/3961] modified model metaclass to replace column definitions with properties that modify the columns value member instead of removing them until instantiation --- cassandraengine/columns.py | 45 +++++++++++++++---- cassandraengine/models.py | 27 ++++++----- .../tests/model/test_class_construction.py | 4 +- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index 67f697ea25..20f54ba8d7 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -21,6 +21,8 @@ def __init__(self, primary_key=False, db_field=None, default=None, null=False): self.default = default self.null = null + self.value = None + def validate(self, value): """ Returns a cleaned and validated value. Raises a ValidationError @@ -40,6 +42,14 @@ def to_python(self, value): """ return value + def to_database(self, value): + """ + Converts python value into database value + """ + if value is None and self.has_default: + return self.get_default() + return value + @property def has_default(self): return bool(self.default) @@ -48,6 +58,33 @@ def has_default(self): def is_primary_key(self): return self.primary_key + #methods for replacing column definitions with properties that interact + #with a column's value member + #this will allow putting logic behind value access (lazy loading, etc) + def _getval(self): + """ This columns value getter """ + return self.value + + def _setval(self, val): + """ This columns value setter """ + self.value = val + + def _delval(self): + """ This columns value deleter """ + raise NotImplementedError + + def get_property(self, allow_delete=True): + """ + Returns the property object that will set and get this + column's value and be assigned to this column's model attribute + """ + getval = lambda slf: self._getval() + setval = lambda slf, val: self._setval(val) + delval = lambda slf: self._delval() + if not allow_delete: + return property(getval, setval) + return property(getval, setval, delval) + def get_default(self): if self.has_default: if callable(self.default): @@ -55,14 +92,6 @@ def get_default(self): else: return self.default - def to_database(self, value): - """ - Converts python value into database value - """ - if value is None and self.has_default: - return self.get_default() - return value - def get_column_def(self): """ Returns a column definition for CQL table definition diff --git a/cassandraengine/models.py b/cassandraengine/models.py index 040682d4f8..3d1388f946 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -32,7 +32,7 @@ def find(cls, pk): @property def pk(self): - """ Returns the object's primary key, regardless of it's name """ + """ Returns the object's primary key """ return getattr(self, self._pk_name) #dynamic column methods @@ -78,26 +78,31 @@ def __new__(cls, name, bases, attrs): #and set default column names _columns = {} pk_name = None + + def _transform_column(col_name, col_obj): + _columns[col_name] = col_obj + col_obj.set_db_name(col_name) + allow_delete = not col_obj.primary_key + attrs[col_name] = col_obj.get_property(allow_delete=allow_delete) + + #transform column definitions for k,v in attrs.items(): if isinstance(v, columns.BaseColumn): if v.is_primary_key: if pk_name: raise ModelException("More than one primary key defined for {}".format(name)) pk_name = k - _columns[k] = attrs.pop(k) - _columns[k].set_db_name(k) + _transform_column(k,v) #set primary key if it's not already defined if not pk_name: - _columns['id'] = columns.UUID(primary_key=True) - _columns['id'].set_db_name('id') - pk_name = 'id' + k,v = 'id', columns.UUID(primary_key=True) + _transform_column(k,v) + pk_name = k - #setup pk shortcut + #setup primary key shortcut if pk_name != 'pk': - pk_get = lambda self: getattr(self, pk_name) - pk_set = lambda self, val: setattr(self, pk_name, val) - attrs['pk'] = property(pk_get, pk_set) + attrs['pk'] = _columns[pk_name].get_property(allow_delete=False) #check for duplicate column names col_names = set() @@ -107,7 +112,7 @@ def __new__(cls, name, bases, attrs): col_names.add(v.db_field) #get column family name - cf_name = attrs.pop('db_name', None) or name + cf_name = attrs.pop('db_name', name) #create db_name -> model name map for loading db_map = {} diff --git a/cassandraengine/tests/model/test_class_construction.py b/cassandraengine/tests/model/test_class_construction.py index cc24a8c08b..5e3dbe1968 100644 --- a/cassandraengine/tests/model/test_class_construction.py +++ b/cassandraengine/tests/model/test_class_construction.py @@ -19,8 +19,8 @@ class TestModel(Model): text = columns.Text() self.assertHasAttr(TestModel, '_columns') - self.assertNotHasAttr(TestModel, 'id') - self.assertNotHasAttr(TestModel, 'text') + self.assertHasAttr(TestModel, 'id') + self.assertHasAttr(TestModel, 'text') inst = TestModel() self.assertHasAttr(inst, 'id') From 83cf23da7c9f0dfc3bc36884c69ab9e334ea8d12 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 11 Nov 2012 22:14:40 -0800 Subject: [PATCH 0014/3961] cleaning things up a bit --- README.md | 9 ++++----- cassandraengine/columns.py | 5 +++++ cassandraengine/manager.py | 8 ++------ cassandraengine/models.py | 2 ++ cassandraengine/query.py | 11 +++++++++++ 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 136523416c..b3349e0d80 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@ cassandraengine =============== -Python Cassandra ORM in the style of django / mongoengine +Cassandra ORM for Python in the style of the Django orm and mongoengine In it's current state you can define column families, create and delete column families based on your model definiteions, save models and retrieve models by their primary keys. -That's about it. Also, the CQL stuff is pretty simple at this point. +That's about it. Also, the CQL stuff is very basic at this point. ##TODO -* Complex queries (class Q(object)) -* Match column names to mongoengine field names? +* Real querying * mongoengine fields? URLField, EmbeddedDocument, ListField, DictField * column ttl? * ForeignKey/DBRef fields? * dynamic column support -* tests * query functionality +* Match column names to mongoengine field names? * nice column and model class __repr__ diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index 20f54ba8d7..22a533c6d4 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -197,11 +197,16 @@ def __init__(self, **kwargs): #TODO: research supercolumns #http://wiki.apache.org/cassandra/DataModel class List(BaseColumn): + #checkout cql.cqltypes.ListType def __init__(self, **kwargs): super(DateTime, self).__init__(**kwargs) raise NotImplementedError class Dict(BaseColumn): + #checkout cql.cqltypes.MapType def __init__(self, **kwargs): super(DateTime, self).__init__(**kwargs) raise NotImplementedError + +#checkout cql.cqltypes.SetType +#checkout cql.cqltypes.CompositeType diff --git a/cassandraengine/manager.py b/cassandraengine/manager.py index 3f7ec7f66e..de061ce0fb 100644 --- a/cassandraengine/manager.py +++ b/cassandraengine/manager.py @@ -23,9 +23,7 @@ def column_family_name(self): return cf_name def __call__(self, **kwargs): - """ - filter shortcut - """ + """ filter shortcut """ return self.filter(**kwargs) def find(self, pk): @@ -71,9 +69,7 @@ def _save_instance(self, instance): QuerySet(self.model).save(instance) def _delete_instance(self, instance): - """ - Deletes a single instance - """ + """ Deletes a single instance """ QuerySet(self.model).delete_instance(instance) #----column family create/delete---- diff --git a/cassandraengine/models.py b/cassandraengine/models.py index 3d1388f946..fdc965581d 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -119,11 +119,13 @@ def _transform_column(col_name, col_obj): for name, col in _columns.items(): db_map[col.db_field] = name + #add management members to the class attrs['_columns'] = _columns attrs['_db_map'] = db_map attrs['_pk_name'] = pk_name attrs['_dynamic_columns'] = {} + #create the class and add a manager to it klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) klass.objects = Manager(klass) return klass diff --git a/cassandraengine/query.py b/cassandraengine/query.py index 025d135ab6..817b03960e 100644 --- a/cassandraengine/query.py +++ b/cassandraengine/query.py @@ -2,11 +2,22 @@ from cassandraengine.connection import get_connection +#CQL 3 reference: +#http://www.datastax.com/docs/1.1/references/cql/index + +class Query(object): + + pass + class QuerySet(object): #TODO: querysets should be immutable #TODO: querysets should be executed lazily #TODO: conflicting filter args should raise exception unless a force kwarg is supplied + #CQL supports ==, >, >=, <, <=, IN (a,b,c,..n) + #REVERSE, LIMIT + #ORDER BY + def __init__(self, model, query_args={}): super(QuerySet, self).__init__() self.model = model From b8103dd33bd29c83572cdf013759a865bf227437 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 12 Nov 2012 21:40:34 -0800 Subject: [PATCH 0015/3961] removed restriction on multiple primary keys reworked metaclass to preserve column definition order adjusted primary key declaration in table definition cql removed dynamic columns --- README.md | 3 -- cassandraengine/columns.py | 12 +++++- cassandraengine/models.py | 39 +++++++------------ cassandraengine/query.py | 8 ++-- .../tests/model/test_class_construction.py | 19 +++++---- cassandraengine/tests/model/test_model_io.py | 18 --------- 6 files changed, 41 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index b3349e0d80..cc2d2fc976 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,8 @@ based on your model definiteions, save models and retrieve models by their prima That's about it. Also, the CQL stuff is very basic at this point. ##TODO -* Real querying -* mongoengine fields? URLField, EmbeddedDocument, ListField, DictField * column ttl? * ForeignKey/DBRef fields? -* dynamic column support * query functionality * Match column names to mongoengine field names? * nice column and model class __repr__ diff --git a/cassandraengine/columns.py b/cassandraengine/columns.py index 22a533c6d4..bcbf39e7f5 100644 --- a/cassandraengine/columns.py +++ b/cassandraengine/columns.py @@ -9,6 +9,8 @@ class BaseColumn(object): #the cassandra type this column maps to db_type = None + instance_counter = 0 + def __init__(self, primary_key=False, db_field=None, default=None, null=False): """ :param primary_key: bool flag, there can be only one primary key per doc @@ -23,6 +25,10 @@ def __init__(self, primary_key=False, db_field=None, default=None, null=False): self.value = None + #keep track of instantiation order + self.position = BaseColumn.instance_counter + BaseColumn.instance_counter += 1 + def validate(self, value): """ Returns a cleaned and validated value. Raises a ValidationError @@ -97,8 +103,8 @@ def get_column_def(self): Returns a column definition for CQL table definition """ dterms = [self.db_field, self.db_type] - if self.primary_key: - dterms.append('PRIMARY KEY') + #if self.primary_key: + #dterms.append('PRIMARY KEY') return ' '.join(dterms) def set_db_name(self, name): @@ -196,6 +202,8 @@ def __init__(self, **kwargs): #TODO: research supercolumns #http://wiki.apache.org/cassandra/DataModel +#checkout composite columns: +#http://www.datastax.com/dev/blog/introduction-to-composite-columns-part-1 class List(BaseColumn): #checkout cql.cqltypes.ListType def __init__(self, **kwargs): diff --git a/cassandraengine/models.py b/cassandraengine/models.py index fdc965581d..76f3d99cd6 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from cassandraengine import columns from cassandraengine.exceptions import ModelException @@ -17,8 +18,6 @@ def __init__(self, **values): for k,v in values.items(): if k in self._columns: setattr(self, k, v) - else: - self._dynamic_columns[k] = v #set excluded columns to None for k in self._columns.keys(): @@ -35,16 +34,6 @@ def pk(self): """ Returns the object's primary key """ 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(): @@ -76,7 +65,7 @@ def __new__(cls, name, bases, attrs): """ #move column definitions into _columns dict #and set default column names - _columns = {} + _columns = OrderedDict() pk_name = None def _transform_column(col_name, col_obj): @@ -85,20 +74,20 @@ def _transform_column(col_name, col_obj): allow_delete = not col_obj.primary_key attrs[col_name] = col_obj.get_property(allow_delete=allow_delete) - #transform column definitions - for k,v in attrs.items(): - if isinstance(v, columns.BaseColumn): - if v.is_primary_key: - if pk_name: - raise ModelException("More than one primary key defined for {}".format(name)) - pk_name = k - _transform_column(k,v) - - #set primary key if it's not already defined - if not pk_name: + #import ipdb; ipdb.set_trace() + column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.BaseColumn)] + column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) + + #prepend primary key if none has been defined + if not any([v.primary_key for k,v in column_definitions]): k,v = 'id', columns.UUID(primary_key=True) + column_definitions = [(k,v)] + column_definitions + + #transform column definitions + for k,v in column_definitions: + if pk_name is None and v.primary_key: + pk_name = k _transform_column(k,v) - pk_name = k #setup primary key shortcut if pk_name != 'pk': diff --git a/cassandraengine/query.py b/cassandraengine/query.py index 817b03960e..8cd7328519 100644 --- a/cassandraengine/query.py +++ b/cassandraengine/query.py @@ -158,16 +158,18 @@ def _create_column_family(self): qs = ['CREATE TABLE {}'.format(self.column_family_name)] #add column types + pkeys = [] qtypes = [] def add_column(col): s = '{} {}'.format(col.db_field, col.db_type) - if col.primary_key: s += ' PRIMARY KEY' + if col.primary_key: pkeys.append(col.db_field) qtypes.append(s) - add_column(self.model._columns[self.model._pk_name]) + #add_column(self.model._columns[self.model._pk_name]) for name, col in self.model._columns.items(): - if col.primary_key: continue add_column(col) + qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) + qs += ['({})'.format(', '.join(qtypes))] qs = ' '.join(qs) diff --git a/cassandraengine/tests/model/test_class_construction.py b/cassandraengine/tests/model/test_class_construction.py index 5e3dbe1968..7da6f1e92a 100644 --- a/cassandraengine/tests/model/test_class_construction.py +++ b/cassandraengine/tests/model/test_class_construction.py @@ -28,13 +28,6 @@ class TestModel(Model): self.assertIsNone(inst.id) self.assertIsNone(inst.text) - def test_multiple_primary_keys_fail(self): - """Test attempting to define multiple primary keys fails""" - with self.assertRaises(ModelException): - class MultiPK(Model): - id1 = columns.Integer(primary_key=True) - id2 = columns.Integer(primary_key=True) - def test_db_map(self): """ Tests that the db_map is properly defined @@ -58,3 +51,15 @@ class BadNames(Model): words = columns.Text() content = columns.Text(db_field='words') + def test_column_ordering_is_preserved(self): + """ + Tests that the _columns dics retains the ordering of the class definition + """ + + class Stuff(Model): + words = columns.Text() + content = columns.Text() + numbers = columns.Integer() + + self.assertEquals(Stuff._columns.keys(), ['id', 'words', 'content', 'numbers']) + diff --git a/cassandraengine/tests/model/test_model_io.py b/cassandraengine/tests/model/test_model_io.py index 8adf46913c..788b36bb92 100644 --- a/cassandraengine/tests/model/test_model_io.py +++ b/cassandraengine/tests/model/test_model_io.py @@ -52,21 +52,3 @@ 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']) - From fc1de7259604dbb973903fd58fba332790ad9c43 Mon Sep 17 00:00:00 2001 From: Eric Scrivner Date: Mon, 12 Nov 2012 22:04:12 -0800 Subject: [PATCH 0016/3961] Add Vagrantfile - Add complete virtual environment with Python 2.7 and Cassandra 1.1.6 for testing. --- Vagrantfile | 46 ++++++++++++++++ manifests/default.pp | 66 +++++++++++++++++++++++ modules/cassandra/files/cassandra.upstart | 18 +++++++ 3 files changed, 130 insertions(+) create mode 100644 Vagrantfile create mode 100644 manifests/default.pp create mode 100644 modules/cassandra/files/cassandra.upstart diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000000..6d8ccc8ad1 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,46 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant::Config.run do |config| + # All Vagrant configuration is done here. The most common configuration + # options are documented and commented below. For a complete reference, + # please see the online documentation at vagrantup.com. + + # Every Vagrant virtual environment requires a box to build off of. + config.vm.box = "precise" + + # The url from where the 'config.vm.box' box will be fetched if it + # doesn't already exist on the user's system. + config.vm.box_url = "https://s3-us-west-2.amazonaws.com/graphplatform.swmirror/precise64.box" + + # Boot with a GUI so you can see the screen. (Default is headless) + # config.vm.boot_mode = :gui + + # Assign this VM to a host-only network IP, allowing you to access it + # via the IP. Host-only networks can talk to the host machine as well as + # any other machines on the same network, but cannot be accessed (through this + # network interface) by any external networks. + config.vm.network :hostonly, "192.168.33.10" + + config.vm.customize ["modifyvm", :id, "--memory", "2048"] + + # Forward a port from the guest to the host, which allows for outside + # computers to access the VM, whereas host only networking does not. + config.vm.forward_port 80, 8080 + + # Share an additional folder to the guest VM. The first argument is + # an identifier, the second is the path on the guest to mount the + # folder, and the third is the path on the host to the actual folder. + # config.vm.share_folder "v-data", "/vagrant_data", "../data" + #config.vm.share_folder "v-root" "/vagrant", ".", :nfs => true + + # Provision with puppet + config.vm.provision :shell, :inline => "apt-get update" + + config.vm.provision :puppet, :options => ['--verbose', '--debug'] do |puppet| + puppet.facter = {'hostname' => 'cassandraengine'} + # puppet.manifests_path = "puppet/manifests" + # puppet.manifest_file = "site.pp" + puppet.module_path = "modules" + end +end \ No newline at end of file diff --git a/manifests/default.pp b/manifests/default.pp new file mode 100644 index 0000000000..1cf748cb71 --- /dev/null +++ b/manifests/default.pp @@ -0,0 +1,66 @@ +# Basic virtualbox configuration +Exec { path => "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } + +node basenode { + package{["build-essential", "git-core", "vim"]: + ensure => installed + } +} + +class xfstools { + package{['lvm2', 'xfsprogs']: + ensure => installed + } +} +class java { + package {['openjdk-7-jre-headless']: + ensure => installed + } +} + +class cassandra { + include xfstools + include java + + package {"wget": + ensure => latest + } + + file {"/etc/init/cassandra.conf": + source => "puppet:///modules/cassandra/cassandra.upstart", + owner => root + } + + exec {"download-cassandra": + cwd => "/tmp", + command => "wget http://download.nextag.com/apache/cassandra/1.1.6/apache-cassandra-1.1.6-bin.tar.gz", + creates => "/tmp/apache-cassandra-1.1.6-bin.tar.gz", + require => [Package["wget"], File["/etc/init/cassandra.conf"]] + } + + exec {"install-cassandra": + cwd => "/tmp", + command => "tar -xzf apache-cassandra-1.1.6-bin.tar.gz; mv apache-cassandra-1.1.6 /usr/local/cassandra", + require => Exec["download-cassandra"], + creates => "/usr/local/cassandra/bin/cassandra" + } + + service {"cassandra": + ensure => running, + require => Exec["install-cassandra"] + } +} + +node cassandraengine inherits basenode { + include cassandra + + package {["python-pip", "python-dev"]: + ensure => installed + } + + exec {"install-requirements": + cwd => "/vagrant", + command => "pip install -r requirements.txt", + require => [Package["python-pip"], Package["python-dev"]] + } +} diff --git a/modules/cassandra/files/cassandra.upstart b/modules/cassandra/files/cassandra.upstart new file mode 100644 index 0000000000..05278cad04 --- /dev/null +++ b/modules/cassandra/files/cassandra.upstart @@ -0,0 +1,18 @@ +# Cassandra Upstart +# + +description "Cassandra" + +start on runlevel [2345] +stop on runlevel [!2345] + +respawn + +exec /usr/local/cassandra/bin/cassandra -f + + + +pre-stop script + kill `cat /tmp/cassandra.pid` + sleep 3 +end script \ No newline at end of file From fd881636ea01d58673d12389fe98df925bad6eb1 Mon Sep 17 00:00:00 2001 From: Eric Scrivner Date: Mon, 12 Nov 2012 22:04:41 -0800 Subject: [PATCH 0017/3961] Add Ignore For Vagrant - Add ignore for .vagrant file --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f24cd9952d..878f8ebd3c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ var sdist develop-eggs .installed.cfg - +.vagrant # Installer logs pip-log.txt From 718003362102c078d608afbc6ccb3fb740d79e0b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 13 Nov 2012 21:17:21 -0800 Subject: [PATCH 0018/3961] fixed a problem with the value management of instances --- cassandraengine/models.py | 7 +++--- .../tests/model/test_class_construction.py | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cassandraengine/models.py b/cassandraengine/models.py index 76f3d99cd6..2b425bbc8e 100644 --- a/cassandraengine/models.py +++ b/cassandraengine/models.py @@ -71,10 +71,7 @@ def __new__(cls, name, bases, attrs): def _transform_column(col_name, col_obj): _columns[col_name] = col_obj col_obj.set_db_name(col_name) - allow_delete = not col_obj.primary_key - attrs[col_name] = col_obj.get_property(allow_delete=allow_delete) - #import ipdb; ipdb.set_trace() column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.BaseColumn)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) @@ -91,7 +88,9 @@ def _transform_column(col_name, col_obj): #setup primary key shortcut if pk_name != 'pk': - attrs['pk'] = _columns[pk_name].get_property(allow_delete=False) + pk_get = lambda self: getattr(self, pk_name) + pk_set = lambda self, val: setattr(self, pk_name, val) + attrs['pk'] = property(pk_get, pk_set) #check for duplicate column names col_names = set() diff --git a/cassandraengine/tests/model/test_class_construction.py b/cassandraengine/tests/model/test_class_construction.py index 7da6f1e92a..b1659fa122 100644 --- a/cassandraengine/tests/model/test_class_construction.py +++ b/cassandraengine/tests/model/test_class_construction.py @@ -18,10 +18,12 @@ def test_column_attributes_handled_correctly(self): class TestModel(Model): text = columns.Text() + #check class attributes self.assertHasAttr(TestModel, '_columns') - self.assertHasAttr(TestModel, 'id') + self.assertNotHasAttr(TestModel, 'id') self.assertHasAttr(TestModel, 'text') + #check instance attributes inst = TestModel() self.assertHasAttr(inst, 'id') self.assertHasAttr(inst, 'text') @@ -63,3 +65,21 @@ class Stuff(Model): self.assertEquals(Stuff._columns.keys(), ['id', 'words', 'content', 'numbers']) + def test_value_managers_are_keeping_model_instances_isolated(self): + """ + Tests that instance value managers are isolated from other instances + """ + class Stuff(Model): + num = columns.Integer() + + inst1 = Stuff(num=5) + inst2 = Stuff(num=7) + + self.assertNotEquals(inst1.num, inst2.num) + self.assertEquals(inst1.num, 5) + self.assertEquals(inst2.num, 7) + + def test_meta_data_is_not_inherited(self): + """ + Test that metadata defined in one class, is not inherited by subclasses + """ From eab79f6900c3d6bb69702e04c58bb535519c2640 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 18 Nov 2012 17:34:35 -0800 Subject: [PATCH 0019/3961] changing name to cqlengine --- cassandraengine/__init__.py | 3 -- .../tests/columns/test_validation.py | 16 -------- cqlengine/__init__.py | 3 ++ {cassandraengine => cqlengine}/columns.py | 37 ++++++++++++++++++- {cassandraengine => cqlengine}/connection.py | 0 {cassandraengine => cqlengine}/exceptions.py | 2 +- {cassandraengine => cqlengine}/manager.py | 2 +- {cassandraengine => cqlengine}/models.py | 22 +++++------ {cassandraengine => cqlengine}/query.py | 2 +- .../tests/__init__.py | 0 {cassandraengine => cqlengine}/tests/base.py | 0 .../tests/columns/__init__.py | 0 cqlengine/tests/columns/test_validation.py | 16 ++++++++ .../tests/model/__init__.py | 0 .../tests/model/test_class_construction.py | 8 ++-- .../tests/model/test_model_io.py | 6 +-- .../tests/model/test_validation.py | 0 17 files changed, 74 insertions(+), 43 deletions(-) delete mode 100644 cassandraengine/__init__.py delete mode 100644 cassandraengine/tests/columns/test_validation.py create mode 100644 cqlengine/__init__.py rename {cassandraengine => cqlengine}/columns.py (87%) rename {cassandraengine => cqlengine}/connection.py (100%) rename {cassandraengine => cqlengine}/exceptions.py (75%) rename {cassandraengine => cqlengine}/manager.py (98%) rename {cassandraengine => cqlengine}/models.py (89%) rename {cassandraengine => cqlengine}/query.py (99%) rename {cassandraengine => cqlengine}/tests/__init__.py (100%) rename {cassandraengine => cqlengine}/tests/base.py (100%) rename {cassandraengine => cqlengine}/tests/columns/__init__.py (100%) create mode 100644 cqlengine/tests/columns/test_validation.py rename {cassandraengine => cqlengine}/tests/model/__init__.py (100%) rename {cassandraengine => cqlengine}/tests/model/test_class_construction.py (93%) rename {cassandraengine => cqlengine}/tests/model/test_model_io.py (91%) rename {cassandraengine => cqlengine}/tests/model/test_validation.py (100%) diff --git a/cassandraengine/__init__.py b/cassandraengine/__init__.py deleted file mode 100644 index af95d0b003..0000000000 --- a/cassandraengine/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from cassandraengine.columns import * -from cassandraengine.models import Model - diff --git a/cassandraengine/tests/columns/test_validation.py b/cassandraengine/tests/columns/test_validation.py deleted file mode 100644 index e42519cc3b..0000000000 --- a/cassandraengine/tests/columns/test_validation.py +++ /dev/null @@ -1,16 +0,0 @@ -#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/cqlengine/__init__.py b/cqlengine/__init__.py new file mode 100644 index 0000000000..1af8944a10 --- /dev/null +++ b/cqlengine/__init__.py @@ -0,0 +1,3 @@ +from cqlengine.columns import * +from cqlengine.models import Model + diff --git a/cassandraengine/columns.py b/cqlengine/columns.py similarity index 87% rename from cassandraengine/columns.py rename to cqlengine/columns.py index bcbf39e7f5..25a6f59b34 100644 --- a/cassandraengine/columns.py +++ b/cqlengine/columns.py @@ -2,12 +2,43 @@ import re from uuid import uuid1, uuid4 -from cassandraengine.exceptions import ValidationError +from cqlengine.exceptions import ValidationError + +class BaseValueManager(object): + + def __init__(self, instance, column, value): + self.instance = instance + self.column = column + self.initial_value = value + self.value = value + + def deleted(self): + return self.value is None and self.initial_value is not None + + def getval(self): + return self.value + + def setval(self, val): + self.value = val + + def delval(self): + self.value = None + + def get_property(self): + _get = lambda slf: self.getval() + _set = lambda slf, val: self.setval(val) + _del = lambda slf: self.delval() + + if self.column.can_delete: + return property(_get, _set, _del) + else: + return property(_get, _set) class BaseColumn(object): #the cassandra type this column maps to db_type = None + value_manager = BaseValueManager instance_counter = 0 @@ -64,6 +95,10 @@ def has_default(self): def is_primary_key(self): return self.primary_key + @property + def can_delete(self): + return not self.primary_key + #methods for replacing column definitions with properties that interact #with a column's value member #this will allow putting logic behind value access (lazy loading, etc) diff --git a/cassandraengine/connection.py b/cqlengine/connection.py similarity index 100% rename from cassandraengine/connection.py rename to cqlengine/connection.py diff --git a/cassandraengine/exceptions.py b/cqlengine/exceptions.py similarity index 75% rename from cassandraengine/exceptions.py rename to cqlengine/exceptions.py index 4b20f7a334..3bb026f214 100644 --- a/cassandraengine/exceptions.py +++ b/cqlengine/exceptions.py @@ -1,4 +1,4 @@ -#cassandraengine exceptions +#cqlengine exceptions class ModelException(BaseException): pass class ValidationError(BaseException): pass diff --git a/cassandraengine/manager.py b/cqlengine/manager.py similarity index 98% rename from cassandraengine/manager.py rename to cqlengine/manager.py index de061ce0fb..4d4cbb73bc 100644 --- a/cassandraengine/manager.py +++ b/cqlengine/manager.py @@ -1,6 +1,6 @@ #manager class -from cassandraengine.query import QuerySet +from cqlengine.query import QuerySet class Manager(object): diff --git a/cassandraengine/models.py b/cqlengine/models.py similarity index 89% rename from cassandraengine/models.py rename to cqlengine/models.py index 2b425bbc8e..b44cdebe76 100644 --- a/cassandraengine/models.py +++ b/cqlengine/models.py @@ -1,8 +1,8 @@ from collections import OrderedDict -from cassandraengine import columns -from cassandraengine.exceptions import ModelException -from cassandraengine.manager import Manager +from cqlengine import columns +from cqlengine.exceptions import ModelException +from cqlengine.manager import Manager class BaseModel(object): """ @@ -11,18 +11,14 @@ class BaseModel(object): #table names will be generated automatically from it's model name and package #however, you can alse define them manually here - db_name = None + db_name = None def __init__(self, **values): - #set columns from values - for k,v in values.items(): - if k in self._columns: - setattr(self, k, v) - - #set excluded columns to None - for k in self._columns.keys(): - if k not in values: - setattr(self, k, None) + self._values = {} + for name, column in self._columns.items(): + value_mngr = column.value_manager(self, column, values.get(name, None)) + self._values[name] = value_mngr + setattr(self, name, value_mngr.get_property()) @classmethod def find(cls, pk): diff --git a/cassandraengine/query.py b/cqlengine/query.py similarity index 99% rename from cassandraengine/query.py rename to cqlengine/query.py index 8cd7328519..70343d6bce 100644 --- a/cassandraengine/query.py +++ b/cqlengine/query.py @@ -1,6 +1,6 @@ import copy -from cassandraengine.connection import get_connection +from cqlengine.connection import get_connection #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index diff --git a/cassandraengine/tests/__init__.py b/cqlengine/tests/__init__.py similarity index 100% rename from cassandraengine/tests/__init__.py rename to cqlengine/tests/__init__.py diff --git a/cassandraengine/tests/base.py b/cqlengine/tests/base.py similarity index 100% rename from cassandraengine/tests/base.py rename to cqlengine/tests/base.py diff --git a/cassandraengine/tests/columns/__init__.py b/cqlengine/tests/columns/__init__.py similarity index 100% rename from cassandraengine/tests/columns/__init__.py rename to cqlengine/tests/columns/__init__.py diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py new file mode 100644 index 0000000000..f916fb0ebe --- /dev/null +++ b/cqlengine/tests/columns/test_validation.py @@ -0,0 +1,16 @@ +#tests the behavior of the column classes + +from cqlengine.tests.base import BaseCassEngTestCase + +from cqlengine.columns import BaseColumn +from cqlengine.columns import Bytes +from cqlengine.columns import Ascii +from cqlengine.columns import Text +from cqlengine.columns import Integer +from cqlengine.columns import DateTime +from cqlengine.columns import UUID +from cqlengine.columns import Boolean +from cqlengine.columns import Float +from cqlengine.columns import Decimal + + diff --git a/cassandraengine/tests/model/__init__.py b/cqlengine/tests/model/__init__.py similarity index 100% rename from cassandraengine/tests/model/__init__.py rename to cqlengine/tests/model/__init__.py diff --git a/cassandraengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py similarity index 93% rename from cassandraengine/tests/model/test_class_construction.py rename to cqlengine/tests/model/test_class_construction.py index b1659fa122..bf6f3522d7 100644 --- a/cassandraengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -1,8 +1,8 @@ -from cassandraengine.tests.base import BaseCassEngTestCase +from cqlengine.tests.base import BaseCassEngTestCase -from cassandraengine.exceptions import ModelException -from cassandraengine.models import Model -from cassandraengine import columns +from cqlengine.exceptions import ModelException +from cqlengine.models import Model +from cqlengine import columns class TestModelClassFunction(BaseCassEngTestCase): """ diff --git a/cassandraengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py similarity index 91% rename from cassandraengine/tests/model/test_model_io.py rename to cqlengine/tests/model/test_model_io.py index 788b36bb92..a080726ba6 100644 --- a/cassandraengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,8 +1,8 @@ from unittest import skip -from cassandraengine.tests.base import BaseCassEngTestCase +from cqlengine.tests.base import BaseCassEngTestCase -from cassandraengine.models import Model -from cassandraengine import columns +from cqlengine.models import Model +from cqlengine import columns class TestModel(Model): count = columns.Integer() diff --git a/cassandraengine/tests/model/test_validation.py b/cqlengine/tests/model/test_validation.py similarity index 100% rename from cassandraengine/tests/model/test_validation.py rename to cqlengine/tests/model/test_validation.py From 39176b9660b0c722ad9f0a97e6a1711294455efa Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 18 Nov 2012 17:48:21 -0800 Subject: [PATCH 0020/3961] adding valuemanager class and setup to model metaclass --- cqlengine/models.py | 14 ++++++++++---- cqlengine/tests/model/test_class_construction.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index b44cdebe76..3fa7c6bfcd 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -18,7 +18,6 @@ def __init__(self, **values): for name, column in self._columns.items(): value_mngr = column.value_manager(self, column, values.get(name, None)) self._values[name] = value_mngr - setattr(self, name, value_mngr.get_property()) @classmethod def find(cls, pk): @@ -67,6 +66,15 @@ def __new__(cls, name, bases, attrs): def _transform_column(col_name, col_obj): _columns[col_name] = col_obj col_obj.set_db_name(col_name) + #set properties + _get = lambda self: self._values[col_name].getval() + _set = lambda self, val: self._values[col_name].setval(val) + _del = lambda self: self._values[col_name].delval() + if col_obj.can_delete: + attrs[col_name] = property(_get, _set) + else: + attrs[col_name] = property(_get, _set, _del) + column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.BaseColumn)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) @@ -84,9 +92,7 @@ def _transform_column(col_name, col_obj): #setup primary key shortcut if pk_name != 'pk': - pk_get = lambda self: getattr(self, pk_name) - pk_set = lambda self, val: setattr(self, pk_name, val) - attrs['pk'] = property(pk_get, pk_set) + attrs['pk'] = attrs[pk_name] #check for duplicate column names col_names = set() diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index bf6f3522d7..949e40fa77 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -20,7 +20,7 @@ class TestModel(Model): #check class attributes self.assertHasAttr(TestModel, '_columns') - self.assertNotHasAttr(TestModel, 'id') + self.assertHasAttr(TestModel, 'id') self.assertHasAttr(TestModel, 'text') #check instance attributes From 4152a37e80cc2fcad18af5d1f7f7672a258bf935 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 20 Nov 2012 21:48:41 -0800 Subject: [PATCH 0021/3961] adding nosetests to vagrant node --- manifests/default.pp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/default.pp b/manifests/default.pp index 1cf748cb71..3c5ad95642 100644 --- a/manifests/default.pp +++ b/manifests/default.pp @@ -54,7 +54,7 @@ node cassandraengine inherits basenode { include cassandra - package {["python-pip", "python-dev"]: + package {["python-pip", "python-dev", "python-nose"]: ensure => installed } From 61177eb3401ab151ed3072368f8cfb29e289b436 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 20 Nov 2012 23:22:58 -0800 Subject: [PATCH 0022/3961] starting to write the QuerySet class --- cqlengine/exceptions.py | 6 +- cqlengine/query.py | 134 ++++++++++++++++++++++--- cqlengine/tests/query/__init__.py | 0 cqlengine/tests/query/test_queryset.py | 52 ++++++++++ 4 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 cqlengine/tests/query/__init__.py create mode 100644 cqlengine/tests/query/test_queryset.py diff --git a/cqlengine/exceptions.py b/cqlengine/exceptions.py index 3bb026f214..3a5444e9b1 100644 --- a/cqlengine/exceptions.py +++ b/cqlengine/exceptions.py @@ -1,4 +1,6 @@ #cqlengine exceptions -class ModelException(BaseException): pass -class ValidationError(BaseException): pass +class CQLEngineException(BaseException): pass +class ModelException(CQLEngineException): pass +class ValidationError(CQLEngineException): pass +class QueryException(CQLEngineException): pass diff --git a/cqlengine/query.py b/cqlengine/query.py index 70343d6bce..f410391097 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,18 +1,58 @@ +from collections import namedtuple import copy from cqlengine.connection import get_connection +from cqlengine.exceptions import QueryException #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index -class Query(object): +WhereFilter = namedtuple('WhereFilter', ['column', 'operator', 'value']) - pass +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 + _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): #TODO: querysets should be immutable #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) #REVERSE, LIMIT @@ -21,9 +61,22 @@ class QuerySet(object): 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 + #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 #----query generation / execution---- @@ -31,7 +84,16 @@ def _execute_query(self): conn = get_connection() 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 @property @@ -67,16 +129,36 @@ def first(self): pass 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): - qargs = copy.deepcopy(self.query_args) - qargs.update(kwargs) - return QuerySet(self.model, query_args=qargs) + #add arguments to the where clause filters + clone = copy.deepcopy(self) + 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): - """ Need to invert the logic for all kwargs """ - pass + operator = QueryOperator.get_operator(raw_statement.operator) + + statement = WhereFilter(column, operator, val) + clone._where.append(statement) + + return clone def count(self): """ Returns the number of rows matched by this query """ @@ -95,6 +177,32 @@ def find(self, pk): self._cursor.execute(qs, {self.model._pk_name:pk}) 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---- def save(self, instance): @@ -137,7 +245,7 @@ def save(self, instance): cur.execute(qs, field_values) #----delete--- - def delete(self): + def delete(self, columns=[]): """ Deletes the contents of a query """ diff --git a/cqlengine/tests/query/__init__.py b/cqlengine/tests/query/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py new file mode 100644 index 0000000000..ef1bd9c3f2 --- /dev/null +++ b/cqlengine/tests/query/test_queryset.py @@ -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 + """ From 24fb184f711c22ce2147e6bfd680af4201206e7b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 21 Nov 2012 07:39:13 -0800 Subject: [PATCH 0023/3961] removing some outdated property stuff --- cqlengine/columns.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 25a6f59b34..6ff219cfe9 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -99,33 +99,6 @@ def is_primary_key(self): def can_delete(self): return not self.primary_key - #methods for replacing column definitions with properties that interact - #with a column's value member - #this will allow putting logic behind value access (lazy loading, etc) - def _getval(self): - """ This columns value getter """ - return self.value - - def _setval(self, val): - """ This columns value setter """ - self.value = val - - def _delval(self): - """ This columns value deleter """ - raise NotImplementedError - - def get_property(self, allow_delete=True): - """ - Returns the property object that will set and get this - column's value and be assigned to this column's model attribute - """ - getval = lambda slf: self._getval() - setval = lambda slf, val: self._setval(val) - delval = lambda slf: self._delval() - if not allow_delete: - return property(getval, setval) - return property(getval, setval, delval) - def get_default(self): if self.has_default: if callable(self.default): From dee0ace2d76e0101c6d31180e1787ce25fd97821 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 21 Nov 2012 07:40:09 -0800 Subject: [PATCH 0024/3961] working out queryoperator logic and structure updating filter arg parsing and storage adding where clause creation --- cqlengine/query.py | 108 ++++++++++++++++++++++--- cqlengine/tests/query/test_queryset.py | 5 ++ 2 files changed, 100 insertions(+), 13 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index f410391097..56a27e6192 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,5 +1,7 @@ from collections import namedtuple import copy +from hashlib import md5 +from time import time from cqlengine.connection import get_connection from cqlengine.exceptions import QueryException @@ -7,13 +9,65 @@ #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index -WhereFilter = namedtuple('WhereFilter', ['column', 'operator', 'value']) - class QueryOperatorException(QueryException): pass class QueryOperator(object): + # The symbol that identifies this operator in filter kwargs + # ie: colname__ symbol = None + # The comparator symbol this operator + # uses in cql + cql_symbol = None + + def __init__(self, column, value): + self.column = column + self.value = value + + #the identifier is a unique key that will be used in string + #replacement on query strings, it's created from a hash + #of this object's id and the time + self.identifier = md5(str(id(self)) + str(time())).hexdigest() + + #perform validation on this operator + self.validate_operator() + self.validate_value() + + @property + def cql(self): + """ + Returns this operator's portion of the WHERE clause + :param valname: the dict key that this operator's compare value will be found in + """ + return '{} {} :{}'.format(self.column.db_field, self.cql_symbol, self.identifier) + + def validate_operator(self): + """ + Checks that this operator can be used on the column provided + """ + if self.symbol is None: + raise QueryOperatorException("{} is not a valid operator, use one with 'symbol' defined".format(self.__class__.__name__)) + if self.cql_symbol is None: + raise QueryOperatorException("{} is not a valid operator, use one with 'cql_symbol' defined".format(self.__class__.__name__)) + + def validate_value(self): + """ + Checks that the compare value works with this operator + + *doesn't do anything by default + """ + pass + + def get_dict(self): + """ + Returns this operators contribution to the cql.query arg dictionanry + + ie: if this column's name is colname, and the identifier is colval, + this should return the dict: {'colval':} + SELECT * FROM column_family WHERE colname=:colval + """ + return {self.identifier: self.value} + @classmethod def get_operator(cls, symbol): if not hasattr(cls, 'opmap'): @@ -26,33 +80,40 @@ def _recurse(klass): pass _recurse(QueryOperator) try: - return QueryOperator.opmap[symbol.upper()] + return QueryOperator.opmap[symbol.upper()](column) except KeyError: raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) class EqualsOperator(QueryOperator): symbol = 'EQ' + cql_symbol = '=' class InOperator(QueryOperator): symbol = 'IN' + cql_symbol = 'IN' class GreaterThanOperator(QueryOperator): symbol = "GT" + cql_symbol = '>' class GreaterThanOrEqualOperator(QueryOperator): symbol = "GTE" + cql_symbol = '>=' class LessThanOperator(QueryOperator): symbol = "LT" + cql_symbol = '<' class LessThanOrEqualOperator(QueryOperator): symbol = "LTE" + cql_symbol = '<=' class QuerySet(object): #TODO: querysets should be immutable #TODO: querysets should be executed lazily #TODO: support specifying offset and limit (use slice) (maybe return a mutated queryset) #TODO: support specifying columns to exclude or select only + #TODO: cache results in this instance, but don't copy them on deepcopy #CQL supports ==, >, >=, <, <=, IN (a,b,c,..n) #REVERSE, LIMIT @@ -84,11 +145,26 @@ def _execute_query(self): conn = get_connection() self._cursor = conn.cursor() + def _validate_where_syntax(self): + """ + Checks that a filterset will not create invalid cql + """ + #TODO: check that there's either a = or IN relationship with a primary key or indexed field + def _where_clause(self): """ Returns a where clause based on the given filter args """ - pass + self._validate_where_syntax() + return ' AND '.join([f.cql for f in self._where]) + + def _where_values(self): + """ + Returns the value dict to be passed to the cql query + """ + values = {} + for where in self._where: values.update(where.get_dict()) + return values def _select_query(self): """ @@ -133,12 +209,17 @@ def all(self): clone._where = [] return clone - def _parse_filter_arg(self, arg, val): + def _parse_filter_arg(self, arg): + """ + Parses a filter arg in the format: + __ + :returns: colname, op tuple + """ statement = arg.split('__') if len(statement) == 1: - return WhereFilter(arg, None, val) + return arg, None elif len(statement) == 2: - return WhereFilter(statement[0], statement[1], val) + return statement[0], statement[1] else: raise QueryException("Can't parse '{}'".format(arg)) @@ -146,17 +227,18 @@ def filter(self, **kwargs): #add arguments to the where clause filters clone = copy.deepcopy(self) for arg, val in kwargs.items(): - raw_statement = self._parse_filter_arg(arg, val) + col_name, col_op = self._parse_filter_arg(arg) #resolve column and operator try: - column = self.model._columns[raw_statement.column] + column = self.model._columns[col_name] except KeyError: - raise QueryException("Can't resolve column name: '{}'".format(raw_statement.column)) + raise QueryException("Can't resolve column name: '{}'".format(col_name)) - operator = QueryOperator.get_operator(raw_statement.operator) + #get query operator, or use equals if not supplied + operator_class = QueryOperator.get_operator(col_op or 'EQ') + operator = operator_class(column, val) - statement = WhereFilter(column, operator, val) - clone._where.append(statement) + clone._where.append(operator) return clone diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index ef1bd9c3f2..7f6de601b8 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -11,6 +11,11 @@ def test_query_filter_parsing(self): Tests the queryset filter method """ + 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 + """ + def test_where_clause_generation(self): """ Tests the where clause creation From 1fcf80909510872af1e60044d625ca6129683ea6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 22 Nov 2012 12:39:22 -0800 Subject: [PATCH 0025/3961] adding select querystring generation moving the manager functionality into the queryset and modifying the model and query classes to work with it --- cqlengine/columns.py | 6 +- cqlengine/manager.py | 23 +---- cqlengine/models.py | 29 +++++- cqlengine/query.py | 93 ++++++++++++------- .../tests/model/test_class_construction.py | 7 +- cqlengine/tests/model/test_model_io.py | 4 + cqlengine/tests/query/test_queryoperators.py | 0 cqlengine/tests/query/test_queryset.py | 52 ++++++++++- requirements.txt | 2 + 9 files changed, 153 insertions(+), 63 deletions(-) create mode 100644 cqlengine/tests/query/test_queryoperators.py diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 6ff219cfe9..e81e893005 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -42,9 +42,11 @@ class BaseColumn(object): instance_counter = 0 - def __init__(self, primary_key=False, db_field=None, default=None, null=False): + def __init__(self, primary_key=False, index=False, db_field=None, default=None, null=False): """ - :param primary_key: bool flag, there can be only one primary key per doc + :param primary_key: bool flag, indicates this column is a primary key. The first primary key defined + on a model is the partition key, all others are cluster keys + :param index: bool flag, indicates an index should be created for this column :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: boolean, is the field nullable? diff --git a/cqlengine/manager.py b/cqlengine/manager.py index 4d4cbb73bc..b2c2ee0b52 100644 --- a/cqlengine/manager.py +++ b/cqlengine/manager.py @@ -2,6 +2,7 @@ from cqlengine.query import QuerySet +#TODO: refactor this into the QuerySet class Manager(object): def __init__(self, model): @@ -27,22 +28,9 @@ def __call__(self, **kwargs): return self.filter(**kwargs) def find(self, pk): - """ - Returns the row corresponding to the primary key value given - """ - values = QuerySet(self.model).find(pk) - if values is None: return - - #change the column names to model names - #in case they are different - field_dict = {} - db_map = self.model._db_map - for key, val in values.items(): - if key in db_map: - field_dict[db_map[key]] = val - else: - field_dict[key] = val - return self.model(**field_dict) + """ Returns the row corresponding to the primary key set given """ + #TODO: rework this to work with multiple primary keys + return QuerySet(self.model).find(pk) def all(self): return QuerySet(self.model).all() @@ -50,9 +38,6 @@ def all(self): def filter(self, **kwargs): return QuerySet(self.model).filter(**kwargs) - def exclude(self, **kwargs): - return QuerySet(self.model).exclude(**kwargs) - def create(self, **kwargs): return self.model(**kwargs).save() diff --git a/cqlengine/models.py b/cqlengine/models.py index 3fa7c6bfcd..ca51a21e8c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -2,7 +2,7 @@ from cqlengine import columns from cqlengine.exceptions import ModelException -from cqlengine.manager import Manager +from cqlengine.query import QuerySet class BaseModel(object): """ @@ -19,11 +19,28 @@ def __init__(self, **values): value_mngr = column.value_manager(self, column, values.get(name, None)) self._values[name] = value_mngr + #TODO: note any deferred or only fields so they're not deleted + @classmethod def find(cls, pk): """ Loads a document by it's primary key """ + #TODO: rework this to work with multiple primary keys cls.objects.find(pk) + @classmethod + def column_family_name(cls): + """ + Returns the column family name if it's been defined + otherwise, it creates it from the module and class name + """ + if cls.db_name: + return cls.db_name + cf_name = cls.__module__ + '.' + cls.__name__ + cf_name = cf_name.replace('.', '_') + #trim to less than 48 characters or cassandra will complain + cf_name = cf_name[-48:] + return cf_name + @property def pk(self): """ Returns the object's primary key """ @@ -45,12 +62,14 @@ def as_dict(self): def save(self): is_new = self.pk is None self.validate() - self.objects._save_instance(self) + #self.objects._save_instance(self) + self.objects.save(self) return self def delete(self): """ Deletes this instance """ - self.objects._delete_instance(self) + #self.objects._delete_instance(self) + self.objects.delete_instance(self) class ModelMetaClass(type): @@ -115,9 +134,9 @@ def _transform_column(col_name, col_obj): attrs['_pk_name'] = pk_name attrs['_dynamic_columns'] = {} - #create the class and add a manager to it + #create the class and add a QuerySet to it klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) - klass.objects = Manager(klass) + klass.objects = QuerySet(klass) return klass diff --git a/cqlengine/query.py b/cqlengine/query.py index 56a27e6192..7b6c91b0cf 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -80,7 +80,7 @@ def _recurse(klass): pass _recurse(QueryOperator) try: - return QueryOperator.opmap[symbol.upper()](column) + return QueryOperator.opmap[symbol.upper()] except KeyError: raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) @@ -119,10 +119,10 @@ class QuerySet(object): #REVERSE, LIMIT #ORDER BY - def __init__(self, model, query_args={}): + def __init__(self, model): super(QuerySet, self).__init__() self.model = model - self.column_family_name = self.model.objects.column_family_name + self.column_family_name = self.model.column_family_name() #Where clause filters self._where = [] @@ -141,65 +141,82 @@ def __init__(self, model, query_args={}): self._cursor = None #----query generation / execution---- - def _execute_query(self): - conn = get_connection() - self._cursor = conn.cursor() def _validate_where_syntax(self): - """ - Checks that a filterset will not create invalid cql - """ + """ Checks that a filterset will not create invalid cql """ #TODO: check that there's either a = or IN relationship with a primary key or indexed field + #TODO: abuse this to see if we can get cql to raise an exception def _where_clause(self): - """ - Returns a where clause based on the given filter args - """ + """ Returns a where clause based on the given filter args """ self._validate_where_syntax() return ' AND '.join([f.cql for f in self._where]) def _where_values(self): - """ - Returns the value dict to be passed to the cql query - """ + """ Returns the value dict to be passed to the cql query """ values = {} - for where in self._where: values.update(where.get_dict()) + for where in self._where: + values.update(where.get_dict()) return values - def _select_query(self): + def _select_query(self, count=False): """ Returns a select clause based on the given filter args + + :param count: indicates this should return a count query only """ - pass + qs = [] + if count: + qs += ['SELECT COUNT(*)'] + else: + fields = self.models._columns.keys() + if self._defer_fields: + fields = [f for f in fields if f not in self._defer_fields] + elif self._only_fields: + fields = [f for f in fields if f in self._only_fields] + db_fields = [self.model._columns[f].db_fields for f in fields] + qs += ['SELECT {}'.format(', '.join(db_fields))] - @property - def cursor(self): - if self._cursor is None: - self._cursor = self._execute_query() - return self._cursor + qs += ['FROM {}'.format(self.column_family_name)] + + if self._where: + qs += ['WHERE {}'.format(self._where_clause())] + + #TODO: add support for limit, start, order by, and reverse + return ' '.join(qs) #----Reads------ def __iter__(self): if self._cursor is None: - self._execute_query() + conn = get_connection() + self._cursor = conn.cursor() + self._cursor.execute(self._select_query(), self._where_values()) return self + def _construct_instance(self, values): + #translate column names to model names + field_dict = {} + db_map = self.model._db_map + for key, val in values.items(): + if key in db_map: + field_dict[db_map[key]] = val + else: + field_dict[key] = val + return self.model(**field_dict) + def _get_next(self): - """ - Gets the next cursor result - Returns a db_field->value dict - """ + """ Gets the next cursor result """ cur = self._cursor values = cur.fetchone() if values is None: return names = [i[0] for i in cur.description] value_dict = dict(zip(names, values)) - return value_dict + return self._construct_instance(value_dict) def next(self): - values = self._get_next() - if values is None: raise StopIteration - return values + instance = self._get_next() + if instance is None: raise StopIteration + return instance def first(self): pass @@ -242,15 +259,22 @@ def filter(self, **kwargs): return clone + def __call__(self, **kwargs): + return self.filter(**kwargs) + def count(self): """ Returns the number of rows matched by this query """ - qs = 'SELECT COUNT(*) FROM {}'.format(self.column_family_name) + con = get_connection() + cur = con.cursor() + cur.execute(self._select_query(count=True), self._where_values()) + return cur.fetchone() def find(self, pk): """ loads one document identified by it's primary key """ #TODO: make this a convenience wrapper of the filter method + #TODO: rework this to work with multiple primary keys 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) @@ -326,6 +350,9 @@ def save(self, instance): cur = conn.cursor() cur.execute(qs, field_values) + def create(self, **kwargs): + return self.model(**kwargs).save() + #----delete--- def delete(self, columns=[]): """ diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 949e40fa77..9b8f4f359f 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -18,7 +18,7 @@ def test_column_attributes_handled_correctly(self): class TestModel(Model): text = columns.Text() - #check class attributes + #check class attibutes self.assertHasAttr(TestModel, '_columns') self.assertHasAttr(TestModel, 'id') self.assertHasAttr(TestModel, 'text') @@ -79,6 +79,11 @@ class Stuff(Model): self.assertEquals(inst1.num, 5) self.assertEquals(inst2.num, 7) + def test_normal_fields_can_be_defined_between_primary_keys(self): + """ + Tests tha non primary key fields can be defined between primary key fields + """ + def test_meta_data_is_not_inherited(self): """ Test that metadata defined in one class, is not inherited by subclasses diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index a080726ba6..5d0824203d 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -52,3 +52,7 @@ def test_nullable_columns_are_saved_properly(self): Tests that nullable columns save without any trouble """ + def test_column_deleting_works_properly(self): + """ + """ + diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 7f6de601b8..fe58dc725e 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -3,25 +3,58 @@ from cqlengine.exceptions import ModelException from cqlengine.models import Model from cqlengine import columns +from cqlengine import query + +class TestModel(Model): + test_id = columns.Integer(primary_key=True) + attempt_id = columns.Integer(primary_key=True) + descriptions = columns.Text() + expected_result = columns.Integer() + test_result = columns.Integer(index=True) class TestQuerySet(BaseCassEngTestCase): def test_query_filter_parsing(self): """ - Tests the queryset filter method + Tests the queryset filter method parses it's kwargs properly """ + query1 = TestModel.objects(test_id=5) + assert len(query1._where) == 1 + + op = query1._where[0] + assert isinstance(op, query.EqualsOperator) + assert op.value == 5 + + query2 = query1.filter(expected_result__gte=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 """ + with self.assertRaises(query.QueryException): + query0 = TestModel.objects(nonsense=5) def test_where_clause_generation(self): """ Tests the where clause creation """ + query1 = TestModel.objects(test_id=5) + ids = [o.identifier for o in query1._where] + where = query1._where_clause() + assert where == 'test_id = :{}'.format(*ids) + + query2 = query1.filter(expected_result__gte=1) + ids = [o.identifier for o in query2._where] + where = query2._where_clause() + assert where == 'test_id = :{} AND expected_result >= :{}'.format(*ids) - def test_querystring_generation(self): + + def test_querystring_construction(self): """ Tests the select querystring creation """ @@ -30,6 +63,11 @@ def test_queryset_is_immutable(self): """ Tests that calling a queryset function that changes it's state returns a new queryset """ + query1 = TestModel.objects(test_id=5) + assert len(query1._where) == 1 + + query2 = query1.filter(expected_result__gte=1) + assert len(query2._where) == 2 def test_queryset_slicing(self): """ @@ -45,13 +83,21 @@ 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 """ + query1 = TestModel.objects(test_id=5) + assert len(query1._where) == 1 + + query2 = query1.filter(expected_result__gte=1) + assert len(query2._where) == 2 + + query3 = query2.all() + assert len(query3._where) == 0 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): + def test_defining_only_or_defer_on_nonexistant_fields_fails(self): """ Tests that setting only or defer fields that don't exist raises an exception """ diff --git a/requirements.txt b/requirements.txt index b6734c8844..8411d39556 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ cql==1.2.0 +ipython==0.13.1 +ipdb==0.7 From b7d91fe869fef8fcdebafc83b771db907df73f16 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 22 Nov 2012 23:16:49 -0800 Subject: [PATCH 0026/3961] writing query delete method added queryset usage tests refactoring how column names are stored and calculated --- cqlengine/columns.py | 13 ++++-- cqlengine/models.py | 13 ++---- cqlengine/query.py | 54 ++++++++++++++-------- cqlengine/tests/query/test_queryset.py | 63 ++++++++++++++++++++++++-- 4 files changed, 111 insertions(+), 32 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index e81e893005..0eeb8051d0 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -56,6 +56,9 @@ def __init__(self, primary_key=False, index=False, db_field=None, default=None, self.default = default self.null = null + #the column name in the model definition + self.column_name = None + self.value = None #keep track of instantiation order @@ -112,17 +115,21 @@ def get_column_def(self): """ Returns a column definition for CQL table definition """ - dterms = [self.db_field, self.db_type] + dterms = [self.db_field_name, self.db_type] #if self.primary_key: #dterms.append('PRIMARY KEY') return ' '.join(dterms) - def set_db_name(self, name): + def set_column_name(self, name): """ Sets the column name during document class construction This value will be ignored if db_field is set in __init__ """ - self.db_field = self.db_field or name + self.column_name = name + + @property + def db_field_name(self): + return self.db_field or self.column_name class Bytes(BaseColumn): db_type = 'blob' diff --git a/cqlengine/models.py b/cqlengine/models.py index ca51a21e8c..2d9de4ed5a 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -62,13 +62,11 @@ def as_dict(self): def save(self): is_new = self.pk is None self.validate() - #self.objects._save_instance(self) self.objects.save(self) return self def delete(self): """ Deletes this instance """ - #self.objects._delete_instance(self) self.objects.delete_instance(self) @@ -84,7 +82,7 @@ def __new__(cls, name, bases, attrs): def _transform_column(col_name, col_obj): _columns[col_name] = col_obj - col_obj.set_db_name(col_name) + col_obj.set_column_name(col_name) #set properties _get = lambda self: self._values[col_name].getval() _set = lambda self, val: self._values[col_name].setval(val) @@ -94,7 +92,6 @@ def _transform_column(col_name, col_obj): else: attrs[col_name] = property(_get, _set, _del) - column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.BaseColumn)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) @@ -116,9 +113,9 @@ def _transform_column(col_name, col_obj): #check for duplicate column names col_names = set() for k,v in _columns.items(): - if v.db_field in col_names: - raise ModelException("{} defines the column {} more than once".format(name, v.db_field)) - col_names.add(v.db_field) + if v.db_field_name in col_names: + raise ModelException("{} defines the column {} more than once".format(name, v.db_field_name)) + col_names.add(v.db_field_name) #get column family name cf_name = attrs.pop('db_name', name) @@ -126,7 +123,7 @@ def _transform_column(col_name, col_obj): #create db_name -> model name map for loading db_map = {} for name, col in _columns.items(): - db_map[col.db_field] = name + db_map[col.db_field_name] = name #add management members to the class attrs['_columns'] = _columns diff --git a/cqlengine/query.py b/cqlengine/query.py index 7b6c91b0cf..b2c8c9e89a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -39,7 +39,7 @@ def cql(self): Returns this operator's portion of the WHERE clause :param valname: the dict key that this operator's compare value will be found in """ - return '{} {} :{}'.format(self.column.db_field, self.cql_symbol, self.identifier) + return '{} {} :{}'.format(self.column.db_field_name, self.cql_symbol, self.identifier) def validate_operator(self): """ @@ -140,6 +140,12 @@ def __init__(self, model): self._cursor = None + def __unicode__(self): + return self._select_query() + + def __str__(self): + return str(self.__unicode__()) + #----query generation / execution---- def _validate_where_syntax(self): @@ -169,12 +175,12 @@ def _select_query(self, count=False): if count: qs += ['SELECT COUNT(*)'] else: - fields = self.models._columns.keys() + fields = self.model._columns.keys() if self._defer_fields: fields = [f for f in fields if f not in self._defer_fields] elif self._only_fields: fields = [f for f in fields if f in self._only_fields] - db_fields = [self.model._columns[f].db_fields for f in fields] + db_fields = [self.model._columns[f].db_field_name for f in fields] qs += ['SELECT {}'.format(', '.join(db_fields))] qs += ['FROM {}'.format(self.column_family_name)] @@ -182,7 +188,10 @@ def _select_query(self, count=False): if self._where: qs += ['WHERE {}'.format(self._where_clause())] - #TODO: add support for limit, start, order by, and reverse + if not count: + #TODO: add support for limit, start, order by, and reverse + pass + return ' '.join(qs) #----Reads------ @@ -191,6 +200,7 @@ def __iter__(self): conn = get_connection() self._cursor = conn.cursor() self._cursor.execute(self._select_query(), self._where_values()) + self._rowcount = self._cursor.rowcount return self def _construct_instance(self, values): @@ -267,7 +277,10 @@ def count(self): con = get_connection() cur = con.cursor() cur.execute(self._select_query(count=True), self._where_values()) - return cur.fetchone() + return cur.fetchone()[0] + + def __len__(self): + return self.count() def find(self, pk): """ @@ -319,23 +332,14 @@ def save(self, instance): prior to calling this. """ assert type(instance) == self.model + #organize data value_pairs = [] - - #get pk - col = self.model._columns[self.model._pk_name] values = instance.as_dict() - value_pairs += [(col.db_field, values.get(self.model._pk_name))] #get defined fields and their column names for name, col in self.model._columns.items(): - if col.is_primary_key: continue - value_pairs += [(col.db_field, values.get(name))] - - #add dynamic fields - for key, val in values.items(): - if key in self.model._columns: continue - value_pairs += [(key, val)] + value_pairs += [(col.db_field_name, values.get(name))] #construct query string field_names = zip(*value_pairs)[0] @@ -357,7 +361,21 @@ def create(self, **kwargs): def delete(self, columns=[]): """ Deletes the contents of a query + + :returns: number of rows deleted """ + qs = ['DELETE FROM {}'.format(self.column_family_name)] + if self._where: + qs += ['WHERE {}'.format(self._where_clause())] + qs = ' '.join(qs) + + #TODO: Return number of rows deleted + con = get_connection() + cur = con.cursor() + cur.execute(qs, self._where_values()) + return cur.fetchone() + + def delete_instance(self, instance): """ Deletes one instance """ @@ -378,8 +396,8 @@ def _create_column_family(self): pkeys = [] qtypes = [] def add_column(col): - s = '{} {}'.format(col.db_field, col.db_type) - if col.primary_key: pkeys.append(col.db_field) + s = '{} {}'.format(col.db_field_name, col.db_type) + if col.primary_key: pkeys.append(col.db_field_name) qtypes.append(s) #add_column(self.model._columns[self.model._pk_name]) for name, col in self.model._columns.items(): diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index fe58dc725e..9ac767b51f 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -8,11 +8,11 @@ class TestModel(Model): test_id = columns.Integer(primary_key=True) attempt_id = columns.Integer(primary_key=True) - descriptions = columns.Text() + description = columns.Text() expected_result = columns.Integer() test_result = columns.Integer(index=True) -class TestQuerySet(BaseCassEngTestCase): +class TestQuerySetOperation(BaseCassEngTestCase): def test_query_filter_parsing(self): """ @@ -54,7 +54,7 @@ def test_where_clause_generation(self): assert where == 'test_id = :{} AND expected_result >= :{}'.format(*ids) - def test_querystring_construction(self): + def test_querystring_generation(self): """ Tests the select querystring creation """ @@ -101,3 +101,60 @@ def test_defining_only_or_defer_on_nonexistant_fields_fails(self): """ Tests that setting only or defer fields that don't exist raises an exception """ + +class TestQuerySetUsage(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestQuerySetUsage, cls).setUpClass() + try: TestModel.objects._delete_column_family() + except: pass + TestModel.objects._create_column_family() + + TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30) + TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30) + TestModel.objects.create(test_id=0, attempt_id=2, description='try3', expected_result=15, test_result=30) + TestModel.objects.create(test_id=0, attempt_id=3, description='try4', expected_result=20, test_result=25) + + TestModel.objects.create(test_id=1, attempt_id=0, description='try5', expected_result=5, test_result=25) + TestModel.objects.create(test_id=1, attempt_id=1, description='try6', expected_result=10, test_result=25) + TestModel.objects.create(test_id=1, attempt_id=2, description='try7', expected_result=15, test_result=25) + TestModel.objects.create(test_id=1, attempt_id=3, description='try8', expected_result=20, test_result=20) + + TestModel.objects.create(test_id=2, attempt_id=0, description='try9', expected_result=50, test_result=40) + TestModel.objects.create(test_id=2, attempt_id=1, description='try10', expected_result=60, test_result=40) + TestModel.objects.create(test_id=2, attempt_id=2, description='try11', expected_result=70, test_result=45) + TestModel.objects.create(test_id=2, attempt_id=3, description='try12', expected_result=75, test_result=45) + + @classmethod + def tearDownClass(cls): + super(TestQuerySetUsage, cls).tearDownClass() + TestModel.objects._delete_column_family() + + def test_count(self): + q = TestModel.objects(test_id=0) + assert q.count() == 4 + + def test_iteration(self): + q = TestModel.objects(test_id=0) + #tuple of expected attempt_id, expected_result values + import ipdb; ipdb.set_trace() + 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 + + q = TestModel.objects(attempt_id=3) + 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 + + + From 277f1496e08117d0fd0721305bbb125e11227891 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 23 Nov 2012 13:37:45 -0800 Subject: [PATCH 0027/3961] made keyspace a model attribute made keyspace a required parameter for get_connection moved create/delete column family and keyspaces into a management module --- cqlengine/connection.py | 46 +++++++++----- cqlengine/management.py | 50 +++++++++++++++ cqlengine/models.py | 5 +- cqlengine/query.py | 62 ++++++------------- .../tests/model/test_class_construction.py | 5 ++ cqlengine/tests/model/test_model_io.py | 15 ++++- cqlengine/tests/query/test_queryset.py | 10 +-- 7 files changed, 125 insertions(+), 68 deletions(-) create mode 100644 cqlengine/management.py diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 3cebc3acbf..19a767ac45 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -3,25 +3,39 @@ #http://cassandra.apache.org/doc/cql/CQL.html import cql +from cqlengine.exceptions import CQLEngineException + +class CQLConnectionError(CQLEngineException): pass _keyspace = 'cassengine_test' -_conn = None -def get_connection(): - global _conn - if _conn is None: - _conn = cql.connect('127.0.0.1', 9160) - _conn.set_cql_version('3.0.0') - try: - _conn.set_initial_keyspace(_keyspace) - except cql.ProgrammingError, e: - #http://www.datastax.com/docs/1.0/references/cql/CREATE_KEYSPACE - cur = _conn.cursor() - cur.execute("""create keyspace {} - with strategy_class = 'SimpleStrategy' - and strategy_options:replication_factor=1;""".format(_keyspace)) - _conn.set_initial_keyspace(_keyspace) - return _conn +#TODO: look into the cql connection pool class +_conn = {} +def get_connection(keyspace, create_missing_keyspace=True): + con = _conn.get(keyspace) + if con is None: + con = cql.connect('127.0.0.1', 9160) + con.set_cql_version('3.0.0') + + if keyspace: + try: + con.set_initial_keyspace(keyspace) + except cql.ProgrammingError, e: + if create_missing_keyspace: + from cqlengine.management import create_keyspace + create_keyspace(keyspace) + #http://www.datastax.com/docs/1.0/references/cql/CREATE_KEYSPACE + cur = con.cursor() + cur.execute("""create keyspace {} + with strategy_class = 'SimpleStrategy' + and strategy_options:replication_factor=1;""".format(keyspace)) + con.set_initial_keyspace(keyspace) + else: + raise CQLConnectionError('"{}" is not an existing keyspace'.format(keyspace)) + + _conn[keyspace] = con + + return con #cli examples here: #http://wiki.apache.org/cassandra/CassandraCli diff --git a/cqlengine/management.py b/cqlengine/management.py new file mode 100644 index 0000000000..07c24acd6e --- /dev/null +++ b/cqlengine/management.py @@ -0,0 +1,50 @@ +from cqlengine.connection import get_connection + + +def create_keyspace(name): + con = get_connection(None) + cur = con.cursor() + cur.execute("""create keyspace {} + with strategy_class = 'SimpleStrategy' + and strategy_options:replication_factor=1;""".format(name)) + +def delete_keyspace(name): + pass + +def create_column_family(model): + #TODO: check for existing column family + #construct query string + qs = ['CREATE TABLE {}'.format(model.column_family_name())] + + #add column types + pkeys = [] + qtypes = [] + def add_column(col): + s = '{} {}'.format(col.db_field_name, col.db_type) + if col.primary_key: pkeys.append(col.db_field_name) + qtypes.append(s) + for name, col in model._columns.items(): + add_column(col) + + qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) + + qs += ['({})'.format(', '.join(qtypes))] + qs = ' '.join(qs) + + #add primary key + conn = get_connection(model.keyspace) + cur = conn.cursor() + try: + cur.execute(qs) + except BaseException, e: + if 'Cannot add already existing column family' not in e.message: + raise + + +def delete_column_family(model): + #TODO: check that model exists + conn = get_connection(model.keyspace) + cur = conn.cursor() + cur.execute('drop table {};'.format(model.column_family_name())) + + pass diff --git a/cqlengine/models.py b/cqlengine/models.py index 2d9de4ed5a..7b2fa8ead1 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -9,10 +9,13 @@ class BaseModel(object): The base model class, don't inherit from this, inherit from Model, defined below """ - #table names will be generated automatically from it's model name and package + #table names will be generated automatically from it's model and package name #however, you can alse define them manually here db_name = None + #the keyspace for this model + keyspace = 'cqlengine' + def __init__(self, **values): self._values = {} for name, column in self._columns.items(): diff --git a/cqlengine/query.py b/cqlengine/query.py index b2c8c9e89a..71853fe169 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -46,15 +46,23 @@ def validate_operator(self): Checks that this operator can be used on the column provided """ if self.symbol is None: - raise QueryOperatorException("{} is not a valid operator, use one with 'symbol' defined".format(self.__class__.__name__)) + raise QueryOperatorException( + "{} is not a valid operator, use one with 'symbol' defined".format( + self.__class__.__name__ + ) + ) if self.cql_symbol is None: - raise QueryOperatorException("{} is not a valid operator, use one with 'cql_symbol' defined".format(self.__class__.__name__)) + raise QueryOperatorException( + "{} is not a valid operator, use one with 'cql_symbol' defined".format( + self.__class__.__name__ + ) + ) def validate_value(self): """ Checks that the compare value works with this operator - *doesn't do anything by default + Doesn't do anything by default """ pass @@ -109,7 +117,6 @@ class LessThanOrEqualOperator(QueryOperator): cql_symbol = '<=' class QuerySet(object): - #TODO: querysets should be immutable #TODO: querysets should be executed lazily #TODO: support specifying offset and limit (use slice) (maybe return a mutated queryset) #TODO: support specifying columns to exclude or select only @@ -195,9 +202,10 @@ def _select_query(self, count=False): return ' '.join(qs) #----Reads------ + def __iter__(self): if self._cursor is None: - conn = get_connection() + conn = get_connection(self.model.keyspace) self._cursor = conn.cursor() self._cursor.execute(self._select_query(), self._where_values()) self._rowcount = self._cursor.rowcount @@ -274,7 +282,7 @@ def __call__(self, **kwargs): def count(self): """ Returns the number of rows matched by this query """ - con = get_connection() + con = get_connection(self.model.keyspace) cur = con.cursor() cur.execute(self._select_query(count=True), self._where_values()) return cur.fetchone()[0] @@ -291,7 +299,7 @@ def find(self, pk): 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() + conn = get_connection(self.model.keyspace) self._cursor = conn.cursor() self._cursor.execute(qs, {self.model._pk_name:pk}) return self._get_next() @@ -350,7 +358,7 @@ def save(self, instance): qs += ["({})".format(', '.join([':'+f for f in field_names]))] qs = ' '.join(qs) - conn = get_connection() + conn = get_connection(self.model.keyspace) cur = conn.cursor() cur.execute(qs, field_values) @@ -370,7 +378,7 @@ def delete(self, columns=[]): qs = ' '.join(qs) #TODO: Return number of rows deleted - con = get_connection() + con = get_connection(self.model.keyspace) cur = con.cursor() cur.execute(qs, self._where_values()) return cur.fetchone() @@ -384,42 +392,8 @@ def delete_instance(self, instance): qs += ['WHERE {0}=:{0}'.format(pk_name)] qs = ' '.join(qs) - conn = get_connection() + conn = get_connection(self.model.keyspace) cur = conn.cursor() cur.execute(qs, {pk_name:instance.pk}) - def _create_column_family(self): - #construct query string - qs = ['CREATE TABLE {}'.format(self.column_family_name)] - - #add column types - pkeys = [] - qtypes = [] - def add_column(col): - s = '{} {}'.format(col.db_field_name, col.db_type) - if col.primary_key: pkeys.append(col.db_field_name) - qtypes.append(s) - #add_column(self.model._columns[self.model._pk_name]) - for name, col in self.model._columns.items(): - add_column(col) - - qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) - - qs += ['({})'.format(', '.join(qtypes))] - qs = ' '.join(qs) - - #add primary key - conn = get_connection() - cur = conn.cursor() - try: - cur.execute(qs) - except BaseException, e: - if 'Cannot add already existing column family' not in e.message: - raise - - def _delete_column_family(self): - conn = get_connection() - cur = conn.cursor() - cur.execute('drop table {};'.format(self.column_family_name)) - diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 9b8f4f359f..45b83d3d5c 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -84,6 +84,11 @@ def test_normal_fields_can_be_defined_between_primary_keys(self): Tests tha non primary key fields can be defined between primary key fields """ + def test_model_keyspace_attribute_must_be_a_string(self): + """ + Tests that users can't set the keyspace to None, or something else + """ + def test_meta_data_is_not_inherited(self): """ Test that metadata defined in one class, is not inherited by subclasses diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 5d0824203d..fe558eb936 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,6 +1,9 @@ from unittest import skip from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.management import create_column_family +from cqlengine.management import delete_column_family +from cqlengine.models import Model from cqlengine.models import Model from cqlengine import columns @@ -12,9 +15,15 @@ class TestModel(Model): class TestModelIO(BaseCassEngTestCase): - def setUp(self): - super(TestModelIO, self).setUp() - TestModel.objects._create_column_family() + @classmethod + def setUpClass(cls): + super(TestModelIO, cls).setUpClass() + create_column_family(TestModel) + + @classmethod + def tearDownClass(cls): + super(TestModelIO, cls).tearDownClass() + delete_column_family(TestModel) def test_model_save_and_load(self): """ diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 9ac767b51f..b59f9ab7ee 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -1,6 +1,8 @@ from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException +from cqlengine.management import create_column_family +from cqlengine.management import delete_column_family from cqlengine.models import Model from cqlengine import columns from cqlengine import query @@ -107,9 +109,9 @@ class TestQuerySetUsage(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestQuerySetUsage, cls).setUpClass() - try: TestModel.objects._delete_column_family() + try: delete_column_family(TestModel) except: pass - TestModel.objects._create_column_family() + create_column_family(TestModel) TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30) TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30) @@ -129,7 +131,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): super(TestQuerySetUsage, cls).tearDownClass() - TestModel.objects._delete_column_family() + #TestModel.objects._delete_column_family() + delete_column_family(TestModel) def test_count(self): q = TestModel.objects(test_id=0) @@ -138,7 +141,6 @@ def test_count(self): def test_iteration(self): q = TestModel.objects(test_id=0) #tuple of expected attempt_id, expected_result values - import ipdb; ipdb.set_trace() compare_set = set([(0,5), (1,10), (2,15), (3,20)]) for t in q: val = t.attempt_id, t.expected_result From d67006919a723d831cd5c6d68b6bf5128a2cdb7e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 23 Nov 2012 14:19:40 -0800 Subject: [PATCH 0028/3961] refactoring exceptions --- cqlengine/exceptions.py | 1 - cqlengine/query.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/exceptions.py b/cqlengine/exceptions.py index 3a5444e9b1..0b1ff55f1e 100644 --- a/cqlengine/exceptions.py +++ b/cqlengine/exceptions.py @@ -3,4 +3,3 @@ class CQLEngineException(BaseException): pass class ModelException(CQLEngineException): pass class ValidationError(CQLEngineException): pass -class QueryException(CQLEngineException): pass diff --git a/cqlengine/query.py b/cqlengine/query.py index 71853fe169..86837e93f9 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,11 +4,12 @@ from time import time from cqlengine.connection import get_connection -from cqlengine.exceptions import QueryException +from cqlengine.exceptions import CQLEngineException #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index +class QueryException(CQLEngineException): pass class QueryOperatorException(QueryException): pass class QueryOperator(object): From fdae4779048e00f80173d0f94a3bd7bdb178710e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 23 Nov 2012 16:17:26 -0800 Subject: [PATCH 0029/3961] adding column indexing support adding some where clause validation added supporting tests --- cqlengine/columns.py | 7 ++ cqlengine/management.py | 75 +++++++++------- cqlengine/models.py | 17 +++- cqlengine/query.py | 8 +- .../tests/model/test_class_construction.py | 5 ++ cqlengine/tests/query/test_queryset.py | 87 +++++++++++++++++-- 6 files changed, 157 insertions(+), 42 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 0eeb8051d0..9ce1bb6bf1 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -52,6 +52,7 @@ def __init__(self, primary_key=False, index=False, db_field=None, default=None, :param null: boolean, is the field nullable? """ self.primary_key = primary_key + self.index = index self.db_field = db_field self.default = default self.null = null @@ -129,8 +130,14 @@ def set_column_name(self, name): @property def db_field_name(self): + """ Returns the name of the cql name of this column """ return self.db_field or self.column_name + @property + def db_index_name(self): + """ Returns the name of the cql index """ + return 'index_{}'.format(self.db_field_name) + class Bytes(BaseColumn): db_type = 'blob' diff --git a/cqlengine/management.py b/cqlengine/management.py index 07c24acd6e..29fe02ba66 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,50 +1,65 @@ from cqlengine.connection import get_connection - def create_keyspace(name): con = get_connection(None) cur = con.cursor() - cur.execute("""create keyspace {} - with strategy_class = 'SimpleStrategy' - and strategy_options:replication_factor=1;""".format(name)) + cur.execute("""CREATE KEYSPACE {} + WITH strategy_class = 'SimpleStrategy' + AND strategy_options:replication_factor=1;""".format(name)) def delete_keyspace(name): - pass + con = get_connection(None) + cur = con.cursor() + cur.execute("DROP KEYSPACE {}".format(name)) def create_column_family(model): - #TODO: check for existing column family #construct query string - qs = ['CREATE TABLE {}'.format(model.column_family_name())] + cf_name = model.column_family_name() - #add column types - pkeys = [] - qtypes = [] - def add_column(col): - s = '{} {}'.format(col.db_field_name, col.db_type) - if col.primary_key: pkeys.append(col.db_field_name) - qtypes.append(s) - for name, col in model._columns.items(): - add_column(col) + conn = get_connection(model.keyspace) + cur = conn.cursor() - qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) + #check for an existing column family + ks_info = conn.client.describe_keyspace(model.keyspace) + if not any([cf_name == cf.name for cf in ks_info.cf_defs]): + qs = ['CREATE TABLE {}'.format(cf_name)] - qs += ['({})'.format(', '.join(qtypes))] - qs = ' '.join(qs) + #add column types + pkeys = [] + qtypes = [] + def add_column(col): + s = '{} {}'.format(col.db_field_name, col.db_type) + if col.primary_key: pkeys.append(col.db_field_name) + qtypes.append(s) + for name, col in model._columns.items(): + add_column(col) + + qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) + + qs += ['({})'.format(', '.join(qtypes))] + qs = ' '.join(qs) - #add primary key - conn = get_connection(model.keyspace) - cur = conn.cursor() - try: cur.execute(qs) - except BaseException, e: - if 'Cannot add already existing column family' not in e.message: - raise + + indexes = [c for n,c in model._columns.items() if c.index] + if indexes: + import ipdb; ipdb.set_trace() + for column in indexes: + #TODO: check for existing index... + #can that be determined from the connection client? + qs = ['CREATE INDEX {}'.format(column.db_index_name)] + qs += ['ON {}'.format(cf_name)] + qs += ['({})'.format(column.db_field_name)] + qs = ' '.join(qs) + cur.execute(qs) def delete_column_family(model): - #TODO: check that model exists + #check that model exists + cf_name = model.column_family_name() conn = get_connection(model.keyspace) - cur = conn.cursor() - cur.execute('drop table {};'.format(model.column_family_name())) + ks_info = conn.client.describe_keyspace(model.keyspace) + if any([cf_name == cf.name for cf in ks_info.cf_defs]): + cur = conn.cursor() + cur.execute('drop table {};'.format(cf_name)) - pass diff --git a/cqlengine/models.py b/cqlengine/models.py index 7b2fa8ead1..862f8e340c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -4,6 +4,8 @@ from cqlengine.exceptions import ModelException from cqlengine.query import QuerySet +class ModelDefinitionException(ModelException): pass + class BaseModel(object): """ The base model class, don't inherit from this, inherit from Model, defined below @@ -37,12 +39,12 @@ def column_family_name(cls): otherwise, it creates it from the module and class name """ if cls.db_name: - return cls.db_name + return cls.db_name.lower() cf_name = cls.__module__ + '.' + cls.__name__ cf_name = cf_name.replace('.', '_') #trim to less than 48 characters or cassandra will complain cf_name = cf_name[-48:] - return cf_name + return cf_name.lower() @property def pk(self): @@ -66,6 +68,7 @@ def save(self): is_new = self.pk is None self.validate() self.objects.save(self) + #delete any fields that have been deleted / set to none return self def delete(self): @@ -120,13 +123,19 @@ def _transform_column(col_name, col_obj): raise ModelException("{} defines the column {} more than once".format(name, v.db_field_name)) col_names.add(v.db_field_name) + #check for indexes on models with multiple primary keys + if len([1 for k,v in column_definitions if v.primary_key]) > 1: + if len([1 for k,v in column_definitions if v.index]) > 0: + raise ModelDefinitionException( + 'Indexes on models with multiple primary keys is not supported') + #get column family name cf_name = attrs.pop('db_name', name) #create db_name -> model name map for loading db_map = {} - for name, col in _columns.items(): - db_map[col.db_field_name] = name + for field_name, col in _columns.items(): + db_map[col.db_field_name] = field_name #add management members to the class attrs['_columns'] = _columns diff --git a/cqlengine/query.py b/cqlengine/query.py index 86837e93f9..83f78ef7f2 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -97,7 +97,7 @@ class EqualsOperator(QueryOperator): symbol = 'EQ' cql_symbol = '=' -class InOperator(QueryOperator): +class InOperator(EqualsOperator): symbol = 'IN' cql_symbol = 'IN' @@ -158,7 +158,11 @@ def __str__(self): def _validate_where_syntax(self): """ Checks that a filterset will not create invalid cql """ - #TODO: check that there's either a = or IN relationship with a primary key or indexed field + + #check that there's either a = or IN relationship with a primary key or indexed field + equal_ops = [w for w in self._where if isinstance(w, EqualsOperator)] + if not any([w.column.primary_key or w.column.index for w in equal_ops]): + raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') #TODO: abuse this to see if we can get cql to raise an exception def _where_clause(self): diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 45b83d3d5c..d2d88a0d98 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -89,6 +89,11 @@ def test_model_keyspace_attribute_must_be_a_string(self): Tests that users can't set the keyspace to None, or something else """ + def test_indexes_arent_allowed_on_models_with_multiple_primary_keys(self): + """ + Tests that attempting to define an index on a model with multiple primary keys fails + """ + def test_meta_data_is_not_inherited(self): """ Test that metadata defined in one class, is not inherited by subclasses diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index b59f9ab7ee..03fdb39674 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -12,6 +12,13 @@ class TestModel(Model): attempt_id = columns.Integer(primary_key=True) description = columns.Text() expected_result = columns.Integer() + test_result = columns.Integer() + +class IndexedTestModel(Model): + test_id = columns.Integer(primary_key=True) + attempt_id = columns.Integer(index=True) + description = columns.Text() + expected_result = columns.Integer() test_result = columns.Integer(index=True) class TestQuerySetOperation(BaseCassEngTestCase): @@ -104,14 +111,15 @@ def test_defining_only_or_defer_on_nonexistant_fields_fails(self): Tests that setting only or defer fields that don't exist raises an exception """ -class TestQuerySetUsage(BaseCassEngTestCase): +class BaseQuerySetUsage(BaseCassEngTestCase): @classmethod def setUpClass(cls): - super(TestQuerySetUsage, cls).setUpClass() - try: delete_column_family(TestModel) - except: pass + super(BaseQuerySetUsage, cls).setUpClass() + delete_column_family(TestModel) + delete_column_family(IndexedTestModel) create_column_family(TestModel) + create_column_family(IndexedTestModel) TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30) TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30) @@ -128,13 +136,32 @@ def setUpClass(cls): TestModel.objects.create(test_id=2, attempt_id=2, description='try11', expected_result=70, test_result=45) TestModel.objects.create(test_id=2, attempt_id=3, description='try12', expected_result=75, test_result=45) + IndexedTestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30) + IndexedTestModel.objects.create(test_id=1, attempt_id=1, description='try2', expected_result=10, test_result=30) + IndexedTestModel.objects.create(test_id=2, attempt_id=2, description='try3', expected_result=15, test_result=30) + IndexedTestModel.objects.create(test_id=3, attempt_id=3, description='try4', expected_result=20, test_result=25) + + IndexedTestModel.objects.create(test_id=4, attempt_id=0, description='try5', expected_result=5, test_result=25) + IndexedTestModel.objects.create(test_id=5, attempt_id=1, description='try6', expected_result=10, test_result=25) + IndexedTestModel.objects.create(test_id=6, attempt_id=2, description='try7', expected_result=15, test_result=25) + IndexedTestModel.objects.create(test_id=7, attempt_id=3, description='try8', expected_result=20, test_result=20) + + IndexedTestModel.objects.create(test_id=8, attempt_id=0, description='try9', expected_result=50, test_result=40) + IndexedTestModel.objects.create(test_id=9, attempt_id=1, description='try10', expected_result=60, test_result=40) + IndexedTestModel.objects.create(test_id=10, attempt_id=2, description='try11', expected_result=70, test_result=45) + IndexedTestModel.objects.create(test_id=11, attempt_id=3, description='try12', expected_result=75, test_result=45) + @classmethod def tearDownClass(cls): - super(TestQuerySetUsage, cls).tearDownClass() - #TestModel.objects._delete_column_family() + super(BaseQuerySetUsage, cls).tearDownClass() delete_column_family(TestModel) + delete_column_family(IndexedTestModel) + +class TestQuerySetCountAndSelection(BaseQuerySetUsage): def test_count(self): + assert TestModel.objects.count() == 12 + q = TestModel.objects(test_id=0) assert q.count() == 4 @@ -157,6 +184,54 @@ def test_iteration(self): assert val in compare_set compare_set.remove(val) assert len(compare_set) == 0 + + def test_delete(self): + TestModel.objects.create(test_id=3, attempt_id=0, description='try9', expected_result=50, test_result=40) + TestModel.objects.create(test_id=3, attempt_id=1, description='try10', expected_result=60, test_result=40) + TestModel.objects.create(test_id=3, attempt_id=2, description='try11', expected_result=70, test_result=45) + TestModel.objects.create(test_id=3, attempt_id=3, description='try12', expected_result=75, test_result=45) + + assert TestModel.objects.count() == 16 + assert TestModel.objects(test_id=3).count() == 4 + + TestModel.objects(test_id=3).delete() + + assert TestModel.objects.count() == 12 + assert TestModel.objects(test_id=3).count() == 0 + +class TestQuerySetValidation(BaseQuerySetUsage): + + def test_primary_key_or_index_must_be_specified(self): + """ + Tests that queries that don't have an equals relation to a primary key or indexed field fail + """ + with self.assertRaises(query.QueryException): + q = TestModel.objects(test_result=25) + iter(q) + + def test_primary_key_or_index_must_have_equal_relation_filter(self): + """ + Tests that queries that don't have non equal (>,<, etc) relation to a primary key or indexed field fail + """ + with self.assertRaises(query.QueryException): + q = TestModel.objects(test_id__gt=0) + iter(q) + + + def test_indexed_field_can_be_queried(self): + """ + 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 + + + + + + + From aa6b151d068f17218d9e83279a9521f1c70bdccf Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 23 Nov 2012 16:18:14 -0800 Subject: [PATCH 0030/3961] removing the manager file --- cqlengine/manager.py | 65 -------------------------------------------- 1 file changed, 65 deletions(-) delete mode 100644 cqlengine/manager.py diff --git a/cqlengine/manager.py b/cqlengine/manager.py deleted file mode 100644 index b2c2ee0b52..0000000000 --- a/cqlengine/manager.py +++ /dev/null @@ -1,65 +0,0 @@ -#manager class - -from cqlengine.query import QuerySet - -#TODO: refactor this into the QuerySet -class Manager(object): - - def __init__(self, model): - super(Manager, self).__init__() - self.model = model - - @property - def column_family_name(self): - """ - Returns the column family name if it's been defined - otherwise, it creates it from the module and class name - """ - if self.model.db_name: - return self.model.db_name - cf_name = self.model.__module__ + '.' + self.model.__name__ - cf_name = cf_name.replace('.', '_') - #trim to less than 48 characters or cassandra will complain - cf_name = cf_name[-48:] - return cf_name - - def __call__(self, **kwargs): - """ filter shortcut """ - return self.filter(**kwargs) - - def find(self, pk): - """ Returns the row corresponding to the primary key set given """ - #TODO: rework this to work with multiple primary keys - return QuerySet(self.model).find(pk) - - def all(self): - return QuerySet(self.model).all() - - def filter(self, **kwargs): - return QuerySet(self.model).filter(**kwargs) - - def create(self, **kwargs): - return self.model(**kwargs).save() - - def delete(self, **kwargs): - pass - - #----single instance methods---- - def _save_instance(self, instance): - """ - The business end of save, this is called by the models - save method and calls the Query save method. This should - only be called by the model saving itself - """ - QuerySet(self.model).save(instance) - - def _delete_instance(self, instance): - """ Deletes a single instance """ - QuerySet(self.model).delete_instance(instance) - - #----column family create/delete---- - def _create_column_family(self): - QuerySet(self.model)._create_column_family() - - def _delete_column_family(self): - QuerySet(self.model)._delete_column_family() From f75442456f8ebbfad3510cb1c42d4672671f677f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 23 Nov 2012 17:40:15 -0800 Subject: [PATCH 0031/3961] cleaning up --- cqlengine/columns.py | 21 ++------------------- cqlengine/connection.py | 38 -------------------------------------- cqlengine/management.py | 1 - cqlengine/query.py | 6 +----- 4 files changed, 3 insertions(+), 63 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 9ce1bb6bf1..bf27a87460 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -221,24 +221,7 @@ def __init__(self, **kwargs): class Counter(BaseColumn): def __init__(self, **kwargs): - super(DateTime, self).__init__(**kwargs) - raise NotImplementedError - -#TODO: research supercolumns -#http://wiki.apache.org/cassandra/DataModel -#checkout composite columns: -#http://www.datastax.com/dev/blog/introduction-to-composite-columns-part-1 -class List(BaseColumn): - #checkout cql.cqltypes.ListType - def __init__(self, **kwargs): - super(DateTime, self).__init__(**kwargs) - raise NotImplementedError - -class Dict(BaseColumn): - #checkout cql.cqltypes.MapType - def __init__(self, **kwargs): - super(DateTime, self).__init__(**kwargs) + super(Counter, self).__init__(**kwargs) raise NotImplementedError -#checkout cql.cqltypes.SetType -#checkout cql.cqltypes.CompositeType +#TODO: Foreign key fields diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 19a767ac45..dc1a7467fb 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -24,12 +24,6 @@ def get_connection(keyspace, create_missing_keyspace=True): if create_missing_keyspace: from cqlengine.management import create_keyspace create_keyspace(keyspace) - #http://www.datastax.com/docs/1.0/references/cql/CREATE_KEYSPACE - cur = con.cursor() - cur.execute("""create keyspace {} - with strategy_class = 'SimpleStrategy' - and strategy_options:replication_factor=1;""".format(keyspace)) - con.set_initial_keyspace(keyspace) else: raise CQLConnectionError('"{}" is not an existing keyspace'.format(keyspace)) @@ -37,35 +31,3 @@ def get_connection(keyspace, create_missing_keyspace=True): return con -#cli examples here: -#http://wiki.apache.org/cassandra/CassandraCli -#http://www.datastax.com/docs/1.0/dml/using_cql -#http://stackoverflow.com/questions/10871595/how-do-you-create-a-counter-columnfamily-with-cql3-in-cassandra -""" -try: - cur.execute("create table colfam (id int PRIMARY KEY);") -except cql.ProgrammingError: - #cur.execute('drop table colfam;') - pass -""" - -#http://www.datastax.com/docs/1.0/references/cql/INSERT -#updates/inserts do the same thing - -""" -cur.execute('insert into colfam (id, content) values (:id, :content)', dict(id=1, content='yo!')) - -cur.execute('select * from colfam WHERE id=1;') -cur.fetchone() - -cur.execute("update colfam set content='hey' where id=1;") -cur.execute('select * from colfam WHERE id=1;') -cur.fetchone() - -cur.execute('delete from colfam WHERE id=1;') -cur.execute('delete from colfam WHERE id in (1);') -""" - -#TODO: Add alter altering existing schema functionality -#http://www.datastax.com/docs/1.0/references/cql/ALTER_COLUMNFAMILY - diff --git a/cqlengine/management.py b/cqlengine/management.py index 29fe02ba66..5b2ad6aa42 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -43,7 +43,6 @@ def add_column(col): indexes = [c for n,c in model._columns.items() if c.index] if indexes: - import ipdb; ipdb.set_trace() for column in indexes: #TODO: check for existing index... #can that be determined from the connection client? diff --git a/cqlengine/query.py b/cqlengine/query.py index 83f78ef7f2..ce6a859087 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -118,15 +118,11 @@ class LessThanOrEqualOperator(QueryOperator): cql_symbol = '<=' class QuerySet(object): - #TODO: querysets should be executed lazily #TODO: support specifying offset and limit (use slice) (maybe return a mutated queryset) #TODO: support specifying columns to exclude or select only + #TODO: support ORDER BY #TODO: cache results in this instance, but don't copy them on deepcopy - #CQL supports ==, >, >=, <, <=, IN (a,b,c,..n) - #REVERSE, LIMIT - #ORDER BY - def __init__(self, model): super(QuerySet, self).__init__() self.model = model From e0530a883ad500628c19bf5fd051e668a54bfd40 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 23 Nov 2012 21:19:46 -0800 Subject: [PATCH 0032/3961] removing the find method adding initial support for cached queryset results added a dirty fix for iterating over a queryset more than once --- cqlengine/models.py | 6 --- cqlengine/query.py | 52 ++++++++++++++------------ cqlengine/tests/model/test_model_io.py | 6 +-- cqlengine/tests/query/test_queryset.py | 23 ++++++++++++ 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 862f8e340c..9111b7596c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -26,12 +26,6 @@ def __init__(self, **values): #TODO: note any deferred or only fields so they're not deleted - @classmethod - def find(cls, pk): - """ Loads a document by it's primary key """ - #TODO: rework this to work with multiple primary keys - cls.objects.find(pk) - @classmethod def column_family_name(cls): """ diff --git a/cqlengine/query.py b/cqlengine/query.py index ce6a859087..8ee3d6653d 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -142,6 +142,9 @@ def __init__(self, model): self._defer_fields = [] self._only_fields = [] + #results cache + self._result_cache = None + self._cursor = None def __unicode__(self): @@ -150,6 +153,22 @@ def __unicode__(self): def __str__(self): return str(self.__unicode__()) + def __call__(self, **kwargs): + return self.filter(**kwargs) + + def __deepcopy__(self, memo): + clone = self.__class__(self.model) + for k,v in self.__dict__.items(): + if k in ['_result_cache']: + clone.__dict__[k] = None + else: + clone.__dict__[k] = copy.deepcopy(v, memo) + + return clone + + def __len__(self): + return self.count() + #----query generation / execution---- def _validate_where_syntax(self): @@ -205,6 +224,7 @@ def _select_query(self, count=False): #----Reads------ def __iter__(self): + #TODO: cache results if self._cursor is None: conn = get_connection(self.model.keyspace) self._cursor = conn.cursor() @@ -234,11 +254,14 @@ def _get_next(self): def next(self): instance = self._get_next() - if instance is None: raise StopIteration + if instance is None: + #TODO: this is inefficient, we should be caching the results + self._cursor = None + raise StopIteration return instance def first(self): - pass + return iter(self)._get_next() def all(self): clone = copy.deepcopy(self) @@ -278,11 +301,9 @@ def filter(self, **kwargs): return clone - def __call__(self, **kwargs): - return self.filter(**kwargs) - def count(self): """ Returns the number of rows matched by this query """ + #TODO: check for previous query execution and return row count if it exists con = get_connection(self.model.keyspace) cur = con.cursor() cur.execute(self._select_query(count=True), self._where_values()) @@ -291,20 +312,6 @@ def count(self): def __len__(self): return self.count() - def find(self, pk): - """ - loads one document identified by it's primary key - """ - #TODO: make this a convenience wrapper of the filter method - #TODO: rework this to work with multiple primary keys - 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(self.model.keyspace) - self._cursor = conn.cursor() - self._cursor.execute(qs, {self.model._pk_name:pk}) - return self._get_next() - def _only_or_defer(self, action, fields): clone = copy.deepcopy(self) if clone._defer_fields or clone._only_fields: @@ -313,7 +320,9 @@ def _only_or_defer(self, action, fields): #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__)) + raise QueryException( + "Can't resolve fields {} in {}".format( + ', '.join(missing_fields), self.model.__name__)) if action == 'defer': clone._defer_fields = fields @@ -378,12 +387,9 @@ def delete(self, columns=[]): qs += ['WHERE {}'.format(self._where_clause())] qs = ' '.join(qs) - #TODO: Return number of rows deleted con = get_connection(self.model.keyspace) cur = con.cursor() cur.execute(qs, self._where_values()) - return cur.fetchone() - def delete_instance(self, instance): diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index fe558eb936..f02a61ec43 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -30,7 +30,7 @@ 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) + tm2 = TestModel.objects(id=tm.pk).first() for cname in tm._columns.keys(): self.assertEquals(getattr(tm, cname), getattr(tm2, cname)) @@ -44,7 +44,7 @@ def test_model_updating_works_properly(self): tm.count = 100 tm.save() - tm2 = TestModel.objects.find(tm.pk) + tm2 = TestModel.objects(id=tm.pk).first() self.assertEquals(tm.count, tm2.count) def test_model_deleting_works_properly(self): @@ -53,7 +53,7 @@ def test_model_deleting_works_properly(self): """ tm = TestModel.objects.create(count=8, text='123456789') tm.delete() - tm2 = TestModel.objects.find(tm.pk) + tm2 = TestModel.objects(id=tm.pk).first() self.assertIsNone(tm2) def test_nullable_columns_are_saved_properly(self): diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 03fdb39674..8d91d2da60 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -199,6 +199,29 @@ def test_delete(self): assert TestModel.objects.count() == 12 assert TestModel.objects(test_id=3).count() == 0 +class TestQuerySetIterator(BaseQuerySetUsage): + + 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 + + #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 + + + class TestQuerySetValidation(BaseQuerySetUsage): def test_primary_key_or_index_must_be_specified(self): From 854de8d375ae25168b7da633b631dbf5a68d8bd3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 23 Nov 2012 21:33:43 -0800 Subject: [PATCH 0033/3961] adding some todos --- cqlengine/columns.py | 17 +++++++++++------ cqlengine/query.py | 7 +++---- cqlengine/tests/model/test_model_io.py | 2 -- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index bf27a87460..a1ba0a3c57 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -55,6 +55,8 @@ def __init__(self, primary_key=False, index=False, db_field=None, default=None, self.index = index self.db_field = db_field self.default = default + + #TODO: make this required, since fields won't actually be nulled, but deleted self.null = null #the column name in the model definition @@ -116,10 +118,7 @@ def get_column_def(self): """ Returns a column definition for CQL table definition """ - dterms = [self.db_field_name, self.db_type] - #if self.primary_key: - #dterms.append('PRIMARY KEY') - return ' '.join(dterms) + return '{} {}'.format(self.db_field_name, self.db_type) def set_column_name(self, name): """ @@ -214,14 +213,20 @@ def to_database(self, value): class Decimal(BaseColumn): db_type = 'decimal' - #TODO: this + #TODO: decimal field def __init__(self, **kwargs): super(DateTime, self).__init__(**kwargs) raise NotImplementedError class Counter(BaseColumn): + #TODO: counter field def __init__(self, **kwargs): super(Counter, self).__init__(**kwargs) raise NotImplementedError -#TODO: Foreign key fields +class ForeignKey(BaseColumn): + #TODO: Foreign key field + def __init__(self, **kwargs): + super(ForeignKey, self).__init__(**kwargs) + raise NotImplementedError + diff --git a/cqlengine/query.py b/cqlengine/query.py index 8ee3d6653d..c9195c9465 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -17,8 +17,7 @@ class QueryOperator(object): # ie: colname__ symbol = None - # The comparator symbol this operator - # uses in cql + # The comparator symbol this operator uses in cql cql_symbol = None def __init__(self, column, value): @@ -372,6 +371,8 @@ def save(self, instance): cur = conn.cursor() cur.execute(qs, field_values) + #TODO: delete deleted / nulled fields + def create(self, **kwargs): return self.model(**kwargs).save() @@ -379,8 +380,6 @@ def create(self, **kwargs): def delete(self, columns=[]): """ Deletes the contents of a query - - :returns: number of rows deleted """ qs = ['DELETE FROM {}'.format(self.column_family_name)] if self._where: diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index f02a61ec43..cf43f96dc0 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -11,8 +11,6 @@ class TestModel(Model): count = columns.Integer() text = columns.Text() -#class TestModel2(Model): - class TestModelIO(BaseCassEngTestCase): @classmethod From 394266ab983e53c7ce579072fa033ac2e95c97e5 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 24 Nov 2012 10:28:52 -0800 Subject: [PATCH 0034/3961] adding support for order_by --- cqlengine/query.py | 81 ++++++++++++++++++- .../tests/model/test_class_construction.py | 5 ++ cqlengine/tests/query/test_queryset.py | 28 +++++++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index c9195c9465..46891787a5 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -119,7 +119,6 @@ class LessThanOrEqualOperator(QueryOperator): class QuerySet(object): #TODO: support specifying offset and limit (use slice) (maybe return a mutated queryset) #TODO: support specifying columns to exclude or select only - #TODO: support ORDER BY #TODO: cache results in this instance, but don't copy them on deepcopy def __init__(self, model): @@ -131,7 +130,7 @@ def __init__(self, model): self._where = [] #ordering arguments - self._order = [] + self._order = None #subset selection self._limit = None @@ -215,7 +214,9 @@ def _select_query(self, count=False): qs += ['WHERE {}'.format(self._where_clause())] if not count: - #TODO: add support for limit, start, order by, and reverse + if self._order: + qs += ['ORDER BY {}'.format(self._order)] + #TODO: add support for limit, start, and reverse pass return ' '.join(qs) @@ -231,6 +232,14 @@ def __iter__(self): self._rowcount = self._cursor.rowcount return self + def __getitem__(self, s): + + if isinstance(s, slice): + pass + else: + s = long(s) + pass + def _construct_instance(self, values): #translate column names to model names field_dict = {} @@ -300,6 +309,39 @@ def filter(self, **kwargs): return clone + def order_by(self, colname): + """ + orders the result set. + ordering can only select one column, and it must be the second column in a composite primary key + + Default order is ascending, prepend a '-' to the column name for descending + """ + if colname is None: + clone = copy.deepcopy(self) + clone._order = None + return clone + + order_type = 'DESC' if colname.startswith('-') else 'ASC' + colname = colname.replace('-', '') + + column = self.model._columns.get(colname) + if column is None: + raise QueryException("Can't resolve the column name: '{}'".format(colname)) + + #validate the column selection + if not column.primary_key: + raise QueryException( + "Can't order on '{}', can only order on (clustered) primary keys".format(colname)) + + pks = [v for k,v in self.model._columns.items() if v.primary_key] + if column == pks[0]: + raise QueryException( + "Can't order by the first primary key, clustering (secondary) keys only") + + clone = copy.deepcopy(self) + clone._order = '{} {}'.format(column.db_field_name, order_type) + return clone + def count(self): """ Returns the number of rows matched by this query """ #TODO: check for previous query execution and return row count if it exists @@ -311,6 +353,39 @@ def count(self): def __len__(self): return self.count() +# def set_limits(self, start, limit): +# """ Sets the start and limit parameters """ +# #validate +# if not (start is None or isinstance(start, (int, long))): +# raise QueryException("'start' must be None, or an int or long") +# if not (limit is None or isinstance(limit, (int, long))): +# raise QueryException("'limit' must be None, or an int or long") +# +# clone = copy.deepcopy(self) +# clone._start = start +# clone._limit = limit +# return clone +# +# def start(self, v): +# """ Sets the limit """ +# if not (v is None or isinstance(v, (int, long))): +# raise TypeError +# if v == self._start: +# return self +# clone = copy.deepcopy(self) +# clone._start = v +# return clone +# +# def limit(self, v): +# """ Sets the limit """ +# if not (v is None or isinstance(v, (int, long))): +# raise TypeError +# if v == self._limit: +# return self +# clone = copy.deepcopy(self) +# clone._limit = v +# return clone + def _only_or_defer(self, action, fields): clone = copy.deepcopy(self) if clone._defer_fields or clone._only_fields: diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index d2d88a0d98..fd218e1a5c 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -84,6 +84,11 @@ def test_normal_fields_can_be_defined_between_primary_keys(self): Tests tha non primary key fields can be defined between primary key fields """ + def test_at_least_one_non_primary_key_column_is_required(self): + """ + Tests that an error is raised if a model doesn't contain at least one primary key field + """ + def test_model_keyspace_attribute_must_be_a_string(self): """ Tests that users can't set the keyspace to None, or something else diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 8d91d2da60..256e681697 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -220,7 +220,35 @@ def test_multiple_iterations_work_properly(self): compare_set.remove(val) assert len(compare_set) == 0 +class TestQuerySetOrdering(BaseQuerySetUsage): + def test_order_by_success_case(self): + + q = TestModel.objects(test_id=0).order_by('attempt_id') + expected_order = [0,1,2,3] + for model, expect in zip(q,expected_order): + assert model.attempt_id == expect + + q = q.order_by('-attempt_id') + expected_order.reverse() + for model, expect in zip(q,expected_order): + assert model.attempt_id == expect + + def test_ordering_by_non_second_primary_keys_fail(self): + + with self.assertRaises(query.QueryException): + q = TestModel.objects(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') + + def test_ordering_on_indexed_columns_fails(self): + with self.assertRaises(query.QueryException): + q = IndexedTestModel.objects(test_id=0).order_by('attempt_id') + +class TestQuerySetSlicing(BaseQuerySetUsage): + pass class TestQuerySetValidation(BaseQuerySetUsage): From 301aa5c1dfb22a4009a7fa7e1a48e4d7e59d69dd Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 24 Nov 2012 16:58:03 -0800 Subject: [PATCH 0035/3961] adding support for select LIMIT --- cqlengine/query.py | 108 ++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 46891787a5..7efd1929a9 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -117,7 +117,7 @@ class LessThanOrEqualOperator(QueryOperator): cql_symbol = '<=' class QuerySet(object): - #TODO: support specifying offset and limit (use slice) (maybe return a mutated queryset) + #TODO: delete empty columns on save #TODO: support specifying columns to exclude or select only #TODO: cache results in this instance, but don't copy them on deepcopy @@ -132,9 +132,9 @@ def __init__(self, model): #ordering arguments self._order = None - #subset selection - self._limit = None - self._start = None + #CQL has a default limit of 10000, it's defined here + #because explicit is better than implicit + self._limit = 10000 #see the defer and only methods self._defer_fields = [] @@ -190,34 +190,28 @@ def _where_values(self): values.update(where.get_dict()) return values - def _select_query(self, count=False): + def _select_query(self): """ Returns a select clause based on the given filter args - - :param count: indicates this should return a count query only """ - qs = [] - if count: - qs += ['SELECT COUNT(*)'] - else: - fields = self.model._columns.keys() - if self._defer_fields: - fields = [f for f in fields if f not in self._defer_fields] - elif self._only_fields: - fields = [f for f in fields if f in self._only_fields] - db_fields = [self.model._columns[f].db_field_name for f in fields] - qs += ['SELECT {}'.format(', '.join(db_fields))] - + fields = self.model._columns.keys() + if self._defer_fields: + fields = [f for f in fields if f not in self._defer_fields] + elif self._only_fields: + fields = [f for f in fields if f in self._only_fields] + db_fields = [self.model._columns[f].db_field_name for f in fields] + + qs = ['SELECT {}'.format(', '.join(db_fields))] qs += ['FROM {}'.format(self.column_family_name)] if self._where: qs += ['WHERE {}'.format(self._where_clause())] - if not count: - if self._order: - qs += ['ORDER BY {}'.format(self._order)] - #TODO: add support for limit, start, and reverse - pass + if self._order: + qs += ['ORDER BY {}'.format(self._order)] + + if self._limit: + qs += ['LIMIT {}'.format(self._limit)] return ' '.join(qs) @@ -235,10 +229,15 @@ def __iter__(self): def __getitem__(self, s): if isinstance(s, slice): - pass + #return a new query with limit defined + #start and step are not supported + if s.start: raise QueryException('CQL does not support START') + if s.step: raise QueryException('step is not supported') + return self.limit(s.stop) else: + #return the object at this index s = long(s) - pass + raise NotImplementedError def _construct_instance(self, values): #translate column names to model names @@ -345,46 +344,33 @@ def order_by(self, colname): def count(self): """ Returns the number of rows matched by this query """ #TODO: check for previous query execution and return row count if it exists + qs = ['SELECT COUNT(*)'] + qs += ['FROM {}'.format(self.column_family_name)] + if self._where: + qs += ['WHERE {}'.format(self._where_clause())] + qs = ' '.join(qs) + con = get_connection(self.model.keyspace) cur = con.cursor() - cur.execute(self._select_query(count=True), self._where_values()) + cur.execute(qs, self._where_values()) return cur.fetchone()[0] - def __len__(self): - return self.count() + def limit(self, v): + """ + Sets the limit on the number of results returned + CQL has a default limit of 10,000 + """ + if not (v is None or isinstance(v, (int, long))): + raise TypeError + if v == self._limit: + return self + + if v < 0: + raise QueryException("Negative limit is not allowed") -# def set_limits(self, start, limit): -# """ Sets the start and limit parameters """ -# #validate -# if not (start is None or isinstance(start, (int, long))): -# raise QueryException("'start' must be None, or an int or long") -# if not (limit is None or isinstance(limit, (int, long))): -# raise QueryException("'limit' must be None, or an int or long") -# -# clone = copy.deepcopy(self) -# clone._start = start -# clone._limit = limit -# return clone -# -# def start(self, v): -# """ Sets the limit """ -# if not (v is None or isinstance(v, (int, long))): -# raise TypeError -# if v == self._start: -# return self -# clone = copy.deepcopy(self) -# clone._start = v -# return clone -# -# def limit(self, v): -# """ Sets the limit """ -# if not (v is None or isinstance(v, (int, long))): -# raise TypeError -# if v == self._limit: -# return self -# clone = copy.deepcopy(self) -# clone._limit = v -# return clone + clone = copy.deepcopy(self) + clone._limit = v + return clone def _only_or_defer(self, action, fields): clone = copy.deepcopy(self) From 2cbf582b0dfc3d05c8494506c54386efd68f1832 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 24 Nov 2012 17:33:14 -0800 Subject: [PATCH 0036/3961] adding column delete on save --- cqlengine/columns.py | 1 + cqlengine/models.py | 18 +++++++++++------- cqlengine/query.py | 20 +++++++++++++++++++- cqlengine/tests/model/test_model_io.py | 7 +------ cqlengine/tests/query/test_queryset.py | 10 ---------- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index a1ba0a3c57..9236d44e2e 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -12,6 +12,7 @@ def __init__(self, instance, column, value): self.initial_value = value self.value = value + @property def deleted(self): return self.value is None and self.initial_value is not None diff --git a/cqlengine/models.py b/cqlengine/models.py index 9111b7596c..3893bb9e0a 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -24,7 +24,7 @@ def __init__(self, **values): value_mngr = column.value_manager(self, column, values.get(name, None)) self._values[name] = value_mngr - #TODO: note any deferred or only fields so they're not deleted + #TODO: note any absent fields so they're not deleted @classmethod def column_family_name(cls): @@ -75,13 +75,16 @@ class ModelMetaClass(type): def __new__(cls, name, bases, attrs): """ """ - #move column definitions into _columns dict + #move column definitions into columns dict #and set default column names - _columns = OrderedDict() + columns = OrderedDict() + primary_keys = OrderedDict() pk_name = None def _transform_column(col_name, col_obj): - _columns[col_name] = col_obj + columns[col_name] = col_obj + if col_obj.primary_key: + primary_keys[col_name] = col_obj col_obj.set_column_name(col_name) #set properties _get = lambda self: self._values[col_name].getval() @@ -112,7 +115,7 @@ def _transform_column(col_name, col_obj): #check for duplicate column names col_names = set() - for k,v in _columns.items(): + for v in columns.values(): if v.db_field_name in col_names: raise ModelException("{} defines the column {} more than once".format(name, v.db_field_name)) col_names.add(v.db_field_name) @@ -128,11 +131,12 @@ def _transform_column(col_name, col_obj): #create db_name -> model name map for loading db_map = {} - for field_name, col in _columns.items(): + for field_name, col in columns.items(): db_map[col.db_field_name] = field_name #add management members to the class - attrs['_columns'] = _columns + attrs['_columns'] = columns + attrs['_primary_keys'] = primary_keys attrs['_db_map'] = db_map attrs['_pk_name'] = pk_name attrs['_dynamic_columns'] = {} diff --git a/cqlengine/query.py b/cqlengine/query.py index 7efd1929a9..e6cccc9fc7 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -432,7 +432,25 @@ def save(self, instance): cur = conn.cursor() cur.execute(qs, field_values) - #TODO: delete deleted / nulled fields + #TODO: delete deleted / nulled columns + deleted = [k for k,v in instance._values.items() if v.deleted] + if deleted: + import ipdb; ipdb.set_trace() + del_fields = [self.model._columns[f] for f in deleted] + del_fields = [f.db_field_name for f in del_fields if not f.primary_key] + pks = self.model._primary_keys + qs = ['DELETE {}'.format(', '.join(del_fields))] + qs += ['FROM {}'.format(self.column_family_name)] + qs += ['WHERE'] + eq = lambda col: '{0} = :{0}'.format(v.db_field_name) + qs += [' AND '.join([eq(f) for f in pks.values()])] + qs = ' '.join(qs) + + pk_dict = dict([(v.db_field_name, getattr(instance, k)) for k,v in pks.items()]) + cur.execute(qs, pk_dict) + + + def create(self, **kwargs): return self.model(**kwargs).save() diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index cf43f96dc0..09a7280979 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -10,7 +10,7 @@ class TestModel(Model): count = columns.Integer() text = columns.Text() - + class TestModelIO(BaseCassEngTestCase): @classmethod @@ -54,11 +54,6 @@ def test_model_deleting_works_properly(self): tm2 = TestModel.objects(id=tm.pk).first() self.assertIsNone(tm2) - def test_nullable_columns_are_saved_properly(self): - """ - Tests that nullable columns save without any trouble - """ - def test_column_deleting_works_properly(self): """ """ diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 256e681697..b3c1449ad8 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -78,16 +78,6 @@ def test_queryset_is_immutable(self): query2 = query1.filter(expected_result__gte=1) assert len(query2._where) == 2 - 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 From 67dd9151a9abbde3aca24390eb5fceee2bf5b89e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 24 Nov 2012 17:37:57 -0800 Subject: [PATCH 0037/3961] fixing name conflict --- cqlengine/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 3893bb9e0a..f51c5dd27e 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -77,12 +77,12 @@ def __new__(cls, name, bases, attrs): """ #move column definitions into columns dict #and set default column names - columns = OrderedDict() + column_dict = OrderedDict() primary_keys = OrderedDict() pk_name = None def _transform_column(col_name, col_obj): - columns[col_name] = col_obj + column_dict[col_name] = col_obj if col_obj.primary_key: primary_keys[col_name] = col_obj col_obj.set_column_name(col_name) @@ -115,7 +115,7 @@ def _transform_column(col_name, col_obj): #check for duplicate column names col_names = set() - for v in columns.values(): + for v in column_dict.values(): if v.db_field_name in col_names: raise ModelException("{} defines the column {} more than once".format(name, v.db_field_name)) col_names.add(v.db_field_name) @@ -131,11 +131,11 @@ def _transform_column(col_name, col_obj): #create db_name -> model name map for loading db_map = {} - for field_name, col in columns.items(): + for field_name, col in column_dict.items(): db_map[col.db_field_name] = field_name #add management members to the class - attrs['_columns'] = columns + attrs['_columns'] = column_dict attrs['_primary_keys'] = primary_keys attrs['_db_map'] = db_map attrs['_pk_name'] = pk_name From 405a0240ad651b3d5967028f27073a0b9fa8f6b2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 24 Nov 2012 17:38:19 -0800 Subject: [PATCH 0038/3961] refactoring column null arg to required --- cqlengine/columns.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 9236d44e2e..b62666001f 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -43,22 +43,20 @@ class BaseColumn(object): instance_counter = 0 - def __init__(self, primary_key=False, index=False, db_field=None, default=None, null=False): + def __init__(self, primary_key=False, index=False, db_field=None, default=None, required=True): """ :param primary_key: bool flag, indicates this column is a primary key. The first primary key defined on a model is the partition key, all others are cluster keys :param index: bool flag, indicates an index should be created for this column :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: boolean, is the field nullable? + :param required: boolean, is the field required? """ self.primary_key = primary_key self.index = index self.db_field = db_field self.default = default - - #TODO: make this required, since fields won't actually be nulled, but deleted - self.null = null + self.required = required #the column name in the model definition self.column_name = None @@ -77,8 +75,8 @@ def validate(self, value): if value is None: if self.has_default: return self.get_default() - elif not self.null: - raise ValidationError('null values are not allowed') + elif self.required: + raise ValidationError('{} - None values are not allowed'.format(self.column_name or self.db_field)) return value def to_python(self, value): From 823e21fdee28b8b1fa284fcd475b98a48d179520 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 24 Nov 2012 17:48:40 -0800 Subject: [PATCH 0039/3961] testing and debugging column deleting --- cqlengine/query.py | 7 ++++--- cqlengine/tests/model/test_model_io.py | 10 +++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index e6cccc9fc7..a62b172d18 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -417,7 +417,9 @@ def save(self, instance): #get defined fields and their column names for name, col in self.model._columns.items(): - value_pairs += [(col.db_field_name, values.get(name))] + val = values.get(name) + if val is None: continue + value_pairs += [(col.db_field_name, val)] #construct query string field_names = zip(*value_pairs)[0] @@ -435,14 +437,13 @@ def save(self, instance): #TODO: delete deleted / nulled columns deleted = [k for k,v in instance._values.items() if v.deleted] if deleted: - import ipdb; ipdb.set_trace() del_fields = [self.model._columns[f] for f in deleted] del_fields = [f.db_field_name for f in del_fields if not f.primary_key] pks = self.model._primary_keys qs = ['DELETE {}'.format(', '.join(del_fields))] qs += ['FROM {}'.format(self.column_family_name)] qs += ['WHERE'] - eq = lambda col: '{0} = :{0}'.format(v.db_field_name) + eq = lambda col: '{0} = :{0}'.format(v.column.db_field_name) qs += [' AND '.join([eq(f) for f in pks.values()])] qs = ' '.join(qs) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 09a7280979..f408e46ce9 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -9,7 +9,7 @@ class TestModel(Model): count = columns.Integer() - text = columns.Text() + text = columns.Text(required=False) class TestModelIO(BaseCassEngTestCase): @@ -57,4 +57,12 @@ def test_model_deleting_works_properly(self): def test_column_deleting_works_properly(self): """ """ + tm = TestModel.objects.create(count=8, text='123456789') + tm.text = None + tm.save() + + tm2 = TestModel.objects(id=tm.pk).first() + assert tm2.text is None + assert tm2._values['text'].initial_value is None + From 2db6426d0278b24b751b9271b2f92eb4199de383 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 24 Nov 2012 23:11:09 -0800 Subject: [PATCH 0040/3961] added fault tolerant connection manager, which will be easily extended to support connection pooling added support for multiple connections removed dependence on setting keyspace name on opening connection --- cqlengine/connection.py | 103 +++++++++++++++++- cqlengine/management.py | 92 ++++++++-------- cqlengine/models.py | 8 +- cqlengine/query.py | 33 +++--- cqlengine/tests/base.py | 7 ++ cqlengine/tests/management/__init__.py | 0 cqlengine/tests/management/test_management.py | 0 7 files changed, 169 insertions(+), 74 deletions(-) create mode 100644 cqlengine/tests/management/__init__.py create mode 100644 cqlengine/tests/management/test_management.py diff --git a/cqlengine/connection.py b/cqlengine/connection.py index dc1a7467fb..6cf5c1ff4a 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -2,17 +2,67 @@ #http://code.google.com/a/apache-extras.org/p/cassandra-dbapi2 / #http://cassandra.apache.org/doc/cql/CQL.html +from collections import namedtuple +import random + import cql + from cqlengine.exceptions import CQLEngineException +from thrift.transport.TTransport import TTransportException + + class CQLConnectionError(CQLEngineException): pass -_keyspace = 'cassengine_test' +Host = namedtuple('Host', ['name', 'port']) +_hosts = [] +_host_idx = 0 +_conn= None +_username = None +_password = None + +def _set_conn(host): + """ + """ + global _conn + _conn = cql.connect(host.name, host.port, user=_username, password=_password) + _conn.set_cql_version('3.0.0') + +def setup(hosts, username=None, password=None): + """ + Records the hosts and connects to one of them + + :param hosts: list of hosts, strings in the :, or just + """ + global _hosts + global _username + global _password + + _username = username + _password = password + + for host in hosts: + host = host.strip() + host = host.split(':') + if len(host) == 1: + _hosts.append(Host(host[0], 9160)) + elif len(host) == 2: + _hosts.append(Host(*host)) + else: + raise CQLConnectionError("Can't parse {}".format(''.join(host))) + + if not _hosts: + raise CQLConnectionError("At least one host required") + + random.shuffle(_hosts) + host = _hosts[_host_idx] + _set_conn(host) + #TODO: look into the cql connection pool class -_conn = {} -def get_connection(keyspace, create_missing_keyspace=True): - con = _conn.get(keyspace) +_old_conn = {} +def get_connection(keyspace=None, create_missing_keyspace=True): + con = _old_conn.get(keyspace) if con is None: con = cql.connect('127.0.0.1', 9160) con.set_cql_version('3.0.0') @@ -27,7 +77,50 @@ def get_connection(keyspace, create_missing_keyspace=True): else: raise CQLConnectionError('"{}" is not an existing keyspace'.format(keyspace)) - _conn[keyspace] = con + _old_conn[keyspace] = con return con +class connection_manager(object): + """ + Connection failure tolerant connection manager. Written to be used in a 'with' block for connection pooling + """ + def __init__(self): + if not _hosts: + raise CQLConnectionError("No connections have been configured, call cqlengine.connection.setup") + self.keyspace = None + self.con = _conn + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + pass + + def execute(self, query, params={}): + """ + Gets a connection from the pool and executes the given query, returns the cursor + + if there's a connection problem, this will silently create a new connection pool + from the available hosts, and remove the problematic host from the host list + """ + global _host_idx + + for i in range(len(_hosts)): + try: + cur = self.con.cursor() + cur.execute(query, params) + return cur + except TTransportException: + #TODO: check for other errors raised in the event of a connection / server problem + #move to the next connection and set the connection pool + self.con_pool.return_connection(self.con) + self.con = None + _host_idx += 1 + _host_idx %= len(_hosts) + host = _hosts[_host_idx] + _set_conn(host) + self.con = _conn + + raise CQLConnectionError("couldn't reach a Cassandra server") + diff --git a/cqlengine/management.py b/cqlengine/management.py index 5b2ad6aa42..ac967059b1 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,64 +1,62 @@ from cqlengine.connection import get_connection +from cqlengine.connection import connection_manager def create_keyspace(name): - con = get_connection(None) - cur = con.cursor() - cur.execute("""CREATE KEYSPACE {} - WITH strategy_class = 'SimpleStrategy' - AND strategy_options:replication_factor=1;""".format(name)) + with connection_manager() as con: + con.execute("""CREATE KEYSPACE {} + WITH strategy_class = 'SimpleStrategy' + AND strategy_options:replication_factor=1;""".format(name)) def delete_keyspace(name): - con = get_connection(None) - cur = con.cursor() - cur.execute("DROP KEYSPACE {}".format(name)) + with connection_manager() as con: + con.execute("DROP KEYSPACE {}".format(name)) def create_column_family(model): #construct query string cf_name = model.column_family_name() + raw_cf_name = model.column_family_name(include_keyspace=False) + + with connection_manager() as con: + #check for an existing column family + ks_info = con.con.client.describe_keyspace(model.keyspace) + if not any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): + qs = ['CREATE TABLE {}'.format(cf_name)] + + #add column types + pkeys = [] + qtypes = [] + def add_column(col): + s = '{} {}'.format(col.db_field_name, col.db_type) + if col.primary_key: pkeys.append(col.db_field_name) + qtypes.append(s) + for name, col in model._columns.items(): + add_column(col) + + qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) + + qs += ['({})'.format(', '.join(qtypes))] + qs = ' '.join(qs) - conn = get_connection(model.keyspace) - cur = conn.cursor() - - #check for an existing column family - ks_info = conn.client.describe_keyspace(model.keyspace) - if not any([cf_name == cf.name for cf in ks_info.cf_defs]): - qs = ['CREATE TABLE {}'.format(cf_name)] - - #add column types - pkeys = [] - qtypes = [] - def add_column(col): - s = '{} {}'.format(col.db_field_name, col.db_type) - if col.primary_key: pkeys.append(col.db_field_name) - qtypes.append(s) - for name, col in model._columns.items(): - add_column(col) - - qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) - - qs += ['({})'.format(', '.join(qtypes))] - qs = ' '.join(qs) - - cur.execute(qs) + con.execute(qs) - indexes = [c for n,c in model._columns.items() if c.index] - if indexes: - for column in indexes: - #TODO: check for existing index... - #can that be determined from the connection client? - qs = ['CREATE INDEX {}'.format(column.db_index_name)] - qs += ['ON {}'.format(cf_name)] - qs += ['({})'.format(column.db_field_name)] - qs = ' '.join(qs) - cur.execute(qs) + indexes = [c for n,c in model._columns.items() if c.index] + if indexes: + for column in indexes: + #TODO: check for existing index... + #can that be determined from the connection client? + qs = ['CREATE INDEX {}'.format(column.db_index_name)] + qs += ['ON {}'.format(cf_name)] + qs += ['({})'.format(column.db_field_name)] + qs = ' '.join(qs) + con.execute(qs) def delete_column_family(model): #check that model exists cf_name = model.column_family_name() - conn = get_connection(model.keyspace) - ks_info = conn.client.describe_keyspace(model.keyspace) - if any([cf_name == cf.name for cf in ks_info.cf_defs]): - cur = conn.cursor() - cur.execute('drop table {};'.format(cf_name)) + raw_cf_name = model.column_family_name(include_keyspace=False) + with connection_manager() as con: + ks_info = con.con.client.describe_keyspace(model.keyspace) + if any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): + con.execute('drop table {};'.format(cf_name)) diff --git a/cqlengine/models.py b/cqlengine/models.py index f51c5dd27e..b17df09fb3 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -24,10 +24,8 @@ def __init__(self, **values): value_mngr = column.value_manager(self, column, values.get(name, None)) self._values[name] = value_mngr - #TODO: note any absent fields so they're not deleted - @classmethod - def column_family_name(cls): + def column_family_name(cls, include_keyspace=True): """ Returns the column family name if it's been defined otherwise, it creates it from the module and class name @@ -38,7 +36,9 @@ def column_family_name(cls): cf_name = cf_name.replace('.', '_') #trim to less than 48 characters or cassandra will complain cf_name = cf_name[-48:] - return cf_name.lower() + cf_name = cf_name.lower() + if not include_keyspace: return cf_name + return '{}.{}'.format(cls.keyspace, cf_name) @property def pk(self): diff --git a/cqlengine/query.py b/cqlengine/query.py index a62b172d18..bf5c1464d8 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,6 +4,7 @@ from time import time from cqlengine.connection import get_connection +from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException #CQL 3 reference: @@ -220,7 +221,7 @@ def _select_query(self): def __iter__(self): #TODO: cache results if self._cursor is None: - conn = get_connection(self.model.keyspace) + conn = get_connection() self._cursor = conn.cursor() self._cursor.execute(self._select_query(), self._where_values()) self._rowcount = self._cursor.rowcount @@ -350,10 +351,9 @@ def count(self): qs += ['WHERE {}'.format(self._where_clause())] qs = ' '.join(qs) - con = get_connection(self.model.keyspace) - cur = con.cursor() - cur.execute(qs, self._where_values()) - return cur.fetchone()[0] + with connection_manager() as con: + cur = con.execute(qs, self._where_values()) + return cur.fetchone()[0] def limit(self, v): """ @@ -430,11 +430,10 @@ def save(self, instance): qs += ["({})".format(', '.join([':'+f for f in field_names]))] qs = ' '.join(qs) - conn = get_connection(self.model.keyspace) - cur = conn.cursor() - cur.execute(qs, field_values) + with connection_manager() as con: + con.execute(qs, field_values) - #TODO: delete deleted / nulled columns + #delete deleted / nulled columns deleted = [k for k,v in instance._values.items() if v.deleted] if deleted: del_fields = [self.model._columns[f] for f in deleted] @@ -448,10 +447,10 @@ def save(self, instance): qs = ' '.join(qs) pk_dict = dict([(v.db_field_name, getattr(instance, k)) for k,v in pks.items()]) - cur.execute(qs, pk_dict) - - + with connection_manager() as con: + con.execute(qs, pk_dict) + def create(self, **kwargs): return self.model(**kwargs).save() @@ -466,9 +465,8 @@ def delete(self, columns=[]): qs += ['WHERE {}'.format(self._where_clause())] qs = ' '.join(qs) - con = get_connection(self.model.keyspace) - cur = con.cursor() - cur.execute(qs, self._where_values()) + with connection_manager() as con: + con.execute(qs, self._where_values()) def delete_instance(self, instance): @@ -478,8 +476,7 @@ def delete_instance(self, instance): qs += ['WHERE {0}=:{0}'.format(pk_name)] qs = ' '.join(qs) - conn = get_connection(self.model.keyspace) - cur = conn.cursor() - cur.execute(qs, {pk_name:instance.pk}) + with connection_manager() as con: + con.execute(qs, {pk_name:instance.pk}) diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index ac1f31d8f1..d94ea69b04 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -1,7 +1,14 @@ from unittest import TestCase +from cqlengine import connection class BaseCassEngTestCase(TestCase): + @classmethod + def setUpClass(cls): + super(BaseCassEngTestCase, cls).setUpClass() + if not connection._hosts: + connection.setup(['localhost']) + def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), "{} doesn't have attribute: {}".format(obj, attr)) diff --git a/cqlengine/tests/management/__init__.py b/cqlengine/tests/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py new file mode 100644 index 0000000000..e69de29bb2 From 6816148ef5e1c9d3a173bfdc52043f94f338e827 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 10:47:32 -0800 Subject: [PATCH 0041/3961] removing and get_connection and editing files depending on it --- cqlengine/connection.py | 22 ---------------------- cqlengine/management.py | 1 - cqlengine/query.py | 7 +++---- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 6cf5c1ff4a..84d6d1eb5b 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -59,28 +59,6 @@ def setup(hosts, username=None, password=None): _set_conn(host) -#TODO: look into the cql connection pool class -_old_conn = {} -def get_connection(keyspace=None, create_missing_keyspace=True): - con = _old_conn.get(keyspace) - if con is None: - con = cql.connect('127.0.0.1', 9160) - con.set_cql_version('3.0.0') - - if keyspace: - try: - con.set_initial_keyspace(keyspace) - except cql.ProgrammingError, e: - if create_missing_keyspace: - from cqlengine.management import create_keyspace - create_keyspace(keyspace) - else: - raise CQLConnectionError('"{}" is not an existing keyspace'.format(keyspace)) - - _old_conn[keyspace] = con - - return con - class connection_manager(object): """ Connection failure tolerant connection manager. Written to be used in a 'with' block for connection pooling diff --git a/cqlengine/management.py b/cqlengine/management.py index ac967059b1..97c565c12b 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,4 +1,3 @@ -from cqlengine.connection import get_connection from cqlengine.connection import connection_manager def create_keyspace(name): diff --git a/cqlengine/query.py b/cqlengine/query.py index bf5c1464d8..921e8d19af 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -3,7 +3,6 @@ from hashlib import md5 from time import time -from cqlengine.connection import get_connection from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException @@ -221,9 +220,9 @@ def _select_query(self): def __iter__(self): #TODO: cache results if self._cursor is None: - conn = get_connection() - self._cursor = conn.cursor() - self._cursor.execute(self._select_query(), self._where_values()) + #TODO: the query and caching should happen in the same function + with connection_manager() as con: + self._cursor = con.execute(self._select_query(), self._where_values()) self._rowcount = self._cursor.rowcount return self From 3038afbfc17a02d3e105222689f6ddc2bcacc9e6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 11:00:42 -0800 Subject: [PATCH 0042/3961] adding BSD3 license and authors file --- AUTHORS | 8 ++++++++ LICENSE | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 AUTHORS create mode 100644 LICENSE diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000000..3a69691a3e --- /dev/null +++ b/AUTHORS @@ -0,0 +1,8 @@ +PRIMARY AUTHORS + +Blake Eggleston + +CONTRIBUTORS + +Eric Scrivner + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..3833bbf351 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2012, Blake Eggleston and AUTHORS +All rights reserved. + +* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of the cqlengine nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From f0686a50af396454ab06bf4b9a86091119111282 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 11:49:33 -0800 Subject: [PATCH 0043/3961] updating readme --- README.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cc2d2fc976..7405b89433 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,73 @@ -cassandraengine +cqlengine =============== -Cassandra ORM for Python in the style of the Django orm and mongoengine +cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine -In it's current state you can define column families, create and delete column families -based on your model definiteions, save models and retrieve models by their primary keys. +[Documentation](https://github.com/bdeggleston/cqlengine/wiki/cqlengine-documentation) -That's about it. Also, the CQL stuff is very basic at this point. +[Report a Bug](https://github.com/bdeggleston/cqlengine/issues) -##TODO -* column ttl? -* ForeignKey/DBRef fields? -* query functionality -* Match column names to mongoengine field names? -* nice column and model class __repr__ +[Users Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users) +[Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) +## Installation +TODO + +## Getting Started + +```python +#first, define a model +>>> from cqlengine import columns +>>> from cqlengine.models import Model + +>>> class ExampleModel(Model): +>>> example_id = columns.UUID(primary_key=True) +>>> example_type = columns.Integer(index=True) +>>> created_at = columns.DateTime() +>>> description = columns.Text(required=False) + +#next, setup the connection to your cassandra server(s)... +>>> from cqlengine import connection +>>> connection.setup(['127.0.0.1:9160']) + +#...and create your CQL table +>>> from cqlengine.management import create_column_family +>>> create_column_family(ExampleModel) + +#now we can create some rows: +>>> em1 = ExampleModel.objects.create(example_type=0, description="example1") +>>> em2 = ExampleModel.objects.create(example_type=0, description="example2") +>>> em3 = ExampleModel.objects.create(example_type=0, description="example3") +>>> em4 = ExampleModel.objects.create(example_type=0, description="example4") +>>> em5 = ExampleModel.objects.create(example_type=1, description="example5") +>>> em6 = ExampleModel.objects.create(example_type=1, description="example6") +>>> em7 = ExampleModel.objects.create(example_type=1, description="example7") +>>> em8 = ExampleModel.objects.create(example_type=1, description="example8") +# Note: the UUID and DateTime columns will create uuid4 and datetime.now +# values automatically if we don't specify them when creating new rows + +#and now we can run some queries against our table +>>> ExampleModel.objects.count() +8 +>>> q = ExampleModel.objects(example_type=1) +>>> q.count() +4 +>>> for instance in q: +>>> print q.description +example5 +example6 +example7 +example8 + +#here we are applying additional filtering to an existing query +#query objects are immutable, so calling filter returns a new +#query object +>>> q2 = q.filter(example_id=em5.example_id) + +>>> q2.count() +1 +>>> for instance in q2: +>>> print q.description +example5 +``` From 1038b6b13251b8784ddc302c524e5d6ddc0cf5ff Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 11:54:49 -0800 Subject: [PATCH 0044/3961] added automatic keyspace generation to create_column_family added checks for existing keyspaces in create_keyspace and delete_keyspace --- cqlengine/management.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 97c565c12b..de0e93a45c 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -2,19 +2,25 @@ def create_keyspace(name): with connection_manager() as con: - con.execute("""CREATE KEYSPACE {} - WITH strategy_class = 'SimpleStrategy' - AND strategy_options:replication_factor=1;""".format(name)) + if name not in [k.name for k in con.con.client.describe_keyspaces()]: + con.execute("""CREATE KEYSPACE {} + WITH strategy_class = 'SimpleStrategy' + AND strategy_options:replication_factor=1;""".format(name)) def delete_keyspace(name): with connection_manager() as con: - con.execute("DROP KEYSPACE {}".format(name)) + if name in [k.name for k in con.con.client.describe_keyspaces()]: + con.execute("DROP KEYSPACE {}".format(name)) -def create_column_family(model): +def create_column_family(model, create_missing_keyspace=True): #construct query string cf_name = model.column_family_name() raw_cf_name = model.column_family_name(include_keyspace=False) + #create missing keyspace + if create_missing_keyspace: + create_keyspace(model.keyspace) + with connection_manager() as con: #check for an existing column family ks_info = con.con.client.describe_keyspace(model.keyspace) From 382a63875b83ec7a2ab267559e60caec42abfe55 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 12:16:33 -0800 Subject: [PATCH 0045/3961] changing docs url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7405b89433..ba909d4c8c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ cqlengine cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine -[Documentation](https://github.com/bdeggleston/cqlengine/wiki/cqlengine-documentation) +[Documentation](https://github.com/bdeggleston/cqlengine/wiki/Documentation) [Report a Bug](https://github.com/bdeggleston/cqlengine/issues) From 11e217203c3d6523a03fbdd7fdff8d04494038e6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 14:44:05 -0800 Subject: [PATCH 0046/3961] refactoring BaseColumn name --- cqlengine/columns.py | 28 ++++++++++++++-------------- cqlengine/models.py | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index b62666001f..98abb96511 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -35,7 +35,7 @@ def get_property(self): else: return property(_get, _set) -class BaseColumn(object): +class Column(object): #the cassandra type this column maps to db_type = None @@ -64,8 +64,8 @@ def __init__(self, primary_key=False, index=False, db_field=None, default=None, self.value = None #keep track of instantiation order - self.position = BaseColumn.instance_counter - BaseColumn.instance_counter += 1 + self.position = Column.instance_counter + Column.instance_counter += 1 def validate(self, value): """ @@ -136,16 +136,16 @@ def db_index_name(self): """ Returns the name of the cql index """ return 'index_{}'.format(self.db_field_name) -class Bytes(BaseColumn): +class Bytes(Column): db_type = 'blob' -class Ascii(BaseColumn): +class Ascii(Column): db_type = 'ascii' -class Text(BaseColumn): +class Text(Column): db_type = 'text' -class Integer(BaseColumn): +class Integer(Column): db_type = 'int' def validate(self, value): @@ -161,13 +161,13 @@ def to_python(self, value): def to_database(self, value): return self.validate(value) -class DateTime(BaseColumn): +class DateTime(Column): db_type = 'timestamp' def __init__(self, **kwargs): super(DateTime, self).__init__(**kwargs) raise NotImplementedError -class UUID(BaseColumn): +class UUID(Column): """ Type 1 or 4 UUID """ @@ -186,7 +186,7 @@ def validate(self, value): raise ValidationError("{} is not a valid uuid".format(value)) return _UUID(val) -class Boolean(BaseColumn): +class Boolean(Column): db_type = 'boolean' def to_python(self, value): @@ -195,7 +195,7 @@ def to_python(self, value): def to_database(self, value): return bool(value) -class Float(BaseColumn): +class Float(Column): db_type = 'double' def validate(self, value): @@ -210,20 +210,20 @@ def to_python(self, value): def to_database(self, value): return self.validate(value) -class Decimal(BaseColumn): +class Decimal(Column): db_type = 'decimal' #TODO: decimal field def __init__(self, **kwargs): super(DateTime, self).__init__(**kwargs) raise NotImplementedError -class Counter(BaseColumn): +class Counter(Column): #TODO: counter field def __init__(self, **kwargs): super(Counter, self).__init__(**kwargs) raise NotImplementedError -class ForeignKey(BaseColumn): +class ForeignKey(Column): #TODO: Foreign key field def __init__(self, **kwargs): super(ForeignKey, self).__init__(**kwargs) diff --git a/cqlengine/models.py b/cqlengine/models.py index b17df09fb3..7b5dc707de 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -103,6 +103,7 @@ def _transform_column(col_name, col_obj): k,v = 'id', columns.UUID(primary_key=True) column_definitions = [(k,v)] + column_definitions + #TODO: check that the defined columns don't conflict with any of the Model API's existing attributes/methods #transform column definitions for k,v in column_definitions: if pk_name is None and v.primary_key: From bf62e234ecd86e040eb74827d8c9d3cc5bd71f53 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 15:20:02 -0800 Subject: [PATCH 0047/3961] added double/single precision option to float field fixing some old references to BaseColumn --- cqlengine/columns.py | 4 ++++ cqlengine/models.py | 2 +- cqlengine/tests/columns/test_validation.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 98abb96511..df7a3a3480 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -198,6 +198,10 @@ def to_database(self, value): class Float(Column): db_type = 'double' + def __init__(self, double_precision=True, **kwargs): + self.db_type = 'double' if double_precision else 'float' + super(Float, self).__init__(**kwargs) + def validate(self, value): try: return float(value) diff --git a/cqlengine/models.py b/cqlengine/models.py index 7b5dc707de..d977742fbc 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -95,7 +95,7 @@ def _transform_column(col_name, col_obj): else: attrs[col_name] = property(_get, _set, _del) - column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.BaseColumn)] + column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.Column)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) #prepend primary key if none has been defined diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index f916fb0ebe..4043dfe89a 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -2,7 +2,7 @@ from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.columns import BaseColumn +from cqlengine.columns import Column from cqlengine.columns import Bytes from cqlengine.columns import Ascii from cqlengine.columns import Text From f62cc9dab6b7f8a3a5c80c324216805881094eea Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 18:28:13 -0800 Subject: [PATCH 0048/3961] adding scary alpha warning --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ba909d4c8c..a218535d91 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and m [Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) +**NOTE: cqlengine is in alpha and under development, some features may change (hopefully with notice). Make sure to check the changelog and test your app before upgrading** + ## Installation TODO From bd940eff49e1877ffbb4c9487a67d451db2c8349 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 18:28:35 -0800 Subject: [PATCH 0049/3961] adding datetime and decimal columns and tests --- cqlengine/columns.py | 17 +++++--- cqlengine/models.py | 4 +- cqlengine/query.py | 7 +-- cqlengine/tests/columns/test_validation.py | 51 ++++++++++++++++++++++ 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index df7a3a3480..617b0d4b12 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -1,4 +1,5 @@ #column field types +from datetime import datetime import re from uuid import uuid1, uuid4 @@ -165,7 +166,17 @@ class DateTime(Column): db_type = 'timestamp' def __init__(self, **kwargs): super(DateTime, self).__init__(**kwargs) - raise NotImplementedError + + def to_python(self, value): + if isinstance(value, datetime): + return value + return datetime.fromtimestamp(value) + + def to_database(self, value): + value = super(DateTime, self).to_database(value) + if not isinstance(value, datetime): + raise ValidationError("'{}' is not a datetime object".format(value)) + return value.strftime('%Y-%m-%d %H:%M:%S') class UUID(Column): """ @@ -216,10 +227,6 @@ def to_database(self, value): class Decimal(Column): db_type = 'decimal' - #TODO: decimal field - def __init__(self, **kwargs): - super(DateTime, self).__init__(**kwargs) - raise NotImplementedError class Counter(Column): #TODO: counter field diff --git a/cqlengine/models.py b/cqlengine/models.py index d977742fbc..c629d2041f 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -21,7 +21,9 @@ class BaseModel(object): def __init__(self, **values): self._values = {} for name, column in self._columns.items(): - value_mngr = column.value_manager(self, column, values.get(name, None)) + value = values.get(name, None) + if value is not None: value = column.to_python(value) + value_mngr = column.value_manager(self, column, value) self._values[name] = value_mngr @classmethod diff --git a/cqlengine/query.py b/cqlengine/query.py index 921e8d19af..bb7a5e519f 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -117,9 +117,8 @@ class LessThanOrEqualOperator(QueryOperator): cql_symbol = '<=' class QuerySet(object): - #TODO: delete empty columns on save - #TODO: support specifying columns to exclude or select only #TODO: cache results in this instance, but don't copy them on deepcopy + #TODO: support multiple iterators def __init__(self, model): super(QuerySet, self).__init__() @@ -224,7 +223,9 @@ def __iter__(self): with connection_manager() as con: self._cursor = con.execute(self._select_query(), self._where_values()) self._rowcount = self._cursor.rowcount - return self + return self + else: + raise QueryException("QuerySet only supports a single iterator at a time, though this will be fixed shortly") def __getitem__(self, s): diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 4043dfe89a..3f70ab12cd 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -1,4 +1,6 @@ #tests the behavior of the column classes +from datetime import datetime +from decimal import Decimal as D from cqlengine.tests.base import BaseCassEngTestCase @@ -13,4 +15,53 @@ from cqlengine.columns import Float from cqlengine.columns import Decimal +from cqlengine.management import create_column_family, delete_column_family +from cqlengine.models import Model + +class TestDatetime(BaseCassEngTestCase): + class DatetimeTest(Model): + test_id = Integer(primary_key=True) + created_at = DateTime() + + @classmethod + def setUpClass(cls): + super(TestDatetime, cls).setUpClass() + create_column_family(cls.DatetimeTest) + + @classmethod + def tearDownClass(cls): + super(TestDatetime, cls).tearDownClass() + delete_column_family(cls.DatetimeTest) + + def test_datetime_io(self): + now = datetime.now() + dt = self.DatetimeTest.objects.create(test_id=0, created_at=now) + dt2 = self.DatetimeTest.objects(test_id=0).first() + assert dt2.created_at.timetuple()[:6] == now.timetuple()[:6] + +class TestDecimal(BaseCassEngTestCase): + class DecimalTest(Model): + test_id = Integer(primary_key=True) + dec_val = Decimal() + + @classmethod + def setUpClass(cls): + super(TestDecimal, cls).setUpClass() + create_column_family(cls.DecimalTest) + + @classmethod + def tearDownClass(cls): + super(TestDecimal, cls).tearDownClass() + delete_column_family(cls.DecimalTest) + + def test_datetime_io(self): + dt = self.DecimalTest.objects.create(test_id=0, dec_val=D('0.00')) + dt2 = self.DecimalTest.objects(test_id=0).first() + assert dt2.dec_val == dt.dec_val + + dt = self.DecimalTest.objects.create(test_id=0, dec_val=5) + dt2 = self.DecimalTest.objects(test_id=0).first() + assert dt2.dec_val == D('5') + + From 168c1136023e81df93f7c1ea69e25ebfe24e0a61 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 18:56:38 -0800 Subject: [PATCH 0050/3961] adding setuptools stuff --- MANIFEST.in | 5 +++++ cqlengine/__init__.py | 2 ++ setup.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 MANIFEST.in create mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000..d7e1cb6199 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include AUTHORS +include LICENSE +include README.md +recursive-include cqlengine * + diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 1af8944a10..0a826654f8 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,3 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model +__version__ = '0.0.1-ALPHA' + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..afd921b17c --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +from setuptools import setup, find_packages + +version = '0.0.1-ALPHA' + +long_desc = """ +cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine + +[Documentation](https://github.com/bdeggleston/cqlengine/wiki/Documentation) + +[Report a Bug](https://github.com/bdeggleston/cqlengine/issues) + +[Users Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users) + +[Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) + +**NOTE: cqlengine is in alpha and under development, some features may change (hopefully with notice). Make sure to check the changelog and test your app before upgrading** +""" + +setup( + name='cqlengine', + version=version, + description='Cassandra CQL ORM for Python in the style of the Django orm and mongoengine', + long_description=long_desc, + classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Environment :: Plugins", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='cassandra,cql,orm', + author='Blake Eggleston', + author_email='bdeggleston@gmail.com', + url='https://github.com/bdeggleston/cqlengine', + license='BSD', + packages=find_packages(), + include_package_data=True, +) + From f6c51ae55e23a3fdaeb0d9fc9b0bca61acaaf5e2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 19:04:28 -0800 Subject: [PATCH 0051/3961] adding cql dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index afd921b17c..8fba346530 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ "Topic :: Software Development :: Libraries :: Python Modules", ], keywords='cassandra,cql,orm', + install_requires = ['cql'], author='Blake Eggleston', author_email='bdeggleston@gmail.com', url='https://github.com/bdeggleston/cqlengine', From 1aa7f4ea11a0a96da69ce5b8a7dffad7386ad794 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 21:17:14 -0800 Subject: [PATCH 0052/3961] adding github dependency link --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8fba346530..fbe3451c70 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ name='cqlengine', version=version, description='Cassandra CQL ORM for Python in the style of the Django orm and mongoengine', + dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/0.0.1-ALPHA.tar.gz#egg=cqlengine-0.0.1-ALPHA.tar.gz'], long_description=long_desc, classifiers = [ "Development Status :: 3 - Alpha", From 823ebbf8c7346b6d44908414d6e6318fa6dd7e55 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 21:49:24 -0800 Subject: [PATCH 0053/3961] removing old reference to con_pool from connection --- cqlengine/connection.py | 1 - setup.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 84d6d1eb5b..65147f6ac9 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -92,7 +92,6 @@ def execute(self, query, params={}): except TTransportException: #TODO: check for other errors raised in the event of a connection / server problem #move to the next connection and set the connection pool - self.con_pool.return_connection(self.con) self.con = None _host_idx += 1 _host_idx %= len(_hosts) diff --git a/setup.py b/setup.py index fbe3451c70..570fe6ac31 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,9 @@ from setuptools import setup, find_packages +#next time: +#python setup.py register +#python setup.py sdist upload + version = '0.0.1-ALPHA' long_desc = """ From 35b83d448577c0b90937cd1dcda994e2b3680de2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 21:56:25 -0800 Subject: [PATCH 0054/3961] updating version --- cqlengine/__init__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 0a826654f8..6822d1af02 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.1-ALPHA' +__version__ = '0.0.2-ALPHA' diff --git a/setup.py b/setup.py index 570fe6ac31..e422ca0be6 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.1-ALPHA' +version = '0.0.2-ALPHA' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine @@ -24,7 +24,7 @@ name='cqlengine', version=version, description='Cassandra CQL ORM for Python in the style of the Django orm and mongoengine', - dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/0.0.1-ALPHA.tar.gz#egg=cqlengine-0.0.1-ALPHA.tar.gz'], + dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/0.0.2-ALPHA.tar.gz#egg=cqlengine-0.0.2-ALPHA'], long_description=long_desc, classifiers = [ "Development Status :: 3 - Alpha", From 69546acca86118e848bd72b7bddc74cd67595326 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 25 Nov 2012 22:02:01 -0800 Subject: [PATCH 0055/3961] adding pip instructions --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a218535d91..72780d257d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and m **NOTE: cqlengine is in alpha and under development, some features may change (hopefully with notice). Make sure to check the changelog and test your app before upgrading** ## Installation -TODO +``` +pip install cqlengine +``` ## Getting Started From d844c0ddeb669108195ce1b6a0a119a176002d4a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 26 Nov 2012 21:54:53 -0800 Subject: [PATCH 0056/3961] adding queryset result caching and array indexing / slicing of querysets --- cqlengine/query.py | 117 +++++++++++++++---------- cqlengine/tests/query/test_queryset.py | 49 ++++++++++- 2 files changed, 116 insertions(+), 50 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index bb7a5e519f..74e8810cc1 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -117,8 +117,6 @@ class LessThanOrEqualOperator(QueryOperator): cql_symbol = '<=' class QuerySet(object): - #TODO: cache results in this instance, but don't copy them on deepcopy - #TODO: support multiple iterators def __init__(self, model): super(QuerySet, self).__init__() @@ -140,9 +138,9 @@ def __init__(self, model): self._only_fields = [] #results cache - self._result_cache = None - self._cursor = None + self._result_cache = None + self._result_idx = None def __unicode__(self): return self._select_query() @@ -156,7 +154,7 @@ def __call__(self, **kwargs): def __deepcopy__(self, memo): clone = self.__class__(self.model) for k,v in self.__dict__.items(): - if k in ['_result_cache']: + if k in ['_cursor', '_result_cache', '_result_idx']: clone.__dict__[k] = None else: clone.__dict__[k] = copy.deepcopy(v, memo) @@ -216,29 +214,65 @@ def _select_query(self): #----Reads------ - def __iter__(self): - #TODO: cache results - if self._cursor is None: - #TODO: the query and caching should happen in the same function - with connection_manager() as con: - self._cursor = con.execute(self._select_query(), self._where_values()) - self._rowcount = self._cursor.rowcount - return self + def _execute_query(self): + with connection_manager() as con: + self._cursor = con.execute(self._select_query(), self._where_values()) + self._result_cache = [None]*self._cursor.rowcount + + def _fill_result_cache_to_idx(self, idx): + if self._result_cache is None: + self._execute_query() + if self._result_idx is None: + self._result_idx = -1 + + names = [i[0] for i in self._cursor.description] + qty = idx - self._result_idx + if qty < 1: + return else: - raise QueryException("QuerySet only supports a single iterator at a time, though this will be fixed shortly") + for values in self._cursor.fetchmany(qty): + value_dict = dict(zip(names, values)) + self._result_idx += 1 + self._result_cache[self._result_idx] = self._construct_instance(value_dict) + + def __iter__(self): + if self._result_cache is None: + self._execute_query() + + for idx in range(len(self._result_cache)): + instance = self._result_cache[idx] + if instance is None: + self._fill_result_cache_to_idx(idx) + yield self._result_cache[idx] def __getitem__(self, s): + if self._result_cache is None: + self._execute_query() + + num_results = len(self._result_cache) if isinstance(s, slice): - #return a new query with limit defined - #start and step are not supported - if s.start: raise QueryException('CQL does not support START') - if s.step: raise QueryException('step is not supported') - return self.limit(s.stop) + #calculate the amount of results that need to be loaded + end = num_results if s.step is None else s.step + if end < 0: + end += num_results + else: + end -= 1 + self._fill_result_cache_to_idx(end) + return self._result_cache[s.start:s.stop:s.step] else: #return the object at this index s = long(s) - raise NotImplementedError + + #handle negative indexing + if s < 0: s += num_results + + if s >= num_results: + raise IndexError + else: + self._fill_result_cache_to_idx(s) + return self._result_cache[s] + def _construct_instance(self, values): #translate column names to model names @@ -251,25 +285,11 @@ def _construct_instance(self, values): field_dict[key] = val return self.model(**field_dict) - def _get_next(self): - """ Gets the next cursor result """ - cur = self._cursor - values = cur.fetchone() - if values is None: return - names = [i[0] for i in cur.description] - value_dict = dict(zip(names, values)) - return self._construct_instance(value_dict) - - def next(self): - instance = self._get_next() - if instance is None: - #TODO: this is inefficient, we should be caching the results - self._cursor = None - raise StopIteration - return instance - def first(self): - return iter(self)._get_next() + try: + return iter(self).next() + except StopIteration: + return None def all(self): clone = copy.deepcopy(self) @@ -345,15 +365,18 @@ def order_by(self, colname): def count(self): """ Returns the number of rows matched by this query """ #TODO: check for previous query execution and return row count if it exists - qs = ['SELECT COUNT(*)'] - qs += ['FROM {}'.format(self.column_family_name)] - if self._where: - qs += ['WHERE {}'.format(self._where_clause())] - qs = ' '.join(qs) - - with connection_manager() as con: - cur = con.execute(qs, self._where_values()) - return cur.fetchone()[0] + if self._result_cache is None: + qs = ['SELECT COUNT(*)'] + qs += ['FROM {}'.format(self.column_family_name)] + if self._where: + qs += ['WHERE {}'.format(self._where_clause())] + qs = ' '.join(qs) + + with connection_manager() as con: + cur = con.execute(qs, self._where_values()) + return cur.fetchone()[0] + else: + return len(self._result_cache) def limit(self, v): """ diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index b3c1449ad8..b90888a002 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -210,6 +210,19 @@ def test_multiple_iterations_work_properly(self): 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 + + class TestQuerySetOrdering(BaseQuerySetUsage): def test_order_by_success_case(self): @@ -238,7 +251,37 @@ def test_ordering_on_indexed_columns_fails(self): q = IndexedTestModel.objects(test_id=0).order_by('attempt_id') class TestQuerySetSlicing(BaseQuerySetUsage): - pass + + def test_out_of_range_index_raises_error(self): + q = TestModel.objects(test_id=0).order_by('attempt_id') + with self.assertRaises(IndexError): + q[10] + + def test_array_indexing_works_properly(self): + q = TestModel.objects(test_id=0).order_by('attempt_id') + expected_order = [0,1,2,3] + for i in range(len(q)): + assert q[i].attempt_id == expected_order[i] + + def test_negative_indexing_works_properly(self): + q = TestModel.objects(test_id=0).order_by('attempt_id') + expected_order = [0,1,2,3] + assert q[-1].attempt_id == expected_order[-1] + assert q[-2].attempt_id == expected_order[-2] + + def test_slicing_works_properly(self): + q = TestModel.objects(test_id=0).order_by('attempt_id') + expected_order = [0,1,2,3] + for model, expect in zip(q[1:3], expected_order[1:3]): + assert model.attempt_id == expect + + def test_negative_slicing(self): + q = TestModel.objects(test_id=0).order_by('attempt_id') + expected_order = [0,1,2,3] + for model, expect in zip(q[-3:], expected_order[-3:]): + assert model.attempt_id == expect + for model, expect in zip(q[:-1], expected_order[:-1]): + assert model.attempt_id == expect class TestQuerySetValidation(BaseQuerySetUsage): @@ -248,7 +291,7 @@ def test_primary_key_or_index_must_be_specified(self): """ with self.assertRaises(query.QueryException): q = TestModel.objects(test_result=25) - iter(q) + [i for i in q] def test_primary_key_or_index_must_have_equal_relation_filter(self): """ @@ -256,7 +299,7 @@ def test_primary_key_or_index_must_have_equal_relation_filter(self): """ with self.assertRaises(query.QueryException): q = TestModel.objects(test_id__gt=0) - iter(q) + [i for i in q] def test_indexed_field_can_be_queried(self): From 74181e2f31545c47c0feb3bcc23bf04ca6e85501 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 26 Nov 2012 22:11:29 -0800 Subject: [PATCH 0057/3961] making the autogenerated table names a bit more readable --- cqlengine/models.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index c629d2041f..6afa240008 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -1,4 +1,5 @@ from collections import OrderedDict +import re from cqlengine import columns from cqlengine.exceptions import ModelException @@ -34,8 +35,16 @@ def column_family_name(cls, include_keyspace=True): """ if cls.db_name: return cls.db_name.lower() - cf_name = cls.__module__ + '.' + cls.__name__ - cf_name = cf_name.replace('.', '_') + + camelcase = re.compile(r'([a-z])([A-Z])') + ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) + + cf_name = '' + module = cls.__module__.split('.') + if module: + cf_name = ccase(module[-1]) + '_' + + cf_name += ccase(cls.__name__) #trim to less than 48 characters or cassandra will complain cf_name = cf_name[-48:] cf_name = cf_name.lower() From 70bea617f41b86a89435e43a703aaba1f809f01b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 26 Nov 2012 22:13:21 -0800 Subject: [PATCH 0058/3961] updating version --- cqlengine/__init__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 6822d1af02..90414d015b 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.2-ALPHA' +__version__ = '0.0.3-ALPHA' diff --git a/setup.py b/setup.py index e422ca0be6..8b7adfe59c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.2-ALPHA' +version = '0.0.3-ALPHA' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine @@ -24,7 +24,7 @@ name='cqlengine', version=version, description='Cassandra CQL ORM for Python in the style of the Django orm and mongoengine', - dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/0.0.2-ALPHA.tar.gz#egg=cqlengine-0.0.2-ALPHA'], + dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/{0}.tar.gz#egg=cqlengine-{0}'.format(version)], long_description=long_desc, classifiers = [ "Development Status :: 3 - Alpha", From 6c006578302a91a33d6a26fb6ad2aa9ceac42b90 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 28 Nov 2012 21:58:56 -0800 Subject: [PATCH 0059/3961] added create shortcut classmethod on BaseModel --- README.md | 16 ++++++++-------- cqlengine/models.py | 4 ++++ cqlengine/tests/model/test_model_io.py | 6 +++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 72780d257d..5656f0e2ce 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,14 @@ pip install cqlengine >>> create_column_family(ExampleModel) #now we can create some rows: ->>> em1 = ExampleModel.objects.create(example_type=0, description="example1") ->>> em2 = ExampleModel.objects.create(example_type=0, description="example2") ->>> em3 = ExampleModel.objects.create(example_type=0, description="example3") ->>> em4 = ExampleModel.objects.create(example_type=0, description="example4") ->>> em5 = ExampleModel.objects.create(example_type=1, description="example5") ->>> em6 = ExampleModel.objects.create(example_type=1, description="example6") ->>> em7 = ExampleModel.objects.create(example_type=1, description="example7") ->>> em8 = ExampleModel.objects.create(example_type=1, description="example8") +>>> em1 = ExampleModel.create(example_type=0, description="example1") +>>> em2 = ExampleModel.create(example_type=0, description="example2") +>>> em3 = ExampleModel.create(example_type=0, description="example3") +>>> em4 = ExampleModel.create(example_type=0, description="example4") +>>> em5 = ExampleModel.create(example_type=1, description="example5") +>>> em6 = ExampleModel.create(example_type=1, description="example6") +>>> em7 = ExampleModel.create(example_type=1, description="example7") +>>> em8 = ExampleModel.create(example_type=1, description="example8") # Note: the UUID and DateTime columns will create uuid4 and datetime.now # values automatically if we don't specify them when creating new rows diff --git a/cqlengine/models.py b/cqlengine/models.py index 6afa240008..11f7e3f64a 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -69,6 +69,10 @@ def as_dict(self): values[name] = col.to_database(getattr(self, name, None)) return values + @classmethod + def create(cls, **kwargs): + return cls.objects.create(**kwargs) + def save(self): is_new = self.pk is None self.validate() diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index f408e46ce9..85100eeb13 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -27,7 +27,7 @@ def test_model_save_and_load(self): """ Tests that models can be saved and retrieved """ - tm = TestModel.objects.create(count=8, text='123456789') + tm = TestModel.create(count=8, text='123456789') tm2 = TestModel.objects(id=tm.pk).first() for cname in tm._columns.keys(): @@ -49,7 +49,7 @@ def test_model_deleting_works_properly(self): """ Tests that an instance's delete method deletes the instance """ - tm = TestModel.objects.create(count=8, text='123456789') + tm = TestModel.create(count=8, text='123456789') tm.delete() tm2 = TestModel.objects(id=tm.pk).first() self.assertIsNone(tm2) @@ -57,7 +57,7 @@ def test_model_deleting_works_properly(self): def test_column_deleting_works_properly(self): """ """ - tm = TestModel.objects.create(count=8, text='123456789') + tm = TestModel.create(count=8, text='123456789') tm.text = None tm.save() From 40dda4d839cb8b2f90aedcaabe5da82d4b587f83 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 28 Nov 2012 22:03:23 -0800 Subject: [PATCH 0060/3961] adding changelog --- changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 changelog diff --git a/changelog b/changelog new file mode 100644 index 0000000000..9ab74a3b7b --- /dev/null +++ b/changelog @@ -0,0 +1,10 @@ +CHANGELOG + +0.0.4-ALPHA (in progress) +* added create method shortcut to the model class + +0.0.3-ALPHA +* added queryset result caching +* added queryset array index access and slicing +* updating table name generation (more readable) + From fd39fac6aae5de612895820af1fbeab766207604 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 29 Nov 2012 21:23:03 -0800 Subject: [PATCH 0061/3961] adding IDE stuff --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 878f8ebd3c..6290413ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,11 @@ develop-eggs # Installer logs pip-log.txt +# IDE +.project +.pydevproject +.settings/ + # Unit test / coverage reports .coverage .tox From ba7c8c87ee20c97ed05f3a05e12c70cf7c6a5312 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 29 Nov 2012 21:24:02 -0800 Subject: [PATCH 0062/3961] adding get method to QuerySet --- changelog | 1 + cqlengine/models.py | 5 +- cqlengine/query.py | 35 +++++++++--- cqlengine/tests/query/test_queryset.py | 76 ++++++++++++++++++++------ 4 files changed, 90 insertions(+), 27 deletions(-) diff --git a/changelog b/changelog index 9ab74a3b7b..911753f940 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,7 @@ CHANGELOG 0.0.4-ALPHA (in progress) +* added .get() method to QuerySet * added create method shortcut to the model class 0.0.3-ALPHA diff --git a/cqlengine/models.py b/cqlengine/models.py index 11f7e3f64a..0c201cdb6e 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -3,7 +3,7 @@ from cqlengine import columns from cqlengine.exceptions import ModelException -from cqlengine.query import QuerySet +from cqlengine.query import QuerySet, QueryException class ModelDefinitionException(ModelException): pass @@ -11,6 +11,9 @@ class BaseModel(object): """ The base model class, don't inherit from this, inherit from Model, defined below """ + + class DoesNotExist(QueryException): pass + class MultipleObjectsReturned(QueryException): pass #table names will be generated automatically from it's model and package name #however, you can alse define them manually here diff --git a/cqlengine/query.py b/cqlengine/query.py index 74e8810cc1..c85c6c68df 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -215,13 +215,13 @@ def _select_query(self): #----Reads------ def _execute_query(self): - with connection_manager() as con: - self._cursor = con.execute(self._select_query(), self._where_values()) - self._result_cache = [None]*self._cursor.rowcount + if self._result_cache is None: + with connection_manager() as con: + self._cursor = con.execute(self._select_query(), self._where_values()) + self._result_cache = [None]*self._cursor.rowcount def _fill_result_cache_to_idx(self, idx): - if self._result_cache is None: - self._execute_query() + self._execute_query() if self._result_idx is None: self._result_idx = -1 @@ -236,8 +236,7 @@ def _fill_result_cache_to_idx(self, idx): self._result_cache[self._result_idx] = self._construct_instance(value_dict) def __iter__(self): - if self._result_cache is None: - self._execute_query() + self._execute_query() for idx in range(len(self._result_cache)): instance = self._result_cache[idx] @@ -246,8 +245,7 @@ def __iter__(self): yield self._result_cache[idx] def __getitem__(self, s): - if self._result_cache is None: - self._execute_query() + self._execute_query() num_results = len(self._result_cache) @@ -329,6 +327,25 @@ def filter(self, **kwargs): return clone + def get(self, **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() + self._execute_query() + if len(self._result_cache) == 0: + raise self.model.DoesNotExist + elif len(self._result_cache) > 1: + raise self.model.MultipleObjectsReturned( + '{} objects found'.format(len(self._result_cache))) + else: + return self[0] + + + def order_by(self, colname): """ orders the result set. diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index b90888a002..326bdc9b11 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -147,7 +147,7 @@ def tearDownClass(cls): delete_column_family(TestModel) delete_column_family(IndexedTestModel) -class TestQuerySetCountAndSelection(BaseQuerySetUsage): +class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): def test_count(self): assert TestModel.objects.count() == 12 @@ -175,22 +175,6 @@ def test_iteration(self): compare_set.remove(val) assert len(compare_set) == 0 - def test_delete(self): - TestModel.objects.create(test_id=3, attempt_id=0, description='try9', expected_result=50, test_result=40) - TestModel.objects.create(test_id=3, attempt_id=1, description='try10', expected_result=60, test_result=40) - TestModel.objects.create(test_id=3, attempt_id=2, description='try11', expected_result=70, test_result=45) - TestModel.objects.create(test_id=3, attempt_id=3, description='try12', expected_result=75, test_result=45) - - assert TestModel.objects.count() == 16 - assert TestModel.objects(test_id=3).count() == 4 - - TestModel.objects(test_id=3).delete() - - assert TestModel.objects.count() == 12 - assert TestModel.objects(test_id=3).count() == 0 - -class TestQuerySetIterator(BaseQuerySetUsage): - def test_multiple_iterations_work_properly(self): """ Tests that iterating over a query set more than once works """ q = TestModel.objects(test_id=0) @@ -222,6 +206,41 @@ def test_multiple_iterators_are_isolated(self): assert iter1.next().attempt_id == attempt_id assert iter2.next().attempt_id == attempt_id + def test_get_success_case(self): + """ + Tests that the .get() method works on new and existing querysets + """ + m = TestModel.objects.get(test_id=0, attempt_id=0) + assert isinstance(m, TestModel) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = TestModel.objects(test_id=0, attempt_id=0) + m = q.get() + assert isinstance(m, TestModel) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = TestModel.objects(test_id=0) + m = q.get(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 + """ + with self.assertRaises(TestModel.DoesNotExist): + TestModel.objects.get(test_id=100) + + def test_get_multipleobjects_exception(self): + """ + Tests that get calls that return multiple results raise a MultipleObjectsReturned error + """ + with self.assertRaises(TestModel.MultipleObjectsReturned): + TestModel.objects.get(test_id=1) + class TestQuerySetOrdering(BaseQuerySetUsage): @@ -310,6 +329,29 @@ def test_indexed_field_can_be_queried(self): count = q.count() assert q.count() == 4 +class TestQuerySetDelete(BaseQuerySetUsage): + + def test_delete(self): + TestModel.objects.create(test_id=3, attempt_id=0, description='try9', expected_result=50, test_result=40) + TestModel.objects.create(test_id=3, attempt_id=1, description='try10', expected_result=60, test_result=40) + TestModel.objects.create(test_id=3, attempt_id=2, description='try11', expected_result=70, test_result=45) + TestModel.objects.create(test_id=3, attempt_id=3, description='try12', expected_result=75, test_result=45) + + assert TestModel.objects.count() == 16 + assert TestModel.objects(test_id=3).count() == 4 + + TestModel.objects(test_id=3).delete() + + assert TestModel.objects.count() == 12 + assert TestModel.objects(test_id=3).count() == 0 + + def test_delete_without_partition_key(self): + + pass + + def test_delete_without_any_where_args(self): + pass + From 10d753be8def8a3cd10bd5cd67ee7d0068882923 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 29 Nov 2012 21:41:24 -0800 Subject: [PATCH 0063/3961] adding partition key validation to delete method --- changelog | 1 + cqlengine/query.py | 7 +++++-- cqlengine/tests/query/test_queryset.py | 9 ++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/changelog b/changelog index 911753f940..6d805ec337 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,7 @@ CHANGELOG 0.0.4-ALPHA (in progress) +* added partition key validation to QuerySet delete method * added .get() method to QuerySet * added create method shortcut to the model class diff --git a/cqlengine/query.py b/cqlengine/query.py index c85c6c68df..3904ae86ee 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -500,9 +500,12 @@ def delete(self, columns=[]): """ Deletes the contents of a query """ + #validate where clause + partition_key = self.model._primary_keys.values()[0] + if not any([c.column == partition_key for c in self._where]): + raise QueryException("The partition key must be defined on delete queries") qs = ['DELETE FROM {}'.format(self.column_family_name)] - if self._where: - qs += ['WHERE {}'.format(self._where_clause())] + qs += ['WHERE {}'.format(self._where_clause())] qs = ' '.join(qs) with connection_manager() as con: diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 326bdc9b11..842d978a2f 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -346,11 +346,14 @@ def test_delete(self): assert TestModel.objects(test_id=3).count() == 0 def test_delete_without_partition_key(self): - - pass + """ Tests that attempting to delete a model without defining a partition key fails """ + with self.assertRaises(query.QueryException): + TestModel.objects(attempt_id=0).delete() def test_delete_without_any_where_args(self): - pass + """ Tests that attempting to delete a whole table without any arguments will fail """ + with self.assertRaises(query.QueryException): + TestModel.objects(attempt_id=0).delete() From 1cd37d65b0162250a2b68f84304262aaa3b79e71 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 1 Dec 2012 09:43:09 -0800 Subject: [PATCH 0064/3961] removing foreign key stub --- cqlengine/columns.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 617b0d4b12..387aaf3d8f 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -234,9 +234,3 @@ def __init__(self, **kwargs): super(Counter, self).__init__(**kwargs) raise NotImplementedError -class ForeignKey(Column): - #TODO: Foreign key field - def __init__(self, **kwargs): - super(ForeignKey, self).__init__(**kwargs) - raise NotImplementedError - From 798686cd9d7fc570a82dff48ddbafbdb8a2b57e2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 2 Dec 2012 10:00:38 -0800 Subject: [PATCH 0065/3961] changing the create_column_family and delete_column_family management functions to create_table and delete_table, respectively --- README.md | 4 ++-- changelog | 2 ++ cqlengine/management.py | 4 ++-- cqlengine/tests/columns/test_validation.py | 10 +++++----- cqlengine/tests/model/test_model_io.py | 8 ++++---- cqlengine/tests/query/test_queryset.py | 16 ++++++++-------- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 5656f0e2ce..313003ea0e 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ pip install cqlengine >>> connection.setup(['127.0.0.1:9160']) #...and create your CQL table ->>> from cqlengine.management import create_column_family ->>> create_column_family(ExampleModel) +>>> from cqlengine.management import create_table +>>> create_table(ExampleModel) #now we can create some rows: >>> em1 = ExampleModel.create(example_type=0, description="example1") diff --git a/changelog b/changelog index 6d805ec337..4f4db4ff88 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,8 @@ CHANGELOG 0.0.4-ALPHA (in progress) +* changing create_column_family management function to create_table +* changing delete_column_family management function to delete_table * added partition key validation to QuerySet delete method * added .get() method to QuerySet * added create method shortcut to the model class diff --git a/cqlengine/management.py b/cqlengine/management.py index de0e93a45c..481e30364d 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -12,7 +12,7 @@ def delete_keyspace(name): if name in [k.name for k in con.con.client.describe_keyspaces()]: con.execute("DROP KEYSPACE {}".format(name)) -def create_column_family(model, create_missing_keyspace=True): +def create_table(model, create_missing_keyspace=True): #construct query string cf_name = model.column_family_name() raw_cf_name = model.column_family_name(include_keyspace=False) @@ -56,7 +56,7 @@ def add_column(col): con.execute(qs) -def delete_column_family(model): +def delete_table(model): #check that model exists cf_name = model.column_family_name() raw_cf_name = model.column_family_name(include_keyspace=False) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 3f70ab12cd..0c2b6c7da2 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -15,7 +15,7 @@ from cqlengine.columns import Float from cqlengine.columns import Decimal -from cqlengine.management import create_column_family, delete_column_family +from cqlengine.management import create_table, delete_table from cqlengine.models import Model class TestDatetime(BaseCassEngTestCase): @@ -26,12 +26,12 @@ class DatetimeTest(Model): @classmethod def setUpClass(cls): super(TestDatetime, cls).setUpClass() - create_column_family(cls.DatetimeTest) + create_table(cls.DatetimeTest) @classmethod def tearDownClass(cls): super(TestDatetime, cls).tearDownClass() - delete_column_family(cls.DatetimeTest) + delete_table(cls.DatetimeTest) def test_datetime_io(self): now = datetime.now() @@ -47,12 +47,12 @@ class DecimalTest(Model): @classmethod def setUpClass(cls): super(TestDecimal, cls).setUpClass() - create_column_family(cls.DecimalTest) + create_table(cls.DecimalTest) @classmethod def tearDownClass(cls): super(TestDecimal, cls).tearDownClass() - delete_column_family(cls.DecimalTest) + delete_table(cls.DecimalTest) def test_datetime_io(self): dt = self.DecimalTest.objects.create(test_id=0, dec_val=D('0.00')) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 85100eeb13..f65b44efd2 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,8 +1,8 @@ from unittest import skip from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.management import create_column_family -from cqlengine.management import delete_column_family +from cqlengine.management import create_table +from cqlengine.management import delete_table from cqlengine.models import Model from cqlengine.models import Model from cqlengine import columns @@ -16,12 +16,12 @@ class TestModelIO(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestModelIO, cls).setUpClass() - create_column_family(TestModel) + create_table(TestModel) @classmethod def tearDownClass(cls): super(TestModelIO, cls).tearDownClass() - delete_column_family(TestModel) + delete_table(TestModel) def test_model_save_and_load(self): """ diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 842d978a2f..ad3cb73e15 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -1,8 +1,8 @@ from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException -from cqlengine.management import create_column_family -from cqlengine.management import delete_column_family +from cqlengine.management import create_table +from cqlengine.management import delete_table from cqlengine.models import Model from cqlengine import columns from cqlengine import query @@ -106,10 +106,10 @@ class BaseQuerySetUsage(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(BaseQuerySetUsage, cls).setUpClass() - delete_column_family(TestModel) - delete_column_family(IndexedTestModel) - create_column_family(TestModel) - create_column_family(IndexedTestModel) + delete_table(TestModel) + delete_table(IndexedTestModel) + create_table(TestModel) + create_table(IndexedTestModel) TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30) TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30) @@ -144,8 +144,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): super(BaseQuerySetUsage, cls).tearDownClass() - delete_column_family(TestModel) - delete_column_family(IndexedTestModel) + delete_table(TestModel) + delete_table(IndexedTestModel) class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): From 112da2a6a89ea2a920fe4faa189c2b20bb575c26 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 2 Dec 2012 10:44:49 -0800 Subject: [PATCH 0066/3961] Adding sphinx documentation --- .gitignore | 3 + changelog | 1 + docs/Makefile | 153 +++++++++++++++++++++ docs/conf.py | 242 +++++++++++++++++++++++++++++++++ docs/index.rst | 100 ++++++++++++++ docs/make.bat | 190 ++++++++++++++++++++++++++ docs/topics/columns.rst | 100 ++++++++++++++ docs/topics/connection.rst | 19 +++ docs/topics/manage_schemas.rst | 43 ++++++ docs/topics/models.rst | 136 ++++++++++++++++++ docs/topics/queryset.rst | 225 ++++++++++++++++++++++++++++++ requirements.txt | 1 + 12 files changed, 1213 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/topics/columns.rst create mode 100644 docs/topics/connection.rst create mode 100644 docs/topics/manage_schemas.rst create mode 100644 docs/topics/models.rst create mode 100644 docs/topics/queryset.rst diff --git a/.gitignore b/.gitignore index 6290413ccc..bd169dfade 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ develop-eggs # Installer logs pip-log.txt +# Docs +html/ + # IDE .project .pydevproject diff --git a/changelog b/changelog index 4f4db4ff88..a75b17c221 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,7 @@ CHANGELOG 0.0.4-ALPHA (in progress) +* added Sphinx docs * changing create_column_family management function to create_table * changing delete_column_family management function to delete_table * added partition key validation to QuerySet delete method diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..ca198684e1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cqlengine.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cqlengine.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/cqlengine" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/cqlengine" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000..e41747b73e --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +# +# cqlengine documentation build configuration file, created by +# sphinx-quickstart on Sat Dec 1 09:50:49 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'cqlengine' +copyright = u'2012, Blake Eggleston' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.0.3' +# The full version, including alpha/beta/rc tags. +release = '0.0.3' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'cqlenginedoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'cqlengine.tex', u'cqlengine Documentation', + u'Blake Eggleston', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'cqlengine', u'cqlengine Documentation', + [u'Blake Eggleston'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'cqlengine', u'cqlengine Documentation', + u'Blake Eggleston', 'cqlengine', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000000..2559a1006e --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,100 @@ + +.. _index: + +======================= +cqlengine documentation +======================= + +cqlengine is a Cassandra CQL ORM for Python with an interface similar to the Django orm and mongoengine + +:ref:`getting-started` + +Contents: + +.. toctree:: + :maxdepth: 2 + + topics/models + topics/queryset + topics/columns + topics/connection + topics/manage_schemas + +.. _getting-started: + +Getting Started +=============== + + .. code-block:: python + + #first, define a model + >>> from cqlengine import columns + >>> from cqlengine import Model + + >>> class ExampleModel(Model): + >>> example_id = columns.UUID(primary_key=True) + >>> example_type = columns.Integer(index=True) + >>> created_at = columns.DateTime() + >>> description = columns.Text(required=False) + + #next, setup the connection to your cassandra server(s)... + >>> from cqlengine import connection + >>> connection.setup(['127.0.0.1:9160']) + + #...and create your CQL table + >>> from cqlengine.management import create_table + >>> create_table(ExampleModel) + + #now we can create some rows: + >>> em1 = ExampleModel.create(example_type=0, description="example1") + >>> em2 = ExampleModel.create(example_type=0, description="example2") + >>> em3 = ExampleModel.create(example_type=0, description="example3") + >>> em4 = ExampleModel.create(example_type=0, description="example4") + >>> em5 = ExampleModel.create(example_type=1, description="example5") + >>> em6 = ExampleModel.create(example_type=1, description="example6") + >>> em7 = ExampleModel.create(example_type=1, description="example7") + >>> em8 = ExampleModel.create(example_type=1, description="example8") + # Note: the UUID and DateTime columns will create uuid4 and datetime.now + # values automatically if we don't specify them when creating new rows + + #and now we can run some queries against our table + >>> ExampleModel.objects.count() + 8 + >>> q = ExampleModel.objects(example_type=1) + >>> q.count() + 4 + >>> for instance in q: + >>> print q.description + example5 + example6 + example7 + example8 + + #here we are applying additional filtering to an existing query + #query objects are immutable, so calling filter returns a new + #query object + >>> q2 = q.filter(example_id=em5.example_id) + + >>> q2.count() + 1 + >>> for instance in q2: + >>> print q.description + example5 + + +`Report a Bug `_ + +`Users Mailing List `_ + +`Dev Mailing List `_ + +**NOTE: cqlengine is in alpha and under development, some features may change. Make sure to check the changelog and test your app before upgrading** + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000..6be2277f78 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\cqlengine.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\cqlengine.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst new file mode 100644 index 0000000000..dc87f8cbdf --- /dev/null +++ b/docs/topics/columns.rst @@ -0,0 +1,100 @@ +======= +Columns +======= + +.. module:: cqlengine.columns + +.. class:: Bytes() + + Stores arbitrary bytes (no validation), expressed as hexadecimal :: + + columns.Bytes() + + +.. class:: Ascii() + + Stores a US-ASCII character string :: + + columns.Ascii() + + +.. class:: Text() + + Stores a UTF-8 encoded string :: + + columns.Text() + +.. class:: Integer() + + Stores an integer value :: + + columns.Integer() + +.. class:: DateTime() + + Stores a datetime value. + + Python's datetime.now callable is set as the default value for this column :: + + columns.DateTime() + +.. class:: UUID() + + Stores a type 1 or type 4 UUID. + + Python's uuid.uuid4 callable is set as the default value for this column. :: + + columns.UUID() + +.. class:: Boolean() + + Stores a boolean True or False value :: + + columns.Boolean() + +.. class:: Float() + + Stores a floating point value :: + + columns.Float() + + **options** + + :attr:`~columns.Float.double_precision` + If True, stores a double precision float value, otherwise single precision. Defaults to True. + +.. class:: Decimal() + + Stores a variable precision decimal value :: + + columns.Decimal() + +Column Options +============== + + Each column can be defined with optional arguments to modify the way they behave. While some column types may define additional column options, these are the options that are available on all columns: + + .. attribute:: BaseColumn.primary_key + + If True, this column is created as a primary key field. A model can have multiple primary keys. Defaults to False. + + *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys.* + + .. attribute:: BaseColumn.index + + If True, an index will be created for this column. Defaults to False. + + *Note: Indexes can only be created on models with one primary key* + + .. attribute:: BaseColumn.db_field + + Explicitly sets the name of the column in the database table. If this is left blank, the column name will be the same as the name of the column attribute. Defaults to None. + + .. attribute:: BaseColumn.default + + The default value for this column. If a model instance is saved without a value for this column having been defined, the default value will be used. This can be either a value or a callable object (ie: datetime.now is a valid default argument). + + .. attribute:: BaseColumn.required + + If True, this model cannot be saved without a value defined for this column. Defaults to True. Primary key fields cannot have their required fields set to False. + diff --git a/docs/topics/connection.rst b/docs/topics/connection.rst new file mode 100644 index 0000000000..14bc2341c3 --- /dev/null +++ b/docs/topics/connection.rst @@ -0,0 +1,19 @@ +============== +Connection +============== + +.. module:: cqlengine.connection + +The setup function in `cqlengine.connection` records the Cassandra servers to connect to. +If there is a problem with one of the servers, cqlengine will try to connect to each of the other connections before failing. + +.. function:: setup(hosts [, username=None, password=None]) + + :param hosts: list of hosts, strings in the :, or just + :type hosts: list + + Records the hosts and connects to one of them + +See the example at :ref:`getting-started` + + diff --git a/docs/topics/manage_schemas.rst b/docs/topics/manage_schemas.rst new file mode 100644 index 0000000000..2174e56f6a --- /dev/null +++ b/docs/topics/manage_schemas.rst @@ -0,0 +1,43 @@ +=============== +Managing Schmas +=============== + +.. module:: cqlengine.management + +Once a connection has been made to Cassandra, you can use the functions in ``cqlengine.management`` to create and delete keyspaces, as well as create and delete tables for defined models + +.. function:: create_keyspace(name) + + :param name: the keyspace name to create + :type name: string + + creates a keyspace with the given name + +.. function:: delete_keyspace(name) + + :param name: the keyspace name to delete + :type name: string + + deletes the keyspace with the given name + +.. function:: create_table(model [, create_missing_keyspace=True]) + + :param model: the :class:`~cqlengine.model.Model` class to make a table with + :type model: :class:`~cqlengine.model.Model` + :param create_missing_keyspace: *Optional* If True, the model's keyspace will be created if it does not already exist. Defaults to ``True`` + :type create_missing_keyspace: bool + + creates a CQL table for the given model + +.. function:: delete_table(model) + + :param model: the :class:`~cqlengine.model.Model` class to delete a column family for + :type model: :class:`~cqlengine.model.Model` + + deletes the CQL table for the given model + + + +See the example at :ref:`getting-started` + + diff --git a/docs/topics/models.rst b/docs/topics/models.rst new file mode 100644 index 0000000000..3ddefc37e4 --- /dev/null +++ b/docs/topics/models.rst @@ -0,0 +1,136 @@ +====== +Models +====== + +.. module:: cqlengine.models + +A model is a python class representing a CQL table. + +Example +======= + +This example defines a Person table, with the columns ``first_name`` and ``last_name`` + +.. code-block:: python + + from cqlengine import columns + from cqlengine.models import Model + + class Person(Model): + first_name = columns.Text() + last_name = columns.Text() + + +The Person model would create this CQL table: + +.. code-block:: sql + + CREATE TABLE cqlengine.person ( + id uuid, + first_name text, + last_name text, + PRIMARY KEY (id) + ) + +Columns +======= + + Columns in your models map to columns in your CQL table. You define CQL columns by defining column attributes on your model classes. For a model to be valid it needs at least one primary key column (defined automatically if you don't define one) and one non-primary key column. + + Just as in CQL, the order you define your columns in is important, and is the same order they are defined in on a model's corresponding table. + +Column Types +============ + + Each column on your model definitions needs to an instance of a Column class. The column types that are included with cqlengine as of this writing are: + + * :class:`~cqlengine.columns.Bytes` + * :class:`~cqlengine.columns.Ascii` + * :class:`~cqlengine.columns.Text` + * :class:`~cqlengine.columns.Integer` + * :class:`~cqlengine.columns.DateTime` + * :class:`~cqlengine.columns.UUID` + * :class:`~cqlengine.columns.Boolean` + * :class:`~cqlengine.columns.Float` + * :class:`~cqlengine.columns.Decimal` + + A time uuid field is in the works. + +Column Options +-------------- + + Each column can be defined with optional arguments to modify the way they behave. While some column types may define additional column options, these are the options that are available on all columns: + + :attr:`~cqlengine.columns.BaseColumn.primary_key` + If True, this column is created as a primary key field. A model can have multiple primary keys. Defaults to False. + + *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys.* + + :attr:`~cqlengine.columns.BaseColumn.index` + If True, an index will be created for this column. Defaults to False. + + *Note: Indexes can only be created on models with one primary key* + + :attr:`~cqlengine.columns.BaseColumn.db_field` + Explicitly sets the name of the column in the database table. If this is left blank, the column name will be the same as the name of the column attribute. Defaults to None. + + :attr:`~cqlengine.columns.BaseColumn.default` + The default value for this column. If a model instance is saved without a value for this column having been defined, the default value will be used. This can be either a value or a callable object (ie: datetime.now is a valid default argument). + + :attr:`~cqlengine.columns.BaseColumn.required` + If True, this model cannot be saved without a value defined for this column. Defaults to True. Primary key fields cannot have their required fields set to False. + +Model Methods +============= + Below are the methods that can be called on model instances. + +.. class:: Model(\*\*values) + + Creates an instance of the model. Pass in keyword arguments for columns you've defined on the model. + + *Example* + + .. code-block:: python + + #using the person model from earlier: + class Person(Model): + first_name = columns.Text() + last_name = columns.Text() + + person = Person(first_name='Blake', last_name='Eggleston') + person.first_name #returns 'Blake' + person.last_name #returns 'Eggleston' + + + .. method:: save() + + Saves an object to the database + + *Example* + + .. code-block:: python + + #create a person instance + person = Person(first_name='Kimberly', last_name='Eggleston') + #saves it to Cassandra + person.save() + + + .. method:: delete() + + Deletes the object from the database. + +Model Attributes +================ + + .. attribute:: Model.db_name + + *Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix + + .. attribute:: Model.keyspace + + *Optional.* Sets the name of the keyspace used by this model. Defaulst to cqlengine + +Automatic Primary Keys +====================== + CQL requires that all tables define at least one primary key. If a model definition does not include a primary key column, cqlengine will automatically add a uuid primary key column named ``id``. diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst new file mode 100644 index 0000000000..63b76d03a6 --- /dev/null +++ b/docs/topics/queryset.rst @@ -0,0 +1,225 @@ +============== +Making Queries +============== + +.. module:: cqlengine.query + +Retrieving objects +================== + Once you've populated Cassandra with data, you'll probably want to retrieve some of it. This is accomplished with QuerySet objects. This section will describe how to use QuerySet objects to retrieve the data you're looking for. + +Retrieving all objects +---------------------- + The simplest query you can make is to return all objects from a table. + + This is accomplished with the ``.all()`` method, which returns a QuerySet of all objects in a table + + Using the Person example model, we would get all Person objects like this: + + .. code-block:: python + + all_objects = Person.objects.all() + +.. _retrieving-objects-with-filters: + +Retrieving objects with filters +---------------------------------------- + Typically, you'll want to query only a subset of the records in your database. + + That can be accomplished with the QuerySet's ``.filter(\*\*)`` method. + + For example, given the model definition: + + .. code-block:: python + + class Automobile(Model): + manufacturer = columns.Text(primary_key=True) + year = columns.Integer(primary_key=True) + model = columns.Text() + price = columns.Decimal() + + ...and assuming the Automobile table contains a record of every car model manufactured in the last 20 years or so, we can retrieve only the cars made by a single manufacturer like this: + + + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + + We can then further filter our query with another call to **.filter** + + .. code-block:: python + + q = q.filter(year=2012) + + *Note: all queries involving any filtering MUST define either an '=' or an 'in' relation to either a primary key column, or an indexed column.* + +Accessing objects in a QuerySet +=============================== + + There are several methods for getting objects out of a queryset + + * iterating over the queryset + .. code-block:: python + + for car in Automobile.objects.all(): + #...do something to the car instance + pass + + * list index + .. code-block:: python + + q = Automobile.objects.all() + q[0] #returns the first result + q[1] #returns the second result + + + * list slicing + .. code-block:: python + + q = Automobile.objects.all() + q[1:] #returns all results except the first + q[1:9] #returns a slice of the results + + *Note: CQL does not support specifying a start position in it's queries. Therefore, accessing elements using array indexing / slicing will load every result up to the index value requested* + + * calling :attr:`get() ` on the queryset + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year=2012) + car = q.get() + + this returns the object matching the queryset + + * calling :attr:`first() ` on the queryset + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year=2012) + car = q.first() + + this returns the first value in the queryset + +.. _query-filtering-operators: + +Filtering Operators +=================== + + :attr:`Equal To ` + + The default filtering operator. + + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year=2012) #year == 2012 + + In addition to simple equal to queries, cqlengine also supports querying with other operators by appending a ``__`` to the field name on the filtering call + + :attr:`in (__in) ` + + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year__in=[2011, 2012]) + + :attr:`> (__gt) ` + + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year__gt=2010) # year > 2010 + + :attr:`>= (__gte) ` + + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year__gte=2010) # year >= 2010 + + :attr:`< (__lt) ` + + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year__lt=2012) # year < 2012 + + :attr:`<= (__lte) ` + + .. code-block:: python + + q = Automobile.objects.filter(manufacturer='Tesla') + q = q.filter(year__lte=2012) # year <= 2012 + +QuerySets are imutable +====================== + + When calling any method that changes a queryset, the method does not actually change the queryset object it's called on, but returns a new queryset object with the attributes of the original queryset, plus the attributes added in the method call. + + *Example* + + .. code-block:: python + + #this produces 3 different querysets + #q does not change after it's initial definition + q = Automobiles.objects.filter(year=2012) + tesla2012 = q.filter(manufacturer='Tesla') + honda2012 = q.filter(manufacturer='Honda') + +Ordering QuerySets +================== + + Since Cassandra is essentially a distributed hash table on steroids, the order you get records back in will not be particularly predictable. + + However, you can set a column to order on with the ``.order_by(column_name)`` method. + + *Example* + + .. code-block:: python + + #sort ascending + q = Automobiles.objects.all().order_by('year') + #sort descending + q = Automobiles.objects.all().order_by('-year') + + *Note: Cassandra only supports ordering on a clustering key. In other words, to support ordering results, your model must have more than one primary key, and you must order on a primary key, excluding the first one.* + + *For instance, given our Automobile model, year is the only column we can order on.* + +QuerySet method reference +========================= + +.. class:: QuerySet + + .. method:: all() + + Returns a queryset matching all rows + + .. method:: count() + + Returns the number of matching rows in your QuerySet + + .. method:: filter(\*\*values) + + :param values: See :ref:`retrieving-objects-with-filters` + + Returns a QuerySet filtered on the keyword arguments + + .. method:: get(\*\*values) + + :param values: See :ref:`retrieving-objects-with-filters` + + Returns a single object matching the QuerySet. If no objects are matched, a :attr:`~models.Model.DoesNotExist` exception is raised. If more than one object is found, a :attr:`~models.Model.MultipleObjectsReturned` exception is raised. + + .. method:: limit(num) + + Limits the number of results returned by Cassandra. + + *Note that CQL's default limit is 10,000, so all queries without a limit set explicitly will have an implicit limit of 10,000* + + .. method:: order_by(field_name) + + :param field_name: the name of the field to order on. *Note: the field_name must be a clustering key* + :type field_name: string + + Sets the field to order on. diff --git a/requirements.txt b/requirements.txt index 8411d39556..a16d2a9cc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ cql==1.2.0 ipython==0.13.1 ipdb==0.7 +Sphinx==1.1.3 From db3a7751fb08f2041219d9900bc7ad3e73f8cd35 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 2 Dec 2012 10:51:42 -0800 Subject: [PATCH 0067/3961] updating documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 313003ea0e..4eceed1fb6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ cqlengine cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine -[Documentation](https://github.com/bdeggleston/cqlengine/wiki/Documentation) +[Documentation](https://cqlengine.readthedocs.org/en/latest/) [Report a Bug](https://github.com/bdeggleston/cqlengine/issues) From 7afe9300ff37c788536434fd966c274e6611700f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 2 Dec 2012 11:02:09 -0800 Subject: [PATCH 0068/3961] updating version# --- changelog | 2 +- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog b/changelog index a75b17c221..652dda1d7c 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,6 @@ CHANGELOG -0.0.4-ALPHA (in progress) +0.0.4-ALPHA * added Sphinx docs * changing create_column_family management function to create_table * changing delete_column_family management function to delete_table diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 90414d015b..8fed278532 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.3-ALPHA' +__version__ = '0.0.4-ALPHA' diff --git a/docs/conf.py b/docs/conf.py index e41747b73e..e15b8f3ec1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.0.3' +version = '0.0.4' # The full version, including alpha/beta/rc tags. -release = '0.0.3' +release = '0.0.4-ALPHA' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 8b7adfe59c..de11c701bf 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.3-ALPHA' +version = '0.0.4-ALPHA' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From 192f1914b3ddfe4ca647908481afdf977ea6fb79 Mon Sep 17 00:00:00 2001 From: Eric Scrivner Date: Mon, 3 Dec 2012 20:22:42 -0800 Subject: [PATCH 0069/3961] Add Connection Pooling --- cqlengine/connection.py | 88 +++++++++++++++++-- cqlengine/tests/management/test_management.py | 37 ++++++++ requirements.txt | 1 + 3 files changed, 121 insertions(+), 5 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 65147f6ac9..52063d1b51 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -3,6 +3,7 @@ #http://cassandra.apache.org/doc/cql/CQL.html from collections import namedtuple +import Queue import random import cql @@ -20,6 +21,7 @@ class CQLConnectionError(CQLEngineException): pass _conn= None _username = None _password = None +_max_connections = 10 def _set_conn(host): """ @@ -28,7 +30,7 @@ def _set_conn(host): _conn = cql.connect(host.name, host.port, user=_username, password=_password) _conn.set_cql_version('3.0.0') -def setup(hosts, username=None, password=None): +def setup(hosts, username=None, password=None, max_connections=10): """ Records the hosts and connects to one of them @@ -37,10 +39,11 @@ def setup(hosts, username=None, password=None): global _hosts global _username global _password - + global _max_connections _username = username _password = password - + _max_connections = max_connections + for host in hosts: host = host.strip() host = host.split(':') @@ -57,7 +60,82 @@ def setup(hosts, username=None, password=None): random.shuffle(_hosts) host = _hosts[_host_idx] _set_conn(host) + + +class ConnectionPool(object): + """Handles pooling of database connections.""" + + # Connection pool queue + _queue = None + + @classmethod + def clear(cls): + """ + Force the connection pool to be cleared. Will close all internal + connections. + """ + try: + while not cls._queue.empty(): + cls._queue.get().close() + except: + pass + @classmethod + def get(cls): + """ + Returns a usable database connection. Uses the internal queue to + determine whether to return an existing connection or to create + a new one. + """ + try: + if cls._queue.empty(): + return cls._create_connection() + return cls._queue.get() + except CQLConnectionError as cqle: + raise cqle + except: + if not cls._queue: + cls._queue = Queue.Queue(maxsize=_max_connections) + return cls._create_connection() + + @classmethod + def put(cls, conn): + """ + Returns a connection to the queue freeing it up for other queries to + use. + + :param conn: The connection to be released + :type conn: connection + """ + try: + if cls._queue.full(): + conn.close() + else: + cls._queue.put(conn) + except: + if not cls._queue: + cls._queue = Queue.Queue(maxsize=_max_connections) + cls._queue.put(conn) + + @classmethod + def _create_connection(cls): + """ + Creates a new connection for the connection pool. + """ + global _hosts + global _username + global _password + + if not _hosts: + raise CQLConnectionError("At least one host required") + + random.shuffle(_hosts) + host = _hosts[_host_idx] + + new_conn = cql.connect(host.name, host.port, user=_username, password=_password) + new_conn.set_cql_version('3.0.0') + return new_conn + class connection_manager(object): """ @@ -67,13 +145,13 @@ def __init__(self): if not _hosts: raise CQLConnectionError("No connections have been configured, call cqlengine.connection.setup") self.keyspace = None - self.con = _conn + self.con = ConnectionPool.get() def __enter__(self): return self def __exit__(self, type, value, traceback): - pass + ConnectionPool.put(self.con) def execute(self, query, params={}): """ diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index e69de29bb2..e491d615a0 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -0,0 +1,37 @@ +from cqlengine.tests.base import BaseCassEngTestCase + +from cqlengine.connection import ConnectionPool + +from mock import Mock + + +class ConnectionPoolTestCase(BaseCassEngTestCase): + """Test cassandra connection pooling.""" + + def setUp(self): + ConnectionPool.clear() + + def test_should_create_single_connection_on_request(self): + """Should create a single connection on first request""" + result = ConnectionPool.get() + self.assertIsNotNone(result) + self.assertEquals(0, ConnectionPool._queue.qsize()) + ConnectionPool._queue.put(result) + self.assertEquals(1, ConnectionPool._queue.qsize()) + + def test_should_close_connection_if_queue_is_full(self): + """Should close additional connections if queue is full""" + connections = [ConnectionPool.get() for x in range(10)] + for conn in connections: + ConnectionPool.put(conn) + fake_conn = Mock() + ConnectionPool.put(fake_conn) + fake_conn.close.assert_called_once_with() + + def test_should_pop_connections_from_queue(self): + """Should pull existing connections off of the queue""" + conn = ConnectionPool.get() + ConnectionPool.put(conn) + self.assertEquals(1, ConnectionPool._queue.qsize()) + self.assertEquals(conn, ConnectionPool.get()) + self.assertEquals(0, ConnectionPool._queue.qsize()) diff --git a/requirements.txt b/requirements.txt index a16d2a9cc7..2415255265 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ cql==1.2.0 ipython==0.13.1 ipdb==0.7 Sphinx==1.1.3 +mock==1.0.1 From 5118c36de91f424a1b63839020fa6cab88734818 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 4 Dec 2012 22:29:50 -0800 Subject: [PATCH 0070/3961] expanding the connection manager a bit --- cqlengine/connection.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 52063d1b51..8e1f70709d 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -23,13 +23,6 @@ class CQLConnectionError(CQLEngineException): pass _password = None _max_connections = 10 -def _set_conn(host): - """ - """ - global _conn - _conn = cql.connect(host.name, host.port, user=_username, password=_password) - _conn.set_cql_version('3.0.0') - def setup(hosts, username=None, password=None, max_connections=10): """ Records the hosts and connects to one of them @@ -58,8 +51,9 @@ def setup(hosts, username=None, password=None, max_connections=10): raise CQLConnectionError("At least one host required") random.shuffle(_hosts) - host = _hosts[_host_idx] - _set_conn(host) + + con = ConnectionPool.get() + ConnectionPool.put(con) class ConnectionPool(object): @@ -129,7 +123,6 @@ def _create_connection(cls): if not _hosts: raise CQLConnectionError("At least one host required") - random.shuffle(_hosts) host = _hosts[_host_idx] new_conn = cql.connect(host.name, host.port, user=_username, password=_password) @@ -146,12 +139,17 @@ def __init__(self): raise CQLConnectionError("No connections have been configured, call cqlengine.connection.setup") self.keyspace = None self.con = ConnectionPool.get() + self.cur = None + + def close(self): + if self.cur: self.cur.close() + ConnectionPool.put(self.con) def __enter__(self): return self def __exit__(self, type, value, traceback): - ConnectionPool.put(self.con) + self.close() def execute(self, query, params={}): """ @@ -164,18 +162,17 @@ def execute(self, query, params={}): for i in range(len(_hosts)): try: - cur = self.con.cursor() - cur.execute(query, params) - return cur + self.cur = self.con.cursor() + self.cur.execute(query, params) + return self.cur except TTransportException: #TODO: check for other errors raised in the event of a connection / server problem #move to the next connection and set the connection pool self.con = None _host_idx += 1 _host_idx %= len(_hosts) - host = _hosts[_host_idx] - _set_conn(host) - self.con = _conn + self.con.close() + self.con = ConnectionPool._create_connection() raise CQLConnectionError("couldn't reach a Cassandra server") From 43cbca139d53067a0884aea4f669bc38967726cb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 4 Dec 2012 22:30:07 -0800 Subject: [PATCH 0071/3961] adding connection pool support to the queryset --- cqlengine/query.py | 27 +++++++++++++++++------- cqlengine/tests/query/test_queryset.py | 29 ++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 3904ae86ee..e75f563382 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -138,7 +138,8 @@ def __init__(self, model): self._only_fields = [] #results cache - self._cursor = None + self._con = None + self._cur = None self._result_cache = None self._result_idx = None @@ -154,7 +155,7 @@ def __call__(self, **kwargs): def __deepcopy__(self, memo): clone = self.__class__(self.model) for k,v in self.__dict__.items(): - if k in ['_cursor', '_result_cache', '_result_idx']: + if k in ['_con', '_cur', '_result_cache', '_result_idx']: clone.__dict__[k] = None else: clone.__dict__[k] = copy.deepcopy(v, memo) @@ -163,6 +164,12 @@ def __deepcopy__(self, memo): def __len__(self): return self.count() + + def __del__(self): + if self._con: + self._con.close() + self._con = None + self._cur = None #----query generation / execution---- @@ -216,24 +223,30 @@ def _select_query(self): def _execute_query(self): if self._result_cache is None: - with connection_manager() as con: - self._cursor = con.execute(self._select_query(), self._where_values()) - self._result_cache = [None]*self._cursor.rowcount + self._con = connection_manager() + self._cur = self._con.execute(self._select_query(), self._where_values()) + self._result_cache = [None]*self._cur.rowcount def _fill_result_cache_to_idx(self, idx): self._execute_query() if self._result_idx is None: self._result_idx = -1 - names = [i[0] for i in self._cursor.description] qty = idx - self._result_idx if qty < 1: return else: - for values in self._cursor.fetchmany(qty): + names = [i[0] for i in self._cur.description] + for values in self._cur.fetchmany(qty): value_dict = dict(zip(names, values)) self._result_idx += 1 self._result_cache[self._result_idx] = self._construct_instance(value_dict) + + #return the connection to the connection pool if we have all objects + if self._result_cache and self._result_cache[-1] is not None: + self._con.close() + self._con = None + self._cur = None def __iter__(self): self._execute_query() diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index ad3cb73e15..31178b16c5 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -355,8 +355,33 @@ def test_delete_without_any_where_args(self): with self.assertRaises(query.QueryException): TestModel.objects(attempt_id=0).delete() - - +class TestQuerySetConnectionHandling(BaseQuerySetUsage): + + def test_conn_is_returned_after_filling_cache(self): + """ + Tests that the queryset returns it's connection after it's fetched all of it's 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)]) + for t in q: + val = t.attempt_id, t.expected_result + assert val in compare_set + compare_set.remove(val) + + assert q._con is None + assert q._cur is None + + def test_conn_is_returned_after_queryset_is_garbage_collected(self): + """ Tests that the connection is returned to the connection pool after the queryset is gc'd """ + from cqlengine.connection import ConnectionPool + assert ConnectionPool._queue.qsize() == 1 + q = TestModel.objects(test_id=0) + v = q[0] + assert ConnectionPool._queue.qsize() == 0 + + del q + assert ConnectionPool._queue.qsize() == 1 From eb28b92dcf455307b9eb4fcb85ddd945443244be Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 4 Dec 2012 22:46:44 -0800 Subject: [PATCH 0072/3961] adding convenience filter methods --- cqlengine/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index 0c201cdb6e..6e75c9acc3 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -75,6 +75,18 @@ def as_dict(self): @classmethod def create(cls, **kwargs): return cls.objects.create(**kwargs) + + @classmethod + def all(cls): + return cls.objects.all() + + @classmethod + def filter(cls, **kwargs): + return cls.objects.filter(**kwargs) + + @classmethod + def get(cls, **kwargs): + return cls.objects.get(**kwargs) def save(self): is_new = self.pk is None From 1935bd1a0b5cca3f010d2c0a07a8de3bd2602b3f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 4 Dec 2012 22:47:47 -0800 Subject: [PATCH 0073/3961] updating version, changelog, etc --- AUTHORS | 3 ++- changelog | 4 ++++ cqlengine/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 3a69691a3e..b361bf0de4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,5 +4,6 @@ Blake Eggleston CONTRIBUTORS -Eric Scrivner +Eric Scrivner - test environment, connection pooling +Jon Haddad - helped hash out some of the architecture diff --git a/changelog b/changelog index 652dda1d7c..d1d9469f68 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.0.5 +* added connection pooling +* adding a few convenience query classmethods to the model class + 0.0.4-ALPHA * added Sphinx docs * changing create_column_family management function to create_table diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 8fed278532..f2c0cbcb0a 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.4-ALPHA' +__version__ = '0.0.5' diff --git a/setup.py b/setup.py index de11c701bf..4471f9c807 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.4-ALPHA' +version = '0.0.5' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From 57359a87ad3a6a4609b8017b75bfa7ef0be0b975 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 5 Dec 2012 11:16:01 -0800 Subject: [PATCH 0074/3961] added timeuuid column --- cqlengine/columns.py | 11 ++++++++ cqlengine/tests/columns/test_validation.py | 33 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 387aaf3d8f..b276b5d75b 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -196,6 +196,17 @@ def validate(self, value): if not self.re_uuid.match(val): raise ValidationError("{} is not a valid uuid".format(value)) return _UUID(val) + +class TimeUUID(UUID): + """ + UUID containing timestamp + """ + + db_type = 'timeuuid' + + def __init__(self, **kwargs): + kwargs.setdefault('default', lambda: uuid1()) + super(TimeUUID, self).__init__(**kwargs) class Boolean(Column): db_type = 'boolean' diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 0c2b6c7da2..a94f87a093 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -4,7 +4,7 @@ from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.columns import Column +from cqlengine.columns import Column, TimeUUID from cqlengine.columns import Bytes from cqlengine.columns import Ascii from cqlengine.columns import Text @@ -63,5 +63,36 @@ def test_datetime_io(self): dt2 = self.DecimalTest.objects(test_id=0).first() assert dt2.dec_val == D('5') +class TestTimeUUID(BaseCassEngTestCase): + class TimeUUIDTest(Model): + test_id = Integer(primary_key=True) + timeuuid = TimeUUID() + + @classmethod + def setUpClass(cls): + super(TestTimeUUID, cls).setUpClass() + create_table(cls.TimeUUIDTest) + + @classmethod + def tearDownClass(cls): + super(TestTimeUUID, cls).tearDownClass() + delete_table(cls.TimeUUIDTest) + + def test_timeuuid_io(self): + t0 = self.TimeUUIDTest.create(test_id=0) + t1 = self.TimeUUIDTest.get(test_id=0) + + assert t1.timeuuid.time == t1.timeuuid.time + + + + + + + + + + + From d38a71ea788b004c78928ee4752c8ecc18e74bfb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 5 Dec 2012 11:16:37 -0800 Subject: [PATCH 0075/3961] updating version# --- changelog | 3 +++ cqlengine/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog b/changelog index d1d9469f68..ad4f5792c6 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.0.6 +* added TimeUUID column + 0.0.5 * added connection pooling * adding a few convenience query classmethods to the model class diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index f2c0cbcb0a..6742297c6f 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.5' +__version__ = '0.0.6' diff --git a/setup.py b/setup.py index 4471f9c807..f79eb17e51 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.5' +version = '0.0.6' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From 119c0ac001f3f09c57de1d5a293f0f2e32020d88 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 5 Dec 2012 15:30:09 -0800 Subject: [PATCH 0076/3961] updating docs version --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e15b8f3ec1..fe149b8571 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.0.4' +version = '0.0.6' # The full version, including alpha/beta/rc tags. -release = '0.0.4-ALPHA' +release = '0.0.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 454f634b3410297ed1b2a7f67f46a044c3a3ee50 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 5 Dec 2012 16:43:58 -0800 Subject: [PATCH 0077/3961] updating setup docs link --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f79eb17e51..927db4f509 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine -[Documentation](https://github.com/bdeggleston/cqlengine/wiki/Documentation) +[Documentation](https://cqlengine.readthedocs.org/en/latest/) [Report a Bug](https://github.com/bdeggleston/cqlengine/issues) @@ -17,7 +17,7 @@ [Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) -**NOTE: cqlengine is in alpha and under development, some features may change (hopefully with notice). Make sure to check the changelog and test your app before upgrading** +**NOTE: cqlengine is in alpha and under development, some features may change. Make sure to check the changelog and test your app before upgrading** """ setup( From e53a5031ae6ba8bf319e24a6617906d0c1067f65 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 7 Dec 2012 15:10:42 -0800 Subject: [PATCH 0078/3961] fixed manual table name bugs --- changelog | 4 +++ cqlengine/models.py | 33 +++++++++---------- .../tests/model/test_class_construction.py | 25 ++++++++++++++ docs/topics/models.rst | 2 +- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/changelog b/changelog index ad4f5792c6..2546c9da0b 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.0.7 +* fixed manual table name bug +* changed model level db_name field to table_name + 0.0.6 * added TimeUUID column diff --git a/cqlengine/models.py b/cqlengine/models.py index 6e75c9acc3..7891fe1325 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -17,7 +17,7 @@ class MultipleObjectsReturned(QueryException): pass #table names will be generated automatically from it's model and package name #however, you can alse define them manually here - db_name = None + table_name = None #the keyspace for this model keyspace = 'cqlengine' @@ -36,21 +36,21 @@ def column_family_name(cls, include_keyspace=True): Returns the column family name if it's been defined otherwise, it creates it from the module and class name """ - if cls.db_name: - return cls.db_name.lower() - - camelcase = re.compile(r'([a-z])([A-Z])') - ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) - cf_name = '' - module = cls.__module__.split('.') - if module: - cf_name = ccase(module[-1]) + '_' - - cf_name += ccase(cls.__name__) - #trim to less than 48 characters or cassandra will complain - cf_name = cf_name[-48:] - cf_name = cf_name.lower() + if cls.table_name: + cf_name = cls.table_name.lower() + else: + camelcase = re.compile(r'([a-z])([A-Z])') + ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) + + module = cls.__module__.split('.') + if module: + cf_name = ccase(module[-1]) + '_' + + cf_name += ccase(cls.__name__) + #trim to less than 48 characters or cassandra will complain + cf_name = cf_name[-48:] + cf_name = cf_name.lower() if not include_keyspace: return cf_name return '{}.{}'.format(cls.keyspace, cf_name) @@ -157,9 +157,6 @@ def _transform_column(col_name, col_obj): raise ModelDefinitionException( 'Indexes on models with multiple primary keys is not supported') - #get column family name - cf_name = attrs.pop('db_name', name) - #create db_name -> model name map for loading db_map = {} for field_name, col in column_dict.items(): diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index fd218e1a5c..d01011ec42 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -3,6 +3,7 @@ from cqlengine.exceptions import ModelException from cqlengine.models import Model from cqlengine import columns +import cqlengine class TestModelClassFunction(BaseCassEngTestCase): """ @@ -103,3 +104,27 @@ def test_meta_data_is_not_inherited(self): """ Test that metadata defined in one class, is not inherited by subclasses """ + +class TestManualTableNaming(BaseCassEngTestCase): + + class RenamedTest(cqlengine.Model): + keyspace = 'whatever' + table_name = 'manual_name' + + id = cqlengine.UUID(primary_key=True) + data = cqlengine.Text() + + def test_proper_table_naming(self): + assert self.RenamedTest.column_family_name(include_keyspace=False) == 'manual_name' + assert self.RenamedTest.column_family_name(include_keyspace=True) == 'whatever.manual_name' + + + + + + + + + + + diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 3ddefc37e4..86bd08d437 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -123,7 +123,7 @@ Model Methods Model Attributes ================ - .. attribute:: Model.db_name + .. attribute:: Model.table_name *Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix From eae926b6710cc8e74cc9a726f7db6a7628b31227 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 7 Dec 2012 15:13:00 -0800 Subject: [PATCH 0079/3961] changing version number --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 6742297c6f..5a99e64c07 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.6' +__version__ = '0.0.7' diff --git a/docs/conf.py b/docs/conf.py index fe149b8571..a45c245c9d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.0.6' +version = '0.0.7' # The full version, including alpha/beta/rc tags. -release = '0.0.6' +release = '0.0.7' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 927db4f509..a9658c482f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.6' +version = '0.0.7' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From 6b202c7e76ad182c1a9290fb0c0cfdd999cdae89 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 11 Dec 2012 18:53:14 -0800 Subject: [PATCH 0080/3961] added ability to specifiy replication factor and replication strategy --- cqlengine/management.py | 11 +++++++---- cqlengine/models.py | 1 + cqlengine/tests/management/test_management.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 481e30364d..4e764b55de 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,11 +1,11 @@ from cqlengine.connection import connection_manager -def create_keyspace(name): +def create_keyspace(name, strategy_class = 'SimpleStrategy', replication_factor=3): with connection_manager() as con: if name not in [k.name for k in con.con.client.describe_keyspaces()]: con.execute("""CREATE KEYSPACE {} - WITH strategy_class = 'SimpleStrategy' - AND strategy_options:replication_factor=1;""".format(name)) + WITH strategy_class = '{}' + AND strategy_options:replication_factor={};""".format(name, strategy_class, replication_factor)) def delete_keyspace(name): with connection_manager() as con: @@ -38,8 +38,11 @@ def add_column(col): add_column(col) qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) - + qs += ['({})'.format(', '.join(qtypes))] + + # add read_repair_chance + qs += ["WITH read_repair_chance = {}".format(model.read_repair_chance)] qs = ' '.join(qs) con.execute(qs) diff --git a/cqlengine/models.py b/cqlengine/models.py index 7891fe1325..e8a270459a 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -21,6 +21,7 @@ class MultipleObjectsReturned(QueryException): pass #the keyspace for this model keyspace = 'cqlengine' + read_repair_chance = 0.1 def __init__(self, **values): self._values = {} diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index e491d615a0..f3862a9527 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -3,6 +3,7 @@ from cqlengine.connection import ConnectionPool from mock import Mock +from cqlengine import management class ConnectionPoolTestCase(BaseCassEngTestCase): @@ -35,3 +36,12 @@ def test_should_pop_connections_from_queue(self): self.assertEquals(1, ConnectionPool._queue.qsize()) self.assertEquals(conn, ConnectionPool.get()) self.assertEquals(0, ConnectionPool._queue.qsize()) + + +class CreateKeyspaceTest(BaseCassEngTestCase): + def test_create_succeeeds(self): + management.create_keyspace('test_keyspace') + management.delete_keyspace('test_keyspace') + + + \ No newline at end of file From fd347707b29d63977a5a37c934e6481c17c0e0f9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 12 Dec 2012 11:58:02 -0800 Subject: [PATCH 0081/3961] updating version# and change log --- changelog | 4 ++++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 2546c9da0b..d8ee94baee 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.0.8 +* added configurable read repair chance to model definitions +* added configurable keyspace strategy class and replication factor to keyspace creator + 0.0.7 * fixed manual table name bug * changed model level db_name field to table_name diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 5a99e64c07..6f020f7620 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.7' +__version__ = '0.0.8' diff --git a/docs/conf.py b/docs/conf.py index a45c245c9d..2068f2641e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.0.7' +version = '0.0.8' # The full version, including alpha/beta/rc tags. -release = '0.0.7' +release = '0.0.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index a9658c482f..45619393e9 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.7' +version = '0.0.8' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From 27e5069db961a23c85fe84e8a12aed9b2d36042f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 12 Dec 2012 12:31:21 -0800 Subject: [PATCH 0082/3961] updated readme --- README.md | 61 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 4eceed1fb6..2a61f645af 100644 --- a/README.md +++ b/README.md @@ -22,43 +22,48 @@ pip install cqlengine ```python #first, define a model ->>> from cqlengine import columns ->>> from cqlengine.models import Model +from cqlengine import columns +from cqlengine.models import Model ->>> class ExampleModel(Model): ->>> example_id = columns.UUID(primary_key=True) ->>> example_type = columns.Integer(index=True) ->>> created_at = columns.DateTime() ->>> description = columns.Text(required=False) +class ExampleModel(Model): + read_repair_chance = 0.05 # optional - defaults to 0.1 + + example_id = columns.UUID(primary_key=True) + example_type = columns.Integer(index=True) + created_at = columns.DateTime() + description = columns.Text(required=False) #next, setup the connection to your cassandra server(s)... ->>> from cqlengine import connection ->>> connection.setup(['127.0.0.1:9160']) +from cqlengine import connection +connection.setup(['127.0.0.1:9160']) #...and create your CQL table ->>> from cqlengine.management import create_table ->>> create_table(ExampleModel) +from cqlengine.management import create_table +create_table(ExampleModel) #now we can create some rows: ->>> em1 = ExampleModel.create(example_type=0, description="example1") ->>> em2 = ExampleModel.create(example_type=0, description="example2") ->>> em3 = ExampleModel.create(example_type=0, description="example3") ->>> em4 = ExampleModel.create(example_type=0, description="example4") ->>> em5 = ExampleModel.create(example_type=1, description="example5") ->>> em6 = ExampleModel.create(example_type=1, description="example6") ->>> em7 = ExampleModel.create(example_type=1, description="example7") ->>> em8 = ExampleModel.create(example_type=1, description="example8") +em1 = ExampleModel.create(example_type=0, description="example1") +em2 = ExampleModel.create(example_type=0, description="example2") +em3 = ExampleModel.create(example_type=0, description="example3") +em4 = ExampleModel.create(example_type=0, description="example4") +em5 = ExampleModel.create(example_type=1, description="example5") +em6 = ExampleModel.create(example_type=1, description="example6") +em7 = ExampleModel.create(example_type=1, description="example7") +em8 = ExampleModel.create(example_type=1, description="example8") # Note: the UUID and DateTime columns will create uuid4 and datetime.now # values automatically if we don't specify them when creating new rows +# alternative syntax for creating new objects +ExampleModel(example_type=0, description="example9").save() + #and now we can run some queries against our table ->>> ExampleModel.objects.count() +ExampleModel.objects.count() 8 ->>> q = ExampleModel.objects(example_type=1) ->>> q.count() +q = ExampleModel.objects(example_type=1) +q.count() 4 ->>> for instance in q: ->>> print q.description +for instance in q: + print q.description example5 example6 example7 @@ -67,11 +72,11 @@ example8 #here we are applying additional filtering to an existing query #query objects are immutable, so calling filter returns a new #query object ->>> q2 = q.filter(example_id=em5.example_id) +q2 = q.filter(example_id=em5.example_id) ->>> q2.count() +q2.count() 1 ->>> for instance in q2: ->>> print q.description +for instance in q2: + print q.description example5 ``` From 3c5b7df847130366fae1c99f0f401a2840531720 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 12 Dec 2012 12:35:26 -0800 Subject: [PATCH 0083/3961] readme improvements --- README.md | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 2a61f645af..d70353999a 100644 --- a/README.md +++ b/README.md @@ -26,44 +26,40 @@ from cqlengine import columns from cqlengine.models import Model class ExampleModel(Model): - read_repair_chance = 0.05 # optional - defaults to 0.1 - + read_repair_chance = 0.05 # optional - defaults to 0.1 example_id = columns.UUID(primary_key=True) example_type = columns.Integer(index=True) created_at = columns.DateTime() description = columns.Text(required=False) #next, setup the connection to your cassandra server(s)... -from cqlengine import connection -connection.setup(['127.0.0.1:9160']) +>>> from cqlengine import connection +>>> connection.setup(['127.0.0.1:9160']) #...and create your CQL table -from cqlengine.management import create_table -create_table(ExampleModel) +>>> from cqlengine.management import create_table +>>> create_table(ExampleModel) #now we can create some rows: -em1 = ExampleModel.create(example_type=0, description="example1") -em2 = ExampleModel.create(example_type=0, description="example2") -em3 = ExampleModel.create(example_type=0, description="example3") -em4 = ExampleModel.create(example_type=0, description="example4") -em5 = ExampleModel.create(example_type=1, description="example5") -em6 = ExampleModel.create(example_type=1, description="example6") -em7 = ExampleModel.create(example_type=1, description="example7") -em8 = ExampleModel.create(example_type=1, description="example8") +>>> em1 = ExampleModel.create(example_type=0, description="example1") +>>> em2 = ExampleModel.create(example_type=0, description="example2") +>>> em3 = ExampleModel.create(example_type=0, description="example3") +>>> em4 = ExampleModel.create(example_type=0, description="example4") +>>> em5 = ExampleModel.create(example_type=1, description="example5") +>>> em6 = ExampleModel.create(example_type=1, description="example6") +>>> em7 = ExampleModel.create(example_type=1, description="example7") +>>> em8 = ExampleModel.create(example_type=1, description="example8") # Note: the UUID and DateTime columns will create uuid4 and datetime.now # values automatically if we don't specify them when creating new rows -# alternative syntax for creating new objects -ExampleModel(example_type=0, description="example9").save() - #and now we can run some queries against our table -ExampleModel.objects.count() +>>> ExampleModel.objects.count() 8 -q = ExampleModel.objects(example_type=1) -q.count() +>>> q = ExampleModel.objects(example_type=1) +>>> q.count() 4 -for instance in q: - print q.description +>>> for instance in q: +>>> print q.description example5 example6 example7 @@ -72,11 +68,11 @@ example8 #here we are applying additional filtering to an existing query #query objects are immutable, so calling filter returns a new #query object -q2 = q.filter(example_id=em5.example_id) +>>> q2 = q.filter(example_id=em5.example_id) -q2.count() +>>> q2.count() 1 -for instance in q2: - print q.description +>>> for instance in q2: +>>> print q.description example5 ``` From b1c9ad6f8e55876b776099b240f1872a3f26a551 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 13 Dec 2012 09:32:52 -0800 Subject: [PATCH 0084/3961] updating docs/readme --- README.md | 4 +--- docs/index.rst | 58 +++++++++++++++++++++++++------------------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 4eceed1fb6..56422d9fce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ cqlengine =============== -cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 ORM for Python with an interface similar to the Django orm and mongoengine [Documentation](https://cqlengine.readthedocs.org/en/latest/) @@ -11,8 +11,6 @@ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and m [Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) -**NOTE: cqlengine is in alpha and under development, some features may change (hopefully with notice). Make sure to check the changelog and test your app before upgrading** - ## Installation ``` pip install cqlengine diff --git a/docs/index.rst b/docs/index.rst index 2559a1006e..c556dfd12b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ cqlengine documentation ======================= -cqlengine is a Cassandra CQL ORM for Python with an interface similar to the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 ORM for Python with an interface similar to the Django orm and mongoengine :ref:`getting-started` @@ -28,43 +28,43 @@ Getting Started .. code-block:: python #first, define a model - >>> from cqlengine import columns - >>> from cqlengine import Model + from cqlengine import columns + from cqlengine import Model - >>> class ExampleModel(Model): - >>> example_id = columns.UUID(primary_key=True) - >>> example_type = columns.Integer(index=True) - >>> created_at = columns.DateTime() - >>> description = columns.Text(required=False) + class ExampleModel(Model): + example_id = columns.UUID(primary_key=True) + example_type = columns.Integer(index=True) + created_at = columns.DateTime() + description = columns.Text(required=False) #next, setup the connection to your cassandra server(s)... - >>> from cqlengine import connection - >>> connection.setup(['127.0.0.1:9160']) + from cqlengine import connection + connection.setup(['127.0.0.1:9160']) #...and create your CQL table - >>> from cqlengine.management import create_table - >>> create_table(ExampleModel) + from cqlengine.management import create_table + create_table(ExampleModel) #now we can create some rows: - >>> em1 = ExampleModel.create(example_type=0, description="example1") - >>> em2 = ExampleModel.create(example_type=0, description="example2") - >>> em3 = ExampleModel.create(example_type=0, description="example3") - >>> em4 = ExampleModel.create(example_type=0, description="example4") - >>> em5 = ExampleModel.create(example_type=1, description="example5") - >>> em6 = ExampleModel.create(example_type=1, description="example6") - >>> em7 = ExampleModel.create(example_type=1, description="example7") - >>> em8 = ExampleModel.create(example_type=1, description="example8") + em1 = ExampleModel.create(example_type=0, description="example1") + em2 = ExampleModel.create(example_type=0, description="example2") + em3 = ExampleModel.create(example_type=0, description="example3") + em4 = ExampleModel.create(example_type=0, description="example4") + em5 = ExampleModel.create(example_type=1, description="example5") + em6 = ExampleModel.create(example_type=1, description="example6") + em7 = ExampleModel.create(example_type=1, description="example7") + em8 = ExampleModel.create(example_type=1, description="example8") # Note: the UUID and DateTime columns will create uuid4 and datetime.now # values automatically if we don't specify them when creating new rows #and now we can run some queries against our table - >>> ExampleModel.objects.count() + ExampleModel.objects.count() 8 - >>> q = ExampleModel.objects(example_type=1) - >>> q.count() + q = ExampleModel.objects(example_type=1) + q.count() 4 - >>> for instance in q: - >>> print q.description + for instance in q: + print q.description example5 example6 example7 @@ -73,12 +73,12 @@ Getting Started #here we are applying additional filtering to an existing query #query objects are immutable, so calling filter returns a new #query object - >>> q2 = q.filter(example_id=em5.example_id) + q2 = q.filter(example_id=em5.example_id) - >>> q2.count() + q2.count() 1 - >>> for instance in q2: - >>> print q.description + for instance in q2: + print q.description example5 From 96a213b4b142af07e055a2d280c47a0a141f0e27 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 13 Dec 2012 09:38:35 -0800 Subject: [PATCH 0085/3961] editing doc index --- docs/index.rst | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index c556dfd12b..44b5b9b9fe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,33 +38,33 @@ Getting Started description = columns.Text(required=False) #next, setup the connection to your cassandra server(s)... - from cqlengine import connection - connection.setup(['127.0.0.1:9160']) + >>> from cqlengine import connection + >>> connection.setup(['127.0.0.1:9160']) #...and create your CQL table - from cqlengine.management import create_table - create_table(ExampleModel) + >>> from cqlengine.management import create_table + >>> create_table(ExampleModel) #now we can create some rows: - em1 = ExampleModel.create(example_type=0, description="example1") - em2 = ExampleModel.create(example_type=0, description="example2") - em3 = ExampleModel.create(example_type=0, description="example3") - em4 = ExampleModel.create(example_type=0, description="example4") - em5 = ExampleModel.create(example_type=1, description="example5") - em6 = ExampleModel.create(example_type=1, description="example6") - em7 = ExampleModel.create(example_type=1, description="example7") - em8 = ExampleModel.create(example_type=1, description="example8") + >>> em1 = ExampleModel.create(example_type=0, description="example1") + >>> em2 = ExampleModel.create(example_type=0, description="example2") + >>> em3 = ExampleModel.create(example_type=0, description="example3") + >>> em4 = ExampleModel.create(example_type=0, description="example4") + >>> em5 = ExampleModel.create(example_type=1, description="example5") + >>> em6 = ExampleModel.create(example_type=1, description="example6") + >>> em7 = ExampleModel.create(example_type=1, description="example7") + >>> em8 = ExampleModel.create(example_type=1, description="example8") # Note: the UUID and DateTime columns will create uuid4 and datetime.now # values automatically if we don't specify them when creating new rows #and now we can run some queries against our table - ExampleModel.objects.count() + >>> ExampleModel.objects.count() 8 - q = ExampleModel.objects(example_type=1) - q.count() + >>> q = ExampleModel.objects(example_type=1) + >>> q.count() 4 - for instance in q: - print q.description + >>> for instance in q: + >>> print q.description example5 example6 example7 @@ -73,12 +73,12 @@ Getting Started #here we are applying additional filtering to an existing query #query objects are immutable, so calling filter returns a new #query object - q2 = q.filter(example_id=em5.example_id) + >>> q2 = q.filter(example_id=em5.example_id) - q2.count() + >>> q2.count() 1 - for instance in q2: - print q.description + >>> for instance in q2: + >>> print q.description example5 From 585c75a5bb4e0d0d482b9805c7dd40da715dfe7a Mon Sep 17 00:00:00 2001 From: Eric Scrivner Date: Thu, 13 Dec 2012 20:58:20 -0800 Subject: [PATCH 0086/3961] Fix Vagrant Box URL --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 6d8ccc8ad1..191c5e8cdc 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -11,7 +11,7 @@ Vagrant::Config.run do |config| # The url from where the 'config.vm.box' box will be fetched if it # doesn't already exist on the user's system. - config.vm.box_url = "https://s3-us-west-2.amazonaws.com/graphplatform.swmirror/precise64.box" + config.vm.box_url = "http://files.vagrantup.com/precise64.box" # Boot with a GUI so you can see the screen. (Default is headless) # config.vm.boot_mode = :gui @@ -43,4 +43,4 @@ Vagrant::Config.run do |config| # puppet.manifest_file = "site.pp" puppet.module_path = "modules" end -end \ No newline at end of file +end From dc902163455f36c8ba34337373b24814829b028e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 22 Dec 2012 21:41:09 -0800 Subject: [PATCH 0087/3961] adding IDEA project files --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index bd169dfade..16029f08ec 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,11 @@ html/ .project .pydevproject .settings/ +.idea/ +*.iml + +.DS_Store + # Unit test / coverage reports .coverage From cd4f9afbed749dd735880785b2061e93657032e9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 22 Dec 2012 21:41:47 -0800 Subject: [PATCH 0088/3961] fixing column inheritance and short circuiting table name inheritance --- cqlengine/models.py | 22 ++++++++++++++++--- .../tests/model/test_class_construction.py | 17 ++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index e8a270459a..255b7ce808 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -16,8 +16,8 @@ class DoesNotExist(QueryException): pass class MultipleObjectsReturned(QueryException): pass #table names will be generated automatically from it's model and package name - #however, you can alse define them manually here - table_name = None + #however, you can also define them manually here + table_name = None #the keyspace for this model keyspace = 'cqlengine' @@ -112,6 +112,12 @@ def __new__(cls, name, bases, attrs): primary_keys = OrderedDict() pk_name = None + #get inherited properties + inherited_columns = OrderedDict() + for base in bases: + for k,v in getattr(base, '_defined_columns', {}).items(): + inherited_columns.setdefault(k,v) + def _transform_column(col_name, col_obj): column_dict[col_name] = col_obj if col_obj.primary_key: @@ -129,7 +135,13 @@ def _transform_column(col_name, col_obj): column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.Column)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) - #prepend primary key if none has been defined + column_definitions = inherited_columns.items() + column_definitions + + #columns defined on model, excludes automatically + #defined columns + defined_columns = OrderedDict(column_definitions) + + #prepend primary key if one hasn't been defined if not any([v.primary_key for k,v in column_definitions]): k,v = 'id', columns.UUID(primary_key=True) column_definitions = [(k,v)] + column_definitions @@ -163,9 +175,13 @@ def _transform_column(col_name, col_obj): for field_name, col in column_dict.items(): db_map[col.db_field_name] = field_name + #short circuit table_name inheritance + attrs['table_name'] = attrs.get('table_name') + #add management members to the class attrs['_columns'] = column_dict attrs['_primary_keys'] = primary_keys + attrs['_defined_columns'] = defined_columns attrs['_db_map'] = db_map attrs['_pk_name'] = pk_name attrs['_dynamic_columns'] = {} diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index d01011ec42..77b1d73775 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -80,6 +80,19 @@ class Stuff(Model): self.assertEquals(inst1.num, 5) self.assertEquals(inst2.num, 7) + def test_superclass_fields_are_inherited(self): + """ + Tests that fields defined on the super class are inherited properly + """ + class TestModel(Model): + text = columns.Text() + + class InheritedModel(TestModel): + numbers = columns.Integer() + + assert 'text' in InheritedModel._columns + assert 'numbers' in InheritedModel._columns + def test_normal_fields_can_be_defined_between_primary_keys(self): """ Tests tha non primary key fields can be defined between primary key fields @@ -118,6 +131,10 @@ def test_proper_table_naming(self): assert self.RenamedTest.column_family_name(include_keyspace=False) == 'manual_name' assert self.RenamedTest.column_family_name(include_keyspace=True) == 'whatever.manual_name' + def test_manual_table_name_is_not_inherited(self): + class InheritedTest(self.RenamedTest): pass + assert InheritedTest.table_name is None + From 3e1f96c4f0d77fdadb81bb1d7578b1aa36546a98 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 22 Dec 2012 21:45:42 -0800 Subject: [PATCH 0089/3961] adding not about short circuited table_name inheritance --- docs/topics/models.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 86bd08d437..86788a2530 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -125,7 +125,7 @@ Model Attributes .. attribute:: Model.table_name - *Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix + *Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix. Manually defined table names are not inherited. .. attribute:: Model.keyspace From 31633bb1a46f3d26b1d9390fefc9232de49f9b6e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 22 Dec 2012 22:06:06 -0800 Subject: [PATCH 0090/3961] updating version number and change log --- changelog | 4 ++++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index d8ee94baee..a724be7dfa 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.0.9 +* fixed column inheritance bug +* manually defined table names are no longer inherited by subclasses + 0.0.8 * added configurable read repair chance to model definitions * added configurable keyspace strategy class and replication factor to keyspace creator diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 6f020f7620..a3f227db2b 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.8' +__version__ = '0.0.9' diff --git a/docs/conf.py b/docs/conf.py index 2068f2641e..d4defc9b6e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.0.8' +version = '0.0.9' # The full version, including alpha/beta/rc tags. -release = '0.0.8' +release = '0.0.9' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 45619393e9..470df3540c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.8' +version = '0.0.9' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From e7d4f9bf9f260e50f70045da55a13e60c06a1c67 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 6 Jan 2013 09:31:37 -0800 Subject: [PATCH 0091/3961] Changing base exception from BaseException to Exception --- cqlengine/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/exceptions.py b/cqlengine/exceptions.py index 0b1ff55f1e..87b5cdc4c5 100644 --- a/cqlengine/exceptions.py +++ b/cqlengine/exceptions.py @@ -1,5 +1,5 @@ #cqlengine exceptions -class CQLEngineException(BaseException): pass +class CQLEngineException(Exception): pass class ModelException(CQLEngineException): pass class ValidationError(CQLEngineException): pass From 79900278450364f8d064dcaaa7ac712a349c95bf Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 6 Jan 2013 09:37:20 -0800 Subject: [PATCH 0092/3961] fixed bug where default values that evaluate to false were ignored --- cqlengine/columns.py | 2 +- cqlengine/tests/columns/test_validation.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index b276b5d75b..df01bef72e 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -97,7 +97,7 @@ def to_database(self, value): @property def has_default(self): - return bool(self.default) + return self.default is not None @property def is_primary_key(self): diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index a94f87a093..00a4097b18 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -84,6 +84,15 @@ def test_timeuuid_io(self): assert t1.timeuuid.time == t1.timeuuid.time +class TestInteger(BaseCassEngTestCase): + class IntegerTest(Model): + test_id = UUID(primary_key=True) + value = Integer(default=0) + + def test_default_zero_fields_validate(self): + """ Tests that integer columns with a default value of 0 validate """ + it = self.IntegerTest() + it.validate() From e529f33dbed84f5efd74dffc8e0192e95aa08afe Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 6 Jan 2013 10:13:04 -0800 Subject: [PATCH 0093/3961] adding magic methods for model instance equality comparisons --- cqlengine/models.py | 6 +++ .../tests/model/test_equality_operations.py | 52 +++++++++++++++++++ cqlengine/tests/model/test_model_io.py | 1 - 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 cqlengine/tests/model/test_equality_operations.py diff --git a/cqlengine/models.py b/cqlengine/models.py index 255b7ce808..bf9d157aca 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -31,6 +31,12 @@ def __init__(self, **values): value_mngr = column.value_manager(self, column, value) self._values[name] = value_mngr + def __eq__(self, other): + return self.as_dict() == other.as_dict() + + def __ne__(self, other): + return not self.__eq__(other) + @classmethod def column_family_name(cls, include_keyspace=True): """ diff --git a/cqlengine/tests/model/test_equality_operations.py b/cqlengine/tests/model/test_equality_operations.py new file mode 100644 index 0000000000..4d5b95219b --- /dev/null +++ b/cqlengine/tests/model/test_equality_operations.py @@ -0,0 +1,52 @@ +from unittest import skip +from cqlengine.tests.base import BaseCassEngTestCase + +from cqlengine.management import create_table +from cqlengine.management import delete_table +from cqlengine.models import Model +from cqlengine import columns + +class TestModel(Model): + count = columns.Integer() + text = columns.Text(required=False) + +class TestEqualityOperators(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestEqualityOperators, cls).setUpClass() + create_table(TestModel) + + def setUp(self): + super(TestEqualityOperators, self).setUp() + self.t0 = TestModel.create(count=5, text='words') + self.t1 = TestModel.create(count=5, text='words') + + @classmethod + def tearDownClass(cls): + super(TestEqualityOperators, cls).tearDownClass() + delete_table(TestModel) + + def test_an_instance_evaluates_as_equal_to_itself(self): + """ + """ + assert self.t0 == self.t0 + + def test_two_instances_referencing_the_same_rows_and_different_values_evaluate_not_equal(self): + """ + """ + t0 = TestModel.get(id=self.t0.id) + t0.text = 'bleh' + assert t0 != self.t0 + + def test_two_instances_referencing_the_same_rows_and_values_evaluate_equal(self): + """ + """ + t0 = TestModel.get(id=self.t0.id) + assert t0 == self.t0 + + def test_two_instances_referencing_different_rows_evaluate_to_not_equal(self): + """ + """ + assert self.t0 != self.t1 + diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index f65b44efd2..73eb1e2496 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -4,7 +4,6 @@ from cqlengine.management import create_table from cqlengine.management import delete_table from cqlengine.models import Model -from cqlengine.models import Model from cqlengine import columns class TestModel(Model): From 16e4a6293288d272c0e8d02f9ae6e682bebca761 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 6 Jan 2013 10:28:07 -0800 Subject: [PATCH 0094/3961] adding min & max length validation to the Text column type --- cqlengine/columns.py | 17 +++++++++ cqlengine/tests/columns/test_validation.py | 43 ++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index df01bef72e..2f6ee4f4ac 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -146,6 +146,23 @@ class Ascii(Column): class Text(Column): db_type = 'text' + def __init__(self, *args, **kwargs): + self.min_length = kwargs.pop('min_length', 1 if kwargs.get('required', True) else None) + self.max_length = kwargs.pop('max_length', None) + super(Text, self).__init__(*args, **kwargs) + + def validate(self, value): + value = super(Text, self).validate(value) + if not isinstance(value, (basestring, bytearray)) and value is not None: + raise ValidationError('{} is not a string'.format(type(value))) + if self.max_length: + if len(value) > self.max_length: + raise ValidationError('{} is longer than {} characters'.format(self.column_name, self.max_length)) + if self.min_length: + if len(value) < self.min_length: + raise ValidationError('{} is shorter than {} characters'.format(self.column_name, self.min_length)) + return value + class Integer(Column): db_type = 'int' diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 00a4097b18..4921b67256 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -1,6 +1,7 @@ #tests the behavior of the column classes from datetime import datetime from decimal import Decimal as D +from cqlengine import ValidationError from cqlengine.tests.base import BaseCassEngTestCase @@ -94,6 +95,48 @@ def test_default_zero_fields_validate(self): it = self.IntegerTest() it.validate() +class TestText(BaseCassEngTestCase): + + def test_min_length(self): + #min len defaults to 1 + col = Text() + + with self.assertRaises(ValidationError): + col.validate('') + + col.validate('b') + + #test not required defaults to 0 + Text(required=False).validate('') + + #test arbitrary lengths + Text(min_length=0).validate('') + Text(min_length=5).validate('blake') + Text(min_length=5).validate('blaketastic') + with self.assertRaises(ValidationError): + Text(min_length=6).validate('blake') + + def test_max_length(self): + + Text(max_length=5).validate('blake') + with self.assertRaises(ValidationError): + Text(max_length=5).validate('blaketastic') + + def test_type_checking(self): + Text().validate('string') + Text().validate(u'unicode') + Text().validate(bytearray('bytearray')) + + with self.assertRaises(ValidationError): + Text().validate(None) + + with self.assertRaises(ValidationError): + Text().validate(5) + + with self.assertRaises(ValidationError): + Text().validate(True) + + From d0ac0cfcd99cb2619401e886c5fed7f058edfa5f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 6 Jan 2013 10:38:10 -0800 Subject: [PATCH 0095/3961] updating version number, documentation, and changelog --- changelog | 5 +++++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- docs/topics/columns.rst | 8 ++++++++ setup.py | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index a724be7dfa..8763c4ad70 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,10 @@ CHANGELOG +0.1 +* added min_length and max_length validators to the Text column +* added == and != equality operators to model class +* fixed bug with default values that would evaluate to False (ie: 0, '') + 0.0.9 * fixed column inheritance bug * manually defined table names are no longer inherited by subclasses diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index a3f227db2b..33cf4f1397 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.0.9' +__version__ = '0.1' diff --git a/docs/conf.py b/docs/conf.py index d4defc9b6e..f2d45ab7a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.0.9' +version = '0.1' # The full version, including alpha/beta/rc tags. -release = '0.0.9' +release = '0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index dc87f8cbdf..9090b0bc0f 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -24,6 +24,14 @@ Columns columns.Text() + **options** + + :attr:`~columns.Text.min_length` + Sets the minimum length of this string. If this field is not set , and the column is not a required field, it defaults to 0, otherwise 1. + + :attr:`~columns.Text.max_length` + Sets the maximum length of this string. Defaults to None + .. class:: Integer() Stores an integer value :: diff --git a/setup.py b/setup.py index 470df3540c..65c4806c53 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.0.9' +version = '0.1' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From 9386d55513ae9233e2281c8f0ffbabf37ce656f9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 8 Feb 2013 07:04:49 -0800 Subject: [PATCH 0096/3961] fixed a bug in table auto naming, which would fail if the generated name started with an _ --- cqlengine/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index 255b7ce808..f979a84e48 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -52,6 +52,7 @@ def column_family_name(cls, include_keyspace=True): #trim to less than 48 characters or cassandra will complain cf_name = cf_name[-48:] cf_name = cf_name.lower() + cf_name = re.sub(r'^_+', '', cf_name) if not include_keyspace: return cf_name return '{}.{}'.format(cls.keyspace, cf_name) From 2aa17094e3af71bb3cad7b3508f703376973e3d4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 8 Feb 2013 07:28:19 -0800 Subject: [PATCH 0097/3961] adding check for existing indexes --- cqlengine/management.py | 9 +++++++-- cqlengine/tests/model/test_model_io.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 4e764b55de..7031b8585c 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -47,11 +47,16 @@ def add_column(col): con.execute(qs) + #get existing index names + ks_info = con.con.client.describe_keyspace(model.keyspace) + cf_def = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name][0] + idx_names = [i.index_name for i in cf_def.column_metadata] + idx_names = filter(None, idx_names) + indexes = [c for n,c in model._columns.items() if c.index] if indexes: for column in indexes: - #TODO: check for existing index... - #can that be determined from the connection client? + if column.db_index_name in idx_names: continue qs = ['CREATE INDEX {}'.format(column.db_index_name)] qs += ['ON {}'.format(cf_name)] qs += ['({})'.format(column.db_field_name)] diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index f65b44efd2..61e9e73996 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -66,3 +66,9 @@ def test_column_deleting_works_properly(self): assert tm2._values['text'].initial_value is None + def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): + """ + """ + create_table(TestModel) + create_table(TestModel) + From b8ba592ede1d59a2ee5b3d9ef213c8eb7b5ab6f8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 8 Feb 2013 07:32:39 -0800 Subject: [PATCH 0098/3961] version number bump --- changelog | 3 +++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 8763c4ad70..153d5617cd 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.1.1 +* fixed a bug occurring when creating tables from the REPL + 0.1 * added min_length and max_length validators to the Text column * added == and != equality operators to model class diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 33cf4f1397..cb73106c62 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.1' +__version__ = '0.1.1' diff --git a/docs/conf.py b/docs/conf.py index f2d45ab7a1..d25ee3a0bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.1' +version = '0.1.1' # The full version, including alpha/beta/rc tags. -release = '0.1' +release = '0.1.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 65c4806c53..f1878a24b3 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.1' +version = '0.1.1' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From ba53c61b7b66cac2340b845e7cccdfa6911ce805 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 8 Feb 2013 09:11:29 -0800 Subject: [PATCH 0099/3961] fixing potential bug with column family discovery --- cqlengine/management.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 7031b8585c..65d6b0e2e3 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -49,8 +49,8 @@ def add_column(col): #get existing index names ks_info = con.con.client.describe_keyspace(model.keyspace) - cf_def = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name][0] - idx_names = [i.index_name for i in cf_def.column_metadata] + cf_defs = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name] + idx_names = [i.index_name for i in cf_defs[0].column_metadata] if cf_defs else [] idx_names = filter(None, idx_names) indexes = [c for n,c in model._columns.items() if c.index] From 7467f868976f61785229e0461b4a147b1a30d26c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 8 Feb 2013 09:12:12 -0800 Subject: [PATCH 0100/3961] version number bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index cb73106c62..1f8c70ad36 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,5 @@ from cqlengine.columns import * from cqlengine.models import Model -__version__ = '0.1.1' +__version__ = '0.1.2' diff --git a/docs/conf.py b/docs/conf.py index d25ee3a0bb..59c9a1239b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.1.1' +version = '0.1.2' # The full version, including alpha/beta/rc tags. -release = '0.1.1' +release = '0.1.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index f1878a24b3..4190fb384f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.1.1' +version = '0.1.2' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From b76244a2f4acffe6b2e0d5bab5f21f21a5111588 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 8 Feb 2013 09:44:13 -0800 Subject: [PATCH 0101/3961] updating docs --- README.md | 16 ++++++++-------- docs/index.rst | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8af88071ca..3e5b06bfa6 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ class ExampleModel(Model): >>> create_table(ExampleModel) #now we can create some rows: ->>> em1 = ExampleModel.create(example_type=0, description="example1") ->>> em2 = ExampleModel.create(example_type=0, description="example2") ->>> em3 = ExampleModel.create(example_type=0, description="example3") ->>> em4 = ExampleModel.create(example_type=0, description="example4") ->>> em5 = ExampleModel.create(example_type=1, description="example5") ->>> em6 = ExampleModel.create(example_type=1, description="example6") ->>> em7 = ExampleModel.create(example_type=1, description="example7") ->>> em8 = ExampleModel.create(example_type=1, description="example8") +>>> em1 = ExampleModel.create(example_type=0, description="example1", created_at=datetime.now()) +>>> em2 = ExampleModel.create(example_type=0, description="example2", created_at=datetime.now()) +>>> em3 = ExampleModel.create(example_type=0, description="example3", created_at=datetime.now()) +>>> em4 = ExampleModel.create(example_type=0, description="example4", created_at=datetime.now()) +>>> em5 = ExampleModel.create(example_type=1, description="example5", created_at=datetime.now()) +>>> em6 = ExampleModel.create(example_type=1, description="example6", created_at=datetime.now()) +>>> em7 = ExampleModel.create(example_type=1, description="example7", created_at=datetime.now()) +>>> em8 = ExampleModel.create(example_type=1, description="example8", created_at=datetime.now()) # Note: the UUID and DateTime columns will create uuid4 and datetime.now # values automatically if we don't specify them when creating new rows diff --git a/docs/index.rst b/docs/index.rst index 44b5b9b9fe..dbc64e54f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -46,14 +46,14 @@ Getting Started >>> create_table(ExampleModel) #now we can create some rows: - >>> em1 = ExampleModel.create(example_type=0, description="example1") - >>> em2 = ExampleModel.create(example_type=0, description="example2") - >>> em3 = ExampleModel.create(example_type=0, description="example3") - >>> em4 = ExampleModel.create(example_type=0, description="example4") - >>> em5 = ExampleModel.create(example_type=1, description="example5") - >>> em6 = ExampleModel.create(example_type=1, description="example6") - >>> em7 = ExampleModel.create(example_type=1, description="example7") - >>> em8 = ExampleModel.create(example_type=1, description="example8") + >>> em1 = ExampleModel.create(example_type=0, description="example1", created_at=datetime.now()) + >>> em2 = ExampleModel.create(example_type=0, description="example2", created_at=datetime.now()) + >>> em3 = ExampleModel.create(example_type=0, description="example3", created_at=datetime.now()) + >>> em4 = ExampleModel.create(example_type=0, description="example4", created_at=datetime.now()) + >>> em5 = ExampleModel.create(example_type=1, description="example5", created_at=datetime.now()) + >>> em6 = ExampleModel.create(example_type=1, description="example6", created_at=datetime.now()) + >>> em7 = ExampleModel.create(example_type=1, description="example7", created_at=datetime.now()) + >>> em8 = ExampleModel.create(example_type=1, description="example8", created_at=datetime.now()) # Note: the UUID and DateTime columns will create uuid4 and datetime.now # values automatically if we don't specify them when creating new rows From 2d6aab505b6a387ede22b3f80da028306d8ed631 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Feb 2013 10:01:54 -0800 Subject: [PATCH 0102/3961] adding support for 1.2 style keyspace creation --- cqlengine/connection.py | 2 ++ cqlengine/management.py | 46 ++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 8e1f70709d..52bc5cdef1 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -165,6 +165,8 @@ def execute(self, query, params={}): self.cur = self.con.cursor() self.cur.execute(query, params) return self.cur + except cql.ProgrammingError as ex: + raise CQLEngineException(unicode(ex)) except TTransportException: #TODO: check for other errors raised in the event of a connection / server problem #move to the next connection and set the connection pool diff --git a/cqlengine/management.py b/cqlengine/management.py index 65d6b0e2e3..7b4e52abe2 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,11 +1,43 @@ +import json + from cqlengine.connection import connection_manager +from cqlengine.exceptions import CQLEngineException + +def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, durable_writes=True, **replication_values): + """ + creates a keyspace -def create_keyspace(name, strategy_class = 'SimpleStrategy', replication_factor=3): + :param name: name of keyspace to create + :param strategy_class: keyspace replication strategy class + :param replication_factor: keyspace replication factor + :param durable_writes: 1.2 only, write log is bypassed if set to False + :param **replication_values: 1.2 only, additional values to ad to the replication data map + """ with connection_manager() as con: if name not in [k.name for k in con.con.client.describe_keyspaces()]: - con.execute("""CREATE KEYSPACE {} - WITH strategy_class = '{}' - AND strategy_options:replication_factor={};""".format(name, strategy_class, replication_factor)) + + try: + #Try the 1.1 method + con.execute("""CREATE KEYSPACE {} + WITH strategy_class = '{}' + AND strategy_options:replication_factor={};""".format(name, strategy_class, replication_factor)) + except CQLEngineException: + #try the 1.2 method + replication_map = { + 'class': strategy_class, + 'replication_factor':replication_factor + } + replication_map.update(replication_values) + + query = """ + CREATE KEYSPACE {} + WITH REPLICATION = {} + """.format(name, json.dumps(replication_map).replace('"', "'")) + + if strategy_class != 'SimpleStrategy': + query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false') + + con.execute(query) def delete_keyspace(name): with connection_manager() as con: @@ -31,8 +63,8 @@ def create_table(model, create_missing_keyspace=True): pkeys = [] qtypes = [] def add_column(col): - s = '{} {}'.format(col.db_field_name, col.db_type) - if col.primary_key: pkeys.append(col.db_field_name) + s = '"{}" {}'.format(col.db_field_name, col.db_type) + if col.primary_key: pkeys.append('"{}"'.format(col.db_field_name)) qtypes.append(s) for name, col in model._columns.items(): add_column(col) @@ -42,7 +74,7 @@ def add_column(col): qs += ['({})'.format(', '.join(qtypes))] # add read_repair_chance - qs += ["WITH read_repair_chance = {}".format(model.read_repair_chance)] + qs += ['WITH read_repair_chance = {}'.format(model.read_repair_chance)] qs = ' '.join(qs) con.execute(qs) From daadbeb364c59998fd543d7e789bd0f171202209 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Feb 2013 10:12:34 -0800 Subject: [PATCH 0103/3961] adding workaround for detecting existing tables, since 1.2 won't return the column family names with the describe keyspace method --- cqlengine/management.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 7b4e52abe2..28da51c868 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -15,7 +15,6 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, """ with connection_manager() as con: if name not in [k.name for k in con.con.client.describe_keyspaces()]: - try: #Try the 1.1 method con.execute("""CREATE KEYSPACE {} @@ -77,7 +76,13 @@ def add_column(col): qs += ['WITH read_repair_chance = {}'.format(model.read_repair_chance)] qs = ' '.join(qs) - con.execute(qs) + try: + con.execute(qs) + except CQLEngineException as ex: + # 1.2 doesn't return cf names, so we have to examine the exception + # and ignore if it says the column family already exists + if "Cannot add already existing column family" not in unicode(ex): + raise #get existing index names ks_info = con.con.client.describe_keyspace(model.keyspace) From 7fce1cda23d8a402fef5767e6a166ae309ee3597 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Feb 2013 11:03:59 -0800 Subject: [PATCH 0104/3961] fixing index creation to deal with broken schema discovery in cassandra 1.2 --- cqlengine/management.py | 13 ++++++++--- cqlengine/query.py | 10 ++++----- cqlengine/tests/model/test_model_io.py | 30 ++++++++++++++++++++++++++ cqlengine/tests/query/test_queryset.py | 4 ++-- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 28da51c868..2b45622129 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -84,7 +84,7 @@ def add_column(col): if "Cannot add already existing column family" not in unicode(ex): raise - #get existing index names + #get existing index names, skip ones that already exist ks_info = con.con.client.describe_keyspace(model.keyspace) cf_defs = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name] idx_names = [i.index_name for i in cf_defs[0].column_metadata] if cf_defs else [] @@ -96,9 +96,16 @@ def add_column(col): if column.db_index_name in idx_names: continue qs = ['CREATE INDEX {}'.format(column.db_index_name)] qs += ['ON {}'.format(cf_name)] - qs += ['({})'.format(column.db_field_name)] + qs += ['("{}")'.format(column.db_field_name)] qs = ' '.join(qs) - con.execute(qs) + + try: + con.execute(qs) + except CQLEngineException as ex: + # 1.2 doesn't return cf names, so we have to examine the exception + # and ignore if it says the index already exists + if "Index already exists" not in unicode(ex): + raise def delete_table(model): diff --git a/cqlengine/query.py b/cqlengine/query.py index e75f563382..0b1973f7ef 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -39,7 +39,7 @@ def cql(self): Returns this operator's portion of the WHERE clause :param valname: the dict key that this operator's compare value will be found in """ - return '{} {} :{}'.format(self.column.db_field_name, self.cql_symbol, self.identifier) + return '"{}" {} :{}'.format(self.column.db_field_name, self.cql_symbol, self.identifier) def validate_operator(self): """ @@ -205,7 +205,7 @@ def _select_query(self): fields = [f for f in fields if f in self._only_fields] db_fields = [self.model._columns[f].db_field_name for f in fields] - qs = ['SELECT {}'.format(', '.join(db_fields))] + qs = ['SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields]))] qs += ['FROM {}'.format(self.column_family_name)] if self._where: @@ -389,7 +389,7 @@ def order_by(self, colname): "Can't order by the first primary key, clustering (secondary) keys only") clone = copy.deepcopy(self) - clone._order = '{} {}'.format(column.db_field_name, order_type) + clone._order = '"{}" {}'.format(column.db_field_name, order_type) return clone def count(self): @@ -478,7 +478,7 @@ def save(self, instance): field_names = zip(*value_pairs)[0] field_values = dict(value_pairs) qs = ["INSERT INTO {}".format(self.column_family_name)] - qs += ["({})".format(', '.join(field_names))] + qs += ["({})".format(', '.join(['"{}"'.format(f) for f in field_names]))] qs += ['VALUES'] qs += ["({})".format(', '.join([':'+f for f in field_names]))] qs = ' '.join(qs) @@ -492,7 +492,7 @@ def save(self, instance): del_fields = [self.model._columns[f] for f in deleted] del_fields = [f.db_field_name for f in del_fields if not f.primary_key] pks = self.model._primary_keys - qs = ['DELETE {}'.format(', '.join(del_fields))] + qs = ['DELETE {}'.format(', '.join(['"{}"'.format(f) for f in del_fields]))] qs += ['FROM {}'.format(self.column_family_name)] qs += ['WHERE'] eq = lambda col: '{0} = :{0}'.format(v.column.db_field_name) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 68dc18c340..e24da14175 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -71,3 +71,33 @@ def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): create_table(TestModel) create_table(TestModel) +class IndexDefinitionModel(Model): + key = columns.UUID(primary_key=True) + val = columns.Text(index=True) + +class TestIndexedColumnDefinition(BaseCassEngTestCase): + + def test_exception_isnt_raised_if_an_index_is_defined_more_than_once(self): + create_table(IndexDefinitionModel) + create_table(IndexDefinitionModel) + +class ReservedWordModel(Model): + token = columns.Text(primary_key=True) + insert = columns.Integer(index=True) + +class TestQueryQuoting(BaseCassEngTestCase): + + def test_reserved_cql_words_can_be_used_as_column_names(self): + """ + """ + create_table(ReservedWordModel) + + model1 = ReservedWordModel.create(token='1', insert=5) + + model2 = ReservedWordModel.filter(token=1) + + assert len(model2) == 1 + assert model1.token == model2[0].token + assert model1.insert == model2[0].insert + + diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 31178b16c5..c80a9386e9 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -55,12 +55,12 @@ def test_where_clause_generation(self): query1 = TestModel.objects(test_id=5) ids = [o.identifier for o in query1._where] where = query1._where_clause() - assert where == 'test_id = :{}'.format(*ids) + assert where == '"test_id" = :{}'.format(*ids) query2 = query1.filter(expected_result__gte=1) ids = [o.identifier for o in query2._where] where = query2._where_clause() - assert where == 'test_id = :{} AND expected_result >= :{}'.format(*ids) + assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) def test_querystring_generation(self): From f37ee97f2e06f97815c0fdd059798b5f3e121ac0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Feb 2013 12:51:46 -0800 Subject: [PATCH 0105/3961] adding support for the allow filtering flag in cassandra 1.2 --- cqlengine/columns.py | 3 +++ cqlengine/models.py | 4 ++++ cqlengine/query.py | 25 +++++++++++++++++++++++++ cqlengine/tests/query/test_queryset.py | 6 +++++- 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 2f6ee4f4ac..b9d3d7c136 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -59,6 +59,9 @@ def __init__(self, primary_key=False, index=False, db_field=None, default=None, self.default = default self.required = required + #only the model meta class should touch this + self._partition_key = False + #the column name in the model definition self.column_name = None diff --git a/cqlengine/models.py b/cqlengine/models.py index 45475bfa03..07a7b47900 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -118,6 +118,7 @@ def __new__(cls, name, bases, attrs): column_dict = OrderedDict() primary_keys = OrderedDict() pk_name = None + primary_key = None #get inherited properties inherited_columns = OrderedDict() @@ -158,6 +159,8 @@ def _transform_column(col_name, col_obj): for k,v in column_definitions: if pk_name is None and v.primary_key: pk_name = k + primary_key = v + v._partition_key = True _transform_column(k,v) #setup primary key shortcut @@ -191,6 +194,7 @@ def _transform_column(col_name, col_obj): attrs['_defined_columns'] = defined_columns attrs['_db_map'] = db_map attrs['_pk_name'] = pk_name + attrs['_primary_key'] = primary_key attrs['_dynamic_columns'] = {} #create the class and add a QuerySet to it diff --git a/cqlengine/query.py b/cqlengine/query.py index 0b1973f7ef..d76e14c004 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -129,6 +129,8 @@ def __init__(self, model): #ordering arguments self._order = None + self._allow_filtering = False + #CQL has a default limit of 10000, it's defined here #because explicit is better than implicit self._limit = 10000 @@ -180,6 +182,14 @@ def _validate_where_syntax(self): equal_ops = [w for w in self._where if isinstance(w, EqualsOperator)] if not any([w.column.primary_key or w.column.index for w in equal_ops]): raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') + + if not self._allow_filtering: + #if the query is not on an indexed field + if not any([w.column.index for w in equal_ops]): + if not any([w.column._partition_key for w in equal_ops]): + raise QueryException('Filtering on a clustering key without a partition key is not allowed unless allow_filtering() is called on the querset') + + #TODO: abuse this to see if we can get cql to raise an exception def _where_clause(self): @@ -217,6 +227,9 @@ def _select_query(self): if self._limit: qs += ['LIMIT {}'.format(self._limit)] + if self._allow_filtering: + qs += ['ALLOW FILTERING'] + return ' '.join(qs) #----Reads------ @@ -400,6 +413,9 @@ def count(self): qs += ['FROM {}'.format(self.column_family_name)] if self._where: qs += ['WHERE {}'.format(self._where_clause())] + if self._allow_filtering: + qs += ['ALLOW FILTERING'] + qs = ' '.join(qs) with connection_manager() as con: @@ -425,6 +441,15 @@ def limit(self, v): clone._limit = v return clone + def allow_filtering(self): + """ + Enables the unwise practive of querying on a clustering + key without also defining a partition key + """ + clone = copy.deepcopy(self) + clone._allow_filtering = True + return clone + def _only_or_defer(self, action, fields): clone = copy.deepcopy(self) if clone._defer_fields or clone._only_fields: diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index c80a9386e9..3eff054f7b 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -165,7 +165,7 @@ def test_iteration(self): compare_set.remove(val) assert len(compare_set) == 0 - q = TestModel.objects(attempt_id=3) + q = TestModel.objects(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)]) @@ -241,6 +241,10 @@ def test_get_multipleobjects_exception(self): with self.assertRaises(TestModel.MultipleObjectsReturned): TestModel.objects.get(test_id=1) + def test_allow_filtering_flag(self): + """ + """ + class TestQuerySetOrdering(BaseQuerySetUsage): From 584c439434ebe93253fdc39ae098133c0f7db6ee Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Feb 2013 13:35:32 -0800 Subject: [PATCH 0106/3961] updating delete table to work with Cassandra 1.2 --- cqlengine/management.py | 9 +++++---- cqlengine/tests/management/test_management.py | 16 +++++++++++++--- cqlengine/tests/query/test_datetime_queries.py | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 cqlengine/tests/query/test_datetime_queries.py diff --git a/cqlengine/management.py b/cqlengine/management.py index 2b45622129..20ec5741f3 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -109,11 +109,12 @@ def add_column(col): def delete_table(model): - #check that model exists cf_name = model.column_family_name() - raw_cf_name = model.column_family_name(include_keyspace=False) with connection_manager() as con: - ks_info = con.con.client.describe_keyspace(model.keyspace) - if any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): + try: con.execute('drop table {};'.format(cf_name)) + except CQLEngineException as ex: + #don't freak out if the table doesn't exist + if 'Cannot drop non existing column family' not in unicode(ex): + raise diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index f3862a9527..ac542bb8e6 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,9 +1,11 @@ +from cqlengine.management import create_table, delete_table from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.connection import ConnectionPool from mock import Mock from cqlengine import management +from cqlengine.tests.query.test_queryset import TestModel class ConnectionPoolTestCase(BaseCassEngTestCase): @@ -42,6 +44,14 @@ class CreateKeyspaceTest(BaseCassEngTestCase): def test_create_succeeeds(self): management.create_keyspace('test_keyspace') management.delete_keyspace('test_keyspace') - - - \ No newline at end of file + +class DeleteTableTest(BaseCassEngTestCase): + + def test_multiple_deletes_dont_fail(self): + """ + + """ + create_table(TestModel) + + delete_table(TestModel) + delete_table(TestModel) diff --git a/cqlengine/tests/query/test_datetime_queries.py b/cqlengine/tests/query/test_datetime_queries.py new file mode 100644 index 0000000000..f6150a6d76 --- /dev/null +++ b/cqlengine/tests/query/test_datetime_queries.py @@ -0,0 +1 @@ +__author__ = 'bdeggleston' From a20a26340787676847ef5a58e1d43602f6b89874 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Feb 2013 13:35:59 -0800 Subject: [PATCH 0107/3961] fixing datetime query bug --- cqlengine/query.py | 2 +- .../tests/query/test_datetime_queries.py | 48 ++++++++++++++++++- cqlengine/tests/query/test_queryset.py | 2 - 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index d76e14c004..840d7b2fe0 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -74,7 +74,7 @@ def get_dict(self): this should return the dict: {'colval':} SELECT * FROM column_family WHERE colname=:colval """ - return {self.identifier: self.value} + return {self.identifier: self.column.to_database(self.value)} @classmethod def get_operator(cls, symbol): diff --git a/cqlengine/tests/query/test_datetime_queries.py b/cqlengine/tests/query/test_datetime_queries.py index f6150a6d76..044e12328c 100644 --- a/cqlengine/tests/query/test_datetime_queries.py +++ b/cqlengine/tests/query/test_datetime_queries.py @@ -1 +1,47 @@ -__author__ = 'bdeggleston' +from datetime import datetime, timedelta +from uuid import uuid4 + +from cqlengine.tests.base import BaseCassEngTestCase + +from cqlengine.exceptions import ModelException +from cqlengine.management import create_table +from cqlengine.management import delete_table +from cqlengine.models import Model +from cqlengine import columns +from cqlengine import query + +class DateTimeQueryTestModel(Model): + user = columns.Integer(primary_key=True) + day = columns.DateTime(primary_key=True) + data = columns.Text() + +class TestDateTimeQueries(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestDateTimeQueries, cls).setUpClass() + create_table(DateTimeQueryTestModel) + + cls.base_date = datetime.now() - timedelta(days=10) + for x in range(7): + for y in range(10): + DateTimeQueryTestModel.create( + user=x, + day=(cls.base_date+timedelta(days=y)), + data=str(uuid4()) + ) + + + @classmethod + def tearDownClass(cls): + super(TestDateTimeQueries, cls).tearDownClass() + delete_table(DateTimeQueryTestModel) + + def test_range_query(self): + """ Tests that loading from a range of dates works properly """ + start = datetime(*self.base_date.timetuple()[:3]) + end = start + timedelta(days=3) + + results = DateTimeQueryTestModel.filter(user=0, day__gte=start, day__lt=end) + assert len(results) == 3 + diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 3eff054f7b..cf4eb9d1b0 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -245,7 +245,6 @@ def test_allow_filtering_flag(self): """ """ - class TestQuerySetOrdering(BaseQuerySetUsage): def test_order_by_success_case(self): @@ -392,4 +391,3 @@ def test_conn_is_returned_after_queryset_is_garbage_collected(self): - From 64a68e9cf9e045cd64a97a00db2750be02971a1e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 24 Feb 2013 19:43:51 -0800 Subject: [PATCH 0108/3961] fixing date time precision, and using utc time when turning the cal timestamp into a datetime --- cqlengine/columns.py | 5 +++-- cqlengine/tests/query/test_datetime_queries.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index b9d3d7c136..15a02748d7 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -190,13 +190,14 @@ def __init__(self, **kwargs): def to_python(self, value): if isinstance(value, datetime): return value - return datetime.fromtimestamp(value) + return datetime.utcfromtimestamp(value) def to_database(self, value): value = super(DateTime, self).to_database(value) if not isinstance(value, datetime): raise ValidationError("'{}' is not a datetime object".format(value)) - return value.strftime('%Y-%m-%d %H:%M:%S') + epoch = datetime(1970, 1, 1) + return long((value - epoch).total_seconds() * 1000) class UUID(Column): """ diff --git a/cqlengine/tests/query/test_datetime_queries.py b/cqlengine/tests/query/test_datetime_queries.py index 044e12328c..e374bd7d18 100644 --- a/cqlengine/tests/query/test_datetime_queries.py +++ b/cqlengine/tests/query/test_datetime_queries.py @@ -45,3 +45,13 @@ def test_range_query(self): results = DateTimeQueryTestModel.filter(user=0, day__gte=start, day__lt=end) assert len(results) == 3 + def test_datetime_precision(self): + """ Tests that millisecond resolution is preserved when saving datetime objects """ + now = datetime.now() + pk = 1000 + obj = DateTimeQueryTestModel.create(user=pk, day=now, data='energy cheese') + load = DateTimeQueryTestModel.get(user=pk) + + assert abs(now - load.day).total_seconds() < 0.001 + obj.delete() + From b2e40c1b97b313e6d66a97bd075374bcda23f7b7 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 1 Mar 2013 07:36:28 -0800 Subject: [PATCH 0109/3961] updating changelog --- changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/changelog b/changelog index 153d5617cd..aa066984d3 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,11 @@ CHANGELOG +0.1.2 (in progress) +* adding support for allow filtering flag +* updating management functions to work with cassandra 1.2 +* fixed a bug querying datetimes +* modifying datetime serialization to preserver millisecond accuracy + 0.1.1 * fixed a bug occurring when creating tables from the REPL From 320682c76de1db16545771e3ad3e7452c5d33906 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 1 Mar 2013 08:00:05 -0800 Subject: [PATCH 0110/3961] adding support for cal helper functions --- cqlengine/functions.py | 48 ++++++++++++++++++++ cqlengine/models.py | 1 + cqlengine/query.py | 6 ++- cqlengine/tests/query/test_queryoperators.py | 32 +++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 cqlengine/functions.py diff --git a/cqlengine/functions.py b/cqlengine/functions.py new file mode 100644 index 0000000000..f2f6166185 --- /dev/null +++ b/cqlengine/functions.py @@ -0,0 +1,48 @@ +from datetime import datetime + +from cqlengine.exceptions import ValidationError + +class BaseQueryFunction(object): + """ + Base class for filtering functions. Subclasses of these classes can + be passed into .filter() and will be translated into CQL functions in + the resulting query + """ + + _cql_string = None + + def __init__(self, value): + self.value = value + + def to_cql(self, value_id): + """ + Returns a function for cql with the value id as it's argument + """ + return self._cql_string.format(value_id) + +class MinTimeUUID(BaseQueryFunction): + + _cql_string = 'MinTimeUUID(:{})' + + def __init__(self, value): + """ + :param value: the time to create a maximum time uuid from + :type value: datetime + """ + if not isinstance(value, datetime): + raise ValidationError('datetime instance is required') + super(MinTimeUUID, self).__init__(value) + +class MaxTimeUUID(BaseQueryFunction): + + _cql_string = 'MaxTimeUUID(:{})' + + def __init__(self, value): + """ + :param value: the time to create a minimum time uuid from + :type value: datetime + """ + if not isinstance(value, datetime): + raise ValidationError('datetime instance is required') + super(MaxTimeUUID, self).__init__(value) + diff --git a/cqlengine/models.py b/cqlengine/models.py index 07a7b47900..a856c59f2f 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -3,6 +3,7 @@ from cqlengine import columns from cqlengine.exceptions import ModelException +from cqlengine.functions import BaseQueryFunction from cqlengine.query import QuerySet, QueryException class ModelDefinitionException(ModelException): pass diff --git a/cqlengine/query.py b/cqlengine/query.py index 840d7b2fe0..94c4544d5b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -5,6 +5,7 @@ from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException +from cqlengine.functions import BaseQueryFunction #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -39,7 +40,10 @@ def cql(self): Returns this operator's portion of the WHERE clause :param valname: the dict key that this operator's compare value will be found in """ - return '"{}" {} :{}'.format(self.column.db_field_name, self.cql_symbol, self.identifier) + if isinstance(self.value, BaseQueryFunction): + return '"{}" {} {}'.format(self.column.db_field_name, self.cql_symbol, self.value.to_cql(self.identifier)) + else: + return '"{}" {} :{}'.format(self.column.db_field_name, self.cql_symbol, self.identifier) def validate_operator(self): """ diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index e69de29bb2..62ffea9cb3 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -0,0 +1,32 @@ +from datetime import datetime + +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine import columns +from cqlengine import functions +from cqlengine import query + +class TestQuerySetOperation(BaseCassEngTestCase): + + def test_maxtimeuuid_function(self): + """ + Tests that queries with helper functions are generated properly + """ + now = datetime.now() + col = columns.DateTime() + col.set_column_name('time') + qry = query.EqualsOperator(col, functions.MaxTimeUUID(now)) + + assert qry.cql == '"time" = MaxTimeUUID(:{})'.format(qry.identifier) + + def test_mintimeuuid_function(self): + """ + Tests that queries with helper functions are generated properly + """ + now = datetime.now() + col = columns.DateTime() + col.set_column_name('time') + qry = query.EqualsOperator(col, functions.MinTimeUUID(now)) + + assert qry.cql == '"time" = MinTimeUUID(:{})'.format(qry.identifier) + + From b4e09a324c9ba89b142d2ff764a851c69da635dc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 2 Mar 2013 17:13:51 -0800 Subject: [PATCH 0111/3961] testing min and max timeuuid functions with related debugging --- cqlengine/functions.py | 11 +++++ cqlengine/query.py | 5 +- cqlengine/tests/query/test_queryoperators.py | 4 +- cqlengine/tests/query/test_queryset.py | 50 ++++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index f2f6166185..15e93d4659 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -20,6 +20,9 @@ def to_cql(self, value_id): """ return self._cql_string.format(value_id) + def get_value(self): + raise NotImplementedError + class MinTimeUUID(BaseQueryFunction): _cql_string = 'MinTimeUUID(:{})' @@ -33,6 +36,10 @@ def __init__(self, value): raise ValidationError('datetime instance is required') super(MinTimeUUID, self).__init__(value) + def get_value(self): + epoch = datetime(1970, 1, 1) + return long((self.value - epoch).total_seconds() * 1000) + class MaxTimeUUID(BaseQueryFunction): _cql_string = 'MaxTimeUUID(:{})' @@ -46,3 +53,7 @@ def __init__(self, value): raise ValidationError('datetime instance is required') super(MaxTimeUUID, self).__init__(value) + def get_value(self): + epoch = datetime(1970, 1, 1) + return long((self.value - epoch).total_seconds() * 1000) + diff --git a/cqlengine/query.py b/cqlengine/query.py index 94c4544d5b..7a9a74e77a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -78,7 +78,10 @@ def get_dict(self): this should return the dict: {'colval':} SELECT * FROM column_family WHERE colname=:colval """ - return {self.identifier: self.column.to_database(self.value)} + if isinstance(self.value, BaseQueryFunction): + return {self.identifier: self.column.to_database(self.value.get_value())} + else: + return {self.identifier: self.column.to_database(self.value)} @classmethod def get_operator(cls, symbol): diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index 62ffea9cb3..5db4a299af 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -1,7 +1,8 @@ from datetime import datetime +import time from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine import columns +from cqlengine import columns, Model from cqlengine import functions from cqlengine import query @@ -30,3 +31,4 @@ def test_mintimeuuid_function(self): assert qry.cql == '"time" = MinTimeUUID(:{})'.format(qry.identifier) + diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index cf4eb9d1b0..99f45afacd 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -1,6 +1,11 @@ +from datetime import datetime +import time +from uuid import uuid1, uuid4 + from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException +from cqlengine import functions from cqlengine.management import create_table from cqlengine.management import delete_table from cqlengine.models import Model @@ -386,6 +391,51 @@ def test_conn_is_returned_after_queryset_is_garbage_collected(self): del q assert ConnectionPool._queue.qsize() == 1 +class TimeUUIDQueryModel(Model): + partition = columns.UUID(primary_key=True) + time = columns.TimeUUID(primary_key=True) + data = columns.Text(required=False) + +class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestMinMaxTimeUUIDFunctions, cls).setUpClass() + create_table(TimeUUIDQueryModel) + + @classmethod + def tearDownClass(cls): + super(TestMinMaxTimeUUIDFunctions, cls).tearDownClass() + delete_table(TimeUUIDQueryModel) + + def test_success_case(self): + """ Test that the min and max time uuid functions work as expected """ + pk = uuid4() + TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='1') + time.sleep(0.2) + TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='2') + time.sleep(0.2) + midpoint = datetime.utcnow() + time.sleep(0.2) + TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='3') + time.sleep(0.2) + TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='4') + time.sleep(0.2) + + q = TimeUUIDQueryModel.filter(partition=pk, time__lte=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(partition=pk, time__gte=functions.MinTimeUUID(midpoint)) + assert len(q) == 2 + datas = [d.data for d in q] + assert '3' in datas + assert '4' in datas + + From 684501de389f016fad9b9ee1fb352ea0a6c99abb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 2 Mar 2013 19:31:11 -0800 Subject: [PATCH 0112/3961] updating changelog --- changelog | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index aa066984d3..c5f26914a5 100644 --- a/changelog +++ b/changelog @@ -4,7 +4,8 @@ CHANGELOG * adding support for allow filtering flag * updating management functions to work with cassandra 1.2 * fixed a bug querying datetimes -* modifying datetime serialization to preserver millisecond accuracy +* modifying datetime serialization to preserve millisecond accuracy +* adding cql function call generators MaxTimeUUID and MinTimeUUID 0.1.1 * fixed a bug occurring when creating tables from the REPL From 9aba6e469e27692fde5173d9d21c729236d22c9a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 5 Mar 2013 21:26:15 -0800 Subject: [PATCH 0113/3961] adding container columns --- cqlengine/columns.py | 169 +++++++++++++++++- cqlengine/management.py | 2 +- .../tests/columns/test_container_columns.py | 116 ++++++++++++ 3 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 cqlengine/tests/columns/test_container_columns.py diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 15a02748d7..fe7ad9e348 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -1,7 +1,9 @@ #column field types +from copy import copy from datetime import datetime import re from uuid import uuid1, uuid4 +from cql.query import cql_quote from cqlengine.exceptions import ValidationError @@ -10,7 +12,7 @@ class BaseValueManager(object): def __init__(self, instance, column, value): self.instance = instance self.column = column - self.initial_value = value + self.initial_value = copy(value) self.value = value @property @@ -121,7 +123,7 @@ def get_column_def(self): """ Returns a column definition for CQL table definition """ - return '{} {}'.format(self.db_field_name, self.db_type) + return '"{}" {}'.format(self.db_field_name, self.db_type) def set_column_name(self, name): """ @@ -156,6 +158,7 @@ def __init__(self, *args, **kwargs): def validate(self, value): value = super(Text, self).validate(value) + if value is None: return if not isinstance(value, (basestring, bytearray)) and value is not None: raise ValidationError('{} is not a string'.format(type(value))) if self.max_length: @@ -171,6 +174,7 @@ class Integer(Column): def validate(self, value): val = super(Integer, self).validate(value) + if val is None: return try: return long(val) except (TypeError, ValueError): @@ -212,6 +216,7 @@ def __init__(self, default=lambda:uuid4(), **kwargs): def validate(self, value): val = super(UUID, self).validate(value) + if val is None: return from uuid import UUID as _UUID if isinstance(val, _UUID): return val if not self.re_uuid.match(val): @@ -246,6 +251,8 @@ def __init__(self, double_precision=True, **kwargs): super(Float, self).__init__(**kwargs) def validate(self, value): + value = super(Float, self).validate(value) + if value is None: return try: return float(value) except (TypeError, ValueError): @@ -266,3 +273,161 @@ def __init__(self, **kwargs): super(Counter, self).__init__(**kwargs) raise NotImplementedError +class ContainerValueManager(BaseValueManager): + pass + +class ContainerQuoter(object): + """ + contains a single value, which will quote itself for CQL insertion statements + """ + def __init__(self, value): + self.value = value + + def __str__(self): + raise NotImplementedError + +class BaseContainerColumn(Column): + """ + Base Container type + """ + + def __init__(self, value_type, **kwargs): + """ + :param value_type: a column class indicating the types of the value + """ + if not issubclass(value_type, Column): + raise ValidationError('value_type must be a column class') + if issubclass(value_type, BaseContainerColumn): + raise ValidationError('container types cannot be nested') + if value_type.db_type is None: + raise ValidationError('value_type cannot be an abstract column type') + + self.value_type = value_type + self.value_col = self.value_type() + super(BaseContainerColumn, self).__init__(**kwargs) + + def get_column_def(self): + """ + Returns a column definition for CQL table definition + """ + db_type = self.db_type.format(self.value_type.db_type) + return '{} {}'.format(self.db_field_name, db_type) + +class Set(BaseContainerColumn): + """ + Stores a set of unordered, unique values + + http://www.datastax.com/docs/1.2/cql_cli/using/collections + """ + db_type = 'set<{}>' + + class Quoter(ContainerQuoter): + + def __str__(self): + cq = cql_quote + return '{' + ', '.join([cq(v) for v in self.value]) + '}' + + def __init__(self, value_type, strict=True, **kwargs): + """ + :param value_type: a column class indicating the types of the value + :param strict: sets whether non set values will be coerced to set + type on validation, or raise a validation error, defaults to True + """ + self.strict = strict + super(Set, self).__init__(value_type, **kwargs) + + def validate(self, value): + val = super(Set, self).validate(value) + if val is None: return + types = (set,) if self.strict else (set, list, tuple) + if not isinstance(val, types): + if self.strict: + raise ValidationError('{} is not a set object'.format(val)) + else: + raise ValidationError('{} cannot be coerced to a set object'.format(val)) + + return {self.value_col.validate(v) for v in val} + + def to_database(self, value): + return self.Quoter({self.value_col.to_database(v) for v in value}) + +class List(BaseContainerColumn): + """ + Stores a list of ordered values + + http://www.datastax.com/docs/1.2/cql_cli/using/collections_list + """ + db_type = 'list<{}>' + + class Quoter(ContainerQuoter): + + def __str__(self): + cq = cql_quote + return '[' + ', '.join([cq(v) for v in self.value]) + ']' + + def validate(self, value): + val = super(List, self).validate(value) + if val is None: return + if not isinstance(val, (set, list, tuple)): + raise ValidationError('{} is not a list object'.format(val)) + return [self.value_col.validate(v) for v in val] + + def to_database(self, value): + return self.Quoter([self.value_col.to_database(v) for v in value]) + +class Map(BaseContainerColumn): + """ + Stores a key -> value map (dictionary) + + http://www.datastax.com/docs/1.2/cql_cli/using/collections_map + """ + + db_type = 'map<{}, {}>' + + class Quoter(ContainerQuoter): + + def __str__(self): + cq = cql_quote + return '{' + ', '.join([cq(k) + ':' + cq(v) for k,v in self.value.items()]) + '}' + + def __init__(self, key_type, value_type, **kwargs): + """ + :param key_type: a column class indicating the types of the key + :param value_type: a column class indicating the types of the value + """ + if not issubclass(value_type, Column): + raise ValidationError('key_type must be a column class') + if issubclass(value_type, BaseContainerColumn): + raise ValidationError('container types cannot be nested') + if key_type.db_type is None: + raise ValidationError('key_type cannot be an abstract column type') + + self.key_type = key_type + self.key_col = self.key_type() + super(Map, self).__init__(value_type, **kwargs) + + def get_column_def(self): + """ + Returns a column definition for CQL table definition + """ + db_type = self.db_type.format( + self.key_type.db_type, + self.value_type.db_type + ) + return '{} {}'.format(self.db_field_name, db_type) + + def validate(self, value): + val = super(Map, self).validate(value) + if val is None: return + if not isinstance(val, dict): + raise ValidationError('{} is not a dict object'.format(val)) + return {self.key_col.validate(k):self.value_col.validate(v) for k,v in val.items()} + + def to_python(self, value): + if value is not None: + return {self.key_col.to_python(k):self.value_col.to_python(v) for k,v in value.items()} + + def to_database(self, value): + return self.Quoter({self.key_col.to_database(k):self.value_col.to_database(v) for k,v in value.items()}) + + diff --git a/cqlengine/management.py b/cqlengine/management.py index 20ec5741f3..ba8b8145f7 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -62,7 +62,7 @@ def create_table(model, create_missing_keyspace=True): pkeys = [] qtypes = [] def add_column(col): - s = '"{}" {}'.format(col.db_field_name, col.db_type) + s = col.get_column_def() if col.primary_key: pkeys.append('"{}"'.format(col.db_field_name)) qtypes.append(s) for name, col in model._columns.items(): diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py new file mode 100644 index 0000000000..064a7cc32d --- /dev/null +++ b/cqlengine/tests/columns/test_container_columns.py @@ -0,0 +1,116 @@ +from datetime import datetime, timedelta +from uuid import uuid4 + +from cqlengine import Model +from cqlengine import columns +from cqlengine.management import create_table, delete_table +from cqlengine.tests.base import BaseCassEngTestCase + +class TestSetModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + int_set = columns.Set(columns.Integer, required=False) + text_set = columns.Set(columns.Text, required=False) + +class TestSetColumn(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestSetColumn, cls).setUpClass() + delete_table(TestSetModel) + create_table(TestSetModel) + + @classmethod + def tearDownClass(cls): + super(TestSetColumn, cls).tearDownClass() + delete_table(TestSetModel) + + def test_io_success(self): + """ Tests that a basic usage works as expected """ + m1 = TestSetModel.create(int_set={1,2}, text_set={'kai', 'andreas'}) + m2 = TestSetModel.get(partition=m1.partition) + + assert isinstance(m2.int_set, set) + assert isinstance(m2.text_set, set) + + assert 1 in m2.int_set + assert 2 in m2.int_set + + assert 'kai' in m2.text_set + assert 'andreas' in m2.text_set + + +class TestListModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + int_list = columns.List(columns.Integer, required=False) + text_list = columns.List(columns.Text, required=False) + +class TestListColumn(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestListColumn, cls).setUpClass() + delete_table(TestListModel) + create_table(TestListModel) + + @classmethod + def tearDownClass(cls): + super(TestListColumn, cls).tearDownClass() + delete_table(TestListModel) + + def test_io_success(self): + """ Tests that a basic usage works as expected """ + m1 = TestListModel.create(int_list=[1,2], text_list=['kai', 'andreas']) + m2 = TestListModel.get(partition=m1.partition) + + assert isinstance(m2.int_list, tuple) + assert isinstance(m2.text_list, tuple) + + assert len(m2.int_list) == 2 + assert len(m2.text_list) == 2 + + assert m2.int_list[0] == 1 + assert m2.int_list[1] == 2 + + assert m2.text_list[0] == 'kai' + assert m2.text_list[1] == 'andreas' + + +class TestMapModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + int_map = columns.Map(columns.Integer, columns.UUID, required=False) + text_map = columns.Map(columns.Text, columns.DateTime, required=False) + +class TestMapColumn(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestMapColumn, cls).setUpClass() + delete_table(TestMapModel) + create_table(TestMapModel) + + @classmethod + def tearDownClass(cls): + super(TestMapColumn, cls).tearDownClass() + delete_table(TestMapModel) + + def test_io_success(self): + """ Tests that a basic usage works as expected """ + k1 = uuid4() + k2 = uuid4() + now = datetime.now() + then = now + timedelta(days=1) + m1 = TestMapModel.create(int_map={1:k1,2:k2}, text_map={'now':now, 'then':then}) + m2 = TestMapModel.get(partition=m1.partition) + + assert isinstance(m2.int_map, dict) + assert isinstance(m2.text_map, dict) + + assert 1 in m2.int_map + assert 2 in m2.int_map + assert m2.int_map[1] == k1 + assert m2.int_map[2] == k2 + + assert 'now' in m2.text_map + assert 'then' in m2.text_map + assert (now - m2.text_map['now']).total_seconds() < 0.001 + assert (then - m2.text_map['then']).total_seconds() < 0.001 From 7ab2585cb7e4786f56f378674ece0ac0b708de76 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 5 Mar 2013 21:47:15 -0800 Subject: [PATCH 0114/3961] adding tests around container column type validation --- .../tests/columns/test_container_columns.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 064a7cc32d..4a1721e078 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from uuid import uuid4 -from cqlengine import Model +from cqlengine import Model, ValidationError from cqlengine import columns from cqlengine.management import create_table, delete_table from cqlengine.tests.base import BaseCassEngTestCase @@ -38,6 +38,13 @@ def test_io_success(self): assert 'kai' in m2.text_set assert 'andreas' in m2.text_set + def test_type_validation(self): + """ + Tests that attempting to use the wrong types will raise an exception + """ + with self.assertRaises(ValidationError): + TestSetModel.create(int_set={'string', True}, text_set={1, 3.0}) + class TestListModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) @@ -74,6 +81,13 @@ def test_io_success(self): assert m2.text_list[0] == 'kai' assert m2.text_list[1] == 'andreas' + def test_type_validation(self): + """ + Tests that attempting to use the wrong types will raise an exception + """ + with self.assertRaises(ValidationError): + TestListModel.create(int_list=['string', True], text_list=[1, 3.0]) + class TestMapModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) @@ -114,3 +128,10 @@ def test_io_success(self): assert 'then' in m2.text_map assert (now - m2.text_map['now']).total_seconds() < 0.001 assert (then - m2.text_map['then']).total_seconds() < 0.001 + + def test_type_validation(self): + """ + Tests that attempting to use the wrong types will raise an exception + """ + with self.assertRaises(ValidationError): + TestMapModel.create(int_map={'key':2,uuid4():'val'}, text_map={2:5}) From 412412bfb8c1ed68095ac43557f6e8ba27f1e09d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 5 Mar 2013 21:47:57 -0800 Subject: [PATCH 0115/3961] fixing failing unit test --- cqlengine/tests/model/test_model_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index e24da14175..f94b5964c2 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -94,7 +94,7 @@ def test_reserved_cql_words_can_be_used_as_column_names(self): model1 = ReservedWordModel.create(token='1', insert=5) - model2 = ReservedWordModel.filter(token=1) + model2 = ReservedWordModel.filter(token='1') assert len(model2) == 1 assert model1.token == model2[0].token From eb2fe8c21fa12e279124689d4f3bcb274be3ea65 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 5 Mar 2013 21:53:22 -0800 Subject: [PATCH 0116/3961] adding collection types to changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index c5f26914a5..a109cfcf0f 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,7 @@ CHANGELOG 0.1.2 (in progress) +* adding set, list, and map collection types * adding support for allow filtering flag * updating management functions to work with cassandra 1.2 * fixed a bug querying datetimes From f1119db1c24bdad5237987059c11646404ba7d7a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 5 Mar 2013 22:24:11 -0800 Subject: [PATCH 0117/3961] adding functions to the base module --- cqlengine/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 1f8c70ad36..d5e7b74fcc 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,4 +1,5 @@ from cqlengine.columns import * +from cqlengine.functions import * from cqlengine.models import Model __version__ = '0.1.2' From 61235126bf84b86aa71655d5383228355b0b53df Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 5 Mar 2013 22:24:26 -0800 Subject: [PATCH 0118/3961] adding new features to the documentation --- docs/topics/columns.rst | 59 ++++++++++++++++++++++++++++++++++++++++ docs/topics/models.rst | 6 ++-- docs/topics/queryset.rst | 31 +++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 9090b0bc0f..0537e3bccb 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -77,6 +77,65 @@ Columns columns.Decimal() +Collection Type Columns +---------------------------- + + CQLEngine also supports container column types. Each container column requires a column class argument to specify what type of objects it will hold. The Map column requires 2, one for the key, and the other for the value + + *Example* + + .. code-block:: python + + class Person(Model): + first_name = columns.Text() + last_name = columns.Text() + + friends = columns.Set(columns.Text) + enemies = columns.Set(columns.Text) + todo_list = columns.List(columns.Text) + birthdays = columns.Map(columns.Text, columns.DateTime) + + + +.. class:: Set() + + Stores a set of unordered, unique values. Available only with Cassandra 1.2 and above :: + + columns.Set(value_type) + + **options** + + :attr:`~columns.Set.value_type` + The type of objects the set will contain + + :attr:`~columns.Set.strict` + If True, adding this column will raise an exception during save if the value is not a python `set` instance. If False, it will attempt to coerce the value to a set. Defaults to True. + +.. class:: List() + + Stores a list of ordered values. Available only with Cassandra 1.2 and above :: + + columns.List(value_type) + + **options** + + :attr:`~columns.List.value_type` + The type of objects the set will contain + +.. class:: Map() + + Stores a map (dictionary) collection, available only with Cassandra 1.2 and above :: + + columns.Map(key_type, value_type) + + **options** + + :attr:`~columns.Map.key_type` + The type of the map keys + + :attr:`~columns.Map.value_type` + The type of the map values + Column Options ============== diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 86788a2530..0dd2c19cf8 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -50,11 +50,13 @@ Column Types * :class:`~cqlengine.columns.Integer` * :class:`~cqlengine.columns.DateTime` * :class:`~cqlengine.columns.UUID` + * :class:`~cqlengine.columns.TimeUUID` * :class:`~cqlengine.columns.Boolean` * :class:`~cqlengine.columns.Float` * :class:`~cqlengine.columns.Decimal` - - A time uuid field is in the works. + * :class:`~cqlengine.columns.Set` + * :class:`~cqlengine.columns.List` + * :class:`~cqlengine.columns.Map` Column Options -------------- diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 63b76d03a6..ce3a2c818c 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -151,6 +151,33 @@ Filtering Operators q = Automobile.objects.filter(manufacturer='Tesla') q = q.filter(year__lte=2012) # year <= 2012 + +TimeUUID Functions +================== + + In addition to querying using regular values, there are two functions you can pass in when querying TimeUUID columns to help make filtering by them easier. Note that these functions don't actually return a value, but instruct the cql interpreter to use the functions in it's query. + + .. class:: MinTimeUUID(datetime) + + returns the minimum time uuid value possible for the given datetime + + .. class:: MaxTimeUUID(datetime) + + returns the maximum time uuid value possible for the given datetime + + *Example* + + .. code-block:: python + + class DataStream(Model): + time = cqlengine.TimeUUID(primary_key=True) + data = cqlengine.Bytes() + + min_time = datetime(1982, 1, 1) + max_time = datetime(1982, 3, 9) + + DataStream.filter(time__gt=cqlengine.MinTimeUUID(min_time), time__lt=cqlengine.MaxTimeUUID(max_time)) + QuerySets are imutable ====================== @@ -223,3 +250,7 @@ QuerySet method reference :type field_name: string Sets the field to order on. + + .. method:: allow_filtering() + + Enables the (usually) unwise practive of querying on a clustering key without also defining a partition key From cb5945f96cc76d71f81cc878a6d20a54f47cd511 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 6 Mar 2013 20:41:13 -0800 Subject: [PATCH 0119/3961] renaming the value manager's initial_value field to previous_value --- cqlengine/columns.py | 14 ++++++++++++-- cqlengine/tests/model/test_model_io.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index fe7ad9e348..67d9212af2 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -12,12 +12,22 @@ class BaseValueManager(object): def __init__(self, instance, column, value): self.instance = instance self.column = column - self.initial_value = copy(value) + self.previous_value = copy(value) self.value = value @property def deleted(self): - return self.value is None and self.initial_value is not None + return self.value is None and self.previous_value is not None + + @property + def changed(self): + """ + Indicates whether or not this value has changed. + + :rtype: boolean + + """ + return self.value != self.previous_value def getval(self): return self.value diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index f94b5964c2..4c40e2beb3 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -62,7 +62,7 @@ def test_column_deleting_works_properly(self): tm2 = TestModel.objects(id=tm.pk).first() assert tm2.text is None - assert tm2._values['text'].initial_value is None + assert tm2._values['text'].previous_value is None def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): From da3895aeb35b934494a3a5be528c7e4125aa3180 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 6 Mar 2013 21:13:19 -0800 Subject: [PATCH 0120/3961] adding flags for checking if an object has been persisted and if it can be persisted with an update --- cqlengine/models.py | 23 +++++++++++++- cqlengine/query.py | 4 ++- cqlengine/tests/model/test_model_io.py | 42 ++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index a856c59f2f..101a328b25 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -32,6 +32,21 @@ def __init__(self, **values): value_mngr = column.value_manager(self, column, value) self._values[name] = value_mngr + # a flag set by the deserializer to indicate + # that update should be used when persisting changes + self._is_persisted = False + + def _can_update(self): + """ + Called by the save function to check if this should be + persisted with update or insert + + :return: + """ + if not self._is_persisted: return False + pks = self._primary_keys.keys() + return all([not self._values[k].changed for k in self._primary_keys]) + def __eq__(self, other): return self.as_dict() == other.as_dict() @@ -101,7 +116,13 @@ def save(self): is_new = self.pk is None self.validate() self.objects.save(self) - #delete any fields that have been deleted / set to none + + #reset the value managers + for v in self._values.values(): + v.previous_value = v.value + + self._is_persisted = True + return self def delete(self): diff --git a/cqlengine/query.py b/cqlengine/query.py index 7a9a74e77a..e4a1e84dff 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -314,7 +314,9 @@ def _construct_instance(self, values): field_dict[db_map[key]] = val else: field_dict[key] = val - return self.model(**field_dict) + instance = self.model(**field_dict) + instance._is_persisted = True + return instance def first(self): try: diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 4c40e2beb3..707e6736c3 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,4 +1,5 @@ from unittest import skip +from uuid import uuid4 from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import create_table @@ -71,6 +72,47 @@ def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): create_table(TestModel) create_table(TestModel) +class TestCanUpdate(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestCanUpdate, cls).setUpClass() + delete_table(TestModel) + create_table(TestModel) + + @classmethod + def tearDownClass(cls): + super(TestCanUpdate, cls).tearDownClass() + delete_table(TestModel) + + def test_success_case(self): + tm = TestModel(count=8, text='123456789') + + # object hasn't been saved, + # shouldn't be able to update + assert not tm._is_persisted + assert not tm._can_update() + + tm.save() + + # object has been saved, + # should be able to update + assert tm._is_persisted + assert tm._can_update() + + tm.count = 200 + + # primary keys haven't changed, + # should still be able to update + assert tm._can_update() + + tm.id = uuid4() + + # primary keys have changed, + # should not be able to update + assert not tm._can_update() + + class IndexDefinitionModel(Model): key = columns.UUID(primary_key=True) val = columns.Text(index=True) From e77c056f98f638e02e459fc32ea8290110a0a5f9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 6 Mar 2013 22:12:52 -0800 Subject: [PATCH 0121/3961] adding support for either insert or update statements on object persistence --- cqlengine/query.py | 45 +++++++++++++++++++---- cqlengine/tests/model/test_model_io.py | 49 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index e4a1e84dff..da95798472 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -511,14 +511,45 @@ def save(self, instance): #construct query string field_names = zip(*value_pairs)[0] field_values = dict(value_pairs) - qs = ["INSERT INTO {}".format(self.column_family_name)] - qs += ["({})".format(', '.join(['"{}"'.format(f) for f in field_names]))] - qs += ['VALUES'] - qs += ["({})".format(', '.join([':'+f for f in field_names]))] + + qs = [] + if instance._can_update(): + qs += ["UPDATE {}".format(self.column_family_name)] + qs += ["SET"] + + set_statements = [] + #get defined fields and their column names + for name, col in self.model._columns.items(): + if not col.is_primary_key: + val = values.get(name) + if val is None: continue + set_statements += ['"{0}" = :{0}'.format(col.db_field_name)] + qs += [', '.join(set_statements)] + + qs += ['WHERE'] + + where_statements = [] + for name, col in self.model._primary_keys.items(): + where_statements += ['"{0}" = :{0}'.format(col.db_field_name)] + + qs += [' AND '.join(where_statements)] + + # clear the qs if there are not set statements + if not set_statements: qs = [] + + else: + qs += ["INSERT INTO {}".format(self.column_family_name)] + qs += ["({})".format(', '.join(['"{}"'.format(f) for f in field_names]))] + qs += ['VALUES'] + qs += ["({})".format(', '.join([':'+f for f in field_names]))] + qs = ' '.join(qs) - with connection_manager() as con: - con.execute(qs, field_values) + # skip query execution if it's empty + # caused by pointless update queries + if qs: + with connection_manager() as con: + con.execute(qs, field_values) #delete deleted / nulled columns deleted = [k for k,v in instance._values.items() if v.deleted] @@ -529,7 +560,7 @@ def save(self, instance): qs = ['DELETE {}'.format(', '.join(['"{}"'.format(f) for f in del_fields]))] qs += ['FROM {}'.format(self.column_family_name)] qs += ['WHERE'] - eq = lambda col: '{0} = :{0}'.format(v.column.db_field_name) + eq = lambda col: '{0} = :{0}'.format(col.db_field_name) qs += [' AND '.join([eq(f) for f in pks.values()])] qs = ' '.join(qs) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 707e6736c3..5ced331fb3 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,5 +1,6 @@ from unittest import skip from uuid import uuid4 +import random from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import create_table @@ -72,6 +73,53 @@ def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): create_table(TestModel) create_table(TestModel) +class TestMultiKeyModel(Model): + partition = columns.Integer(primary_key=True) + cluster = columns.Integer(primary_key=True) + count = columns.Integer(required=False) + text = columns.Text(required=False) + +class TestUpdating(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestUpdating, cls).setUpClass() + delete_table(TestMultiKeyModel) + create_table(TestMultiKeyModel) + + @classmethod + def tearDownClass(cls): + super(TestUpdating, cls).tearDownClass() + delete_table(TestMultiKeyModel) + + def setUp(self): + super(TestUpdating, self).setUp() + self.instance = TestMultiKeyModel.create( + partition=random.randint(0, 1000), + cluster=random.randint(0, 1000), + count=0, + text='happy' + ) + + def test_vanilla_update(self): + self.instance.count = 5 + self.instance.save() + + check = TestMultiKeyModel.get(partition=self.instance.partition, cluster=self.instance.cluster) + assert check.count == 5 + assert check.text == 'happy' + + def test_deleting_only(self): + self.instance.count = None + self.instance.text = None + self.instance.save() + + check = TestMultiKeyModel.get(partition=self.instance.partition, cluster=self.instance.cluster) + assert check.count is None + assert check.text is None + + + class TestCanUpdate(BaseCassEngTestCase): @classmethod @@ -105,6 +153,7 @@ def test_success_case(self): # primary keys haven't changed, # should still be able to update assert tm._can_update() + tm.save() tm.id = uuid4() From fcae60188e8b6ce7e58ced825bfd56bc5820df3d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 6 Mar 2013 22:14:16 -0800 Subject: [PATCH 0122/3961] updating changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index a109cfcf0f..00dc02bced 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,7 @@ CHANGELOG 0.1.2 (in progress) +* expanding internal save function to use update where appropriate * adding set, list, and map collection types * adding support for allow filtering flag * updating management functions to work with cassandra 1.2 From 54d4b804c4f10926a10c8e7bf9c2703917aead84 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 7 Mar 2013 21:59:55 -0800 Subject: [PATCH 0123/3961] adding test around deleting multi key models --- cqlengine/tests/model/test_model_io.py | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 5ced331fb3..7f708cf214 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -73,12 +73,40 @@ def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): create_table(TestModel) create_table(TestModel) + class TestMultiKeyModel(Model): partition = columns.Integer(primary_key=True) cluster = columns.Integer(primary_key=True) count = columns.Integer(required=False) text = columns.Text(required=False) +class TestDeleting(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestDeleting, cls).setUpClass() + delete_table(TestMultiKeyModel) + create_table(TestMultiKeyModel) + + @classmethod + def tearDownClass(cls): + super(TestDeleting, cls).tearDownClass() + delete_table(TestMultiKeyModel) + + def test_deleting_only_deletes_one_object(self): + partition = random.randint(0,1000) + for i in range(5): + TestMultiKeyModel.create(partition=partition, cluster=i, count=i, text=str(i)) + + assert TestMultiKeyModel.filter(partition=partition).count() == 5 + + TestMultiKeyModel.get(partition=partition, cluster=0).delete() + + assert TestMultiKeyModel.filter(partition=partition).count() == 4 + + TestMultiKeyModel.filter(partition=partition).delete() + + class TestUpdating(BaseCassEngTestCase): @classmethod From dc6031ae9234b9c7333b89ddb0fc95923140df00 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 7 Mar 2013 22:43:42 -0800 Subject: [PATCH 0124/3961] Implementing batch queries (not tested yet) --- cqlengine/models.py | 16 ++- cqlengine/query.py | 209 +++++++++++++++++++++------- cqlengine/tests/test_batch_query.py | 0 3 files changed, 173 insertions(+), 52 deletions(-) create mode 100644 cqlengine/tests/test_batch_query.py diff --git a/cqlengine/models.py b/cqlengine/models.py index 101a328b25..ca9ee77a30 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -4,7 +4,7 @@ from cqlengine import columns from cqlengine.exceptions import ModelException from cqlengine.functions import BaseQueryFunction -from cqlengine.query import QuerySet, QueryException +from cqlengine.query import QuerySet, QueryException, DMLQuery class ModelDefinitionException(ModelException): pass @@ -112,10 +112,13 @@ def filter(cls, **kwargs): def get(cls, **kwargs): return cls.objects.get(**kwargs) - def save(self): + def save(self, batch_obj=None): is_new = self.pk is None self.validate() - self.objects.save(self) + if batch_obj: + DMLQuery(self.__class__, self).batch(batch_obj).save() + else: + DMLQuery(self.__class__, self).save() #reset the value managers for v in self._values.values(): @@ -127,8 +130,13 @@ def save(self): def delete(self): """ Deletes this instance """ - self.objects.delete_instance(self) + DMLQuery(self.__class__, self).delete() + def batch(self, batch_obj): + """ + Returns a batched DML query + """ + return DMLQuery(self.__class__, self).batch(batch_obj) class ModelMetaClass(type): diff --git a/cqlengine/query.py b/cqlengine/query.py index da95798472..3405f0518e 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,7 +1,9 @@ from collections import namedtuple import copy +from datetime import datetime from hashlib import md5 from time import time +from uuid import uuid1 from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException @@ -28,7 +30,7 @@ def __init__(self, column, value): #the identifier is a unique key that will be used in string #replacement on query strings, it's created from a hash #of this object's id and the time - self.identifier = md5(str(id(self)) + str(time())).hexdigest() + self.identifier = uuid1().hex #perform validation on this operator self.validate_operator() @@ -123,6 +125,59 @@ class LessThanOrEqualOperator(QueryOperator): symbol = "LTE" cql_symbol = '<=' +class Consistency(object): + ANY = 'ANY' + ONE = 'ONE' + QUORUM = 'QUORUM' + LOCAL_QUORUM = 'LOCAL_QUORUM' + EACH_QUORUM = 'EACH_QUORUM' + ALL = 'ALL' + +class BatchQuery(object): + """ + Handles the batching of queries + """ + + def __init__(self, consistency=Consistency.ONE, timestamp=None): + self.queries = [] + self.consistency = consistency + if timestamp is not None and not isinstance(timestamp, datetime): + raise CQLEngineException('timestamp object must be an instance of datetime') + self.timestamp = timestamp + + def add_query(self, query, params): + self.queries.append((query, params)) + + def execute(self): + query_list = [] + parameters = {} + + opener = 'BEGIN BATCH USING CONSISTENCY {}'.format(self.consistency) + if self.timestamp: + epoch = datetime(1970, 1, 1) + ts = long((self.timestamp - epoch).total_seconds() * 1000) + opener += ' TIMESTAMP {}'.format(ts) + + query_list = [opener] + for query, params in self.queries: + query_list.append(query) + parameters.update(params) + + query_list.append('APPLY BATCH;') + + with connection_manager() as con: + con.execute('\n'.join(query_list), parameters) + + self.queries = [] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + #don't execute if there was an exception + if exc_type is not None: return + self.execute() + class QuerySet(object): def __init__(self, model): @@ -152,6 +207,8 @@ def __init__(self, model): self._result_cache = None self._result_idx = None + self._batch = None + def __unicode__(self): return self._select_query() @@ -242,6 +299,8 @@ def _select_query(self): #----Reads------ def _execute_query(self): + if self._batch: + raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: self._con = connection_manager() self._cur = self._con.execute(self._select_query(), self._where_values()) @@ -318,6 +377,18 @@ def _construct_instance(self, values): instance._is_persisted = True return instance + def batch(self, batch_obj): + """ + Adds a batch query to the mix + :param batch_obj: + :return: + """ + if not isinstance(batch_obj, BatchQuery): + raise CQLEngineException('batch_obj must be a BatchQuery instance') + clone = copy.deepcopy(self) + clone._batch = batch_obj + return clone + def first(self): try: return iter(self).next() @@ -379,8 +450,6 @@ def get(self, **kwargs): else: return self[0] - - def order_by(self, colname): """ orders the result set. @@ -416,6 +485,8 @@ def order_by(self, colname): def count(self): """ Returns the number of rows matched by this query """ + if self._batch: + raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") #TODO: check for previous query execution and return row count if it exists if self._result_cache is None: qs = ['SELECT COUNT(*)'] @@ -426,7 +497,7 @@ def count(self): qs += ['ALLOW FILTERING'] qs = ' '.join(qs) - + with connection_manager() as con: cur = con.execute(qs, self._where_values()) return cur.fetchone()[0] @@ -435,7 +506,7 @@ def count(self): def limit(self, v): """ - Sets the limit on the number of results returned + Sets the limit on the number of results returned CQL has a default limit of 10,000 """ if not (v is None or isinstance(v, (int, long))): @@ -488,19 +559,64 @@ def defer(self, fields): """ Don't load these fields for the returned query """ return self._only_or_defer('defer', fields) - #----writes---- - def save(self, instance): + def create(self, **kwargs): + return self.model(**kwargs).save(batch_obj=self._batch) + + #----delete--- + def delete(self, columns=[]): + """ + Deletes the contents of a query + """ + #validate where clause + partition_key = self.model._primary_keys.values()[0] + if not any([c.column == partition_key for c in self._where]): + raise QueryException("The partition key must be defined on delete queries") + qs = ['DELETE FROM {}'.format(self.column_family_name)] + qs += ['WHERE {}'.format(self._where_clause())] + qs = ' '.join(qs) + + if self._batch: + self._batch.add_query(qs, self._where_values()) + else: + with connection_manager() as con: + con.execute(qs, self._where_values()) + +class DMLQuery(object): + """ + A query object used for queries performing inserts, updates, or deletes + + this is usually instantiated by the model instance to be modified + + unlike the read query object, this is mutable + """ + + def __init__(self, model, instance=None): + self.model = model + self.column_family_name = self.model.column_family_name() + self.instance = instance + self.batch = None + pass + + def batch(self, batch_obj): + if not isinstance(batch_obj, BatchQuery): + raise CQLEngineException('batch_obj must be a BatchQuery instance') + self.batch = batch_obj + return self + + def save(self): """ Creates / updates a row. This is a blind insert call. - All validation and cleaning needs to happen + All validation and cleaning needs to happen prior to calling this. """ - assert type(instance) == self.model + if self.instance is None: + raise CQLEngineException("DML Query intance attribute is None") + assert type(self.instance) == self.model #organize data value_pairs = [] - values = instance.as_dict() + values = self.instance.as_dict() #get defined fields and their column names for name, col in self.model._columns.items(): @@ -510,10 +626,12 @@ def save(self, instance): #construct query string field_names = zip(*value_pairs)[0] + field_ids = {n:uuid1().hex for n in field_names} field_values = dict(value_pairs) + query_values = {field_ids[n]:field_values[n] for n in field_names} qs = [] - if instance._can_update(): + if self.instance._can_update(): qs += ["UPDATE {}".format(self.column_family_name)] qs += ["SET"] @@ -523,14 +641,14 @@ def save(self, instance): if not col.is_primary_key: val = values.get(name) if val is None: continue - set_statements += ['"{0}" = :{0}'.format(col.db_field_name)] + set_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] qs += [', '.join(set_statements)] qs += ['WHERE'] where_statements = [] for name, col in self.model._primary_keys.items(): - where_statements += ['"{0}" = :{0}'.format(col.db_field_name)] + where_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] qs += [' AND '.join(where_statements)] @@ -541,18 +659,21 @@ def save(self, instance): qs += ["INSERT INTO {}".format(self.column_family_name)] qs += ["({})".format(', '.join(['"{}"'.format(f) for f in field_names]))] qs += ['VALUES'] - qs += ["({})".format(', '.join([':'+f for f in field_names]))] + qs += ["({})".format(', '.join([':'+field_ids[f] for f in field_names]))] qs = ' '.join(qs) # skip query execution if it's empty # caused by pointless update queries if qs: - with connection_manager() as con: - con.execute(qs, field_values) + if self.batch: + self.batch.add_query(qs, query_values) + else: + with connection_manager() as con: + con.execute(qs, query_values) #delete deleted / nulled columns - deleted = [k for k,v in instance._values.items() if v.deleted] + deleted = [k for k,v in self.instance._values.items() if v.deleted] if deleted: del_fields = [self.model._columns[f] for f in deleted] del_fields = [f.db_field_name for f in del_fields if not f.primary_key] @@ -560,44 +681,36 @@ def save(self, instance): qs = ['DELETE {}'.format(', '.join(['"{}"'.format(f) for f in del_fields]))] qs += ['FROM {}'.format(self.column_family_name)] qs += ['WHERE'] - eq = lambda col: '{0} = :{0}'.format(col.db_field_name) + eq = lambda col: '"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name]) qs += [' AND '.join([eq(f) for f in pks.values()])] qs = ' '.join(qs) - pk_dict = dict([(v.db_field_name, getattr(instance, k)) for k,v in pks.items()]) - - with connection_manager() as con: - con.execute(qs, pk_dict) - - - def create(self, **kwargs): - return self.model(**kwargs).save() - - #----delete--- - def delete(self, columns=[]): - """ - Deletes the contents of a query - """ - #validate where clause - partition_key = self.model._primary_keys.values()[0] - if not any([c.column == partition_key for c in self._where]): - raise QueryException("The partition key must be defined on delete queries") - qs = ['DELETE FROM {}'.format(self.column_family_name)] - qs += ['WHERE {}'.format(self._where_clause())] - qs = ' '.join(qs) - - with connection_manager() as con: - con.execute(qs, self._where_values()) - + if self.batch: + self.batch.add_query(qs, query_values) + else: + with connection_manager() as con: + con.execute(qs, query_values) - def delete_instance(self, instance): + def delete(self): """ Deletes one instance """ - pk_name = self.model._pk_name + if self.instance is None: + raise CQLEngineException("DML Query intance attribute is None") + field_values = {} qs = ['DELETE FROM {}'.format(self.column_family_name)] - qs += ['WHERE {0}=:{0}'.format(pk_name)] + qs += ['WHERE'] + where_statements = [] + for name, col in self.model._primary_keys.items(): + field_id = uuid1().hex + field_values[field_id] = col.to_database(getattr(self.instance, name)) + where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] + + qs += [' AND '.join(where_statements)] qs = ' '.join(qs) - with connection_manager() as con: - con.execute(qs, {pk_name:instance.pk}) + if self.batch: + self.batch.add_query(qs, field_values) + else: + with connection_manager() as con: + con.execute(qs, field_values) diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py new file mode 100644 index 0000000000..e69de29bb2 From 241c942334a049cb9d6d3ceac700ea8afad6e4f4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 9 Mar 2013 17:43:10 -0800 Subject: [PATCH 0125/3961] restructuring some of the batch query internals, putting some unit tests around batch querying --- cqlengine/models.py | 42 +++++++++---- cqlengine/query.py | 18 +++--- cqlengine/tests/test_batch_query.py | 93 +++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 20 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index ca9ee77a30..da4e18fc13 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -8,6 +8,22 @@ class ModelDefinitionException(ModelException): pass +class hybrid_classmethod(object): + """ + Allows a method to behave as both a class method and + normal instance method depending on how it's called + """ + + def __init__(self, clsmethod, instmethod): + self.clsmethod = clsmethod + self.instmethod = instmethod + + def __get__(self, instance, owner): + if instance is None: + return self.clsmethod.__get__(owner, owner) + else: + return self.instmethod.__get__(instance, owner) + class BaseModel(object): """ The base model class, don't inherit from this, inherit from Model, defined below @@ -35,6 +51,7 @@ def __init__(self, **values): # a flag set by the deserializer to indicate # that update should be used when persisting changes self._is_persisted = False + self._batch = None def _can_update(self): """ @@ -112,13 +129,10 @@ def filter(cls, **kwargs): def get(cls, **kwargs): return cls.objects.get(**kwargs) - def save(self, batch_obj=None): + def save(self): is_new = self.pk is None self.validate() - if batch_obj: - DMLQuery(self.__class__, self).batch(batch_obj).save() - else: - DMLQuery(self.__class__, self).save() + DMLQuery(self.__class__, self, batch=self._batch).save() #reset the value managers for v in self._values.values(): @@ -130,13 +144,19 @@ def save(self, batch_obj=None): def delete(self): """ Deletes this instance """ - DMLQuery(self.__class__, self).delete() + DMLQuery(self.__class__, self, batch=self._batch).delete() + + @classmethod + def _class_batch(cls, batch): + return cls.objects.batch(batch) + + def _inst_batch(self, batch): + self._batch = batch + return self + + batch = hybrid_classmethod(_class_batch, _inst_batch) + - def batch(self, batch_obj): - """ - Returns a batched DML query - """ - return DMLQuery(self.__class__, self).batch(batch_obj) class ModelMetaClass(type): diff --git a/cqlengine/query.py b/cqlengine/query.py index 3405f0518e..0b60539981 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -138,7 +138,7 @@ class BatchQuery(object): Handles the batching of queries """ - def __init__(self, consistency=Consistency.ONE, timestamp=None): + def __init__(self, consistency=None, timestamp=None): self.queries = [] self.consistency = consistency if timestamp is not None and not isinstance(timestamp, datetime): @@ -149,18 +149,18 @@ def add_query(self, query, params): self.queries.append((query, params)) def execute(self): - query_list = [] - parameters = {} - - opener = 'BEGIN BATCH USING CONSISTENCY {}'.format(self.consistency) + opener = 'BEGIN BATCH' + if self.consistency: + opener += ' USING CONSISTENCY {}'.format(self.consistency) if self.timestamp: epoch = datetime(1970, 1, 1) ts = long((self.timestamp - epoch).total_seconds() * 1000) opener += ' TIMESTAMP {}'.format(ts) query_list = [opener] + parameters = {} for query, params in self.queries: - query_list.append(query) + query_list.append(' ' + query) parameters.update(params) query_list.append('APPLY BATCH;') @@ -560,7 +560,7 @@ def defer(self, fields): return self._only_or_defer('defer', fields) def create(self, **kwargs): - return self.model(**kwargs).save(batch_obj=self._batch) + return self.model(**kwargs).batch(self._batch).save() #----delete--- def delete(self, columns=[]): @@ -590,11 +590,11 @@ class DMLQuery(object): unlike the read query object, this is mutable """ - def __init__(self, model, instance=None): + def __init__(self, model, instance=None, batch=None): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance - self.batch = None + self.batch = batch pass def batch(self, batch_obj): diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index e69de29bb2..c6d509ebad 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -0,0 +1,93 @@ +from unittest import skip +from uuid import uuid4 +import random +from cqlengine import Model, columns +from cqlengine.management import delete_table, create_table +from cqlengine.query import BatchQuery, Consistency +from cqlengine.tests.base import BaseCassEngTestCase + +class TestMultiKeyModel(Model): + partition = columns.Integer(primary_key=True) + cluster = columns.Integer(primary_key=True) + count = columns.Integer(required=False) + text = columns.Text(required=False) + +class BatchQueryTests(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(BatchQueryTests, cls).setUpClass() + delete_table(TestMultiKeyModel) + create_table(TestMultiKeyModel) + + @classmethod + def tearDownClass(cls): + super(BatchQueryTests, cls).tearDownClass() + delete_table(TestMultiKeyModel) + + def setUp(self): + super(BatchQueryTests, self).setUp() + self.pkey = 1 + for obj in TestMultiKeyModel.filter(partition=self.pkey): + obj.delete() + + + + def test_insert_success_case(self): + + b = BatchQuery() + inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') + + with self.assertRaises(TestMultiKeyModel.DoesNotExist): + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + b.execute() + + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + def test_update_success_case(self): + + inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4') + + b = BatchQuery() + + inst.count = 4 + inst.batch(b).save() + + inst2 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) + assert inst2.count == 3 + + b.execute() + + inst3 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) + assert inst3.count == 4 + + def test_delete_success_case(self): + + inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4') + + b = BatchQuery() + + inst.batch(b).delete() + + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + b.execute() + + with self.assertRaises(TestMultiKeyModel.DoesNotExist): + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + def test_context_manager(self): + + with BatchQuery() as b: + for i in range(5): + TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=i, count=3, text='4') + + for i in range(5): + with self.assertRaises(TestMultiKeyModel.DoesNotExist): + TestMultiKeyModel.get(partition=self.pkey, cluster=i) + + for i in range(5): + TestMultiKeyModel.get(partition=self.pkey, cluster=i) + + From 66ca79f7779b6ade94e2ee89641fd934eaeee8ed Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 9 Mar 2013 18:18:20 -0800 Subject: [PATCH 0126/3961] adding documentation for batch queries --- cqlengine/__init__.py | 1 + docs/topics/queryset.rst | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index d5e7b74fcc..184a4c8a38 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,6 +1,7 @@ from cqlengine.columns import * from cqlengine.functions import * from cqlengine.models import Model +from cqlengine.query import BatchQuery __version__ = '0.1.2' diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index ce3a2c818c..6733080cfe 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -213,6 +213,34 @@ Ordering QuerySets *For instance, given our Automobile model, year is the only column we can order on.* +Batch Queries +=============== + + cqlengine now supports batch queries using the BatchQuery class. Batch queries can be started and stopped manually, or within a context manager. To add queries to the batch object, you just need to precede the create/save/delete call with a call to batch, and pass in the batch object. + + You can only create, update, and delete rows with a batch query, attempting to read rows out of the database with a batch query will fail. + + .. code-block:: python + + from cqlengine import BatchQuery + + #using a context manager + with BatchQuery() as b: + now = datetime.now() + em1 = ExampleModel.batch(b).create(example_type=0, description="1", created_at=now) + em2 = ExampleModel.batch(b).create(example_type=0, description="2", created_at=now) + em3 = ExampleModel.batch(b).create(example_type=0, description="3", created_at=now) + + # -- or -- + + #manually + b = BatchQuery() + now = datetime.now() + em1 = ExampleModel.batch(b).create(example_type=0, description="1", created_at=now) + em2 = ExampleModel.batch(b).create(example_type=0, description="2", created_at=now) + em3 = ExampleModel.batch(b).create(example_type=0, description="3", created_at=now) + b.execute() + QuerySet method reference ========================= From af50ced2df9ddd3d0dbcfc41910d79080a65dff0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 10 Mar 2013 14:11:16 -0700 Subject: [PATCH 0127/3961] adding support for partial updates for set columns --- cqlengine/columns.py | 68 ++++++++++++++++++- cqlengine/models.py | 3 +- cqlengine/query.py | 11 ++- .../tests/columns/test_container_columns.py | 26 +++++++ 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 67d9212af2..84b245da0c 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -29,6 +29,9 @@ def changed(self): """ return self.value != self.previous_value + def reset_previous_value(self): + self.previous_value = copy(self.value) + def getval(self): return self.value @@ -283,9 +286,6 @@ def __init__(self, **kwargs): super(Counter, self).__init__(**kwargs) raise NotImplementedError -class ContainerValueManager(BaseValueManager): - pass - class ContainerQuoter(object): """ contains a single value, which will quote itself for CQL insertion statements @@ -323,6 +323,12 @@ def get_column_def(self): db_type = self.db_type.format(self.value_type.db_type) return '{} {}'.format(self.db_field_name, db_type) + def get_update_statement(self, val, prev, ctx): + """ + Used to add partial update statements + """ + raise NotImplementedError + class Set(BaseContainerColumn): """ Stores a set of unordered, unique values @@ -359,8 +365,50 @@ def validate(self, value): return {self.value_col.validate(v) for v in val} def to_database(self, value): + if value is None: return None return self.Quoter({self.value_col.to_database(v) for v in value}) + def get_update_statement(self, val, prev, ctx): + """ + Returns statements that will be added to an object's update statement + also updates the query context + + :param val: the current column value + :param prev: the previous column value + :param ctx: the values that will be passed to the query + :rtype: list + """ + + # remove from Quoter containers, if applicable + if isinstance(val, self.Quoter): val = val.value + if isinstance(prev, self.Quoter): prev = prev.value + + if val is None or val == prev: + # don't return anything if the new value is the same as + # the old one, or if the new value is none + return [] + elif prev is None or not any({v in prev for v in val}): + field = uuid1().hex + ctx[field] = self.Quoter(val) + return ['"{}" = :{}'.format(self.db_field_name, field)] + else: + # partial update time + to_create = val - prev + to_delete = prev - val + statements = [] + + if to_create: + field_id = uuid1().hex + ctx[field_id] = self.Quoter(to_create) + statements += ['"{0}" = "{0}" + :{1}'.format(self.db_field_name, field_id)] + + if to_delete: + field_id = uuid1().hex + ctx[field_id] = self.Quoter(to_delete) + statements += ['"{0}" = "{0}" - :{1}'.format(self.db_field_name, field_id)] + + return statements + class List(BaseContainerColumn): """ Stores a list of ordered values @@ -383,8 +431,15 @@ def validate(self, value): return [self.value_col.validate(v) for v in val] def to_database(self, value): + if value is None: return None return self.Quoter([self.value_col.to_database(v) for v in value]) + def get_update_statement(self, val, prev, values): + """ + http://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm + """ + pass + class Map(BaseContainerColumn): """ Stores a key -> value map (dictionary) @@ -438,6 +493,13 @@ def to_python(self, value): return {self.key_col.to_python(k):self.value_col.to_python(v) for k,v in value.items()} def to_database(self, value): + if value is None: return None return self.Quoter({self.key_col.to_database(k):self.value_col.to_database(v) for k,v in value.items()}) + def get_update_statement(self, val, prev, ctx): + """ + http://www.datastax.com/docs/1.2/cql_cli/using/collections_map#deletion + """ + pass + diff --git a/cqlengine/models.py b/cqlengine/models.py index da4e18fc13..9b6ec3b084 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -136,8 +136,7 @@ def save(self): #reset the value managers for v in self._values.values(): - v.previous_value = v.value - + v.reset_previous_value() self._is_persisted = True return self diff --git a/cqlengine/query.py b/cqlengine/query.py index 0b60539981..fd91e45add 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,6 +4,7 @@ from hashlib import md5 from time import time from uuid import uuid1 +from cqlengine import BaseContainerColumn from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException @@ -641,7 +642,15 @@ def save(self): if not col.is_primary_key: val = values.get(name) if val is None: continue - set_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] + if isinstance(col, BaseContainerColumn): + #remove value from query values, the column will handle it + query_values.pop(field_ids.get(name), None) + + val_mgr = self.instance._values[name] + set_statements += col.get_update_statement(val, val_mgr.previous_value, query_values) + pass + else: + set_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] qs += [', '.join(set_statements)] qs += ['WHERE'] diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 4a1721e078..15f8919288 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -45,6 +45,32 @@ def test_type_validation(self): with self.assertRaises(ValidationError): TestSetModel.create(int_set={'string', True}, text_set={1, 3.0}) + def test_partial_updates(self): + """ Tests that partial udpates work as expected """ + m1 = TestSetModel.create(int_set={1,2,3,4}) + + m1.int_set.add(5) + m1.int_set.remove(1) + assert m1.int_set == {2,3,4,5} + + m1.save() + + m2 = TestSetModel.get(partition=m1.partition) + assert m2.int_set == {2,3,4,5} + + def test_partial_update_creation(self): + """ + Tests that proper update statements are created for a partial set update + :return: + """ + ctx = {} + col = columns.Set(columns.Integer, db_field="TEST") + statements = col.get_update_statement({1,2,3,4}, {2,3,4,5}, ctx) + + assert len([v for v in ctx.values() if {1} == v.value]) == 1 + assert len([v for v in ctx.values() if {5} == v.value]) == 1 + assert len([s for s in statements if '"TEST" = "TEST" -' in s]) == 1 + assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 class TestListModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) From cdb1dc964287442dab13eb2300a2f4140e1d5ffb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 10 Mar 2013 19:13:07 -0700 Subject: [PATCH 0128/3961] adding support for partial updates for list columns --- cqlengine/columns.py | 62 ++++++++++++++++++- .../tests/columns/test_container_columns.py | 28 +++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 84b245da0c..6144d6c129 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -436,9 +436,67 @@ def to_database(self, value): def get_update_statement(self, val, prev, values): """ - http://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm + Returns statements that will be added to an object's update statement + also updates the query context """ - pass + # remove from Quoter containers, if applicable + if isinstance(val, self.Quoter): val = val.value + if isinstance(prev, self.Quoter): prev = prev.value + + def _insert(): + field_id = uuid1().hex + values[field_id] = self.Quoter(val) + return ['"{}" = :{}'.format(self.db_field_name, field_id)] + + if val is None or val == prev: + return [] + elif prev is None: + return _insert() + elif len(val) < len(prev): + return _insert() + else: + # the prepend and append lists, + # if both of these are still None after looking + # at both lists, an insert statement will be returned + prepend = None + append = None + + # the max start idx we want to compare + search_space = len(val) - max(0, len(prev)-1) + + # the size of the sub lists we want to look at + search_size = len(prev) + + for i in range(search_space): + #slice boundary + j = i + search_size + sub = val[i:j] + idx_cmp = lambda idx: prev[idx] == sub[idx] + if idx_cmp(0) and idx_cmp(-1) and prev == sub: + prepend = val[:i] + append = val[j:] + break + + # create update statements + if prepend is append is None: + return _insert() + + statements = [] + if prepend: + field_id = uuid1().hex + # CQL seems to prepend element at a time, starting + # with the element at idx 0, we can either reverse + # it here, or have it inserted in reverse + prepend.reverse() + values[field_id] = self.Quoter(prepend) + statements += ['"{0}" = :{1} + "{0}"'.format(self.db_field_name, field_id)] + + if append: + field_id = uuid1().hex + values[field_id] = self.Quoter(append) + statements += ['"{0}" = "{0}" + :{1}'.format(self.db_field_name, field_id)] + + return statements class Map(BaseContainerColumn): """ diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 15f8919288..11a0af001e 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -114,6 +114,34 @@ def test_type_validation(self): with self.assertRaises(ValidationError): TestListModel.create(int_list=['string', True], text_list=[1, 3.0]) + def test_partial_updates(self): + """ Tests that partial udpates work as expected """ + final = range(10) + initial = final[3:7] + m1 = TestListModel.create(int_list=initial) + + m1.int_list = final + m1.save() + + m2 = TestListModel.get(partition=m1.partition) + assert list(m2.int_list) == final + + def test_partial_update_creation(self): + """ + Tests that proper update statements are created for a partial list update + :return: + """ + final = range(10) + initial = final[3:7] + + ctx = {} + col = columns.List(columns.Integer, db_field="TEST") + statements = col.get_update_statement(final, initial, ctx) + + assert len([v for v in ctx.values() if [0,1,2] == v.value]) == 1 + assert len([v for v in ctx.values() if [7,8,9] == v.value]) == 1 + assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 + assert len([s for s in statements if '+ "TEST"' in s]) == 1 class TestMapModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) From a6332f12a0447458a97a49a5c12533311ea36efb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 10 Mar 2013 19:53:22 -0700 Subject: [PATCH 0129/3961] adding support for partial updates for map columns --- cqlengine/columns.py | 51 ++++++++++++++++++- cqlengine/query.py | 35 +++++++++---- .../tests/columns/test_container_columns.py | 37 ++++++++++++++ 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 6144d6c129..fd5fc7c9ca 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -366,6 +366,7 @@ def validate(self, value): def to_database(self, value): if value is None: return None + if isinstance(value, self.Quoter): return value return self.Quoter({self.value_col.to_database(v) for v in value}) def get_update_statement(self, val, prev, ctx): @@ -380,6 +381,8 @@ def get_update_statement(self, val, prev, ctx): """ # remove from Quoter containers, if applicable + val = self.to_database(val) + prev = self.to_database(prev) if isinstance(val, self.Quoter): val = val.value if isinstance(prev, self.Quoter): prev = prev.value @@ -432,6 +435,7 @@ def validate(self, value): def to_database(self, value): if value is None: return None + if isinstance(value, self.Quoter): return value return self.Quoter([self.value_col.to_database(v) for v in value]) def get_update_statement(self, val, prev, values): @@ -440,6 +444,8 @@ def get_update_statement(self, val, prev, values): also updates the query context """ # remove from Quoter containers, if applicable + val = self.to_database(val) + prev = self.to_database(prev) if isinstance(val, self.Quoter): val = val.value if isinstance(prev, self.Quoter): prev = prev.value @@ -552,12 +558,55 @@ def to_python(self, value): def to_database(self, value): if value is None: return None + if isinstance(value, self.Quoter): return value return self.Quoter({self.key_col.to_database(k):self.value_col.to_database(v) for k,v in value.items()}) def get_update_statement(self, val, prev, ctx): """ http://www.datastax.com/docs/1.2/cql_cli/using/collections_map#deletion """ - pass + # remove from Quoter containers, if applicable + val = self.to_database(val) + prev = self.to_database(prev) + if isinstance(val, self.Quoter): val = val.value + if isinstance(prev, self.Quoter): prev = prev.value + + #get the updated map + update = {k:v for k,v in val.items() if v != prev.get(k)} + + statements = [] + for k,v in update.items(): + key_id = uuid1().hex + val_id = uuid1().hex + ctx[key_id] = k + ctx[val_id] = v + statements += ['"{}"[:{}] = :{}'.format(self.db_field_name, key_id, val_id)] + + return statements + + def get_delete_statement(self, val, prev, ctx): + """ + Returns statements that will be added to an object's delete statement + also updates the query context, used for removing keys from a map + """ + if val is prev is None: + return [] + + val = self.to_database(val) + prev = self.to_database(prev) + if isinstance(val, self.Quoter): val = val.value + if isinstance(prev, self.Quoter): prev = prev.value + + old_keys = set(prev.keys()) + new_keys = set(val.keys()) + del_keys = old_keys - new_keys + + del_statements = [] + for key in del_keys: + field_id = uuid1().hex + ctx[field_id] = key + del_statements += ['"{}"[:{}]'.format(self.db_field_name, field_id)] + + return del_statements diff --git a/cqlengine/query.py b/cqlengine/query.py index fd91e45add..29ab13962b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,7 +4,7 @@ from hashlib import md5 from time import time from uuid import uuid1 -from cqlengine import BaseContainerColumn +from cqlengine import BaseContainerColumn, BaseValueManager, Map from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException @@ -681,17 +681,32 @@ def save(self): with connection_manager() as con: con.execute(qs, query_values) - #delete deleted / nulled columns - deleted = [k for k,v in self.instance._values.items() if v.deleted] - if deleted: - del_fields = [self.model._columns[f] for f in deleted] - del_fields = [f.db_field_name for f in del_fields if not f.primary_key] - pks = self.model._primary_keys - qs = ['DELETE {}'.format(', '.join(['"{}"'.format(f) for f in del_fields]))] + + # delete nulled columns and removed map keys + qs = ['DELETE'] + query_values = {} + + del_statements = [] + for k,v in self.instance._values.items(): + col = v.column + if v.deleted: + del_statements += ['"{}"'.format(col.db_field_name)] + elif isinstance(col, Map): + del_statements += col.get_delete_statement(v.value, v.previous_value, query_values) + + if del_statements: + qs += [', '.join(del_statements)] + qs += ['FROM {}'.format(self.column_family_name)] + qs += ['WHERE'] - eq = lambda col: '"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name]) - qs += [' AND '.join([eq(f) for f in pks.values()])] + where_statements = [] + for name, col in self.model._primary_keys.items(): + field_id = uuid1().hex + query_values[field_id] = field_values[name] + where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] + qs += [' AND '.join(where_statements)] + qs = ' '.join(qs) if self.batch: diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 11a0af001e..28dca78366 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -189,3 +189,40 @@ def test_type_validation(self): """ with self.assertRaises(ValidationError): TestMapModel.create(int_map={'key':2,uuid4():'val'}, text_map={2:5}) + + def test_partial_updates(self): + """ Tests that partial udpates work as expected """ + now = datetime.now() + #derez it a bit + now = datetime(*now.timetuple()[:-3]) + early = now - timedelta(minutes=30) + earlier = early - timedelta(minutes=30) + later = now + timedelta(minutes=30) + + initial = {'now':now, 'early':earlier} + final = {'later':later, 'early':early} + + m1 = TestMapModel.create(text_map=initial) + + m1.text_map = final + m1.save() + + m2 = TestMapModel.get(partition=m1.partition) + assert m2.text_map == final + +# def test_partial_update_creation(self): +# """ +# Tests that proper update statements are created for a partial list update +# :return: +# """ +# final = range(10) +# initial = final[3:7] +# +# ctx = {} +# col = columns.List(columns.Integer, db_field="TEST") +# statements = col.get_update_statement(final, initial, ctx) +# +# assert len([v for v in ctx.values() if [0,1,2] == v.value]) == 1 +# assert len([v for v in ctx.values() if [7,8,9] == v.value]) == 1 +# assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 +# assert len([s for s in statements if '+ "TEST"' in s]) == 1 From bd7c5c080a648e3e9d2e500a208a6aa432159765 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 10 Mar 2013 20:00:39 -0700 Subject: [PATCH 0130/3961] fixing failing unit test --- cqlengine/tests/columns/test_container_columns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 28dca78366..914c7479a8 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -138,7 +138,7 @@ def test_partial_update_creation(self): col = columns.List(columns.Integer, db_field="TEST") statements = col.get_update_statement(final, initial, ctx) - assert len([v for v in ctx.values() if [0,1,2] == v.value]) == 1 + assert len([v for v in ctx.values() if [2,1,0] == v.value]) == 1 assert len([v for v in ctx.values() if [7,8,9] == v.value]) == 1 assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 assert len([s for s in statements if '+ "TEST"' in s]) == 1 From 7f1d0c9e80e692c3244ebfd61a00b1bce0e6a846 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 11 Mar 2013 18:59:27 -0700 Subject: [PATCH 0131/3961] adding user configurable keyspace --- cqlengine/connection.py | 8 ++++++-- cqlengine/management.py | 9 +++++---- cqlengine/models.py | 11 +++++++++-- cqlengine/query.py | 5 ++++- cqlengine/tests/base.py | 2 +- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 52bc5cdef1..9be7922579 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -23,7 +23,7 @@ class CQLConnectionError(CQLEngineException): pass _password = None _max_connections = 10 -def setup(hosts, username=None, password=None, max_connections=10): +def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None): """ Records the hosts and connects to one of them @@ -36,7 +36,11 @@ def setup(hosts, username=None, password=None, max_connections=10): _username = username _password = password _max_connections = max_connections - + + if default_keyspace: + from cqlengine import models + models.DEFAULT_KEYSPACE = default_keyspace + for host in hosts: host = host.strip() host = host.split(':') diff --git a/cqlengine/management.py b/cqlengine/management.py index ba8b8145f7..5ef536cf7d 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -14,7 +14,8 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, :param **replication_values: 1.2 only, additional values to ad to the replication data map """ with connection_manager() as con: - if name not in [k.name for k in con.con.client.describe_keyspaces()]: + if not any([name == k.name for k in con.con.client.describe_keyspaces()]): +# if name not in [k.name for k in con.con.client.describe_keyspaces()]: try: #Try the 1.1 method con.execute("""CREATE KEYSPACE {} @@ -50,11 +51,11 @@ def create_table(model, create_missing_keyspace=True): #create missing keyspace if create_missing_keyspace: - create_keyspace(model.keyspace) + create_keyspace(model._get_keyspace()) with connection_manager() as con: #check for an existing column family - ks_info = con.con.client.describe_keyspace(model.keyspace) + ks_info = con.con.client.describe_keyspace(model._get_keyspace()) if not any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): qs = ['CREATE TABLE {}'.format(cf_name)] @@ -85,7 +86,7 @@ def add_column(col): raise #get existing index names, skip ones that already exist - ks_info = con.con.client.describe_keyspace(model.keyspace) + ks_info = con.con.client.describe_keyspace(model._get_keyspace()) cf_defs = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name] idx_names = [i.index_name for i in cf_defs[0].column_metadata] if cf_defs else [] idx_names = filter(None, idx_names) diff --git a/cqlengine/models.py b/cqlengine/models.py index 9b6ec3b084..b9801bf0bf 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -8,6 +8,8 @@ class ModelDefinitionException(ModelException): pass +DEFAULT_KEYSPACE = 'cqlengine' + class hybrid_classmethod(object): """ Allows a method to behave as both a class method and @@ -37,7 +39,7 @@ class MultipleObjectsReturned(QueryException): pass table_name = None #the keyspace for this model - keyspace = 'cqlengine' + keyspace = None read_repair_chance = 0.1 def __init__(self, **values): @@ -64,6 +66,11 @@ def _can_update(self): pks = self._primary_keys.keys() return all([not self._values[k].changed for k in self._primary_keys]) + @classmethod + def _get_keyspace(cls): + """ Returns the manual keyspace, if set, otherwise the default keyspace """ + return cls.keyspace or DEFAULT_KEYSPACE + def __eq__(self, other): return self.as_dict() == other.as_dict() @@ -93,7 +100,7 @@ def column_family_name(cls, include_keyspace=True): cf_name = cf_name.lower() cf_name = re.sub(r'^_+', '', cf_name) if not include_keyspace: return cf_name - return '{}.{}'.format(cls.keyspace, cf_name) + return '{}.{}'.format(cls._get_keyspace(), cf_name) @property def pk(self): diff --git a/cqlengine/query.py b/cqlengine/query.py index 29ab13962b..36909f1e44 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -184,7 +184,6 @@ class QuerySet(object): def __init__(self, model): super(QuerySet, self).__init__() self.model = model - self.column_family_name = self.model.column_family_name() #Where clause filters self._where = [] @@ -210,6 +209,10 @@ def __init__(self, model): self._batch = None + @property + def column_family_name(self): + return self.model.column_family_name() + def __unicode__(self): return self._select_query() diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index d94ea69b04..0b450cdd8b 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -7,7 +7,7 @@ class BaseCassEngTestCase(TestCase): def setUpClass(cls): super(BaseCassEngTestCase, cls).setUpClass() if not connection._hosts: - connection.setup(['localhost']) + connection.setup(['localhost'], default_keyspace='cqlengine_test') def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), From f04c831ec0b69f4eb44bf6f08b1592dcc4c86b1b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 12 Mar 2013 10:11:55 -0700 Subject: [PATCH 0132/3961] fixing bug caused by batch objects on querysets being copied --- cqlengine/query.py | 6 ++++++ cqlengine/tests/test_batch_query.py | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 36909f1e44..c148f8e44e 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -227,6 +227,12 @@ def __deepcopy__(self, memo): for k,v in self.__dict__.items(): if k in ['_con', '_cur', '_result_cache', '_result_idx']: clone.__dict__[k] = None + elif k == '_batch': + # we need to keep the same batch instance across + # all queryset clones, otherwise the batched queries + # fly off into other batch instances which are never + # executed, thx @dokai + clone.__dict__[k] = self._batch else: clone.__dict__[k] = copy.deepcopy(v, memo) diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index c6d509ebad..fb4aab4a72 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -31,8 +31,6 @@ def setUp(self): for obj in TestMultiKeyModel.filter(partition=self.pkey): obj.delete() - - def test_insert_success_case(self): b = BatchQuery() @@ -90,4 +88,18 @@ def test_context_manager(self): for i in range(5): TestMultiKeyModel.get(partition=self.pkey, cluster=i) + def test_bulk_delete_success_case(self): + + for i in range(1): + for j in range(5): + TestMultiKeyModel.create(partition=i, cluster=j, count=i*j, text='{}:{}'.format(i,j)) + + with BatchQuery() as b: + TestMultiKeyModel.objects.batch(b).filter(partition=0).delete() + assert TestMultiKeyModel.filter(partition=0).count() == 5 + + assert TestMultiKeyModel.filter(partition=0).count() == 0 + #cleanup + for m in TestMultiKeyModel.all(): + m.delete() From 1dd34e0029068a1eb9dcff75f6ac639e69c5b2ee Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 12 Mar 2013 21:57:23 -0700 Subject: [PATCH 0133/3961] updating batch query to work with 1.2 --- cqlengine/query.py | 22 +++++++++------------- cqlengine/tests/test_batch_query.py | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index c148f8e44e..2355edeadf 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -126,22 +126,20 @@ class LessThanOrEqualOperator(QueryOperator): symbol = "LTE" cql_symbol = '<=' -class Consistency(object): - ANY = 'ANY' - ONE = 'ONE' - QUORUM = 'QUORUM' - LOCAL_QUORUM = 'LOCAL_QUORUM' - EACH_QUORUM = 'EACH_QUORUM' - ALL = 'ALL' +class BatchType(object): + Unlogged = 'UNLOGGED' + Counter = 'COUNTER' class BatchQuery(object): """ Handles the batching of queries + + http://www.datastax.com/docs/1.2/cql_cli/cql/BATCH """ - def __init__(self, consistency=None, timestamp=None): + def __init__(self, batch_type=None, timestamp=None): self.queries = [] - self.consistency = consistency + self.batch_type = batch_type if timestamp is not None and not isinstance(timestamp, datetime): raise CQLEngineException('timestamp object must be an instance of datetime') self.timestamp = timestamp @@ -150,13 +148,11 @@ def add_query(self, query, params): self.queries.append((query, params)) def execute(self): - opener = 'BEGIN BATCH' - if self.consistency: - opener += ' USING CONSISTENCY {}'.format(self.consistency) + opener = 'BEGIN ' + (self.batch_type + ' ' if self.batch_type else '') + ' BATCH' if self.timestamp: epoch = datetime(1970, 1, 1) ts = long((self.timestamp - epoch).total_seconds() * 1000) - opener += ' TIMESTAMP {}'.format(ts) + opener += ' USING TIMESTAMP {}'.format(ts) query_list = [opener] parameters = {} diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index fb4aab4a72..6ba608778c 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -3,7 +3,7 @@ import random from cqlengine import Model, columns from cqlengine.management import delete_table, create_table -from cqlengine.query import BatchQuery, Consistency +from cqlengine.query import BatchQuery from cqlengine.tests.base import BaseCassEngTestCase class TestMultiKeyModel(Model): From fb99a4d70e63f6a2541f54175b89b20b0b9ad865 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 12 Mar 2013 21:57:23 -0700 Subject: [PATCH 0134/3961] updating batch query to work with 1.2 --- cqlengine/query.py | 22 ++--- cqlengine/tests/query/test_batch_query.py | 106 ++++++++++++++++++++++ cqlengine/tests/test_batch_query.py | 2 +- 3 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 cqlengine/tests/query/test_batch_query.py diff --git a/cqlengine/query.py b/cqlengine/query.py index c148f8e44e..2355edeadf 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -126,22 +126,20 @@ class LessThanOrEqualOperator(QueryOperator): symbol = "LTE" cql_symbol = '<=' -class Consistency(object): - ANY = 'ANY' - ONE = 'ONE' - QUORUM = 'QUORUM' - LOCAL_QUORUM = 'LOCAL_QUORUM' - EACH_QUORUM = 'EACH_QUORUM' - ALL = 'ALL' +class BatchType(object): + Unlogged = 'UNLOGGED' + Counter = 'COUNTER' class BatchQuery(object): """ Handles the batching of queries + + http://www.datastax.com/docs/1.2/cql_cli/cql/BATCH """ - def __init__(self, consistency=None, timestamp=None): + def __init__(self, batch_type=None, timestamp=None): self.queries = [] - self.consistency = consistency + self.batch_type = batch_type if timestamp is not None and not isinstance(timestamp, datetime): raise CQLEngineException('timestamp object must be an instance of datetime') self.timestamp = timestamp @@ -150,13 +148,11 @@ def add_query(self, query, params): self.queries.append((query, params)) def execute(self): - opener = 'BEGIN BATCH' - if self.consistency: - opener += ' USING CONSISTENCY {}'.format(self.consistency) + opener = 'BEGIN ' + (self.batch_type + ' ' if self.batch_type else '') + ' BATCH' if self.timestamp: epoch = datetime(1970, 1, 1) ts = long((self.timestamp - epoch).total_seconds() * 1000) - opener += ' TIMESTAMP {}'.format(ts) + opener += ' USING TIMESTAMP {}'.format(ts) query_list = [opener] parameters = {} diff --git a/cqlengine/tests/query/test_batch_query.py b/cqlengine/tests/query/test_batch_query.py new file mode 100644 index 0000000000..5b31826a39 --- /dev/null +++ b/cqlengine/tests/query/test_batch_query.py @@ -0,0 +1,106 @@ +from datetime import datetime +from unittest import skip +from uuid import uuid4 +import random +from cqlengine import Model, columns +from cqlengine.management import delete_table, create_table +from cqlengine.query import BatchQuery +from cqlengine.tests.base import BaseCassEngTestCase + +class TestMultiKeyModel(Model): + partition = columns.Integer(primary_key=True) + cluster = columns.Integer(primary_key=True) + count = columns.Integer(required=False) + text = columns.Text(required=False) + +class BatchQueryTests(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(BatchQueryTests, cls).setUpClass() + delete_table(TestMultiKeyModel) + create_table(TestMultiKeyModel) + + @classmethod + def tearDownClass(cls): + super(BatchQueryTests, cls).tearDownClass() + delete_table(TestMultiKeyModel) + + def setUp(self): + super(BatchQueryTests, self).setUp() + self.pkey = 1 + for obj in TestMultiKeyModel.filter(partition=self.pkey): + obj.delete() + + def test_insert_success_case(self): + + b = BatchQuery() + inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') + + with self.assertRaises(TestMultiKeyModel.DoesNotExist): + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + b.execute() + + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + def test_update_success_case(self): + + inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4') + + b = BatchQuery() + + inst.count = 4 + inst.batch(b).save() + + inst2 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) + assert inst2.count == 3 + + b.execute() + + inst3 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) + assert inst3.count == 4 + + def test_delete_success_case(self): + + inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4') + + b = BatchQuery() + + inst.batch(b).delete() + + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + b.execute() + + with self.assertRaises(TestMultiKeyModel.DoesNotExist): + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + def test_context_manager(self): + + with BatchQuery() as b: + for i in range(5): + TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=i, count=3, text='4') + + for i in range(5): + with self.assertRaises(TestMultiKeyModel.DoesNotExist): + TestMultiKeyModel.get(partition=self.pkey, cluster=i) + + for i in range(5): + TestMultiKeyModel.get(partition=self.pkey, cluster=i) + + def test_bulk_delete_success_case(self): + + for i in range(1): + for j in range(5): + TestMultiKeyModel.create(partition=i, cluster=j, count=i*j, text='{}:{}'.format(i,j)) + + with BatchQuery() as b: + TestMultiKeyModel.objects.batch(b).filter(partition=0).delete() + assert TestMultiKeyModel.filter(partition=0).count() == 5 + + assert TestMultiKeyModel.filter(partition=0).count() == 0 + #cleanup + for m in TestMultiKeyModel.all(): + m.delete() + diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index fb4aab4a72..6ba608778c 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -3,7 +3,7 @@ import random from cqlengine import Model, columns from cqlengine.management import delete_table, create_table -from cqlengine.query import BatchQuery, Consistency +from cqlengine.query import BatchQuery from cqlengine.tests.base import BaseCassEngTestCase class TestMultiKeyModel(Model): From 827bf18d6b87640ca37ae60ffd4b636d42a79b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Wed, 13 Mar 2013 23:15:31 +0100 Subject: [PATCH 0135/3961] include cf name in auto generated index name --- cqlengine/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 5ef536cf7d..67c34f6841 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -95,7 +95,7 @@ def add_column(col): if indexes: for column in indexes: if column.db_index_name in idx_names: continue - qs = ['CREATE INDEX {}'.format(column.db_index_name)] + qs = ['CREATE INDEX index_{}_{}'.format(raw_cf_name, column.db_field_name)] qs += ['ON {}'.format(cf_name)] qs += ['("{}")'.format(column.db_field_name)] qs = ' '.join(qs) From fa5547573d478ee57ac0e29ac9bc9c89dc2e596f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Wed, 13 Mar 2013 23:23:17 +0100 Subject: [PATCH 0136/3961] enable indexes on models with multiple primary keys (supported since cassandra 1.2) --- cqlengine/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index b9801bf0bf..aec06b4f89 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -230,12 +230,6 @@ def _transform_column(col_name, col_obj): raise ModelException("{} defines the column {} more than once".format(name, v.db_field_name)) col_names.add(v.db_field_name) - #check for indexes on models with multiple primary keys - if len([1 for k,v in column_definitions if v.primary_key]) > 1: - if len([1 for k,v in column_definitions if v.index]) > 0: - raise ModelDefinitionException( - 'Indexes on models with multiple primary keys is not supported') - #create db_name -> model name map for loading db_map = {} for field_name, col in column_dict.items(): From 1481391fa6ee3c7f993a1057beaaad7926c5dba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Fri, 15 Mar 2013 13:55:05 +0100 Subject: [PATCH 0137/3961] connection.setup 'lazy' parameter --- cqlengine/connection.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 9be7922579..e0afffdb8c 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -23,7 +23,7 @@ class CQLConnectionError(CQLEngineException): pass _password = None _max_connections = 10 -def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None): +def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None, lazy=False): """ Records the hosts and connects to one of them @@ -55,9 +55,10 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp raise CQLConnectionError("At least one host required") random.shuffle(_hosts) - - con = ConnectionPool.get() - ConnectionPool.put(con) + + if not lazy: + con = ConnectionPool.get() + ConnectionPool.put(con) class ConnectionPool(object): From f94283f0335126178952c2fc9c09bb66d15ead4d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 20 Mar 2013 19:24:33 -0700 Subject: [PATCH 0138/3961] fixing in operator per https://github.com/bdeggleston/cqlengine/issues/34 --- cqlengine/query.py | 21 +++++++++++++++++++++ cqlengine/tests/query/test_queryset.py | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/cqlengine/query.py b/cqlengine/query.py index 2355edeadf..0e2fd4e3a1 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -110,6 +110,27 @@ class InOperator(EqualsOperator): symbol = 'IN' cql_symbol = 'IN' + class Quoter(object): + """ + contains a single value, which will quote itself for CQL insertion statements + """ + def __init__(self, value): + self.value = value + + def __str__(self): + from cql.query import cql_quote as cq + return '(' + ', '.join([cq(v) for v in self.value]) + ')' + + def get_dict(self): + if isinstance(self.value, BaseQueryFunction): + return {self.identifier: self.column.to_database(self.value.get_value())} + else: + try: + values = [v for v in self.value] + except TypeError: + raise QueryException("in operator arguments must be iterable, {} found".format(self.value)) + return {self.identifier: self.Quoter([self.column.to_database(v) for v in self.value])} + class GreaterThanOperator(QueryOperator): symbol = "GT" cql_symbol = '>' diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 99f45afacd..cc003079e0 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -436,7 +436,11 @@ def test_success_case(self): assert '4' in datas +class TestInOperator(BaseQuerySetUsage): + def test_success_case(self): + q = TestModel.filter(test_id__in=[0,1]) + assert q.count() == 8 From e10237e8558e2058eee56a28396feea4cb25d50e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 20 Mar 2013 19:28:33 -0700 Subject: [PATCH 0139/3961] changing list column behavior to load lists, not tuples, from the database --- cqlengine/columns.py | 4 ++++ cqlengine/tests/columns/test_container_columns.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index fd5fc7c9ca..f63afff1de 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -433,6 +433,10 @@ def validate(self, value): raise ValidationError('{} is not a list object'.format(val)) return [self.value_col.validate(v) for v in val] + def to_python(self, value): + if value is None: return None + return list(value) + def to_database(self, value): if value is None: return None if isinstance(value, self.Quoter): return value diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 914c7479a8..b598da56bf 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -95,8 +95,8 @@ def test_io_success(self): m1 = TestListModel.create(int_list=[1,2], text_list=['kai', 'andreas']) m2 = TestListModel.get(partition=m1.partition) - assert isinstance(m2.int_list, tuple) - assert isinstance(m2.text_list, tuple) + assert isinstance(m2.int_list, list) + assert isinstance(m2.text_list, list) assert len(m2.int_list) == 2 assert len(m2.text_list) == 2 From cad39537efecb9847ddab2dda9bbe2add6fd0be6 Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Fri, 22 Mar 2013 09:52:18 +0200 Subject: [PATCH 0140/3961] Ignore empty batches instead of sending invalid CQL to Cassandra. --- cqlengine/query.py | 12 ++++++++---- cqlengine/tests/test_batch_query.py | 6 ++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 0e2fd4e3a1..3079986744 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -169,6 +169,10 @@ def add_query(self, query, params): self.queries.append((query, params)) def execute(self): + if len(self.queries) == 0: + # Empty batch is a no-op + return + opener = 'BEGIN ' + (self.batch_type + ' ' if self.batch_type else '') + ' BATCH' if self.timestamp: epoch = datetime(1970, 1, 1) @@ -257,7 +261,7 @@ def __deepcopy__(self, memo): def __len__(self): return self.count() - + def __del__(self): if self._con: self._con.close() @@ -347,7 +351,7 @@ def _fill_result_cache_to_idx(self, idx): value_dict = dict(zip(names, values)) self._result_idx += 1 self._result_cache[self._result_idx] = self._construct_instance(value_dict) - + #return the connection to the connection pool if we have all objects if self._result_cache and self._result_cache[-1] is not None: self._con.close() @@ -389,7 +393,7 @@ def __getitem__(self, s): else: self._fill_result_cache_to_idx(s) return self._result_cache[s] - + def _construct_instance(self, values): #translate column names to model names @@ -476,7 +480,7 @@ def get(self, **kwargs): '{} objects found'.format(len(self._result_cache))) else: return self[0] - + def order_by(self, colname): """ orders the result set. diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index 6ba608778c..7ee98b7084 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -103,3 +103,9 @@ def test_bulk_delete_success_case(self): for m in TestMultiKeyModel.all(): m.delete() + def test_empty_batch(self): + b = BatchQuery() + b.execute() + + with BatchQuery() as b: + pass From beb1f48760cb597f179fb7286b15def4cb4a79fe Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Fri, 22 Mar 2013 10:11:45 +0200 Subject: [PATCH 0141/3961] Allow multiple ORDER BY conditions to be set. Supports both .order_by('foo', 'bar') and chained .order_by() calls. --- cqlengine/query.py | 52 ++++++++++++++------------ cqlengine/tests/query/test_queryset.py | 38 +++++++++++++++---- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 0e2fd4e3a1..7082bf9905 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -206,7 +206,7 @@ def __init__(self, model): self._where = [] #ordering arguments - self._order = None + self._order = [] self._allow_filtering = False @@ -257,7 +257,7 @@ def __deepcopy__(self, memo): def __len__(self): return self.count() - + def __del__(self): if self._con: self._con.close() @@ -313,7 +313,7 @@ def _select_query(self): qs += ['WHERE {}'.format(self._where_clause())] if self._order: - qs += ['ORDER BY {}'.format(self._order)] + qs += ['ORDER BY {}'.format(', '.join(self._order))] if self._limit: qs += ['LIMIT {}'.format(self._limit)] @@ -347,7 +347,7 @@ def _fill_result_cache_to_idx(self, idx): value_dict = dict(zip(names, values)) self._result_idx += 1 self._result_cache[self._result_idx] = self._construct_instance(value_dict) - + #return the connection to the connection pool if we have all objects if self._result_cache and self._result_cache[-1] is not None: self._con.close() @@ -389,7 +389,7 @@ def __getitem__(self, s): else: self._fill_result_cache_to_idx(s) return self._result_cache[s] - + def _construct_instance(self, values): #translate column names to model names @@ -476,38 +476,42 @@ def get(self, **kwargs): '{} objects found'.format(len(self._result_cache))) else: return self[0] - - def order_by(self, colname): + + def order_by(self, *colnames): """ orders the result set. - ordering can only select one column, and it must be the second column in a composite primary key + ordering can only use clustering columns. Default order is ascending, prepend a '-' to the column name for descending """ - if colname is None: + if len(colnames) == 0: clone = copy.deepcopy(self) - clone._order = None + clone._order = [] return clone - order_type = 'DESC' if colname.startswith('-') else 'ASC' - colname = colname.replace('-', '') + conditions = [] + for colname in colnames: + order_type = 'DESC' if colname.startswith('-') else 'ASC' + colname = colname.replace('-', '') - column = self.model._columns.get(colname) - if column is None: - raise QueryException("Can't resolve the column name: '{}'".format(colname)) + column = self.model._columns.get(colname) + if column is None: + raise QueryException("Can't resolve the column name: '{}'".format(colname)) - #validate the column selection - if not column.primary_key: - raise QueryException( - "Can't order on '{}', can only order on (clustered) primary keys".format(colname)) + #validate the column selection + if not column.primary_key: + raise QueryException( + "Can't order on '{}', can only order on (clustered) primary keys".format(colname)) - pks = [v for k,v in self.model._columns.items() if v.primary_key] - if column == pks[0]: - raise QueryException( - "Can't order by the first primary key, clustering (secondary) keys only") + pks = [v for k, v in self.model._columns.items() if v.primary_key] + if column == pks[0]: + raise QueryException( + "Can't order by the first primary key (partition key), clustering (secondary) keys only") + + conditions.append('"{}" {}'.format(column.db_field_name, order_type)) clone = copy.deepcopy(self) - clone._order = '"{}" {}'.format(column.db_field_name, order_type) + clone._order.extend(conditions) return clone def count(self): diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index cc003079e0..660fb8db7f 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -26,6 +26,12 @@ class IndexedTestModel(Model): expected_result = columns.Integer() test_result = columns.Integer(index=True) +class TestMultiClusteringModel(Model): + one = columns.Integer(primary_key=True) + two = columns.Integer(primary_key=True) + three = columns.Integer(primary_key=True) + + class TestQuerySetOperation(BaseCassEngTestCase): def test_query_filter_parsing(self): @@ -115,6 +121,7 @@ def setUpClass(cls): delete_table(IndexedTestModel) create_table(TestModel) create_table(IndexedTestModel) + create_table(TestMultiClusteringModel) TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30) TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30) @@ -151,6 +158,7 @@ def tearDownClass(cls): super(BaseQuerySetUsage, cls).tearDownClass() delete_table(TestModel) delete_table(IndexedTestModel) + delete_table(TestMultiClusteringModel) class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): @@ -179,7 +187,7 @@ def test_iteration(self): 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) @@ -277,6 +285,20 @@ def test_ordering_on_indexed_columns_fails(self): with self.assertRaises(query.QueryException): q = IndexedTestModel.objects(test_id=0).order_by('attempt_id') + def test_ordering_on_multiple_clustering_columns(self): + TestMultiClusteringModel.create(one=1, two=1, three=4) + TestMultiClusteringModel.create(one=1, two=1, three=2) + TestMultiClusteringModel.create(one=1, two=1, three=5) + TestMultiClusteringModel.create(one=1, two=1, three=1) + TestMultiClusteringModel.create(one=1, two=1, three=3) + + results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('-two', '-three') + assert [r.three for r in results] == [5, 4, 3, 2, 1] + + results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('two', 'three') + assert [r.three for r in results] == [1, 2, 3, 4, 5] + + class TestQuerySetSlicing(BaseQuerySetUsage): def test_out_of_range_index_raises_error(self): @@ -344,12 +366,12 @@ def test_delete(self): TestModel.objects.create(test_id=3, attempt_id=1, description='try10', expected_result=60, test_result=40) TestModel.objects.create(test_id=3, attempt_id=2, description='try11', expected_result=70, test_result=45) TestModel.objects.create(test_id=3, attempt_id=3, description='try12', expected_result=75, test_result=45) - + assert TestModel.objects.count() == 16 assert TestModel.objects(test_id=3).count() == 4 - + TestModel.objects(test_id=3).delete() - + assert TestModel.objects.count() == 12 assert TestModel.objects(test_id=3).count() == 0 @@ -364,7 +386,7 @@ def test_delete_without_any_where_args(self): TestModel.objects(attempt_id=0).delete() class TestQuerySetConnectionHandling(BaseQuerySetUsage): - + def test_conn_is_returned_after_filling_cache(self): """ Tests that the queryset returns it's connection after it's fetched all of it's results @@ -376,10 +398,10 @@ def test_conn_is_returned_after_filling_cache(self): val = t.attempt_id, t.expected_result assert val in compare_set compare_set.remove(val) - + assert q._con is None assert q._cur is None - + def test_conn_is_returned_after_queryset_is_garbage_collected(self): """ Tests that the connection is returned to the connection pool after the queryset is gc'd """ from cqlengine.connection import ConnectionPool @@ -387,7 +409,7 @@ def test_conn_is_returned_after_queryset_is_garbage_collected(self): q = TestModel.objects(test_id=0) v = q[0] assert ConnectionPool._queue.qsize() == 0 - + del q assert ConnectionPool._queue.qsize() == 1 From c4d4eb874e79250b855199c375c6f53dd4fc01ea Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Fri, 22 Mar 2013 10:20:03 +0200 Subject: [PATCH 0142/3961] Added a derived Date type. Internally it is stored as a timestamp but the value is exposed as a instance. --- cqlengine/columns.py | 32 ++++++++++++++-- cqlengine/tests/columns/test_validation.py | 43 +++++++++++++++++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index f63afff1de..5cbe6a0946 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -1,6 +1,7 @@ #column field types from copy import copy from datetime import datetime +from datetime import date import re from uuid import uuid1, uuid4 from cql.query import cql_quote @@ -216,6 +217,31 @@ def to_database(self, value): epoch = datetime(1970, 1, 1) return long((value - epoch).total_seconds() * 1000) + +class Date(Column): + db_type = 'timestamp' + + def __init__(self, **kwargs): + super(Date, self).__init__(**kwargs) + + def to_python(self, value): + if isinstance(value, datetime): + return value.date() + elif isinstance(value, date): + return value + + return date.fromtimestamp(value) + + def to_database(self, value): + value = super(Date, self).to_database(value) + if isinstance(value, datetime): + value = value.date() + if not isinstance(value, date): + raise ValidationError("'{}' is not a date object".format(repr(value))) + + return long((value - date(1970, 1, 1)).total_seconds() * 1000) + + class UUID(Column): """ Type 1 or 4 UUID @@ -235,14 +261,14 @@ def validate(self, value): if not self.re_uuid.match(val): raise ValidationError("{} is not a valid uuid".format(value)) return _UUID(val) - + class TimeUUID(UUID): """ UUID containing timestamp """ - + db_type = 'timeuuid' - + def __init__(self, **kwargs): kwargs.setdefault('default', lambda: uuid1()) super(TimeUUID, self).__init__(**kwargs) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 4921b67256..44903a4c85 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -1,5 +1,6 @@ #tests the behavior of the column classes from datetime import datetime +from datetime import date from decimal import Decimal as D from cqlengine import ValidationError @@ -11,6 +12,7 @@ from cqlengine.columns import Text from cqlengine.columns import Integer from cqlengine.columns import DateTime +from cqlengine.columns import Date from cqlengine.columns import UUID from cqlengine.columns import Boolean from cqlengine.columns import Float @@ -40,6 +42,37 @@ def test_datetime_io(self): dt2 = self.DatetimeTest.objects(test_id=0).first() assert dt2.created_at.timetuple()[:6] == now.timetuple()[:6] + +class TestDate(BaseCassEngTestCase): + class DateTest(Model): + test_id = Integer(primary_key=True) + created_at = Date() + + @classmethod + def setUpClass(cls): + super(TestDate, cls).setUpClass() + create_table(cls.DateTest) + + @classmethod + def tearDownClass(cls): + super(TestDate, cls).tearDownClass() + delete_table(cls.DateTest) + + def test_date_io(self): + today = date.today() + self.DateTest.objects.create(test_id=0, created_at=today) + dt2 = self.DateTest.objects(test_id=0).first() + assert dt2.created_at.isoformat() == today.isoformat() + + def test_date_io_using_datetime(self): + now = datetime.utcnow() + self.DateTest.objects.create(test_id=0, created_at=now) + dt2 = self.DateTest.objects(test_id=0).first() + assert not isinstance(dt2.created_at, datetime) + assert isinstance(dt2.created_at, date) + assert dt2.created_at.isoformat() == now.date().isoformat() + + class TestDecimal(BaseCassEngTestCase): class DecimalTest(Model): test_id = Integer(primary_key=True) @@ -63,26 +96,26 @@ def test_datetime_io(self): dt = self.DecimalTest.objects.create(test_id=0, dec_val=5) dt2 = self.DecimalTest.objects(test_id=0).first() assert dt2.dec_val == D('5') - + class TestTimeUUID(BaseCassEngTestCase): class TimeUUIDTest(Model): test_id = Integer(primary_key=True) timeuuid = TimeUUID() - + @classmethod def setUpClass(cls): super(TestTimeUUID, cls).setUpClass() create_table(cls.TimeUUIDTest) - + @classmethod def tearDownClass(cls): super(TestTimeUUID, cls).tearDownClass() delete_table(cls.TimeUUIDTest) - + def test_timeuuid_io(self): t0 = self.TimeUUIDTest.create(test_id=0) t1 = self.TimeUUIDTest.get(test_id=0) - + assert t1.timeuuid.time == t1.timeuuid.time class TestInteger(BaseCassEngTestCase): From 1cffc497e394f1d62b6d6f09d1217b013ea91f61 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Mar 2013 21:54:54 -0700 Subject: [PATCH 0143/3961] adding test around chaining order_by statements --- cqlengine/tests/base.py | 2 +- cqlengine/tests/query/test_queryset.py | 3 +++ requirements.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index 0b450cdd8b..e9fa8a9b19 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -7,7 +7,7 @@ class BaseCassEngTestCase(TestCase): def setUpClass(cls): super(BaseCassEngTestCase, cls).setUpClass() if not connection._hosts: - connection.setup(['localhost'], default_keyspace='cqlengine_test') + connection.setup(['localhost:9170'], default_keyspace='cqlengine_test') def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 660fb8db7f..d5e6fe16db 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -298,6 +298,9 @@ def test_ordering_on_multiple_clustering_columns(self): results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('two', 'three') assert [r.three for r in results] == [1, 2, 3, 4, 5] + results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('two').order_by('three') + assert [r.three for r in results] == [1, 2, 3, 4, 5] + class TestQuerySetSlicing(BaseQuerySetUsage): diff --git a/requirements.txt b/requirements.txt index 2415255265..04a6bc4465 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cql==1.2.0 +cql==1.4.0 ipython==0.13.1 ipdb==0.7 Sphinx==1.1.3 From 92e37c96c81211d57265661dc16b1bccaee40ed6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Mar 2013 22:01:46 -0700 Subject: [PATCH 0144/3961] making date deserialize with utc timezone --- cqlengine/columns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 5cbe6a0946..76830d2c27 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -230,7 +230,7 @@ def to_python(self, value): elif isinstance(value, date): return value - return date.fromtimestamp(value) + return datetime.utcfromtimestamp(value).date() def to_database(self, value): value = super(Date, self).to_database(value) From 5a6c276a887d38da1cae8cfb90188c60bf298a38 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 23 Mar 2013 22:04:00 -0700 Subject: [PATCH 0145/3961] modifying pk comparison to look at the db_field_name, so it's not dependent on the ids being identical --- cqlengine/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a0f758f18f..bb7d036f64 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -604,7 +604,7 @@ def delete(self, columns=[]): """ #validate where clause partition_key = self.model._primary_keys.values()[0] - if not any([c.column == partition_key for c in self._where]): + if not any([c.column.db_field_name == partition_key.db_field_name for c in self._where]): raise QueryException("The partition key must be defined on delete queries") qs = ['DELETE FROM {}'.format(self.column_family_name)] qs += ['WHERE {}'.format(self._where_clause())] From 2be6f3409c0b3fedc0e57bd99072e73571a30852 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 22 Apr 2013 16:02:31 -0700 Subject: [PATCH 0146/3961] Fixed issue with trying to call .close() on a None --- cqlengine/connection.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index e0afffdb8c..2a5ef989eb 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -78,7 +78,7 @@ def clear(cls): cls._queue.get().close() except: pass - + @classmethod def get(cls): """ @@ -124,7 +124,7 @@ def _create_connection(cls): global _hosts global _username global _password - + if not _hosts: raise CQLConnectionError("At least one host required") @@ -133,7 +133,7 @@ def _create_connection(cls): new_conn = cql.connect(host.name, host.port, user=_username, password=_password) new_conn.set_cql_version('3.0.0') return new_conn - + class connection_manager(object): """ @@ -145,7 +145,7 @@ def __init__(self): self.keyspace = None self.con = ConnectionPool.get() self.cur = None - + def close(self): if self.cur: self.cur.close() ConnectionPool.put(self.con) @@ -175,7 +175,6 @@ def execute(self, query, params={}): except TTransportException: #TODO: check for other errors raised in the event of a connection / server problem #move to the next connection and set the connection pool - self.con = None _host_idx += 1 _host_idx %= len(_hosts) self.con.close() From 8ec6fc38a43eea201e7086c455fdccb54d5df7de Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 2 May 2013 22:21:56 -0700 Subject: [PATCH 0147/3961] version number bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 184a4c8a38..ed5a820213 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.1.2' +__version__ = '0.2' diff --git a/docs/conf.py b/docs/conf.py index 59c9a1239b..96c7db6ed6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.1.2' +version = '0.2' # The full version, including alpha/beta/rc tags. -release = '0.1.2' +release = '0.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 4190fb384f..791c75df20 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.1.2' +version = '0.2' long_desc = """ cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine From b8584770c0cf2a0b6c2713a4760b2a4b82ce4546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Sat, 4 May 2013 19:53:46 +0200 Subject: [PATCH 0148/3961] Token function --- cqlengine/functions.py | 11 +++++++++++ cqlengine/query.py | 11 +++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 15e93d4659..3f023e75fb 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -23,6 +23,9 @@ def to_cql(self, value_id): def get_value(self): raise NotImplementedError + def format_cql(self, field, operator, value_id): + return '"{}" {} {}'.format(field, operator, self.to_cql(value_id)) + class MinTimeUUID(BaseQueryFunction): _cql_string = 'MinTimeUUID(:{})' @@ -57,3 +60,11 @@ def get_value(self): epoch = datetime(1970, 1, 1) return long((self.value - epoch).total_seconds() * 1000) +class Token(BaseQueryFunction): + _cql_string = 'token(:{})' + + def format_cql(self, field, operator, value_id): + return 'token("{}") {} {}'.format(field, operator, self.to_cql(value_id)) + + def get_value(self): + return self.value diff --git a/cqlengine/query.py b/cqlengine/query.py index bb7d036f64..a2762d3af0 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -8,7 +8,7 @@ from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException -from cqlengine.functions import BaseQueryFunction +from cqlengine.functions import BaseQueryFunction, Token #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -44,7 +44,7 @@ def cql(self): :param valname: the dict key that this operator's compare value will be found in """ if isinstance(self.value, BaseQueryFunction): - return '"{}" {} {}'.format(self.column.db_field_name, self.cql_symbol, self.value.to_cql(self.identifier)) + return self.value.format_cql(self.column.db_field_name, self.cql_symbol, self.identifier) else: return '"{}" {} :{}'.format(self.column.db_field_name, self.cql_symbol, self.identifier) @@ -275,14 +275,17 @@ def _validate_where_syntax(self): #check that there's either a = or IN relationship with a primary key or indexed field equal_ops = [w for w in self._where if isinstance(w, EqualsOperator)] - if not any([w.column.primary_key or w.column.index for w in equal_ops]): + token_ops = [w for w in self._where if isinstance(w.value, Token)] + if not any([w.column.primary_key or w.column.index for w in equal_ops]) and not token_ops: raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') if not self._allow_filtering: #if the query is not on an indexed field if not any([w.column.index for w in equal_ops]): - if not any([w.column._partition_key for w in equal_ops]): + if not any([w.column._partition_key for w in equal_ops]) and not token_ops: raise QueryException('Filtering on a clustering key without a partition key is not allowed unless allow_filtering() is called on the querset') + if any(not w.column._partition_key for w in token_ops): + raise QueryException('The token() function is only supported on the partition key') #TODO: abuse this to see if we can get cql to raise an exception From 3cb08dd23a5c28ec573bb0fd328b513a8b4c4e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Sat, 4 May 2013 20:05:22 +0200 Subject: [PATCH 0149/3961] do not call count() in QuerySet.__len__ --- cqlengine/query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a2762d3af0..fe8680b4e1 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -260,7 +260,8 @@ def __deepcopy__(self, memo): return clone def __len__(self): - return self.count() + self._execute_query() + return len(self._result_cache) def __del__(self): if self._con: From bc4f7ed78b8d9ee9e6474517d9ab262ca08099ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Sat, 4 May 2013 22:30:27 +0200 Subject: [PATCH 0150/3961] QuerySet.values_list support --- cqlengine/query.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index fe8680b4e1..9ac1f948bb 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -222,6 +222,9 @@ def __init__(self, model): self._defer_fields = [] self._only_fields = [] + self._values_list = False + self._flat_values_list = False + #results cache self._con = None self._cur = None @@ -340,6 +343,9 @@ def _execute_query(self): self._con = connection_manager() self._cur = self._con.execute(self._select_query(), self._where_values()) self._result_cache = [None]*self._cur.rowcount + if self._cur.description: + names = [i[0] for i in self._cur.description] + self._construct_result = self._create_result_constructor(names) def _fill_result_cache_to_idx(self, idx): self._execute_query() @@ -350,14 +356,12 @@ def _fill_result_cache_to_idx(self, idx): if qty < 1: return else: - names = [i[0] for i in self._cur.description] for values in self._cur.fetchmany(qty): - value_dict = dict(zip(names, values)) self._result_idx += 1 - self._result_cache[self._result_idx] = self._construct_instance(value_dict) + self._result_cache[self._result_idx] = self._construct_result(values) #return the connection to the connection pool if we have all objects - if self._result_cache and self._result_cache[-1] is not None: + if self._result_cache and self._result_idx == (len(self._result_cache) - 1): self._con.close() self._con = None self._cur = None @@ -398,6 +402,17 @@ def __getitem__(self, s): self._fill_result_cache_to_idx(s) return self._result_cache[s] + def _create_result_constructor(self, names): + if not self._values_list: + return (lambda values: self._construct_instance(dict(zip(names, values)))) + + db_map = self.model._db_map + columns = [self.model._columns[db_map[name]] for name in names] + if self._flat_values_list: + return (lambda values: columns[0].to_python(values[0])) + else: + # result_cls = namedtuple("{}Tuple".format(self.model.__name__), names) + return (lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values))) def _construct_instance(self, values): #translate column names to model names @@ -620,6 +635,18 @@ def delete(self, columns=[]): with connection_manager() as con: con.execute(qs, self._where_values()) + def values_list(self, *fields, **kwargs): + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' + % (kwargs.keys(),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is called with more than one field.") + clone = self.only(fields) + clone._values_list = True + clone._flat_values_list = flat + return clone + class DMLQuery(object): """ A query object used for queries performing inserts, updates, or deletes From 49bd7415a82f1db81cc8696f804a1ca958787833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Tue, 7 May 2013 00:52:06 +0200 Subject: [PATCH 0151/3961] memory leak fix --- cqlengine/query.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 9ac1f948bb..a4690df04a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -403,30 +403,23 @@ def __getitem__(self, s): return self._result_cache[s] def _create_result_constructor(self, names): + model = self.model + db_map = model._db_map if not self._values_list: - return (lambda values: self._construct_instance(dict(zip(names, values)))) - - db_map = self.model._db_map - columns = [self.model._columns[db_map[name]] for name in names] + def _construct_instance(values): + field_dict = dict((db_map.get(k, k), v) for k, v in zip(names, values)) + instance = model(**field_dict) + instance._is_persisted = True + return instance + return _construct_instance + + columns = [model._columns[db_map[name]] for name in names] if self._flat_values_list: return (lambda values: columns[0].to_python(values[0])) else: # result_cls = namedtuple("{}Tuple".format(self.model.__name__), names) return (lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values))) - def _construct_instance(self, values): - #translate column names to model names - field_dict = {} - db_map = self.model._db_map - for key, val in values.items(): - if key in db_map: - field_dict[db_map[key]] = val - else: - field_dict[key] = val - instance = self.model(**field_dict) - instance._is_persisted = True - return instance - def batch(self, batch_obj): """ Adds a batch query to the mix From edaf83f3aec166b5a28676404b0ad03c95d42547 Mon Sep 17 00:00:00 2001 From: Pandu Rao Date: Wed, 8 May 2013 17:06:09 -0700 Subject: [PATCH 0152/3961] Added guards to ensure that prev and val exist before calling their keys function --- cqlengine/columns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 76830d2c27..8a2dd79ea6 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -627,8 +627,8 @@ def get_delete_statement(self, val, prev, ctx): if isinstance(val, self.Quoter): val = val.value if isinstance(prev, self.Quoter): prev = prev.value - old_keys = set(prev.keys()) - new_keys = set(val.keys()) + old_keys = set(prev.keys()) if prev else set() + new_keys = set(val.keys()) if val else set() del_keys = old_keys - new_keys del_statements = [] From 1bdd1a14b135a6c6c6d6d86118135de983225f79 Mon Sep 17 00:00:00 2001 From: "Gregory R. Doermann" Date: Thu, 9 May 2013 11:21:44 -0600 Subject: [PATCH 0153/3961] Timezone aware datetime fix. --- cqlengine/columns.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 76830d2c27..9c67a1641b 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -214,8 +214,12 @@ def to_database(self, value): value = super(DateTime, self).to_database(value) if not isinstance(value, datetime): raise ValidationError("'{}' is not a datetime object".format(value)) - epoch = datetime(1970, 1, 1) - return long((value - epoch).total_seconds() * 1000) + epoch = datetime(1970, 1, 1, tzinfo=value.tzinfo) + offset = 0 + if epoch.tzinfo: + offset_delta = epoch.tzinfo.utcoffset(epoch) + offset = offset_delta.days*24*3600 + offset_delta.seconds + return long(((value - epoch).total_seconds() - offset) * 1000) class Date(Column): From 29f5ef42acc0dbf73c6ea4a5ca312c3386656096 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 9 May 2013 10:40:05 -0700 Subject: [PATCH 0154/3961] fixing the base test connection --- cqlengine/tests/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index e9fa8a9b19..64e6a3c2a9 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -7,7 +7,7 @@ class BaseCassEngTestCase(TestCase): def setUpClass(cls): super(BaseCassEngTestCase, cls).setUpClass() if not connection._hosts: - connection.setup(['localhost:9170'], default_keyspace='cqlengine_test') + connection.setup(['localhost:9160'], default_keyspace='cqlengine_test') def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), From c7f9a6d9378ded459e65103a9f028ed13071a172 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 9 May 2013 10:56:57 -0700 Subject: [PATCH 0155/3961] adding check to update statement to avoid failure on update from None --- cqlengine/columns.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 8a2dd79ea6..e028c538c9 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -600,6 +600,8 @@ def get_update_statement(self, val, prev, ctx): prev = self.to_database(prev) if isinstance(val, self.Quoter): val = val.value if isinstance(prev, self.Quoter): prev = prev.value + val = val or {} + prev = prev or {} #get the updated map update = {k:v for k,v in val.items() if v != prev.get(k)} From 47a0099708c381969125b18a3cd0416a08a55a1d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 9 May 2013 10:57:50 -0700 Subject: [PATCH 0156/3961] adding additional tests around map column updates --- .../tests/columns/test_container_columns.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index b598da56bf..3533aad3ef 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -210,6 +210,26 @@ def test_partial_updates(self): m2 = TestMapModel.get(partition=m1.partition) assert m2.text_map == final + def test_updates_from_none(self): + """ Tests that updates from None work as expected """ + m = TestMapModel.create(int_map=None) + expected = {1:uuid4()} + m.int_map = expected + m.save() + + m2 = TestMapModel.get(partition=m.partition) + assert m2.int_map == expected + + + def test_updates_to_none(self): + """ Tests that setting the field to None works as expected """ + m = TestMapModel.create(int_map={1:uuid4()}) + m.int_map = None + m.save() + + m2 = TestMapModel.get(partition=m.partition) + assert m2.int_map is None + # def test_partial_update_creation(self): # """ # Tests that proper update statements are created for a partial list update From 558f6981b4ae3b064637fc4b561a1675866eea03 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 9 May 2013 11:59:10 -0700 Subject: [PATCH 0157/3961] added test for date time column with tzinfo --- cqlengine/tests/columns/test_validation.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 44903a4c85..e323c4653d 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -1,6 +1,7 @@ #tests the behavior of the column classes -from datetime import datetime +from datetime import datetime, timedelta from datetime import date +from datetime import tzinfo from decimal import Decimal as D from cqlengine import ValidationError @@ -42,6 +43,18 @@ def test_datetime_io(self): dt2 = self.DatetimeTest.objects(test_id=0).first() assert dt2.created_at.timetuple()[:6] == now.timetuple()[:6] + def test_datetime_tzinfo_io(self): + class TZ(tzinfo): + def utcoffset(self, date_time): + return timedelta(hours=-1) + def dst(self, date_time): + return None + + now = datetime(1982, 1, 1, tzinfo=TZ()) + dt = self.DatetimeTest.objects.create(test_id=0, created_at=now) + dt2 = self.DatetimeTest.objects(test_id=0).first() + assert dt2.created_at.timetuple()[:6] == (now + timedelta(hours=1)).timetuple()[:6] + class TestDate(BaseCassEngTestCase): class DateTest(Model): From 8d23c7ba7adb05a8e6404bb676af67ad866398b6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 9 May 2013 12:03:20 -0700 Subject: [PATCH 0158/3961] updating pypi description --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 791c75df20..af82216686 100644 --- a/setup.py +++ b/setup.py @@ -16,8 +16,6 @@ [Users Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users) [Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) - -**NOTE: cqlengine is in alpha and under development, some features may change. Make sure to check the changelog and test your app before upgrading** """ setup( From 7316fa20c02044e573ee8c769420bebbeba4a097 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 9 May 2013 12:04:24 -0700 Subject: [PATCH 0159/3961] updating changelog --- changelog | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 00dc02bced..3edb19f0fa 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,10 @@ CHANGELOG -0.1.2 (in progress) +0.2.1 (in progress) +* adding support for datetimes with tzinfo (thanks @gdoermann) +* fixing bug in saving map updates (thanks @pandu-rao) + +0.2 * expanding internal save function to use update where appropriate * adding set, list, and map collection types * adding support for allow filtering flag From 2f8752353506cad8d806f362847517a8bb2475cb Mon Sep 17 00:00:00 2001 From: Silvio Tomatis Date: Mon, 13 May 2013 14:45:41 +0200 Subject: [PATCH 0160/3961] Add travis testing --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..86b92c0ac8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: + - "2.7" +services: + - cassandra +before_install: + - sudo sh -c "echo 'JVM_OPTS=\"\${JVM_OPTS} -Djava.net.preferIPv4Stack=false\"' >> /usr/local/cassandra/conf/cassandra-env.sh" + - sudo service cassandra start +install: + - "pip install -r requirements.txt --use-mirrors" + - "pip install pytest --use-mirrors" +script: + - while [ ! -f /var/run/cassandra.pid ] ; do sleep 1 ; done # wait until cassandra is ready + - "py.test cqlengine/tests/" From 4fee79b348a9d19b8cf09bf748762265866c6ce8 Mon Sep 17 00:00:00 2001 From: Silvio Tomatis Date: Mon, 13 May 2013 15:10:14 +0200 Subject: [PATCH 0161/3961] Make all tests pass when run alltogether with 'py.test cqlengine/tests' --- cqlengine/tests/query/test_queryset.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index d5e6fe16db..a77d750299 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -408,13 +408,15 @@ def test_conn_is_returned_after_filling_cache(self): def test_conn_is_returned_after_queryset_is_garbage_collected(self): """ Tests that the connection is returned to the connection pool after the queryset is gc'd """ from cqlengine.connection import ConnectionPool - assert ConnectionPool._queue.qsize() == 1 + # The queue size can be 1 if we just run this file's tests + # It will be 2 when we run 'em all + initial_size = ConnectionPool._queue.qsize() q = TestModel.objects(test_id=0) v = q[0] - assert ConnectionPool._queue.qsize() == 0 + assert ConnectionPool._queue.qsize() == initial_size - 1 del q - assert ConnectionPool._queue.qsize() == 1 + assert ConnectionPool._queue.qsize() == initial_size class TimeUUIDQueryModel(Model): partition = columns.UUID(primary_key=True) From 0022f21c975f1b47e8c8b9f25b73a35bc372a3bf Mon Sep 17 00:00:00 2001 From: Silvio Tomatis Date: Mon, 13 May 2013 15:48:44 +0200 Subject: [PATCH 0162/3961] Add test for boolean column in test_model_io --- cqlengine/tests/model/test_model_io.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 7f708cf214..08e77ded36 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,4 +1,3 @@ -from unittest import skip from uuid import uuid4 import random from cqlengine.tests.base import BaseCassEngTestCase @@ -11,6 +10,7 @@ class TestModel(Model): count = columns.Integer() text = columns.Text(required=False) + a_bool = columns.Boolean(default=False) class TestModelIO(BaseCassEngTestCase): @@ -41,10 +41,12 @@ def test_model_updating_works_properly(self): tm = TestModel.objects.create(count=8, text='123456789') tm.count = 100 + tm.a_bool = True tm.save() tm2 = TestModel.objects(id=tm.pk).first() self.assertEquals(tm.count, tm2.count) + self.assertEquals(tm.a_bool, tm2.a_bool) def test_model_deleting_works_properly(self): """ From 52f87303a4681521d6b93772a7d0a478005e30b1 Mon Sep 17 00:00:00 2001 From: "Gregory R. Doermann" Date: Wed, 15 May 2013 13:46:27 -0600 Subject: [PATCH 0163/3961] Basic (working) counter column. You can add and subtract values. You must use CounterModel when using a CounterColumn. --- cqlengine/__init__.py | 2 +- cqlengine/columns.py | 8 +++++--- cqlengine/models.py | 12 ++++++++++++ cqlengine/query.py | 15 +++++++++++++-- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 184a4c8a38..e46d0e6d49 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,6 +1,6 @@ from cqlengine.columns import * from cqlengine.functions import * -from cqlengine.models import Model +from cqlengine.models import Model, CounterModel from cqlengine.query import BatchQuery __version__ = '0.1.2' diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 9c67a1641b..ce7ce9936c 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -310,11 +310,13 @@ def to_database(self, value): class Decimal(Column): db_type = 'decimal' -class Counter(Column): - #TODO: counter field +class Counter(Integer): + """ Validates like an integer, goes into the database as a counter + """ + db_type = 'counter' + def __init__(self, **kwargs): super(Counter, self).__init__(**kwargs) - raise NotImplementedError class ContainerQuoter(object): """ diff --git a/cqlengine/models.py b/cqlengine/models.py index aec06b4f89..372f3253bf 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -261,3 +261,15 @@ class Model(BaseModel): __metaclass__ = ModelMetaClass +class CounterBaseModel(BaseModel): + def _can_update(self): + # INSERT is not allowed for counter column families + return True + + +class CounterModel(CounterBaseModel): + """ + the db name for the column family can be set as the attribute db_name, or + it will be genertaed from the class name + """ + __metaclass__ = ModelMetaClass diff --git a/cqlengine/query.py b/cqlengine/query.py index bb7d036f64..db8d17a318 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,7 +4,7 @@ from hashlib import md5 from time import time from uuid import uuid1 -from cqlengine import BaseContainerColumn, BaseValueManager, Map +from cqlengine import BaseContainerColumn, BaseValueManager, Map, Counter from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException @@ -676,7 +676,18 @@ def save(self): if not col.is_primary_key: val = values.get(name) if val is None: continue - if isinstance(col, BaseContainerColumn): + if isinstance(col, Counter): + field_ids.pop(name) + value = field_values.pop(name) + if value == 0: + # Don't increment that column + continue + elif value < 0: + sign = '-' + else: + sign = '+' + set_statements += ['{0} = {0} {1} {2}'.format(col.db_field_name, sign, abs(value))] + elif isinstance(col, BaseContainerColumn): #remove value from query values, the column will handle it query_values.pop(field_ids.get(name), None) From 1b72535064d6954b70d41cb2de236457ed711786 Mon Sep 17 00:00:00 2001 From: "Gregory R. Doermann" Date: Fri, 17 May 2013 14:12:14 -0600 Subject: [PATCH 0164/3961] Custom TTL and timestamp information can be passed in on save. --- cqlengine/connection.py | 4 +++- cqlengine/functions.py | 15 +++++++++++++++ cqlengine/models.py | 19 +++++++++++++++---- cqlengine/query.py | 26 ++++++++++++++++++++++---- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 2a5ef989eb..c56d08a928 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -156,13 +156,15 @@ def __enter__(self): def __exit__(self, type, value, traceback): self.close() - def execute(self, query, params={}): + def execute(self, query, params=None): """ Gets a connection from the pool and executes the given query, returns the cursor if there's a connection problem, this will silently create a new connection pool from the available hosts, and remove the problematic host from the host list """ + if params is None: + params = {} global _host_idx for i in range(len(_hosts)): diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 15e93d4659..b2886c9ab4 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -57,3 +57,18 @@ def get_value(self): epoch = datetime(1970, 1, 1) return long((self.value - epoch).total_seconds() * 1000) + +class NotSet(object): + """ + Different from None, so you know when it just wasn't set compared to passing in None + """ + pass + + +def format_timestamp(timestamp): + if isinstance(timestamp, datetime): + epoch = datetime(1970, 1, 1) + ts = long((timestamp - epoch).total_seconds() * 1000) + else : + ts = long(timestamp) + return ts diff --git a/cqlengine/models.py b/cqlengine/models.py index 372f3253bf..33ccb56b92 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -3,9 +3,10 @@ from cqlengine import columns from cqlengine.exceptions import ModelException -from cqlengine.functions import BaseQueryFunction +from cqlengine.functions import BaseQueryFunction, NotSet from cqlengine.query import QuerySet, QueryException, DMLQuery + class ModelDefinitionException(ModelException): pass DEFAULT_KEYSPACE = 'cqlengine' @@ -38,12 +39,20 @@ class MultipleObjectsReturned(QueryException): pass #however, you can also define them manually here table_name = None + #DEFAULT_TTL must be an integer seconds for the default time to live on any insert on the table + #this can be overridden on any given query, but you can set a default on the model + DEFAULT_TTL = None + #the keyspace for this model keyspace = None read_repair_chance = 0.1 - def __init__(self, **values): + def __init__(self, ttl=NotSet, **values): self._values = {} + if ttl == NotSet: + self.ttl = self.DEFAULT_TTL + else: + self.ttl = ttl for name, column in self._columns.items(): value = values.get(name, None) if value is not None: value = column.to_python(value) @@ -136,10 +145,12 @@ def filter(cls, **kwargs): def get(cls, **kwargs): return cls.objects.get(**kwargs) - def save(self): + def save(self, ttl=NotSet, timestamp=None): + if ttl == NotSet: + ttl = self.ttl is_new = self.pk is None self.validate() - DMLQuery(self.__class__, self, batch=self._batch).save() + DMLQuery(self.__class__, self, batch=self._batch).save(ttl, timestamp) #reset the value managers for v in self._values.values(): diff --git a/cqlengine/query.py b/cqlengine/query.py index db8d17a318..eca930a07e 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -8,7 +8,7 @@ from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException -from cqlengine.functions import BaseQueryFunction +from cqlengine.functions import BaseQueryFunction, format_timestamp #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -175,8 +175,7 @@ def execute(self): opener = 'BEGIN ' + (self.batch_type + ' ' if self.batch_type else '') + ' BATCH' if self.timestamp: - epoch = datetime(1970, 1, 1) - ts = long((self.timestamp - epoch).total_seconds() * 1000) + ts = format_timestamp(self.timestamp) opener += ' USING TIMESTAMP {}'.format(ts) query_list = [opener] @@ -638,7 +637,7 @@ def batch(self, batch_obj): self.batch = batch_obj return self - def save(self): + def save(self, ttl=None, timestamp=None): """ Creates / updates a row. This is a blind insert call. @@ -666,8 +665,25 @@ def save(self): query_values = {field_ids[n]:field_values[n] for n in field_names} qs = [] + + using = [] + if ttl is not None: + ttl = int(ttl) + using.append('TTL {} '.format(ttl)) + if timestamp: + ts = format_timestamp(timestamp) + using.append('TIMESTAMP {} '.format(ts)) + + usings = '' + if using: + using = 'AND '.join(using).strip() + usings = ' USING {}'.format(using) + + if self.instance._can_update(): qs += ["UPDATE {}".format(self.column_family_name)] + if usings: + qs += [usings] qs += ["SET"] set_statements = [] @@ -714,6 +730,8 @@ def save(self): qs += ["({})".format(', '.join(['"{}"'.format(f) for f in field_names]))] qs += ['VALUES'] qs += ["({})".format(', '.join([':'+field_ids[f] for f in field_names]))] + if usings: + qs += [usings] qs = ' '.join(qs) From ff67b994835bbd3db97b74602cd9b7efafcd161b Mon Sep 17 00:00:00 2001 From: Pandu Rao Date: Tue, 21 May 2013 17:59:04 -0700 Subject: [PATCH 0165/3961] Fixed #46 delete property only on can_delete --- cqlengine/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index aec06b4f89..1b1181ee83 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -192,9 +192,9 @@ def _transform_column(col_name, col_obj): _set = lambda self, val: self._values[col_name].setval(val) _del = lambda self: self._values[col_name].delval() if col_obj.can_delete: - attrs[col_name] = property(_get, _set) - else: attrs[col_name] = property(_get, _set, _del) + else: + attrs[col_name] = property(_get, _set) column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.Column)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) From bdc0c0399ae4315dba3e4e3e0dfbd4fa267a4a0b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 22 May 2013 11:46:01 -0700 Subject: [PATCH 0166/3961] changing ORM (object row mapper) to Object Mapper, to avoid any confusion regarding support for object relationships --- README.md | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e5b06bfa6..7b244ffeb6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ cqlengine =============== -cqlengine is a Cassandra CQL 3 ORM for Python with an interface similar to the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 Object Mapper for Python with an interface similar to the Django orm and mongoengine [Documentation](https://cqlengine.readthedocs.org/en/latest/) diff --git a/docs/index.rst b/docs/index.rst index dbc64e54f9..6cea2f2c33 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ cqlengine documentation ======================= -cqlengine is a Cassandra CQL 3 ORM for Python with an interface similar to the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 Object Mapper for Python with an interface similar to the Django orm and mongoengine :ref:`getting-started` From 7e56e25d2186c00f75f735d0b74f9aa17de3d483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Sat, 25 May 2013 16:00:49 +0200 Subject: [PATCH 0167/3961] changed .all() behaviour - it clones QuerySet as in Django ORM --- cqlengine/query.py | 24 +++++++++++++++++++++--- cqlengine/tests/query/test_queryset.py | 6 +++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a4690df04a..809dedd85e 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -102,6 +102,19 @@ def _recurse(klass): except KeyError: raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) + # equality operator, used by tests + + def __eq__(self, op): + return self.__class__ is op.__class__ and \ + self.column.db_field_name == op.column.db_field_name and \ + self.value == op.value + + def __ne__(self, op): + return not (self == op) + + def __hash__(self): + return hash(self.column.db_field_name) ^ hash(self.value) + class EqualsOperator(QueryOperator): symbol = 'EQ' cql_symbol = '=' @@ -439,9 +452,7 @@ def first(self): return None def all(self): - clone = copy.deepcopy(self) - clone._where = [] - return clone + return copy.deepcopy(self) def _parse_filter_arg(self, arg): """ @@ -640,6 +651,13 @@ def values_list(self, *fields, **kwargs): clone._flat_values_list = flat return clone + + def __eq__(self, q): + return set(self._where) == set(q._where) + + def __ne__(self, q): + return not (self != q) + class DMLQuery(object): """ A query object used for queries performing inserts, updates, or deletes diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index a77d750299..3c9c43cf9d 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -89,9 +89,9 @@ def test_queryset_is_immutable(self): query2 = query1.filter(expected_result__gte=1) assert len(query2._where) == 2 - def test_the_all_method_clears_where_filter(self): + def test_the_all_method_duplicates_queryset(self): """ - Tests that calling all on a queryset with previously defined filters returns a queryset with no filters + Tests that calling all on a queryset with previously defined filters duplicates queryset """ query1 = TestModel.objects(test_id=5) assert len(query1._where) == 1 @@ -100,7 +100,7 @@ def test_the_all_method_clears_where_filter(self): assert len(query2._where) == 2 query3 = query2.all() - assert len(query3._where) == 0 + assert query3 == query2 def test_defining_only_and_defer_fails(self): """ From ec10c687fb5e04dda360971626a31e20926109a4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 08:10:10 -0700 Subject: [PATCH 0168/3961] removing alpha from pypi info --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index af82216686..860b6739e3 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,6 @@ dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/{0}.tar.gz#egg=cqlengine-{0}'.format(version)], long_description=long_desc, classifiers = [ - "Development Status :: 3 - Alpha", "Environment :: Web Environment", "Environment :: Plugins", "License :: OSI Approved :: BSD License", From c151fbd8b1b55f7ffc0f65b09249e4eaa00f5fbe Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 08:10:52 -0700 Subject: [PATCH 0169/3961] updating pypi description --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 860b6739e3..0f98baecbe 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version = '0.2' long_desc = """ -cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine +cqlengine is a Cassandra CQL Object Mapper for Python in the style of the Django orm and mongoengine [Documentation](https://cqlengine.readthedocs.org/en/latest/) From d1e69bf01df7abdb1b1f2429138e51998d213f5b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 08:16:21 -0700 Subject: [PATCH 0170/3961] adding test around deleting of model attributes --- cqlengine/tests/model/test_class_construction.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 77b1d73775..36aa11e90f 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -117,7 +117,18 @@ def test_meta_data_is_not_inherited(self): """ Test that metadata defined in one class, is not inherited by subclasses """ - + + def test_del_attribute_is_assigned_properly(self): + """ Tests that columns that can be deleted have the del attribute """ + class DelModel(Model): + key = columns.Integer(primary_key=True) + data = columns.Integer(required=False) + + model = DelModel(key=4, data=5) + del model.data + with self.assertRaises(AttributeError): + del model.key + class TestManualTableNaming(BaseCassEngTestCase): class RenamedTest(cqlengine.Model): From 786a21ad0d898dc225cd38dd0a23fb6503ef957c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 22 May 2013 11:46:01 -0700 Subject: [PATCH 0171/3961] changing ORM (object row mapper) to Object Mapper, to avoid any confusion regarding support for object relationships --- README.md | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e5b06bfa6..7b244ffeb6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ cqlengine =============== -cqlengine is a Cassandra CQL 3 ORM for Python with an interface similar to the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 Object Mapper for Python with an interface similar to the Django orm and mongoengine [Documentation](https://cqlengine.readthedocs.org/en/latest/) diff --git a/docs/index.rst b/docs/index.rst index dbc64e54f9..6cea2f2c33 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ cqlengine documentation ======================= -cqlengine is a Cassandra CQL 3 ORM for Python with an interface similar to the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 Object Mapper for Python with an interface similar to the Django orm and mongoengine :ref:`getting-started` From 4826f384823bc85d51a601b7b72a5624c2096249 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 08:10:10 -0700 Subject: [PATCH 0172/3961] removing alpha from pypi info --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index af82216686..860b6739e3 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,6 @@ dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/{0}.tar.gz#egg=cqlengine-{0}'.format(version)], long_description=long_desc, classifiers = [ - "Development Status :: 3 - Alpha", "Environment :: Web Environment", "Environment :: Plugins", "License :: OSI Approved :: BSD License", From d6b984b95223ce538274b3e8d420ef2c441564b7 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 08:10:52 -0700 Subject: [PATCH 0173/3961] updating pypi description --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 860b6739e3..0f98baecbe 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version = '0.2' long_desc = """ -cqlengine is a Cassandra CQL ORM for Python in the style of the Django orm and mongoengine +cqlengine is a Cassandra CQL Object Mapper for Python in the style of the Django orm and mongoengine [Documentation](https://cqlengine.readthedocs.org/en/latest/) From bb3ea850f250f01cd68db2f77644fcd6c7b45fdc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 08:16:21 -0700 Subject: [PATCH 0174/3961] adding test around deleting of model attributes --- cqlengine/tests/model/test_class_construction.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 77b1d73775..36aa11e90f 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -117,7 +117,18 @@ def test_meta_data_is_not_inherited(self): """ Test that metadata defined in one class, is not inherited by subclasses """ - + + def test_del_attribute_is_assigned_properly(self): + """ Tests that columns that can be deleted have the del attribute """ + class DelModel(Model): + key = columns.Integer(primary_key=True) + data = columns.Integer(required=False) + + model = DelModel(key=4, data=5) + del model.data + with self.assertRaises(AttributeError): + del model.key + class TestManualTableNaming(BaseCassEngTestCase): class RenamedTest(cqlengine.Model): From 929d1736eac5e1f03bab6184db7360a8d3b2a339 Mon Sep 17 00:00:00 2001 From: Pandu Rao Date: Sun, 26 May 2013 10:18:31 -0700 Subject: [PATCH 0175/3961] Fixed error in sample code in documentation --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6cea2f2c33..f93a8505c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -64,7 +64,7 @@ Getting Started >>> q.count() 4 >>> for instance in q: - >>> print q.description + >>> print instance.description example5 example6 example7 @@ -78,7 +78,7 @@ Getting Started >>> q2.count() 1 >>> for instance in q2: - >>> print q.description + >>> print instance.description example5 From d9b0eb9bb5dacc00a1efa3c8fcbddb82fe66c147 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 12:20:17 -0600 Subject: [PATCH 0176/3961] duplicate pandu-rao's doc fixes in the readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b244ffeb6..b54711f116 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ class ExampleModel(Model): >>> q.count() 4 >>> for instance in q: ->>> print q.description +>>> print instance.description example5 example6 example7 @@ -71,6 +71,6 @@ example8 >>> q2.count() 1 >>> for instance in q2: ->>> print q.description +>>> print instance.description example5 ``` From cdcccbb99a9ead5a1aea67585d62ec42291ab964 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 26 May 2013 12:20:17 -0600 Subject: [PATCH 0177/3961] duplicate pandu-rao's doc fixes in the readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b244ffeb6..b54711f116 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ class ExampleModel(Model): >>> q.count() 4 >>> for instance in q: ->>> print q.description +>>> print instance.description example5 example6 example7 @@ -71,6 +71,6 @@ example8 >>> q2.count() 1 >>> for instance in q2: ->>> print q.description +>>> print instance.description example5 ``` From 7c43b39237f7a1e0ee28da347705cc45eb89d83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Mon, 27 May 2013 00:34:53 +0200 Subject: [PATCH 0178/3961] clustering order, composite partition keys, Token func, docs & tests --- cqlengine/columns.py | 41 +++++++-- cqlengine/functions.py | 56 ++++++++----- cqlengine/management.py | 17 +++- cqlengine/models.py | 45 ++++++---- cqlengine/query.py | 84 ++++++++++--------- .../tests/model/test_class_construction.py | 24 +++++- .../tests/model/test_clustering_order.py | 35 ++++++++ cqlengine/tests/query/test_queryoperators.py | 20 ++++- cqlengine/tests/query/test_queryset.py | 11 ++- docs/topics/columns.rst | 11 ++- docs/topics/models.rst | 5 +- docs/topics/queryset.rst | 27 ++++++ 12 files changed, 284 insertions(+), 92 deletions(-) create mode 100644 cqlengine/tests/model/test_clustering_order.py diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 3c9bad45ba..ebf8cbc2ee 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -60,7 +60,7 @@ class Column(object): instance_counter = 0 - def __init__(self, primary_key=False, index=False, db_field=None, default=None, required=True): + def __init__(self, primary_key=False, partition_key=False, index=False, db_field=None, default=None, required=True, clustering_order=None): """ :param primary_key: bool flag, indicates this column is a primary key. The first primary key defined on a model is the partition key, all others are cluster keys @@ -69,15 +69,13 @@ def __init__(self, primary_key=False, index=False, db_field=None, default=None, :param default: the default value, can be a value or a callable (no args) :param required: boolean, is the field required? """ - self.primary_key = primary_key + self.partition_key = partition_key + self.primary_key = partition_key or primary_key self.index = index self.db_field = db_field self.default = default self.required = required - - #only the model meta class should touch this - self._partition_key = False - + self.clustering_order = clustering_order #the column name in the model definition self.column_name = None @@ -137,7 +135,7 @@ def get_column_def(self): """ Returns a column definition for CQL table definition """ - return '"{}" {}'.format(self.db_field_name, self.db_type) + return '{} {}'.format(self.cql, self.db_type) def set_column_name(self, name): """ @@ -156,6 +154,13 @@ def db_index_name(self): """ Returns the name of the cql index """ return 'index_{}'.format(self.db_field_name) + @property + def cql(self): + return self.get_cql() + + def get_cql(self): + return '"{}"'.format(self.db_field_name) + class Bytes(Column): db_type = 'blob' @@ -645,4 +650,26 @@ def get_delete_statement(self, val, prev, ctx): return del_statements +class _PartitionKeys(Column): + class value_manager(BaseValueManager): + pass + + def __init__(self, model): + self.model = model + +class _PartitionKeysToken(Column): + """ + virtual column representing token of partition columns. + Used by filter(pk__token=Token(...)) filters + """ + + def __init__(self, model): + self.partition_columns = model._partition_keys.values() + super(_PartitionKeysToken, self).__init__(partition_key=True) + + def to_database(self, value): + raise NotImplementedError + + def get_cql(self): + return "token({})".format(", ".join(c.cql for c in self.partition_columns)) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 3f023e75fb..947841808a 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -1,30 +1,39 @@ from datetime import datetime +from uuid import uuid1 from cqlengine.exceptions import ValidationError -class BaseQueryFunction(object): +class QueryValue(object): """ - Base class for filtering functions. Subclasses of these classes can - be passed into .filter() and will be translated into CQL functions in - the resulting query + Base class for query filter values. Subclasses of these classes can + be passed into .filter() keyword args """ - _cql_string = None + _cql_string = ':{}' - def __init__(self, value): + def __init__(self, value, identifier=None): self.value = value + self.identifier = uuid1().hex if identifier is None else identifier - def to_cql(self, value_id): - """ - Returns a function for cql with the value id as it's argument - """ - return self._cql_string.format(value_id) + def get_cql(self): + return self._cql_string.format(self.identifier) def get_value(self): - raise NotImplementedError + return self.value + + def get_dict(self, column): + return {self.identifier: column.to_database(self.get_value())} + + @property + def cql(self): + return self.get_cql() - def format_cql(self, field, operator, value_id): - return '"{}" {} {}'.format(field, operator, self.to_cql(value_id)) +class BaseQueryFunction(QueryValue): + """ + Base class for filtering functions. Subclasses of these classes can + be passed into .filter() and will be translated into CQL functions in + the resulting query + """ class MinTimeUUID(BaseQueryFunction): @@ -61,10 +70,19 @@ def get_value(self): return long((self.value - epoch).total_seconds() * 1000) class Token(BaseQueryFunction): - _cql_string = 'token(:{})' - def format_cql(self, field, operator, value_id): - return 'token("{}") {} {}'.format(field, operator, self.to_cql(value_id)) + def __init__(self, *values): + if len(values) == 1 and isinstance(values[0], (list, tuple)): + values = values[0] + super(Token, self).__init__(values, [uuid1().hex for i in values]) + + def get_dict(self, column): + items = zip(self.identifier, self.value, column.partition_columns) + return dict( + (id, col.to_database(val)) for id, val, col in items + ) + + def get_cql(self): + token_args = ', '.join(':{}'.format(id) for id in self.identifier) + return "token({})".format(token_args) - def get_value(self): - return self.value diff --git a/cqlengine/management.py b/cqlengine/management.py index 67c34f6841..f5f6a64923 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -61,20 +61,29 @@ def create_table(model, create_missing_keyspace=True): #add column types pkeys = [] + ckeys = [] qtypes = [] def add_column(col): s = col.get_column_def() - if col.primary_key: pkeys.append('"{}"'.format(col.db_field_name)) + if col.primary_key: + keys = (pkeys if col.partition_key else ckeys) + keys.append('"{}"'.format(col.db_field_name)) qtypes.append(s) for name, col in model._columns.items(): add_column(col) - qtypes.append('PRIMARY KEY ({})'.format(', '.join(pkeys))) + qtypes.append('PRIMARY KEY (({}){})'.format(', '.join(pkeys), ckeys and ', ' + ', '.join(ckeys) or '')) qs += ['({})'.format(', '.join(qtypes))] - + + with_qs = ['read_repair_chance = {}'.format(model.read_repair_chance)] + + _order = ["%s %s" % (c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] + if _order: + with_qs.append("clustering order by ({})".format(', '.join(_order))) + # add read_repair_chance - qs += ['WITH read_repair_chance = {}'.format(model.read_repair_chance)] + qs += ['WITH {}'.format(' AND '.join(with_qs))] qs = ' '.join(qs) try: diff --git a/cqlengine/models.py b/cqlengine/models.py index aec06b4f89..f06042d72e 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -102,10 +102,10 @@ def column_family_name(cls, include_keyspace=True): if not include_keyspace: return cf_name return '{}.{}'.format(cls._get_keyspace(), cf_name) - @property - def pk(self): - """ Returns the object's primary key """ - return getattr(self, self._pk_name) + #@property + #def pk(self): + # """ Returns the object's primary key """ + # return getattr(self, self._pk_name) def validate(self): """ Cleans and validates the field values """ @@ -174,7 +174,6 @@ def __new__(cls, name, bases, attrs): column_dict = OrderedDict() primary_keys = OrderedDict() pk_name = None - primary_key = None #get inherited properties inherited_columns = OrderedDict() @@ -210,24 +209,40 @@ def _transform_column(col_name, col_obj): k,v = 'id', columns.UUID(primary_key=True) column_definitions = [(k,v)] + column_definitions + has_partition_keys = any(v.partition_key for (k, v) in column_definitions) + #TODO: check that the defined columns don't conflict with any of the Model API's existing attributes/methods #transform column definitions for k,v in column_definitions: - if pk_name is None and v.primary_key: - pk_name = k - primary_key = v - v._partition_key = True + if not has_partition_keys and v.primary_key: + v.partition_key = True + has_partition_keys = True _transform_column(k,v) - - #setup primary key shortcut - if pk_name != 'pk': + + partition_keys = OrderedDict(k for k in primary_keys.items() if k[1].partition_key) + clustering_keys = OrderedDict(k for k in primary_keys.items() if not k[1].partition_key) + + #setup partition key shortcut + assert partition_keys + if len(partition_keys) == 1: + pk_name = partition_keys.keys()[0] attrs['pk'] = attrs[pk_name] + else: + # composite partition key case + _get = lambda self: tuple(self._values[c].getval() for c in partition_keys.keys()) + _set = lambda self, val: tuple(self._values[c].setval(v) for (c, v) in zip(partition_keys.keys(), val)) + attrs['pk'] = property(_get, _set) - #check for duplicate column names + # some validation col_names = set() for v in column_dict.values(): + # check for duplicate column names if v.db_field_name in col_names: raise ModelException("{} defines the column {} more than once".format(name, v.db_field_name)) + if v.clustering_order and not (v.primary_key and not v.partition_key): + raise ModelException("clustering_order may be specified only for clustering primary keys") + if v.clustering_order and v.clustering_order.lower() not in ('asc', 'desc'): + raise ModelException("invalid clustering order {} for column {}".format(repr(v.clustering_order), v.db_field_name)) col_names.add(v.db_field_name) #create db_name -> model name map for loading @@ -244,9 +259,11 @@ def _transform_column(col_name, col_obj): attrs['_defined_columns'] = defined_columns attrs['_db_map'] = db_map attrs['_pk_name'] = pk_name - attrs['_primary_key'] = primary_key attrs['_dynamic_columns'] = {} + attrs['_partition_keys'] = partition_keys + attrs['_clustering_keys'] = clustering_keys + #create the class and add a QuerySet to it klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) klass.objects = QuerySet(klass) diff --git a/cqlengine/query.py b/cqlengine/query.py index a4690df04a..19974eebbf 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,11 +4,11 @@ from hashlib import md5 from time import time from uuid import uuid1 -from cqlengine import BaseContainerColumn, BaseValueManager, Map +from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns from cqlengine.connection import connection_manager from cqlengine.exceptions import CQLEngineException -from cqlengine.functions import BaseQueryFunction, Token +from cqlengine.functions import QueryValue, Token #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -24,14 +24,16 @@ class QueryOperator(object): # The comparator symbol this operator uses in cql cql_symbol = None + QUERY_VALUE_WRAPPER = QueryValue + def __init__(self, column, value): self.column = column self.value = value - #the identifier is a unique key that will be used in string - #replacement on query strings, it's created from a hash - #of this object's id and the time - self.identifier = uuid1().hex + if isinstance(value, QueryValue): + self.query_value = value + else: + self.query_value = self.QUERY_VALUE_WRAPPER(value) #perform validation on this operator self.validate_operator() @@ -41,12 +43,8 @@ def __init__(self, column, value): def cql(self): """ Returns this operator's portion of the WHERE clause - :param valname: the dict key that this operator's compare value will be found in """ - if isinstance(self.value, BaseQueryFunction): - return self.value.format_cql(self.column.db_field_name, self.cql_symbol, self.identifier) - else: - return '"{}" {} :{}'.format(self.column.db_field_name, self.cql_symbol, self.identifier) + return '{} {} {}'.format(self.column.cql, self.cql_symbol, self.query_value.cql) def validate_operator(self): """ @@ -81,10 +79,7 @@ def get_dict(self): this should return the dict: {'colval':} SELECT * FROM column_family WHERE colname=:colval """ - if isinstance(self.value, BaseQueryFunction): - return {self.identifier: self.column.to_database(self.value.get_value())} - else: - return {self.identifier: self.column.to_database(self.value)} + return self.query_value.get_dict(self.column) @classmethod def get_operator(cls, symbol): @@ -102,34 +97,34 @@ def _recurse(klass): except KeyError: raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) + def __eq__(self, op): + return self.__class__ is op.__class__ and self.column == op.column and self.value == op.value + + def __ne__(self, op): + return not (self == op) + class EqualsOperator(QueryOperator): symbol = 'EQ' cql_symbol = '=' +class IterableQueryValue(QueryValue): + def __init__(self, value): + try: + super(IterableQueryValue, self).__init__(value, [uuid1().hex for i in value]) + except TypeError: + raise QueryException("in operator arguments must be iterable, {} found".format(value)) + + def get_dict(self, column): + return dict((i, column.to_database(v)) for (i, v) in zip(self.identifier, self.value)) + + def get_cql(self): + return '({})'.format(', '.join(':{}'.format(i) for i in self.identifier)) + class InOperator(EqualsOperator): symbol = 'IN' cql_symbol = 'IN' - class Quoter(object): - """ - contains a single value, which will quote itself for CQL insertion statements - """ - def __init__(self, value): - self.value = value - - def __str__(self): - from cql.query import cql_quote as cq - return '(' + ', '.join([cq(v) for v in self.value]) + ')' - - def get_dict(self): - if isinstance(self.value, BaseQueryFunction): - return {self.identifier: self.column.to_database(self.value.get_value())} - else: - try: - values = [v for v in self.value] - except TypeError: - raise QueryException("in operator arguments must be iterable, {} found".format(self.value)) - return {self.identifier: self.Quoter([self.column.to_database(v) for v in self.value])} + QUERY_VALUE_WRAPPER = IterableQueryValue class GreaterThanOperator(QueryOperator): symbol = "GT" @@ -286,9 +281,9 @@ def _validate_where_syntax(self): if not self._allow_filtering: #if the query is not on an indexed field if not any([w.column.index for w in equal_ops]): - if not any([w.column._partition_key for w in equal_ops]) and not token_ops: + if not any([w.column.partition_key for w in equal_ops]) and not token_ops: raise QueryException('Filtering on a clustering key without a partition key is not allowed unless allow_filtering() is called on the querset') - if any(not w.column._partition_key for w in token_ops): + if any(not w.column.partition_key for w in token_ops): raise QueryException('The token() function is only supported on the partition key') @@ -314,7 +309,7 @@ def _select_query(self): if self._defer_fields: fields = [f for f in fields if f not in self._defer_fields] elif self._only_fields: - fields = [f for f in fields if f in self._only_fields] + fields = self._only_fields db_fields = [self.model._columns[f].db_field_name for f in fields] qs = ['SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields]))] @@ -449,7 +444,7 @@ def _parse_filter_arg(self, arg): __ :returns: colname, op tuple """ - statement = arg.split('__') + statement = arg.rsplit('__', 1) if len(statement) == 1: return arg, None elif len(statement) == 2: @@ -466,7 +461,10 @@ def filter(self, **kwargs): try: column = self.model._columns[col_name] except KeyError: - raise QueryException("Can't resolve column name: '{}'".format(col_name)) + if col_name == 'pk__token': + column = columns._PartitionKeysToken(self.model) + else: + raise QueryException("Can't resolve column name: '{}'".format(col_name)) #get query operator, or use equals if not supplied operator_class = QueryOperator.get_operator(col_op or 'EQ') @@ -640,6 +638,12 @@ def values_list(self, *fields, **kwargs): clone._flat_values_list = flat return clone + def __eq__(self, q): + return self._where == q._where + + def __ne__(self, q): + return not (self != q) + class DMLQuery(object): """ A query object used for queries performing inserts, updates, or deletes diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 77b1d73775..02d51f5671 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -117,7 +117,29 @@ def test_meta_data_is_not_inherited(self): """ Test that metadata defined in one class, is not inherited by subclasses """ - + + def test_partition_keys(self): + """ + Test compound partition key definition + """ + class ModelWithPartitionKeys(cqlengine.Model): + c1 = cqlengine.Text(primary_key=True) + p1 = cqlengine.Text(partition_key=True) + p2 = cqlengine.Text(partition_key=True) + + cols = ModelWithPartitionKeys._columns + + self.assertTrue(cols['c1'].primary_key) + self.assertFalse(cols['c1'].partition_key) + + self.assertTrue(cols['p1'].primary_key) + self.assertTrue(cols['p1'].partition_key) + self.assertTrue(cols['p2'].primary_key) + self.assertTrue(cols['p2'].partition_key) + + obj = ModelWithPartitionKeys(p1='a', p2='b') + self.assertEquals(obj.pk, ('a', 'b')) + class TestManualTableNaming(BaseCassEngTestCase): class RenamedTest(cqlengine.Model): diff --git a/cqlengine/tests/model/test_clustering_order.py b/cqlengine/tests/model/test_clustering_order.py new file mode 100644 index 0000000000..92d8e067e8 --- /dev/null +++ b/cqlengine/tests/model/test_clustering_order.py @@ -0,0 +1,35 @@ +import random +from cqlengine.tests.base import BaseCassEngTestCase + +from cqlengine.management import create_table +from cqlengine.management import delete_table +from cqlengine.models import Model +from cqlengine import columns + +class TestModel(Model): + id = columns.Integer(primary_key=True) + clustering_key = columns.Integer(primary_key=True, clustering_order='desc') + +class TestClusteringOrder(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestClusteringOrder, cls).setUpClass() + create_table(TestModel) + + @classmethod + def tearDownClass(cls): + super(TestClusteringOrder, cls).tearDownClass() + delete_table(TestModel) + + def test_clustering_order(self): + """ + Tests that models can be saved and retrieved + """ + items = list(range(20)) + random.shuffle(items) + for i in items: + TestModel.create(id=1, clustering_key=i) + + values = list(TestModel.objects.values_list('clustering_key', flat=True)) + self.assertEquals(values, sorted(items, reverse=True)) diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index 5db4a299af..8f5243fbab 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -17,7 +17,7 @@ def test_maxtimeuuid_function(self): col.set_column_name('time') qry = query.EqualsOperator(col, functions.MaxTimeUUID(now)) - assert qry.cql == '"time" = MaxTimeUUID(:{})'.format(qry.identifier) + assert qry.cql == '"time" = MaxTimeUUID(:{})'.format(qry.value.identifier) def test_mintimeuuid_function(self): """ @@ -28,7 +28,23 @@ def test_mintimeuuid_function(self): col.set_column_name('time') qry = query.EqualsOperator(col, functions.MinTimeUUID(now)) - assert qry.cql == '"time" = MinTimeUUID(:{})'.format(qry.identifier) + assert qry.cql == '"time" = MinTimeUUID(:{})'.format(qry.value.identifier) + def test_token_function(self): + class TestModel(Model): + p1 = columns.Text(partition_key=True) + p2 = columns.Text(partition_key=True) + + func = functions.Token('a', 'b') + + q = TestModel.objects.filter(pk__token__gt=func) + self.assertEquals(q._where[0].cql, 'token("p1", "p2") > token(:{}, :{})'.format(*func.identifier)) + + # Token(tuple()) is also possible for convinience + # it (allows for Token(obj.pk) syntax) + func = functions.Token(('a', 'b')) + + q = TestModel.objects.filter(pk__token__gt=func) + self.assertEquals(q._where[0].cql, 'token("p1", "p2") > token(:{}, :{})'.format(*func.identifier)) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index a77d750299..79ca5af0d3 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -64,12 +64,12 @@ def test_where_clause_generation(self): Tests the where clause creation """ query1 = TestModel.objects(test_id=5) - ids = [o.identifier for o in query1._where] + ids = [o.query_value.identifier for o in query1._where] where = query1._where_clause() assert where == '"test_id" = :{}'.format(*ids) query2 = query1.filter(expected_result__gte=1) - ids = [o.identifier for o in query2._where] + ids = [o.query_value.identifier for o in query2._where] where = query2._where_clause() assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) @@ -470,5 +470,12 @@ def test_success_case(self): assert q.count() == 8 +class TestValuesList(BaseQuerySetUsage): + def test_values_list(self): + q = TestModel.objects.filter(test_id=0, attempt_id=1) + item = q.values_list('test_id', 'attempt_id', 'description', 'expected_result', 'test_result').first() + assert item == [0, 1, 'try2', 10, 30] + item = q.values_list('expected_result', flat=True).first() + assert item == 10 diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 0537e3bccb..0779c4333f 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -145,12 +145,16 @@ Column Options If True, this column is created as a primary key field. A model can have multiple primary keys. Defaults to False. - *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys.* + *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys, unless partition keys are specified manually using* :attr:`BaseColumn.partition_key` + + .. attribute:: BaseColumn.partition_key + + If True, this column is created as partition primary key. There may be many partition keys defined, forming *composite partition key* .. attribute:: BaseColumn.index If True, an index will be created for this column. Defaults to False. - + *Note: Indexes can only be created on models with one primary key* .. attribute:: BaseColumn.db_field @@ -165,3 +169,6 @@ Column Options If True, this model cannot be saved without a value defined for this column. Defaults to True. Primary key fields cannot have their required fields set to False. + .. attribute:: BaseColumn.clustering_order + + Defines CLUSTERING ORDER for this column (valid choices are "asc" (default) or "desc"). It may be specified only for clustering primary keys - more: http://www.datastax.com/docs/1.2/cql_cli/cql/CREATE_TABLE#using-clustering-order diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 0dd2c19cf8..52eb90613d 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -66,7 +66,10 @@ Column Options :attr:`~cqlengine.columns.BaseColumn.primary_key` If True, this column is created as a primary key field. A model can have multiple primary keys. Defaults to False. - *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys.* + *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys, unless partition keys are specified manually using* :attr:`~cqlengine.columns.BaseColumn.partition_key` + + :attr:`~cqlengine.columns.BaseColumn.partition_key` + If True, this column is created as partition primary key. There may be many partition keys defined, forming *composite partition key* :attr:`~cqlengine.columns.BaseColumn.index` If True, an index will be created for this column. Defaults to False. diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 6733080cfe..8490ce3daf 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -178,6 +178,26 @@ TimeUUID Functions DataStream.filter(time__gt=cqlengine.MinTimeUUID(min_time), time__lt=cqlengine.MaxTimeUUID(max_time)) +Token Function +============== + + Token functon may be used only on special, virtual column pk__token, representing token of partition key (it also works for composite partition keys). + Cassandra orders returned items by value of partition key token, so using cqlengine.Token we can easy paginate through all table rows. + + *Example* + + .. code-block:: python + + class Items(Model): + id = cqlengine.Text(primary_key=True) + data = cqlengine.Bytes() + + query = Items.objects.all().limit(10) + + first_page = list(query); + last = first_page[-1] + next_page = list(query.filter(pk__token__gt=cqlengine.Token(last.pk))) + QuerySets are imutable ====================== @@ -213,6 +233,13 @@ Ordering QuerySets *For instance, given our Automobile model, year is the only column we can order on.* +Values Lists +============ + + There is a special QuerySet's method ``.values_list()`` - when called, QuerySet returns lists of values instead of model instances. It may significantly speedup things with lower memory footprint for large responses. + Each tuple contains the value from the respective field passed into the ``values_list()`` call — so the first item is the first field, etc. For example: + + Batch Queries =============== From 97d7e647a304dbfe4ee10539949e7aa12e9e7982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Mon, 27 May 2013 02:14:28 +0200 Subject: [PATCH 0179/3961] Fix for #41 --- cqlengine/models.py | 7 ++++++- cqlengine/tests/query/test_queryset.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 61d001578e..3753e7f78c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -26,6 +26,10 @@ def __get__(self, instance, owner): else: return self.instmethod.__get__(instance, owner) +class QuerySetDescriptor(object): + def __get__(self, obj, model): + return QuerySet(model) + class BaseModel(object): """ The base model class, don't inherit from this, inherit from Model, defined below @@ -34,6 +38,8 @@ class BaseModel(object): class DoesNotExist(QueryException): pass class MultipleObjectsReturned(QueryException): pass + objects = QuerySetDescriptor() + #table names will be generated automatically from it's model and package name #however, you can also define them manually here table_name = None @@ -266,7 +272,6 @@ def _transform_column(col_name, col_obj): #create the class and add a QuerySet to it klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) - klass.objects = QuerySet(klass) return klass diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 5ce12b5ed5..1c3eb95b20 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -479,3 +479,11 @@ def test_values_list(self): item = q.values_list('expected_result', flat=True).first() assert item == 10 +class TestObjectsProperty(BaseQuerySetUsage): + + def test_objects_property_returns_fresh_queryset(self): + assert TestModel.objects._result_cache is None + len(TestModel.objects) # evaluate queryset + assert TestModel.objects._result_cache is None + + From f2a401d0541d3eebd4259827404d9827740e9adf Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Mon, 27 May 2013 10:39:26 +0300 Subject: [PATCH 0180/3961] Allow `datetime.date` values with `columns.DateTime`. Adds support for setting a `datetime.date` value to a `DateTime` column. The value will be transparently converted to a `datetime.datetime` with the time components set to zero. --- cqlengine/columns.py | 11 ++++++++++- cqlengine/tests/columns/test_validation.py | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 3c9bad45ba..07a3842718 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -183,6 +183,7 @@ def validate(self, value): raise ValidationError('{} is shorter than {} characters'.format(self.column_name, self.min_length)) return value + class Integer(Column): db_type = 'int' @@ -200,20 +201,27 @@ def to_python(self, value): def to_database(self, value): return self.validate(value) + class DateTime(Column): db_type = 'timestamp' + def __init__(self, **kwargs): super(DateTime, self).__init__(**kwargs) def to_python(self, value): if isinstance(value, datetime): return value + elif isinstance(value, date): + return datetime(*(value.timetuple()[:6])) return datetime.utcfromtimestamp(value) def to_database(self, value): value = super(DateTime, self).to_database(value) if not isinstance(value, datetime): - raise ValidationError("'{}' is not a datetime object".format(value)) + if isinstance(value, date): + value = datetime(value.year, value.month, value.day) + else: + raise ValidationError("'{}' is not a datetime object".format(value)) epoch = datetime(1970, 1, 1, tzinfo=value.tzinfo) offset = 0 if epoch.tzinfo: @@ -326,6 +334,7 @@ def __init__(self, value): def __str__(self): raise NotImplementedError + class BaseContainerColumn(Column): """ Base Container type diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index e323c4653d..24c1739706 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -55,6 +55,12 @@ def dst(self, date_time): dt2 = self.DatetimeTest.objects(test_id=0).first() assert dt2.created_at.timetuple()[:6] == (now + timedelta(hours=1)).timetuple()[:6] + def test_datetime_date_support(self): + today = date.today() + self.DatetimeTest.objects.create(test_id=0, created_at=today) + dt2 = self.DatetimeTest.objects(test_id=0).first() + assert dt2.created_at.isoformat() == datetime(today.year, today.month, today.day).isoformat() + class TestDate(BaseCassEngTestCase): class DateTest(Model): From b1ce5f2f380dad3d45be52510f6d4075aac49e7d Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Mon, 27 May 2013 10:54:54 +0300 Subject: [PATCH 0181/3961] Log the CQL statements at DEBUG level. This logs the CQL statements emitted by cqlengine in the `cqlengine.cql` logger at DEBUG level. For debugging purposes you can enable that logger to help work out problems with your queries. The logger is not enabled by default by `cqlengine`. --- cqlengine/columns.py | 3 +++ cqlengine/connection.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 3c9bad45ba..7247f75ca5 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -326,6 +326,9 @@ def __init__(self, value): def __str__(self): raise NotImplementedError + def __repr__(self): + return self.__str__() + class BaseContainerColumn(Column): """ Base Container type diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 2a5ef989eb..c0c2b1710e 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -7,11 +7,13 @@ import random import cql +import logging from cqlengine.exceptions import CQLEngineException from thrift.transport.TTransport import TTransportException +LOG = logging.getLogger('cqlengine.cql') class CQLConnectionError(CQLEngineException): pass @@ -167,6 +169,7 @@ def execute(self, query, params={}): for i in range(len(_hosts)): try: + LOG.debug('{} {}'.format(query, repr(params))) self.cur = self.con.cursor() self.cur.execute(query, params) return self.cur From 26d68711469520791dd8a4659b0e245754490dcf Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 30 May 2013 14:28:47 -0700 Subject: [PATCH 0182/3961] ignoring noseids --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 16029f08ec..6d96b7f030 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ html/ #Mr Developer .mr.developer.cfg +.noseids From 3bb7314c60521a93bfbca14e532e257e6b6d1089 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 31 May 2013 09:34:25 -0700 Subject: [PATCH 0183/3961] adding some docstrings to the function classes --- cqlengine/functions.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 947841808a..5a8bfb548b 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -36,6 +36,11 @@ class BaseQueryFunction(QueryValue): """ class MinTimeUUID(BaseQueryFunction): + """ + return a fake timeuuid corresponding to the smallest possible timeuuid for the given timestamp + + http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun + """ _cql_string = 'MinTimeUUID(:{})' @@ -53,6 +58,11 @@ def get_value(self): return long((self.value - epoch).total_seconds() * 1000) class MaxTimeUUID(BaseQueryFunction): + """ + return a fake timeuuid corresponding to the largest possible timeuuid for the given timestamp + + http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun + """ _cql_string = 'MaxTimeUUID(:{})' @@ -70,6 +80,11 @@ def get_value(self): return long((self.value - epoch).total_seconds() * 1000) class Token(BaseQueryFunction): + """ + compute the token for a given partition key + + http://cassandra.apache.org/doc/cql3/CQL.html#tokenFun + """ def __init__(self, *values): if len(values) == 1 and isinstance(values[0], (list, tuple)): From 4a5a11fb5d1ff28a106dc2288d159643cf0d1e2a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 09:51:39 -0700 Subject: [PATCH 0184/3961] removed unused column class --- cqlengine/columns.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 284702dbce..91240e0ecd 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -661,13 +661,6 @@ def get_delete_statement(self, val, prev, ctx): return del_statements -class _PartitionKeys(Column): - class value_manager(BaseValueManager): - pass - - def __init__(self, model): - self.model = model - class _PartitionKeysToken(Column): """ virtual column representing token of partition columns. From 7ef3cde72722a81d5d9c5362b6b3273d293325e0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 09:52:04 -0700 Subject: [PATCH 0185/3961] adding some docstrings and comments --- cqlengine/models.py | 4 +++- cqlengine/query.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 3753e7f78c..842a05edd1 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -220,6 +220,8 @@ def _transform_column(col_name, col_obj): #TODO: check that the defined columns don't conflict with any of the Model API's existing attributes/methods #transform column definitions for k,v in column_definitions: + # this will mark the first primary key column as a partition + # key, if one hasn't been set already if not has_partition_keys and v.primary_key: v.partition_key = True has_partition_keys = True @@ -234,7 +236,7 @@ def _transform_column(col_name, col_obj): pk_name = partition_keys.keys()[0] attrs['pk'] = attrs[pk_name] else: - # composite partition key case + # composite partition key case, get/set a tuple of values _get = lambda self: tuple(self._values[c].getval() for c in partition_keys.keys()) _set = lambda self, val: tuple(self._values[c].setval(v) for (c, v) in zip(partition_keys.keys(), val)) attrs['pk'] = property(_get, _set) diff --git a/cqlengine/query.py b/cqlengine/query.py index 160eab9b39..8aee97e747 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -405,6 +405,9 @@ def __getitem__(self, s): return self._result_cache[s] def _create_result_constructor(self, names): + """ + Returns a function that will be used to instantiate query results + """ model = self.model db_map = model._db_map if not self._values_list: @@ -632,6 +635,7 @@ def delete(self, columns=[]): con.execute(qs, self._where_values()) def values_list(self, *fields, **kwargs): + """ Instructs the query set to return tuples, not model instance """ flat = kwargs.pop('flat', False) if kwargs: raise TypeError('Unexpected keyword arguments to values_list: %s' From e75dfe5ef746323b99050fe30d19a267badfbd4a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 09:52:40 -0700 Subject: [PATCH 0186/3961] adding some todos --- cqlengine/management.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cqlengine/management.py b/cqlengine/management.py index f5f6a64923..25717aa4c6 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -14,6 +14,7 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, :param **replication_values: 1.2 only, additional values to ad to the replication data map """ with connection_manager() as con: + #TODO: check system tables instead of using cql thrifteries if not any([name == k.name for k in con.con.client.describe_keyspaces()]): # if name not in [k.name for k in con.con.client.describe_keyspaces()]: try: @@ -55,6 +56,7 @@ def create_table(model, create_missing_keyspace=True): with connection_manager() as con: #check for an existing column family + #TODO: check system tables instead of using cql thrifteries ks_info = con.con.client.describe_keyspace(model._get_keyspace()) if not any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): qs = ['CREATE TABLE {}'.format(cf_name)] From 532f3275191506dab25ed4d5c8f921632c09c909 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 09:55:45 -0700 Subject: [PATCH 0187/3961] adding cassandra docs url to token function docs --- docs/topics/queryset.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 8490ce3daf..48e26a7cba 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -184,6 +184,8 @@ Token Function Token functon may be used only on special, virtual column pk__token, representing token of partition key (it also works for composite partition keys). Cassandra orders returned items by value of partition key token, so using cqlengine.Token we can easy paginate through all table rows. + See http://cassandra.apache.org/doc/cql3/CQL.html#tokenFun + *Example* .. code-block:: python From 55f16442e36ffea5e35a80a5bb1b128c71f6226f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 10:00:15 -0700 Subject: [PATCH 0188/3961] removing commented out method --- cqlengine/models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 842a05edd1..d0ea201dac 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -108,11 +108,6 @@ def column_family_name(cls, include_keyspace=True): if not include_keyspace: return cf_name return '{}.{}'.format(cls._get_keyspace(), cf_name) - #@property - #def pk(self): - # """ Returns the object's primary key """ - # return getattr(self, self._pk_name) - def validate(self): """ Cleans and validates the field values """ for name, col in self._columns.items(): From 24eba1bbc0aafc9e503747e688cacfdb88ad2fb8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 10:11:38 -0700 Subject: [PATCH 0189/3961] adding contributor guidelines --- CONTRIBUTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..62f6d786bb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +### Contributing code to cqlengine + +Before submitting a pull request, please make sure that it follows these guidelines: + +* Limit yourself to one feature or bug fix per pull request. +* Include unittests that thoroughly test the feature/bug fix +* Write clear, descriptive commit messages. +* Many granular commits are preferred over large monolithic commits +* If you're adding or modifying features, please update the documentation + +If you're working on a big ticket item, please check in on [cqlengine-users](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users). +We'd hate to have to steer you in a different direction after you've already put in a lot of hard work. From dc14e6927505bc949a00c60b7b543330660a5d6a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 10:23:37 -0700 Subject: [PATCH 0190/3961] adding contributor guidelines link to the readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index b54711f116..b0037b9465 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,7 @@ example8 >>> print instance.description example5 ``` + +## Contributing + +If you'd like to contribute to cqlengine, please read the [contributor guidelines](https://github.com/bdeggleston/cqlengine/blob/master/CONTRIBUTING.md) From 286bd0e66af0c1c21a961a06fc4e3177c4ecf57c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 11:33:37 -0700 Subject: [PATCH 0191/3961] adding custom value quoter to boolean column for compatibility with Cassandra 1.2.5+ --- cqlengine/columns.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 91240e0ecd..8559795474 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -52,6 +52,20 @@ def get_property(self): else: return property(_get, _set) +class ValueQuoter(object): + """ + contains a single value, which will quote itself for CQL insertion statements + """ + def __init__(self, value): + self.value = value + + def __str__(self): + raise NotImplementedError + + def __repr__(self): + return self.__str__() + + class Column(object): #the cassandra type this column maps to @@ -293,11 +307,16 @@ def __init__(self, **kwargs): class Boolean(Column): db_type = 'boolean' + class Quoter(ValueQuoter): + """ Cassandra 1.2.5 is stricter about boolean values """ + def __str__(self): + return 'true' if self.value else 'false' + def to_python(self, value): return bool(value) def to_database(self, value): - return bool(value) + return self.Quoter(bool(value)) class Float(Column): db_type = 'double' @@ -329,19 +348,6 @@ def __init__(self, **kwargs): super(Counter, self).__init__(**kwargs) raise NotImplementedError -class ContainerQuoter(object): - """ - contains a single value, which will quote itself for CQL insertion statements - """ - def __init__(self, value): - self.value = value - - def __str__(self): - raise NotImplementedError - - def __repr__(self): - return self.__str__() - class BaseContainerColumn(Column): """ Base Container type @@ -383,7 +389,7 @@ class Set(BaseContainerColumn): """ db_type = 'set<{}>' - class Quoter(ContainerQuoter): + class Quoter(ValueQuoter): def __str__(self): cq = cql_quote @@ -466,7 +472,7 @@ class List(BaseContainerColumn): """ db_type = 'list<{}>' - class Quoter(ContainerQuoter): + class Quoter(ValueQuoter): def __str__(self): cq = cql_quote @@ -563,7 +569,7 @@ class Map(BaseContainerColumn): db_type = 'map<{}, {}>' - class Quoter(ContainerQuoter): + class Quoter(ValueQuoter): def __str__(self): cq = cql_quote From 24b207c18a785270b5ac90e93b7e774d3d80cc56 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 11:58:22 -0700 Subject: [PATCH 0192/3961] adding None handling to DateTime to_database --- cqlengine/columns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 8559795474..69ad188a3c 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -236,6 +236,7 @@ def to_python(self, value): def to_database(self, value): value = super(DateTime, self).to_database(value) + if value is None: return if not isinstance(value, datetime): if isinstance(value, date): value = datetime(value.year, value.month, value.day) From 40bed98b90d58b980f4086e3901cdab5a4be7b05 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 13:24:53 -0700 Subject: [PATCH 0193/3961] making DoesNotExist & MultipleObjectsReturned distinct classes for each model, and inherit from parent model if any --- cqlengine/models.py | 25 ++++++++++++--- cqlengine/query.py | 4 +++ .../tests/model/test_class_construction.py | 31 +++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index d0ea201dac..bd4e65fabe 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -3,8 +3,9 @@ from cqlengine import columns from cqlengine.exceptions import ModelException -from cqlengine.functions import BaseQueryFunction -from cqlengine.query import QuerySet, QueryException, DMLQuery +from cqlengine.query import QuerySet, DMLQuery +from cqlengine.query import DoesNotExist as _DoesNotExist +from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned class ModelDefinitionException(ModelException): pass @@ -35,8 +36,8 @@ class BaseModel(object): The base model class, don't inherit from this, inherit from Model, defined below """ - class DoesNotExist(QueryException): pass - class MultipleObjectsReturned(QueryException): pass + class DoesNotExist(_DoesNotExist): pass + class MultipleObjectsReturned(_MultipleObjectsReturned): pass objects = QuerySetDescriptor() @@ -182,6 +183,7 @@ def __new__(cls, name, bases, attrs): for k,v in getattr(base, '_defined_columns', {}).items(): inherited_columns.setdefault(k,v) + def _transform_column(col_name, col_obj): column_dict[col_name] = col_obj if col_obj.primary_key: @@ -267,6 +269,21 @@ def _transform_column(col_name, col_obj): attrs['_partition_keys'] = partition_keys attrs['_clustering_keys'] = clustering_keys + #setup class exceptions + DoesNotExistBase = None + for base in bases: + DoesNotExistBase = getattr(base, 'DoesNotExist', None) + if DoesNotExistBase is not None: break + DoesNotExistBase = DoesNotExistBase or attrs.pop('DoesNotExist', BaseModel.DoesNotExist) + attrs['DoesNotExist'] = type('DoesNotExist', (DoesNotExistBase,), {}) + + MultipleObjectsReturnedBase = None + for base in bases: + MultipleObjectsReturnedBase = getattr(base, 'MultipleObjectsReturned', None) + if MultipleObjectsReturnedBase is not None: break + MultipleObjectsReturnedBase = DoesNotExistBase or attrs.pop('MultipleObjectsReturned', BaseModel.MultipleObjectsReturned) + attrs['MultipleObjectsReturned'] = type('MultipleObjectsReturned', (MultipleObjectsReturnedBase,), {}) + #create the class and add a QuerySet to it klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) return klass diff --git a/cqlengine/query.py b/cqlengine/query.py index 8aee97e747..3ab71129f0 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -14,6 +14,10 @@ #http://www.datastax.com/docs/1.1/references/cql/index class QueryException(CQLEngineException): pass +class DoesNotExist(QueryException): pass +class MultipleObjectsReturned(QueryException): pass + + class QueryOperatorException(QueryException): pass class QueryOperator(object): diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 36f986508f..c67fcbee4d 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -151,6 +151,37 @@ class DelModel(Model): with self.assertRaises(AttributeError): del model.key + def test_does_not_exist_exceptions_are_not_shared_between_model(self): + """ Tests that DoesNotExist exceptions are not the same exception between models """ + + class Model1(Model): + pass + class Model2(Model): + pass + + try: + raise Model1.DoesNotExist + except Model2.DoesNotExist: + assert False, "Model1 exception should not be caught by Model2" + except Model1.DoesNotExist: + #expected + pass + + def test_does_not_exist_inherits_from_superclass(self): + """ Tests that a DoesNotExist exception can be caught by it's parent class DoesNotExist """ + class Model1(Model): + pass + class Model2(Model1): + pass + + try: + raise Model2.DoesNotExist + except Model1.DoesNotExist: + #expected + pass + except Exception: + assert False, "Model2 exception should not be caught by Model1" + class TestManualTableNaming(BaseCassEngTestCase): class RenamedTest(cqlengine.Model): From 1301486d0e223d9feba7453b49fc44ab3a8707c3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 13:28:26 -0700 Subject: [PATCH 0194/3961] updating changelog --- changelog | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 3edb19f0fa..05714651e4 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,15 @@ CHANGELOG -0.2.1 (in progress) +0.3 +* added support for Token function (thanks @mrk-its) +* added support for compound partition key (thanks @mrk-its)s +* added support for defining clustering key ordering (thanks @mrk-its) +* added values_list to Query class, bypassing object creation if desired (thanks @mrk-its) +* fixed bug with Model.objects caching values (thanks @mrk-its) +* fixed Cassandra 1.2.5 compatibility bug +* updated model exception inheritance + +0.2.1 * adding support for datetimes with tzinfo (thanks @gdoermann) * fixing bug in saving map updates (thanks @pandu-rao) From f2b9c14e2906fc2110fa19ae1ce852640c5216ad Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 13:37:14 -0700 Subject: [PATCH 0195/3961] version number bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index ed5a820213..c9dc969515 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.2' +__version__ = '0.3' diff --git a/docs/conf.py b/docs/conf.py index 96c7db6ed6..9b7e050a50 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.2' +version = '0.3' # The full version, including alpha/beta/rc tags. -release = '0.2' +release = '0.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 0f98baecbe..24545a9b7e 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.2' +version = '0.3' long_desc = """ cqlengine is a Cassandra CQL Object Mapper for Python in the style of the Django orm and mongoengine From 5eef9bd18d94ab0c3d898653799707268b4148e7 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 15:46:53 -0700 Subject: [PATCH 0196/3961] adding tests around io for all column types --- cqlengine/tests/columns/test_value_io.py | 136 +++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 cqlengine/tests/columns/test_value_io.py diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py new file mode 100644 index 0000000000..273ff0d56b --- /dev/null +++ b/cqlengine/tests/columns/test_value_io.py @@ -0,0 +1,136 @@ +from datetime import datetime, timedelta +from decimal import Decimal +from uuid import uuid1, uuid4, UUID +from unittest import SkipTest +from cqlengine.tests.base import BaseCassEngTestCase + +from cqlengine.management import create_table +from cqlengine.management import delete_table +from cqlengine.models import Model +from cqlengine import columns + +class BaseColumnIOTest(BaseCassEngTestCase): + + TEST_MODEL = None + TEST_COLUMN = None + + @property + def PKEY_VAL(self): + raise NotImplementedError + + @property + def DATA_VAL(self): + raise NotImplementedError + + @classmethod + def setUpClass(cls): + super(BaseColumnIOTest, cls).setUpClass() + if not cls.TEST_COLUMN: return + class IOTestModel(Model): + table_name = cls.TEST_COLUMN.db_type + "_io_test_model_{}".format(uuid4().hex[:8]) + pkey = cls.TEST_COLUMN(primary_key=True) + data = cls.TEST_COLUMN() + + cls.TEST_MODEL = IOTestModel + create_table(cls.TEST_MODEL) + + #tupleify + if not isinstance(cls.PKEY_VAL, tuple): + cls.PKEY_VAL = cls.PKEY_VAL, + if not isinstance(cls.DATA_VAL, tuple): + cls.DATA_VAL = cls.DATA_VAL, + + @classmethod + def tearDownClass(cls): + super(BaseColumnIOTest, cls).tearDownClass() + if not cls.TEST_COLUMN: return + delete_table(cls.TEST_MODEL) + + def comparator_converter(self, val): + """ If you want to convert the original value used to compare the model vales """ + return val + + def test_column_io(self): + """ Tests the given models class creates and retrieves values as expected """ + if not self.TEST_COLUMN: return + for pkey, data in zip(self.PKEY_VAL, self.DATA_VAL): + #create + m1 = self.TEST_MODEL.create(pkey=pkey, data=data) + + #get + m2 = self.TEST_MODEL.get(pkey=pkey) + assert m1.pkey == m2.pkey == self.comparator_converter(pkey), self.TEST_COLUMN + assert m1.data == m2.data == self.comparator_converter(data), self.TEST_COLUMN + + #delete + self.TEST_MODEL.filter(pkey=pkey).delete() + +class TestTextIO(BaseColumnIOTest): + + TEST_COLUMN = columns.Text + PKEY_VAL = 'bacon' + DATA_VAL = 'monkey' + +class TestInteger(BaseColumnIOTest): + + TEST_COLUMN = columns.Integer + PKEY_VAL = 5 + DATA_VAL = 6 + +class TestDateTime(BaseColumnIOTest): + + TEST_COLUMN = columns.DateTime + now = datetime(*datetime.now().timetuple()[:6]) + PKEY_VAL = now + DATA_VAL = now + timedelta(days=1) + +class TestDate(BaseColumnIOTest): + + TEST_COLUMN = columns.Date + now = datetime.now().date() + PKEY_VAL = now + DATA_VAL = now + timedelta(days=1) + +class TestUUID(BaseColumnIOTest): + + TEST_COLUMN = columns.UUID + + PKEY_VAL = str(uuid4()), uuid4() + DATA_VAL = str(uuid4()), uuid4() + + def comparator_converter(self, val): + return val if isinstance(val, UUID) else UUID(val) + +class TestTimeUUID(BaseColumnIOTest): + + TEST_COLUMN = columns.TimeUUID + + PKEY_VAL = str(uuid1()), uuid1() + DATA_VAL = str(uuid1()), uuid1() + + def comparator_converter(self, val): + return val if isinstance(val, UUID) else UUID(val) + +class TestBooleanIO(BaseColumnIOTest): + + TEST_COLUMN = columns.Boolean + + PKEY_VAL = True + DATA_VAL = False + +class TestFloatIO(BaseColumnIOTest): + + TEST_COLUMN = columns.Float + + PKEY_VAL = 3.14 + DATA_VAL = -1982.11 + +class TestDecimalIO(BaseColumnIOTest): + + TEST_COLUMN = columns.Decimal + + PKEY_VAL = Decimal('1.35'), 5, '2.4' + DATA_VAL = Decimal('0.005'), 3.5, '8' + + def comparator_converter(self, val): + return Decimal(val) From 229be07bab4c74e13849f623984fd3fe602414f5 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 15:47:22 -0700 Subject: [PATCH 0197/3961] adding some conversion logic to Decimal and UUID columns --- cqlengine/columns.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 69ad188a3c..3f0806345d 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -294,6 +294,12 @@ def validate(self, value): raise ValidationError("{} is not a valid uuid".format(value)) return _UUID(val) + def to_python(self, value): + return self.validate(value) + + def to_database(self, value): + return self.validate(value) + class TimeUUID(UUID): """ UUID containing timestamp @@ -343,6 +349,22 @@ def to_database(self, value): class Decimal(Column): db_type = 'decimal' + def validate(self, value): + from decimal import Decimal as _Decimal + from decimal import InvalidOperation + val = super(Decimal, self).validate(value) + if val is None: return + try: + return _Decimal(val) + except InvalidOperation: + raise ValidationError("'{}' can't be coerced to decimal".format(val)) + + def to_python(self, value): + return self.validate(value) + + def to_database(self, value): + return self.validate(value) + class Counter(Column): #TODO: counter field def __init__(self, **kwargs): From 7a4fe9ae78090c2591f0980cf8614833fb924111 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 15:48:17 -0700 Subject: [PATCH 0198/3961] fixing the get_dict method on the time uuid functions --- cqlengine/functions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 5a8bfb548b..eee1fcab5e 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -57,6 +57,9 @@ def get_value(self): epoch = datetime(1970, 1, 1) return long((self.value - epoch).total_seconds() * 1000) + def get_dict(self, column): + return {self.identifier: self.get_value()} + class MaxTimeUUID(BaseQueryFunction): """ return a fake timeuuid corresponding to the largest possible timeuuid for the given timestamp @@ -79,6 +82,9 @@ def get_value(self): epoch = datetime(1970, 1, 1) return long((self.value - epoch).total_seconds() * 1000) + def get_dict(self, column): + return {self.identifier: self.get_value()} + class Token(BaseQueryFunction): """ compute the token for a given partition key From 45695a9f691357332a60c5ab4749420cce9797f0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 15:49:15 -0700 Subject: [PATCH 0199/3961] version number bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index c9dc969515..25d51303d5 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.3' +__version__ = '0.3.1' diff --git a/docs/conf.py b/docs/conf.py index 9b7e050a50..36e5d084b0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.3' +version = '0.3.1' # The full version, including alpha/beta/rc tags. -release = '0.3' +release = '0.3.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 24545a9b7e..cec6865dca 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.3' +version = '0.3.1' long_desc = """ cqlengine is a Cassandra CQL Object Mapper for Python in the style of the Django orm and mongoengine From 8597bfac6c41f9f83b3c45b54a1536f6a7457ff1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 3 Jun 2013 13:39:40 -0700 Subject: [PATCH 0200/3961] adding test files and starting to rework the pool --- cqlengine/connection.py | 2 ++ cqlengine/tests/connections/__init__.py | 0 cqlengine/tests/connections/test_connection_pool.py | 0 3 files changed, 2 insertions(+) create mode 100644 cqlengine/tests/connections/__init__.py create mode 100644 cqlengine/tests/connections/test_connection_pool.py diff --git a/cqlengine/connection.py b/cqlengine/connection.py index c0c2b1710e..1fb7b53bd1 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -122,6 +122,8 @@ def put(cls, conn): def _create_connection(cls): """ Creates a new connection for the connection pool. + + should only return a valid connection that it's actually connected to """ global _hosts global _username diff --git a/cqlengine/tests/connections/__init__.py b/cqlengine/tests/connections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/connections/test_connection_pool.py b/cqlengine/tests/connections/test_connection_pool.py new file mode 100644 index 0000000000..e69de29bb2 From 55331ad07b39bf6f2f164cb12a20fa9379517065 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 3 Jun 2013 13:45:07 -0700 Subject: [PATCH 0201/3961] making ConnectionPool not use a global state --- cqlengine/connection.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 1fb7b53bd1..e4e2b42dee 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -69,38 +69,35 @@ class ConnectionPool(object): # Connection pool queue _queue = None - @classmethod - def clear(cls): + def __init__(self, hosts): + self._hosts = hosts + self._queue = Queue.Queue(maxsize=_max_connections) + + def clear(self): """ Force the connection pool to be cleared. Will close all internal connections. """ try: - while not cls._queue.empty(): - cls._queue.get().close() + while not self._queue.empty(): + self._queue.get().close() except: pass - @classmethod - def get(cls): + def get(self): """ Returns a usable database connection. Uses the internal queue to determine whether to return an existing connection or to create a new one. """ try: - if cls._queue.empty(): - return cls._create_connection() - return cls._queue.get() + if self._queue.empty(): + return self._create_connection() + return self._queue.get() except CQLConnectionError as cqle: raise cqle - except: - if not cls._queue: - cls._queue = Queue.Queue(maxsize=_max_connections) - return cls._create_connection() - @classmethod - def put(cls, conn): + def put(self, conn): """ Returns a connection to the queue freeing it up for other queries to use. @@ -109,17 +106,16 @@ def put(cls, conn): :type conn: connection """ try: - if cls._queue.full(): + if self._queue.full(): conn.close() else: - cls._queue.put(conn) + self._queue.put(conn) except: - if not cls._queue: - cls._queue = Queue.Queue(maxsize=_max_connections) - cls._queue.put(conn) + if not self._queue: + self._queue = + self._queue.put(conn) - @classmethod - def _create_connection(cls): + def _create_connection(self): """ Creates a new connection for the connection pool. From d742345796812722d94c71bab4bc2a423dc6cd62 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 3 Jun 2013 13:48:58 -0700 Subject: [PATCH 0202/3961] setup now creates a global connection pool --- cqlengine/connection.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index e4e2b42dee..e18c1b089b 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -18,25 +18,18 @@ class CQLConnectionError(CQLEngineException): pass Host = namedtuple('Host', ['name', 'port']) -_hosts = [] -_host_idx = 0 -_conn= None -_username = None -_password = None + _max_connections = 10 +_connection_pool = None -def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None, lazy=False): +def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None, lazy=True): """ Records the hosts and connects to one of them :param hosts: list of hosts, strings in the :, or just """ - global _hosts - global _username - global _password global _max_connections - _username = username - _password = password + global _connection_pool _max_connections = max_connections if default_keyspace: @@ -56,11 +49,11 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp if not _hosts: raise CQLConnectionError("At least one host required") - random.shuffle(_hosts) + _connection_pool = ConnectionPool(_hosts) if not lazy: - con = ConnectionPool.get() - ConnectionPool.put(con) + con = _connection_pool.get() + _connection_pool.put(con) class ConnectionPool(object): @@ -69,8 +62,11 @@ class ConnectionPool(object): # Connection pool queue _queue = None - def __init__(self, hosts): + def __init__(self, hosts, username, password): self._hosts = hosts + self._username = username + self._password = password + self._queue = Queue.Queue(maxsize=_max_connections) def clear(self): From e6a7934fe30ba89187b30f3c696048105d905d02 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 3 Jun 2013 13:55:30 -0700 Subject: [PATCH 0203/3961] fixing references to removed vars --- cqlengine/connection.py | 25 +++++++------------------ cqlengine/tests/base.py | 6 +++--- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index e18c1b089b..a1a18df09b 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -22,7 +22,7 @@ class CQLConnectionError(CQLEngineException): pass _max_connections = 10 _connection_pool = None -def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None, lazy=True): +def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None): """ Records the hosts and connects to one of them @@ -36,6 +36,7 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp from cqlengine import models models.DEFAULT_KEYSPACE = default_keyspace + _hosts = [] for host in hosts: host = host.strip() host = host.split(':') @@ -51,10 +52,6 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp _connection_pool = ConnectionPool(_hosts) - if not lazy: - con = _connection_pool.get() - _connection_pool.put(con) - class ConnectionPool(object): """Handles pooling of database connections.""" @@ -101,14 +98,10 @@ def put(self, conn): :param conn: The connection to be released :type conn: connection """ - try: - if self._queue.full(): - conn.close() - else: - self._queue.put(conn) - except: - if not self._queue: - self._queue = + + if self._queue.full(): + conn.close() + else: self._queue.put(conn) def _create_connection(self): @@ -117,11 +110,7 @@ def _create_connection(self): should only return a valid connection that it's actually connected to """ - global _hosts - global _username - global _password - - if not _hosts: + if not self._hosts: raise CQLConnectionError("At least one host required") host = _hosts[_host_idx] diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index 64e6a3c2a9..a7090c627d 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -6,13 +6,13 @@ class BaseCassEngTestCase(TestCase): @classmethod def setUpClass(cls): super(BaseCassEngTestCase, cls).setUpClass() - if not connection._hosts: + if not connection._connection_pool: connection.setup(['localhost:9160'], default_keyspace='cqlengine_test') def assertHasAttr(self, obj, attr): - self.assertTrue(hasattr(obj, attr), + self.assertTrue(hasattr(obj, attr), "{} doesn't have attribute: {}".format(obj, attr)) def assertNotHasAttr(self, obj, attr): - self.assertFalse(hasattr(obj, attr), + self.assertFalse(hasattr(obj, attr), "{} shouldn't have the attribute: {}".format(obj, attr)) From 37259b963ee155a17d86ae597a2814e92f818991 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 3 Jun 2013 14:48:44 -0700 Subject: [PATCH 0204/3961] Getting connection pooling working with mocking library Verified throwing exception when no servers are available, but correctly recovering and hitting the next server when one is. fixing minor pooling tests ensure we get the right exception back when no servers are available working on tests for retry connections working despite failure Removed old connection_manager and replaced with a simple context manager that allows for easy access to clients within the main pool --- .gitignore | 3 + cqlengine/connection.py | 96 +++++------- cqlengine/management.py | 145 +++++++++--------- cqlengine/query.py | 27 ++-- cqlengine/tests/base.py | 6 +- cqlengine/tests/management/test_management.py | 67 ++++---- cqlengine/tests/query/test_queryset.py | 11 -- 7 files changed, 167 insertions(+), 188 deletions(-) diff --git a/.gitignore b/.gitignore index 6d96b7f030..1e4d8eb344 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ html/ #Mr Developer .mr.developer.cfg .noseids +/commitlog +/data + diff --git a/cqlengine/connection.py b/cqlengine/connection.py index a1a18df09b..a7b700afc3 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -9,8 +9,11 @@ import cql import logging +from copy import copy from cqlengine.exceptions import CQLEngineException +from contextlib import contextmanager + from thrift.transport.TTransport import TTransportException LOG = logging.getLogger('cqlengine.cql') @@ -20,7 +23,9 @@ class CQLConnectionError(CQLEngineException): pass Host = namedtuple('Host', ['name', 'port']) _max_connections = 10 -_connection_pool = None + +# global connection pool +connection_pool = None def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None): """ @@ -29,7 +34,7 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp :param hosts: list of hosts, strings in the :, or just """ global _max_connections - global _connection_pool + global connection_pool _max_connections = max_connections if default_keyspace: @@ -50,16 +55,13 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp if not _hosts: raise CQLConnectionError("At least one host required") - _connection_pool = ConnectionPool(_hosts) + connection_pool = ConnectionPool(_hosts, username, password) class ConnectionPool(object): """Handles pooling of database connections.""" - # Connection pool queue - _queue = None - - def __init__(self, hosts, username, password): + def __init__(self, hosts, username=None, password=None): self._hosts = hosts self._username = username self._password = password @@ -113,58 +115,40 @@ def _create_connection(self): if not self._hosts: raise CQLConnectionError("At least one host required") - host = _hosts[_host_idx] - - new_conn = cql.connect(host.name, host.port, user=_username, password=_password) - new_conn.set_cql_version('3.0.0') - return new_conn - - -class connection_manager(object): - """ - Connection failure tolerant connection manager. Written to be used in a 'with' block for connection pooling - """ - def __init__(self): - if not _hosts: - raise CQLConnectionError("No connections have been configured, call cqlengine.connection.setup") - self.keyspace = None - self.con = ConnectionPool.get() - self.cur = None + hosts = copy(self._hosts) + random.shuffle(hosts) - def close(self): - if self.cur: self.cur.close() - ConnectionPool.put(self.con) - - def __enter__(self): - return self + for host in hosts: + try: + new_conn = cql.connect(host.name, host.port, user=self._username, password=self._password) + new_conn.set_cql_version('3.0.0') + return new_conn + except Exception as e: + logging.debug("Could not establish connection to {}:{}".format(host.name, host.port)) + pass - def __exit__(self, type, value, traceback): - self.close() + raise CQLConnectionError("Could not connect to any server in cluster") - def execute(self, query, params={}): - """ - Gets a connection from the pool and executes the given query, returns the cursor + def execute(self, query, params): + try: + con = self.get() + cur = con.cursor() + cur.execute(query, params) + self.put(con) + return cur + except cql.ProgrammingError as ex: + raise CQLEngineException(unicode(ex)) + except TTransportException: + pass - if there's a connection problem, this will silently create a new connection pool - from the available hosts, and remove the problematic host from the host list - """ - global _host_idx + raise CQLEngineException("Could not execute query against the cluster") - for i in range(len(_hosts)): - try: - LOG.debug('{} {}'.format(query, repr(params))) - self.cur = self.con.cursor() - self.cur.execute(query, params) - return self.cur - except cql.ProgrammingError as ex: - raise CQLEngineException(unicode(ex)) - except TTransportException: - #TODO: check for other errors raised in the event of a connection / server problem - #move to the next connection and set the connection pool - _host_idx += 1 - _host_idx %= len(_hosts) - self.con.close() - self.con = ConnectionPool._create_connection() - - raise CQLConnectionError("couldn't reach a Cassandra server") +def execute(query, params={}): + return connection_pool.execute(query, params) +@contextmanager +def connection_manager(): + global connection_pool + tmp = connection_pool.get() + yield tmp + connection_pool.put(tmp) diff --git a/cqlengine/management.py b/cqlengine/management.py index 25717aa4c6..feddef2ff1 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,6 +1,6 @@ import json -from cqlengine.connection import connection_manager +from cqlengine.connection import connection_manager, execute from cqlengine.exceptions import CQLEngineException def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, durable_writes=True, **replication_values): @@ -15,11 +15,11 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, """ with connection_manager() as con: #TODO: check system tables instead of using cql thrifteries - if not any([name == k.name for k in con.con.client.describe_keyspaces()]): -# if name not in [k.name for k in con.con.client.describe_keyspaces()]: + if not any([name == k.name for k in con.client.describe_keyspaces()]): + # if name not in [k.name for k in con.con.client.describe_keyspaces()]: try: #Try the 1.1 method - con.execute("""CREATE KEYSPACE {} + execute("""CREATE KEYSPACE {} WITH strategy_class = '{}' AND strategy_options:replication_factor={};""".format(name, strategy_class, replication_factor)) except CQLEngineException: @@ -38,12 +38,12 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, if strategy_class != 'SimpleStrategy': query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false') - con.execute(query) + execute(query) def delete_keyspace(name): with connection_manager() as con: - if name in [k.name for k in con.con.client.describe_keyspaces()]: - con.execute("DROP KEYSPACE {}".format(name)) + if name in [k.name for k in con.client.describe_keyspaces()]: + execute("DROP KEYSPACE {}".format(name)) def create_table(model, create_missing_keyspace=True): #construct query string @@ -55,78 +55,81 @@ def create_table(model, create_missing_keyspace=True): create_keyspace(model._get_keyspace()) with connection_manager() as con: - #check for an existing column family - #TODO: check system tables instead of using cql thrifteries - ks_info = con.con.client.describe_keyspace(model._get_keyspace()) - if not any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): - qs = ['CREATE TABLE {}'.format(cf_name)] - - #add column types - pkeys = [] - ckeys = [] - qtypes = [] - def add_column(col): - s = col.get_column_def() - if col.primary_key: - keys = (pkeys if col.partition_key else ckeys) - keys.append('"{}"'.format(col.db_field_name)) - qtypes.append(s) - for name, col in model._columns.items(): - add_column(col) - - qtypes.append('PRIMARY KEY (({}){})'.format(', '.join(pkeys), ckeys and ', ' + ', '.join(ckeys) or '')) - - qs += ['({})'.format(', '.join(qtypes))] - - with_qs = ['read_repair_chance = {}'.format(model.read_repair_chance)] - - _order = ["%s %s" % (c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] - if _order: - with_qs.append("clustering order by ({})".format(', '.join(_order))) - - # add read_repair_chance - qs += ['WITH {}'.format(' AND '.join(with_qs))] + ks_info = con.client.describe_keyspace(model._get_keyspace()) + + #check for an existing column family + #TODO: check system tables instead of using cql thrifteries + if not any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): + qs = ['CREATE TABLE {}'.format(cf_name)] + + #add column types + pkeys = [] + ckeys = [] + qtypes = [] + def add_column(col): + s = col.get_column_def() + if col.primary_key: + keys = (pkeys if col.partition_key else ckeys) + keys.append('"{}"'.format(col.db_field_name)) + qtypes.append(s) + for name, col in model._columns.items(): + add_column(col) + + qtypes.append('PRIMARY KEY (({}){})'.format(', '.join(pkeys), ckeys and ', ' + ', '.join(ckeys) or '')) + + qs += ['({})'.format(', '.join(qtypes))] + + with_qs = ['read_repair_chance = {}'.format(model.read_repair_chance)] + + _order = ["%s %s" % (c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] + if _order: + with_qs.append("clustering order by ({})".format(', '.join(_order))) + + # add read_repair_chance + qs += ['WITH {}'.format(' AND '.join(with_qs))] + qs = ' '.join(qs) + + try: + execute(qs) + except CQLEngineException as ex: + # 1.2 doesn't return cf names, so we have to examine the exception + # and ignore if it says the column family already exists + if "Cannot add already existing column family" not in unicode(ex): + raise + + #get existing index names, skip ones that already exist + with connection_manager() as con: + ks_info = con.client.describe_keyspace(model._get_keyspace()) + + cf_defs = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name] + idx_names = [i.index_name for i in cf_defs[0].column_metadata] if cf_defs else [] + idx_names = filter(None, idx_names) + + indexes = [c for n,c in model._columns.items() if c.index] + if indexes: + for column in indexes: + if column.db_index_name in idx_names: continue + qs = ['CREATE INDEX index_{}_{}'.format(raw_cf_name, column.db_field_name)] + qs += ['ON {}'.format(cf_name)] + qs += ['("{}")'.format(column.db_field_name)] qs = ' '.join(qs) try: - con.execute(qs) + execute(qs) except CQLEngineException as ex: # 1.2 doesn't return cf names, so we have to examine the exception - # and ignore if it says the column family already exists - if "Cannot add already existing column family" not in unicode(ex): + # and ignore if it says the index already exists + if "Index already exists" not in unicode(ex): raise - #get existing index names, skip ones that already exist - ks_info = con.con.client.describe_keyspace(model._get_keyspace()) - cf_defs = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name] - idx_names = [i.index_name for i in cf_defs[0].column_metadata] if cf_defs else [] - idx_names = filter(None, idx_names) - - indexes = [c for n,c in model._columns.items() if c.index] - if indexes: - for column in indexes: - if column.db_index_name in idx_names: continue - qs = ['CREATE INDEX index_{}_{}'.format(raw_cf_name, column.db_field_name)] - qs += ['ON {}'.format(cf_name)] - qs += ['("{}")'.format(column.db_field_name)] - qs = ' '.join(qs) - - try: - con.execute(qs) - except CQLEngineException as ex: - # 1.2 doesn't return cf names, so we have to examine the exception - # and ignore if it says the index already exists - if "Index already exists" not in unicode(ex): - raise - def delete_table(model): cf_name = model.column_family_name() - with connection_manager() as con: - try: - con.execute('drop table {};'.format(cf_name)) - except CQLEngineException as ex: - #don't freak out if the table doesn't exist - if 'Cannot drop non existing column family' not in unicode(ex): - raise + + try: + execute('drop table {};'.format(cf_name)) + except CQLEngineException as ex: + #don't freak out if the table doesn't exist + if 'Cannot drop non existing column family' not in unicode(ex): + raise diff --git a/cqlengine/query.py b/cqlengine/query.py index 3ab71129f0..d3ae8352e0 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -6,7 +6,8 @@ from uuid import uuid1 from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns -from cqlengine.connection import connection_manager +from cqlengine.connection import connection_pool, connection_manager, execute + from cqlengine.exceptions import CQLEngineException from cqlengine.functions import QueryValue, Token @@ -193,8 +194,7 @@ def execute(self): query_list.append('APPLY BATCH;') - with connection_manager() as con: - con.execute('\n'.join(query_list), parameters) + execute('\n'.join(query_list), parameters) self.queries = [] @@ -346,8 +346,7 @@ def _execute_query(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: - self._con = connection_manager() - self._cur = self._con.execute(self._select_query(), self._where_values()) + self._cur = execute(self._select_query(), self._where_values()) self._result_cache = [None]*self._cur.rowcount if self._cur.description: names = [i[0] for i in self._cur.description] @@ -368,7 +367,6 @@ def _fill_result_cache_to_idx(self, idx): #return the connection to the connection pool if we have all objects if self._result_cache and self._result_idx == (len(self._result_cache) - 1): - self._con.close() self._con = None self._cur = None @@ -555,9 +553,8 @@ def count(self): qs = ' '.join(qs) - with connection_manager() as con: - cur = con.execute(qs, self._where_values()) - return cur.fetchone()[0] + cur = execute(qs, self._where_values()) + return cur.fetchone()[0] else: return len(self._result_cache) @@ -635,8 +632,7 @@ def delete(self, columns=[]): if self._batch: self._batch.add_query(qs, self._where_values()) else: - with connection_manager() as con: - con.execute(qs, self._where_values()) + execute(qs, self._where_values()) def values_list(self, *fields, **kwargs): """ Instructs the query set to return tuples, not model instance """ @@ -753,8 +749,7 @@ def save(self): if self.batch: self.batch.add_query(qs, query_values) else: - with connection_manager() as con: - con.execute(qs, query_values) + execute(qs, query_values) # delete nulled columns and removed map keys @@ -787,8 +782,7 @@ def save(self): if self.batch: self.batch.add_query(qs, query_values) else: - with connection_manager() as con: - con.execute(qs, query_values) + execute(qs, query_values) def delete(self): """ Deletes one instance """ @@ -809,7 +803,6 @@ def delete(self): if self.batch: self.batch.add_query(qs, field_values) else: - with connection_manager() as con: - con.execute(qs, field_values) + execute(qs, field_values) diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index a7090c627d..4400881111 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -1,13 +1,13 @@ from unittest import TestCase -from cqlengine import connection +from cqlengine import connection class BaseCassEngTestCase(TestCase): @classmethod def setUpClass(cls): super(BaseCassEngTestCase, cls).setUpClass() - if not connection._connection_pool: - connection.setup(['localhost:9160'], default_keyspace='cqlengine_test') + # todo fix + connection.setup(['localhost:9160'], default_keyspace='cqlengine_test') def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index ac542bb8e6..be49199ff6 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,44 +1,51 @@ +from cqlengine.exceptions import CQLEngineException from cqlengine.management import create_table, delete_table from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.connection import ConnectionPool +from cqlengine.connection import ConnectionPool, Host -from mock import Mock +from mock import Mock, MagicMock, MagicProxy, patch from cqlengine import management from cqlengine.tests.query.test_queryset import TestModel +from cql.thrifteries import ThriftConnection -class ConnectionPoolTestCase(BaseCassEngTestCase): +class ConnectionPoolFailoverTestCase(BaseCassEngTestCase): """Test cassandra connection pooling.""" def setUp(self): - ConnectionPool.clear() - - def test_should_create_single_connection_on_request(self): - """Should create a single connection on first request""" - result = ConnectionPool.get() - self.assertIsNotNone(result) - self.assertEquals(0, ConnectionPool._queue.qsize()) - ConnectionPool._queue.put(result) - self.assertEquals(1, ConnectionPool._queue.qsize()) - - def test_should_close_connection_if_queue_is_full(self): - """Should close additional connections if queue is full""" - connections = [ConnectionPool.get() for x in range(10)] - for conn in connections: - ConnectionPool.put(conn) - fake_conn = Mock() - ConnectionPool.put(fake_conn) - fake_conn.close.assert_called_once_with() - - def test_should_pop_connections_from_queue(self): - """Should pull existing connections off of the queue""" - conn = ConnectionPool.get() - ConnectionPool.put(conn) - self.assertEquals(1, ConnectionPool._queue.qsize()) - self.assertEquals(conn, ConnectionPool.get()) - self.assertEquals(0, ConnectionPool._queue.qsize()) - + self.host = Host('127.0.0.1', '9160') + self.pool = ConnectionPool([self.host]) + + def test_totally_dead_pool(self): + # kill the con + with patch('cqlengine.connection.cql.connect') as mock: + mock.side_effect=CQLEngineException + with self.assertRaises(CQLEngineException): + self.pool.execute("select * from system.peers", {}) + + def test_dead_node(self): + self.pool._hosts.append(self.host) + + # cursor mock needed so set_cql_version doesn't crap out + ok_cur = MagicMock() + + ok_conn = MagicMock() + ok_conn.return_value = ok_cur + + + returns = [CQLEngineException(), ok_conn] + + def side_effect(*args, **kwargs): + result = returns.pop(0) + if isinstance(result, Exception): + raise result + return result + + with patch('cqlengine.connection.cql.connect') as mock: + mock.side_effect = side_effect + conn = self.pool._create_connection() + class CreateKeyspaceTest(BaseCassEngTestCase): def test_create_succeeeds(self): diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 1c3eb95b20..17954d184a 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -405,18 +405,7 @@ def test_conn_is_returned_after_filling_cache(self): assert q._con is None assert q._cur is None - def test_conn_is_returned_after_queryset_is_garbage_collected(self): - """ Tests that the connection is returned to the connection pool after the queryset is gc'd """ - from cqlengine.connection import ConnectionPool - # The queue size can be 1 if we just run this file's tests - # It will be 2 when we run 'em all - initial_size = ConnectionPool._queue.qsize() - q = TestModel.objects(test_id=0) - v = q[0] - assert ConnectionPool._queue.qsize() == initial_size - 1 - del q - assert ConnectionPool._queue.qsize() == initial_size class TimeUUIDQueryModel(Model): partition = columns.UUID(primary_key=True) From 66548e7f243c75d5160d7bfdd9a2aa06ef6186ce Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 3 Jun 2013 17:30:57 -0700 Subject: [PATCH 0205/3961] test comment --- cqlengine/tests/management/test_management.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index be49199ff6..4e271c58ce 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -25,6 +25,9 @@ def test_totally_dead_pool(self): self.pool.execute("select * from system.peers", {}) def test_dead_node(self): + """ + tests that a single dead node doesn't mess up the pool + """ self.pool._hosts.append(self.host) # cursor mock needed so set_cql_version doesn't crap out From f94c61228ce7e1f329685b78f5a894e19f39c5df Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 18:09:11 -0700 Subject: [PATCH 0206/3961] clarifying how the column io tests work --- cqlengine/tests/columns/test_value_io.py | 37 +++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 273ff0d56b..86621d65bf 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -1,7 +1,6 @@ from datetime import datetime, timedelta from decimal import Decimal from uuid import uuid1, uuid4, UUID -from unittest import SkipTest from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import create_table @@ -10,31 +9,35 @@ from cqlengine import columns class BaseColumnIOTest(BaseCassEngTestCase): + """ Tests that values are come out of cassandra in the format we expect """ - TEST_MODEL = None - TEST_COLUMN = None + # The generated test model is assigned here + _test_model = None - @property - def PKEY_VAL(self): - raise NotImplementedError + # the column we want to test + TEST_COLUMN = None - @property - def DATA_VAL(self): - raise NotImplementedError + # the values we want to test against, you can + # use a single value, or multiple comma separated values + PKEY_VAL = None + DATA_VAL = None @classmethod def setUpClass(cls): super(BaseColumnIOTest, cls).setUpClass() + + #if the test column hasn't been defined, bail out if not cls.TEST_COLUMN: return + + # create a table with the given column class IOTestModel(Model): table_name = cls.TEST_COLUMN.db_type + "_io_test_model_{}".format(uuid4().hex[:8]) pkey = cls.TEST_COLUMN(primary_key=True) data = cls.TEST_COLUMN() + cls._test_model = IOTestModel + create_table(cls._test_model) - cls.TEST_MODEL = IOTestModel - create_table(cls.TEST_MODEL) - - #tupleify + #tupleify the tested values if not isinstance(cls.PKEY_VAL, tuple): cls.PKEY_VAL = cls.PKEY_VAL, if not isinstance(cls.DATA_VAL, tuple): @@ -44,7 +47,7 @@ class IOTestModel(Model): def tearDownClass(cls): super(BaseColumnIOTest, cls).tearDownClass() if not cls.TEST_COLUMN: return - delete_table(cls.TEST_MODEL) + delete_table(cls._test_model) def comparator_converter(self, val): """ If you want to convert the original value used to compare the model vales """ @@ -55,15 +58,15 @@ def test_column_io(self): if not self.TEST_COLUMN: return for pkey, data in zip(self.PKEY_VAL, self.DATA_VAL): #create - m1 = self.TEST_MODEL.create(pkey=pkey, data=data) + m1 = self._test_model.create(pkey=pkey, data=data) #get - m2 = self.TEST_MODEL.get(pkey=pkey) + m2 = self._test_model.get(pkey=pkey) assert m1.pkey == m2.pkey == self.comparator_converter(pkey), self.TEST_COLUMN assert m1.data == m2.data == self.comparator_converter(data), self.TEST_COLUMN #delete - self.TEST_MODEL.filter(pkey=pkey).delete() + self._test_model.filter(pkey=pkey).delete() class TestTextIO(BaseColumnIOTest): From f4c7a5bde249dc5d08246a196fec7f664bfcbb5c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 3 Jun 2013 18:12:46 -0700 Subject: [PATCH 0207/3961] more clarification --- cqlengine/tests/columns/test_value_io.py | 95 +++++++++++++----------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 86621d65bf..910bf0bd2c 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -9,44 +9,49 @@ from cqlengine import columns class BaseColumnIOTest(BaseCassEngTestCase): - """ Tests that values are come out of cassandra in the format we expect """ + """ + Tests that values are come out of cassandra in the format we expect + + To test a column type, subclass this test, define the column, and the primary key + and data values you want to test + """ # The generated test model is assigned here _test_model = None # the column we want to test - TEST_COLUMN = None + test_column = None # the values we want to test against, you can # use a single value, or multiple comma separated values - PKEY_VAL = None - DATA_VAL = None + pkey_val = None + data_val = None @classmethod def setUpClass(cls): super(BaseColumnIOTest, cls).setUpClass() #if the test column hasn't been defined, bail out - if not cls.TEST_COLUMN: return + if not cls.test_column: return # create a table with the given column class IOTestModel(Model): - table_name = cls.TEST_COLUMN.db_type + "_io_test_model_{}".format(uuid4().hex[:8]) - pkey = cls.TEST_COLUMN(primary_key=True) - data = cls.TEST_COLUMN() + table_name = cls.test_column.db_type + "_io_test_model_{}".format(uuid4().hex[:8]) + pkey = cls.test_column(primary_key=True) + data = cls.test_column() cls._test_model = IOTestModel create_table(cls._test_model) #tupleify the tested values - if not isinstance(cls.PKEY_VAL, tuple): - cls.PKEY_VAL = cls.PKEY_VAL, - if not isinstance(cls.DATA_VAL, tuple): - cls.DATA_VAL = cls.DATA_VAL, + if not isinstance(cls.pkey_val, tuple): + cls.pkey_val = cls.pkey_val, + if not isinstance(cls.data_val, tuple): + cls.data_val = cls.data_val, @classmethod def tearDownClass(cls): super(BaseColumnIOTest, cls).tearDownClass() - if not cls.TEST_COLUMN: return + if not cls.test_column: return delete_table(cls._test_model) def comparator_converter(self, val): @@ -55,85 +60,87 @@ def comparator_converter(self, val): def test_column_io(self): """ Tests the given models class creates and retrieves values as expected """ - if not self.TEST_COLUMN: return - for pkey, data in zip(self.PKEY_VAL, self.DATA_VAL): + if not self.test_column: return + for pkey, data in zip(self.pkey_val, self.data_val): #create m1 = self._test_model.create(pkey=pkey, data=data) #get m2 = self._test_model.get(pkey=pkey) - assert m1.pkey == m2.pkey == self.comparator_converter(pkey), self.TEST_COLUMN - assert m1.data == m2.data == self.comparator_converter(data), self.TEST_COLUMN + assert m1.pkey == m2.pkey == self.comparator_converter(pkey), self.test_column + assert m1.data == m2.data == self.comparator_converter(data), self.test_column #delete self._test_model.filter(pkey=pkey).delete() class TestTextIO(BaseColumnIOTest): - TEST_COLUMN = columns.Text - PKEY_VAL = 'bacon' - DATA_VAL = 'monkey' + test_column = columns.Text + pkey_val = 'bacon' + data_val = 'monkey' class TestInteger(BaseColumnIOTest): - TEST_COLUMN = columns.Integer - PKEY_VAL = 5 - DATA_VAL = 6 + test_column = columns.Integer + pkey_val = 5 + data_val = 6 class TestDateTime(BaseColumnIOTest): - TEST_COLUMN = columns.DateTime + test_column = columns.DateTime + now = datetime(*datetime.now().timetuple()[:6]) - PKEY_VAL = now - DATA_VAL = now + timedelta(days=1) + pkey_val = now + data_val = now + timedelta(days=1) class TestDate(BaseColumnIOTest): - TEST_COLUMN = columns.Date + test_column = columns.Date + now = datetime.now().date() - PKEY_VAL = now - DATA_VAL = now + timedelta(days=1) + pkey_val = now + data_val = now + timedelta(days=1) class TestUUID(BaseColumnIOTest): - TEST_COLUMN = columns.UUID + test_column = columns.UUID - PKEY_VAL = str(uuid4()), uuid4() - DATA_VAL = str(uuid4()), uuid4() + pkey_val = str(uuid4()), uuid4() + data_val = str(uuid4()), uuid4() def comparator_converter(self, val): return val if isinstance(val, UUID) else UUID(val) class TestTimeUUID(BaseColumnIOTest): - TEST_COLUMN = columns.TimeUUID + test_column = columns.TimeUUID - PKEY_VAL = str(uuid1()), uuid1() - DATA_VAL = str(uuid1()), uuid1() + pkey_val = str(uuid1()), uuid1() + data_val = str(uuid1()), uuid1() def comparator_converter(self, val): return val if isinstance(val, UUID) else UUID(val) class TestBooleanIO(BaseColumnIOTest): - TEST_COLUMN = columns.Boolean + test_column = columns.Boolean - PKEY_VAL = True - DATA_VAL = False + pkey_val = True + data_val = False class TestFloatIO(BaseColumnIOTest): - TEST_COLUMN = columns.Float + test_column = columns.Float - PKEY_VAL = 3.14 - DATA_VAL = -1982.11 + pkey_val = 3.14 + data_val = -1982.11 class TestDecimalIO(BaseColumnIOTest): - TEST_COLUMN = columns.Decimal + test_column = columns.Decimal - PKEY_VAL = Decimal('1.35'), 5, '2.4' - DATA_VAL = Decimal('0.005'), 3.5, '8' + pkey_val = Decimal('1.35'), 5, '2.4' + data_val = Decimal('0.005'), 3.5, '8' def comparator_converter(self, val): return Decimal(val) From 20ba2fcc78e1eecd718190bdc2cef78ed80ac93e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 4 Jun 2013 13:21:12 -0700 Subject: [PATCH 0208/3961] fixing some attribute names that were upsetting py.test reflection --- cqlengine/tests/columns/test_value_io.py | 51 ++++++++++++------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 910bf0bd2c..fda632e965 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta from decimal import Decimal from uuid import uuid1, uuid4, UUID + from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import create_table @@ -17,10 +18,10 @@ class BaseColumnIOTest(BaseCassEngTestCase): """ # The generated test model is assigned here - _test_model = None + _generated_model = None # the column we want to test - test_column = None + column = None # the values we want to test against, you can # use a single value, or multiple comma separated values @@ -32,15 +33,15 @@ def setUpClass(cls): super(BaseColumnIOTest, cls).setUpClass() #if the test column hasn't been defined, bail out - if not cls.test_column: return + if not cls.column: return # create a table with the given column class IOTestModel(Model): - table_name = cls.test_column.db_type + "_io_test_model_{}".format(uuid4().hex[:8]) - pkey = cls.test_column(primary_key=True) - data = cls.test_column() - cls._test_model = IOTestModel - create_table(cls._test_model) + table_name = cls.column.db_type + "_io_test_model_{}".format(uuid4().hex[:8]) + pkey = cls.column(primary_key=True) + data = cls.column() + cls._generated_model = IOTestModel + create_table(cls._generated_model) #tupleify the tested values if not isinstance(cls.pkey_val, tuple): @@ -51,8 +52,8 @@ class IOTestModel(Model): @classmethod def tearDownClass(cls): super(BaseColumnIOTest, cls).tearDownClass() - if not cls.test_column: return - delete_table(cls._test_model) + if not cls.column: return + delete_table(cls._generated_model) def comparator_converter(self, val): """ If you want to convert the original value used to compare the model vales """ @@ -60,34 +61,34 @@ def comparator_converter(self, val): def test_column_io(self): """ Tests the given models class creates and retrieves values as expected """ - if not self.test_column: return + if not self.column: return for pkey, data in zip(self.pkey_val, self.data_val): #create - m1 = self._test_model.create(pkey=pkey, data=data) + m1 = self._generated_model.create(pkey=pkey, data=data) #get - m2 = self._test_model.get(pkey=pkey) - assert m1.pkey == m2.pkey == self.comparator_converter(pkey), self.test_column - assert m1.data == m2.data == self.comparator_converter(data), self.test_column + m2 = self._generated_model.get(pkey=pkey) + assert m1.pkey == m2.pkey == self.comparator_converter(pkey), self.column + assert m1.data == m2.data == self.comparator_converter(data), self.column #delete - self._test_model.filter(pkey=pkey).delete() + self._generated_model.filter(pkey=pkey).delete() class TestTextIO(BaseColumnIOTest): - test_column = columns.Text + column = columns.Text pkey_val = 'bacon' data_val = 'monkey' class TestInteger(BaseColumnIOTest): - test_column = columns.Integer + column = columns.Integer pkey_val = 5 data_val = 6 class TestDateTime(BaseColumnIOTest): - test_column = columns.DateTime + column = columns.DateTime now = datetime(*datetime.now().timetuple()[:6]) pkey_val = now @@ -95,7 +96,7 @@ class TestDateTime(BaseColumnIOTest): class TestDate(BaseColumnIOTest): - test_column = columns.Date + column = columns.Date now = datetime.now().date() pkey_val = now @@ -103,7 +104,7 @@ class TestDate(BaseColumnIOTest): class TestUUID(BaseColumnIOTest): - test_column = columns.UUID + column = columns.UUID pkey_val = str(uuid4()), uuid4() data_val = str(uuid4()), uuid4() @@ -113,7 +114,7 @@ def comparator_converter(self, val): class TestTimeUUID(BaseColumnIOTest): - test_column = columns.TimeUUID + column = columns.TimeUUID pkey_val = str(uuid1()), uuid1() data_val = str(uuid1()), uuid1() @@ -123,21 +124,21 @@ def comparator_converter(self, val): class TestBooleanIO(BaseColumnIOTest): - test_column = columns.Boolean + column = columns.Boolean pkey_val = True data_val = False class TestFloatIO(BaseColumnIOTest): - test_column = columns.Float + column = columns.Float pkey_val = 3.14 data_val = -1982.11 class TestDecimalIO(BaseColumnIOTest): - test_column = columns.Decimal + column = columns.Decimal pkey_val = Decimal('1.35'), 5, '2.4' data_val = Decimal('0.005'), 3.5, '8' From 67055e006e1125446c56c05e07c1bd60afb2b1c8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 4 Jun 2013 13:40:19 -0700 Subject: [PATCH 0209/3961] fixing bug in uuid string validation --- cqlengine/columns.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 3f0806345d..4f93b34ca9 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -290,9 +290,9 @@ def validate(self, value): if val is None: return from uuid import UUID as _UUID if isinstance(val, _UUID): return val - if not self.re_uuid.match(val): - raise ValidationError("{} is not a valid uuid".format(value)) - return _UUID(val) + if isinstance(val, basestring) and self.re_uuid.match(val): + return _UUID(val) + raise ValidationError("{} is not a valid uuid".format(value)) def to_python(self, value): return self.validate(value) From 750c58c27c71e176b27aba8462c18d36e5b764a4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 4 Jun 2013 13:43:58 -0700 Subject: [PATCH 0210/3961] version bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 25d51303d5..a593cdce5c 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.3.1' +__version__ = '0.3.2' diff --git a/docs/conf.py b/docs/conf.py index 36e5d084b0..ecb08cc150 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.3.1' +version = '0.3.2' # The full version, including alpha/beta/rc tags. -release = '0.3.1' +release = '0.3.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index cec6865dca..7692b9f853 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.3.1' +version = '0.3.2' long_desc = """ cqlengine is a Cassandra CQL Object Mapper for Python in the style of the Django orm and mongoengine From 634c0e9ae0c8cbf2daa5fed1f85f4d00f41f1da0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 5 Jun 2013 14:21:58 -0700 Subject: [PATCH 0211/3961] adding support for abstract base classes --- cqlengine/management.py | 4 + cqlengine/models.py | 13 +++- .../tests/model/test_class_construction.py | 74 ++++++++++++++++++- docs/topics/models.rst | 4 + 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index feddef2ff1..90005d2bc2 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -46,6 +46,10 @@ def delete_keyspace(name): execute("DROP KEYSPACE {}".format(name)) def create_table(model, create_missing_keyspace=True): + + if model.__abstract__: + raise CQLEngineException("cannot create table from abstract model") + #construct query string cf_name = model.column_family_name() raw_cf_name = model.column_family_name(include_keyspace=False) diff --git a/cqlengine/models.py b/cqlengine/models.py index bd4e65fabe..99a6c780e0 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -2,7 +2,7 @@ import re from cqlengine import columns -from cqlengine.exceptions import ModelException +from cqlengine.exceptions import ModelException, CQLEngineException from cqlengine.query import QuerySet, DMLQuery from cqlengine.query import DoesNotExist as _DoesNotExist from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned @@ -29,6 +29,8 @@ def __get__(self, instance, owner): class QuerySetDescriptor(object): def __get__(self, obj, model): + if model.__abstract__: + raise CQLEngineException('cannot execute queries against abstract models') return QuerySet(model) class BaseModel(object): @@ -183,6 +185,8 @@ def __new__(cls, name, bases, attrs): for k,v in getattr(base, '_defined_columns', {}).items(): inherited_columns.setdefault(k,v) + #short circuit __abstract__ inheritance + is_abstract = attrs['__abstract__'] = attrs.get('__abstract__', False) def _transform_column(col_name, col_obj): column_dict[col_name] = col_obj @@ -208,7 +212,7 @@ def _transform_column(col_name, col_obj): defined_columns = OrderedDict(column_definitions) #prepend primary key if one hasn't been defined - if not any([v.primary_key for k,v in column_definitions]): + if not is_abstract and not any([v.primary_key for k,v in column_definitions]): k,v = 'id', columns.UUID(primary_key=True) column_definitions = [(k,v)] + column_definitions @@ -228,7 +232,9 @@ def _transform_column(col_name, col_obj): clustering_keys = OrderedDict(k for k in primary_keys.items() if not k[1].partition_key) #setup partition key shortcut - assert partition_keys + if len(partition_keys) == 0: + if not is_abstract: + raise ModelException("at least one partition key must be defined") if len(partition_keys) == 1: pk_name = partition_keys.keys()[0] attrs['pk'] = attrs[pk_name] @@ -294,6 +300,7 @@ class Model(BaseModel): the db name for the column family can be set as the attribute db_name, or it will be genertaed from the class name """ + __abstract__ = True __metaclass__ = ModelMetaClass diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index c67fcbee4d..a37b6baa3c 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -1,6 +1,7 @@ +from cqlengine.query import QueryException from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.exceptions import ModelException +from cqlengine.exceptions import ModelException, CQLEngineException from cqlengine.models import Model from cqlengine import columns import cqlengine @@ -199,8 +200,79 @@ def test_manual_table_name_is_not_inherited(self): class InheritedTest(self.RenamedTest): pass assert InheritedTest.table_name is None +class AbstractModel(Model): + __abstract__ = True +class ConcreteModel(AbstractModel): + pkey = columns.Integer(primary_key=True) + data = columns.Integer() +class AbstractModelWithCol(Model): + __abstract__ = True + pkey = columns.Integer(primary_key=True) + +class ConcreteModelWithCol(AbstractModelWithCol): + data = columns.Integer() + +class AbstractModelWithFullCols(Model): + __abstract__ = True + pkey = columns.Integer(primary_key=True) + data = columns.Integer() + +class TestAbstractModelClasses(BaseCassEngTestCase): + + def test_id_field_is_not_created(self): + """ Tests that an id field is not automatically generated on abstract classes """ + assert not hasattr(AbstractModel, 'id') + assert not hasattr(AbstractModelWithCol, 'id') + + def test_id_field_is_not_created_on_subclass(self): + assert not hasattr(ConcreteModel, 'id') + + def test_abstract_attribute_is_not_inherited(self): + """ Tests that __abstract__ attribute is not inherited """ + assert not ConcreteModel.__abstract__ + assert not ConcreteModelWithCol.__abstract__ + + def test_attempting_to_save_abstract_model_fails(self): + """ Attempting to save a model from an abstract model should fail """ + with self.assertRaises(CQLEngineException): + AbstractModelWithFullCols.create(pkey=1, data=2) + + def test_attempting_to_create_abstract_table_fails(self): + """ Attempting to create a table from an abstract model should fail """ + from cqlengine.management import create_table + with self.assertRaises(CQLEngineException): + create_table(AbstractModelWithFullCols) + + def test_attempting_query_on_abstract_model_fails(self): + """ Tests attempting to execute query with an abstract model fails """ + with self.assertRaises(CQLEngineException): + iter(AbstractModelWithFullCols.objects(pkey=5)).next() + + 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, property) + assert isinstance(ConcreteModelWithCol._columns['pkey'], columns.Column) + + def test_concrete_class_table_creation_cycle(self): + """ Tests that models with inherited abstract classes can be created, and have io performed """ + from cqlengine.management import create_table, delete_table + create_table(ConcreteModelWithCol) + + w1 = ConcreteModelWithCol.create(pkey=5, data=6) + w2 = ConcreteModelWithCol.create(pkey=6, data=7) + + r1 = ConcreteModelWithCol.get(pkey=5) + r2 = ConcreteModelWithCol.get(pkey=6) + + assert w1.pkey == r1.pkey + assert w1.data == r1.data + assert w2.pkey == r2.pkey + assert w2.data == r2.data + + delete_table(ConcreteModelWithCol) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 52eb90613d..5dc322f61b 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -128,6 +128,10 @@ Model Methods Model Attributes ================ + .. attribute:: Model.__abstract__ + + *Optional.* Indicates that this model is only intended to be used as a base class for other models. You can't create tables for abstract models, but checks around schema validity are skipped during class construction. + .. attribute:: Model.table_name *Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix. Manually defined table names are not inherited. From fd27adf80ebe64bafb7972d0a674d6ce5fc2f9e3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 5 Jun 2013 14:25:33 -0700 Subject: [PATCH 0212/3961] updating changelog --- changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/changelog b/changelog index 05714651e4..8111f51f61 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,11 @@ CHANGELOG +0.3.3 +* added abstract base class models + +0.3.2 +* comprehesive rewrite of connection management (thanks @rustyrazorblade) + 0.3 * added support for Token function (thanks @mrk-its) * added support for compound partition key (thanks @mrk-its)s From d6707038df441e6b0e05d7eedcafaa0a59b407f3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 5 Jun 2013 14:25:40 -0700 Subject: [PATCH 0213/3961] version bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index a593cdce5c..d103644c90 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.3.2' +__version__ = '0.3.3' diff --git a/docs/conf.py b/docs/conf.py index ecb08cc150..5d797fee3e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.3.2' +version = '0.3.3' # The full version, including alpha/beta/rc tags. -release = '0.3.2' +release = '0.3.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 7692b9f853..512d55e902 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.3.2' +version = '0.3.3' long_desc = """ cqlengine is a Cassandra CQL Object Mapper for Python in the style of the Django orm and mongoengine From f8654657578cfc09273c8d45c9c829699756b059 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 7 Jun 2013 16:18:59 -0700 Subject: [PATCH 0214/3961] making connection consistency more configurable --- cqlengine/connection.py | 15 +++++++++++---- cqlengine/tests/base.py | 2 +- docs/topics/connection.rst | 11 ++++++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index a7b700afc3..5010aebc1a 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -27,7 +27,7 @@ class CQLConnectionError(CQLEngineException): pass # global connection pool connection_pool = None -def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None): +def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None, consistency='ONE'): """ Records the hosts and connects to one of them @@ -55,16 +55,17 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp if not _hosts: raise CQLConnectionError("At least one host required") - connection_pool = ConnectionPool(_hosts, username, password) + connection_pool = ConnectionPool(_hosts, username, password, consistency) class ConnectionPool(object): """Handles pooling of database connections.""" - def __init__(self, hosts, username=None, password=None): + def __init__(self, hosts, username=None, password=None, consistency=None): self._hosts = hosts self._username = username self._password = password + self._consistency = consistency self._queue = Queue.Queue(maxsize=_max_connections) @@ -120,7 +121,13 @@ def _create_connection(self): for host in hosts: try: - new_conn = cql.connect(host.name, host.port, user=self._username, password=self._password) + new_conn = cql.connect( + host.name, + host.port, + user=self._username, + password=self._password, + consistency_level=self._consistency + ) new_conn.set_cql_version('3.0.0') return new_conn except Exception as e: diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index 4400881111..126049a860 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -1,5 +1,5 @@ from unittest import TestCase -from cqlengine import connection +from cqlengine import connection class BaseCassEngTestCase(TestCase): diff --git a/docs/topics/connection.rst b/docs/topics/connection.rst index 14bc2341c3..48558f9c46 100644 --- a/docs/topics/connection.rst +++ b/docs/topics/connection.rst @@ -7,11 +7,20 @@ Connection The setup function in `cqlengine.connection` records the Cassandra servers to connect to. If there is a problem with one of the servers, cqlengine will try to connect to each of the other connections before failing. -.. function:: setup(hosts [, username=None, password=None]) +.. function:: setup(hosts [, username=None, password=None, consistency='ONE']) :param hosts: list of hosts, strings in the :, or just :type hosts: list + :param username: a username, if required + :type username: str + + :param password: a password, if required + :type password: str + + :param consistency: the consistency level of the connection, defaults to 'ONE' + :type consistency: str + Records the hosts and connects to one of them See the example at :ref:`getting-started` From 226e900fdf5ff85ebd16bb72c4ef37f666f1ad72 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 7 Jun 2013 16:29:28 -0700 Subject: [PATCH 0215/3961] adding some IDE hints to batch and query set descriptors --- cqlengine/models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index 99a6c780e0..64faf6a4d1 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -27,12 +27,25 @@ def __get__(self, instance, owner): else: return self.instmethod.__get__(instance, owner) + def __call__(self, *args, **kwargs): + """ Just a hint to IDEs that it's ok to call this """ + raise NotImplementedError + class QuerySetDescriptor(object): + """ + returns a fresh queryset for the given model + it's declared on everytime it's accessed + """ + def __get__(self, obj, model): 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 """ + raise NotImplementedError + class BaseModel(object): """ The base model class, don't inherit from this, inherit from Model, defined below From 35811d1fab64266655ff335c8afbdd5969c332f6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 7 Jun 2013 17:42:12 -0700 Subject: [PATCH 0216/3961] expanding column documentation --- cqlengine/columns.py | 18 +++++++++++++++--- docs/topics/columns.rst | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 4f93b34ca9..d864780cfc 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -74,14 +74,26 @@ class Column(object): instance_counter = 0 - def __init__(self, primary_key=False, partition_key=False, index=False, db_field=None, default=None, required=True, clustering_order=None): + def __init__(self, + primary_key=False, + partition_key=False, + index=False, + db_field=None, + default=None, + required=True, + clustering_order=None): """ :param primary_key: bool flag, indicates this column is a primary key. The first primary key defined - on a model is the partition key, all others are cluster keys + on a model is the partition key (unless partition keys are set), all others are cluster keys + :param partition_key: indicates that this column should be the partition key, defining + more than one partition key column creates a compound partition key :param index: bool flag, indicates an index should be created for this column :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 required: boolean, is the field required? + :param required: boolean, is the field required? Model validation will raise and + exception if required is set to True and there is a None value assigned + :param clustering_order: only applicable on clustering keys (primary keys that are not partition keys) + determines the order that the clustering keys are sorted on disk """ self.partition_key = partition_key self.primary_key = partition_key or primary_key diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 0779c4333f..61252e7f97 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -149,7 +149,7 @@ Column Options .. attribute:: BaseColumn.partition_key - If True, this column is created as partition primary key. There may be many partition keys defined, forming *composite partition key* + If True, this column is created as partition primary key. There may be many partition keys defined, forming a *composite partition key* .. attribute:: BaseColumn.index From f6fd5b100cf4d91bd0ba15a6c733183a208eeef7 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 14 Jun 2013 15:47:26 -0700 Subject: [PATCH 0217/3961] beginning work on query builder --- cqlengine/query.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/cqlengine/query.py b/cqlengine/query.py index d3ae8352e0..dd511d5939 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -154,6 +154,66 @@ class LessThanOrEqualOperator(QueryOperator): symbol = "LTE" cql_symbol = '<=' +class AbstractColumnDescriptor(object): + """ + exposes cql query operators through pythons + builtin comparator symbols + """ + + def _get_column(self): + raise NotImplementedError + + 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) + + def __ge__(self, other): + return GreaterThanOrEqualOperator(self._get_column(), other) + + def __lt__(self, other): + return LessThanOperator(self._get_column(), other) + + def __le__(self, other): + return LessThanOrEqualOperator(self._get_column(), other) + + +class NamedColumnDescriptor(AbstractColumnDescriptor): + """ describes a named cql column """ + + def __init__(self, name): + self.name = name + +C = NamedColumnDescriptor + +class TableDescriptor(object): + """ describes a cql table """ + + def __init__(self, keyspace, name): + self.keyspace = keyspace + self.name = name + +T = TableDescriptor + +class KeyspaceDescriptor(object): + """ Describes a cql keyspace """ + + def __init__(self, name): + self.name = name + + def table(self, name): + """ + returns a table descriptor with the given + name that belongs to this keyspace + """ + return TableDescriptor(self.name, name) + +K = KeyspaceDescriptor + class BatchType(object): Unlogged = 'UNLOGGED' Counter = 'COUNTER' From 4a977005f3569d1a946d7021486c1ba465b14a08 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 14 Jun 2013 17:52:13 -0700 Subject: [PATCH 0218/3961] fixed #67 (validation on keyname typos) #66 (removed defaults on columns) and #57 and removed autoid --- changelog | 4 +++ cqlengine/columns.py | 12 ------- cqlengine/models.py | 24 ++++++++------ cqlengine/tests/columns/test_validation.py | 17 ++++++++-- .../tests/model/test_class_construction.py | 31 ++++++++++++++----- .../tests/model/test_equality_operations.py | 2 ++ cqlengine/tests/model/test_model_io.py | 10 ++++++ 7 files changed, 68 insertions(+), 32 deletions(-) diff --git a/changelog b/changelog index 8111f51f61..3d94f5fb91 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.4.0 +* removed default values from all column types +* explicit primary key is required (automatic id removed) + 0.3.3 * added abstract base class models diff --git a/cqlengine/columns.py b/cqlengine/columns.py index d864780cfc..67112eb059 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -236,9 +236,6 @@ def to_database(self, value): class DateTime(Column): db_type = 'timestamp' - def __init__(self, **kwargs): - super(DateTime, self).__init__(**kwargs) - def to_python(self, value): if isinstance(value, datetime): return value @@ -265,8 +262,6 @@ def to_database(self, value): class Date(Column): db_type = 'timestamp' - def __init__(self, **kwargs): - super(Date, self).__init__(**kwargs) def to_python(self, value): if isinstance(value, datetime): @@ -294,9 +289,6 @@ class UUID(Column): re_uuid = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') - def __init__(self, default=lambda:uuid4(), **kwargs): - super(UUID, self).__init__(default=default, **kwargs) - def validate(self, value): val = super(UUID, self).validate(value) if val is None: return @@ -319,10 +311,6 @@ class TimeUUID(UUID): db_type = 'timeuuid' - def __init__(self, **kwargs): - kwargs.setdefault('default', lambda: uuid1()) - super(TimeUUID, self).__init__(**kwargs) - class Boolean(Column): db_type = 'boolean' diff --git a/cqlengine/models.py b/cqlengine/models.py index 64faf6a4d1..7240bf16ca 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -2,7 +2,7 @@ import re from cqlengine import columns -from cqlengine.exceptions import ModelException, CQLEngineException +from cqlengine.exceptions import ModelException, CQLEngineException, ValidationError from cqlengine.query import QuerySet, DMLQuery from cqlengine.query import DoesNotExist as _DoesNotExist from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned @@ -50,7 +50,7 @@ class BaseModel(object): """ The base model class, don't inherit from this, inherit from Model, defined below """ - + class DoesNotExist(_DoesNotExist): pass class MultipleObjectsReturned(_MultipleObjectsReturned): pass @@ -60,12 +60,17 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #however, you can also define them manually here table_name = None - #the keyspace for this model + #the keyspace for this model keyspace = None read_repair_chance = 0.1 def __init__(self, **values): self._values = {} + + extra_columns = set(values.keys()) - set(self._columns.keys()) + if extra_columns: + raise ValidationError("Incorrect columns passed: {}".format(extra_columns)) + for name, column in self._columns.items(): value = values.get(name, None) if value is not None: value = column.to_python(value) @@ -111,11 +116,11 @@ def column_family_name(cls, include_keyspace=True): else: camelcase = re.compile(r'([a-z])([A-Z])') ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) - + module = cls.__module__.split('.') if module: cf_name = ccase(module[-1]) + '_' - + cf_name += ccase(cls.__name__) #trim to less than 48 characters or cassandra will complain cf_name = cf_name[-48:] @@ -140,15 +145,15 @@ def as_dict(self): @classmethod def create(cls, **kwargs): return cls.objects.create(**kwargs) - + @classmethod def all(cls): return cls.objects.all() - + @classmethod def filter(cls, **kwargs): return cls.objects.filter(**kwargs) - + @classmethod def get(cls, **kwargs): return cls.objects.get(**kwargs) @@ -226,8 +231,7 @@ def _transform_column(col_name, col_obj): #prepend primary key if one hasn't been defined if not is_abstract and not any([v.primary_key for k,v in column_definitions]): - k,v = 'id', columns.UUID(primary_key=True) - column_definitions = [(k,v)] + column_definitions + raise ModelDefinitionException("At least 1 primary key is required.") has_partition_keys = any(v.partition_key for (k, v) in column_definitions) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 24c1739706..acbf9099a9 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -3,6 +3,7 @@ from datetime import date from datetime import tzinfo from decimal import Decimal as D +from uuid import uuid4, uuid1 from cqlengine import ValidationError from cqlengine.tests.base import BaseCassEngTestCase @@ -119,7 +120,7 @@ def test_datetime_io(self): class TestTimeUUID(BaseCassEngTestCase): class TimeUUIDTest(Model): test_id = Integer(primary_key=True) - timeuuid = TimeUUID() + timeuuid = TimeUUID(default=uuid1()) @classmethod def setUpClass(cls): @@ -132,6 +133,10 @@ def tearDownClass(cls): delete_table(cls.TimeUUIDTest) def test_timeuuid_io(self): + """ + ensures that + :return: + """ t0 = self.TimeUUIDTest.create(test_id=0) t1 = self.TimeUUIDTest.get(test_id=0) @@ -139,8 +144,8 @@ def test_timeuuid_io(self): class TestInteger(BaseCassEngTestCase): class IntegerTest(Model): - test_id = UUID(primary_key=True) - value = Integer(default=0) + test_id = UUID(primary_key=True, default=lambda:uuid4()) + value = Integer(default=0, required=True) def test_default_zero_fields_validate(self): """ Tests that integer columns with a default value of 0 validate """ @@ -190,7 +195,13 @@ def test_type_checking(self): +class TestExtraFieldsRaiseException(BaseCassEngTestCase): + class TestModel(Model): + id = UUID(primary_key=True, default=uuid4) + def test_extra_field(self): + with self.assertRaises(ValidationError): + self.TestModel.create(bacon=5000) diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index a37b6baa3c..3e249be2e4 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -1,8 +1,9 @@ +from uuid import uuid4 from cqlengine.query import QueryException from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException, CQLEngineException -from cqlengine.models import Model +from cqlengine.models import Model, ModelDefinitionException from cqlengine import columns import cqlengine @@ -18,6 +19,7 @@ def test_column_attributes_handled_correctly(self): """ class TestModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) text = columns.Text() #check class attibutes @@ -38,6 +40,7 @@ def test_db_map(self): -the db_map allows columns """ class WildDBNames(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) content = columns.Text(db_field='words_and_whatnot') numbers = columns.Integer(db_field='integers_etc') @@ -61,17 +64,26 @@ def test_column_ordering_is_preserved(self): """ class Stuff(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) words = columns.Text() content = columns.Text() numbers = columns.Integer() self.assertEquals(Stuff._columns.keys(), ['id', 'words', 'content', 'numbers']) + def test_exception_raised_when_creating_class_without_pk(self): + with self.assertRaises(ModelDefinitionException): + class TestModel(Model): + count = columns.Integer() + text = columns.Text(required=False) + + def test_value_managers_are_keeping_model_instances_isolated(self): """ Tests that instance value managers are isolated from other instances """ class Stuff(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) num = columns.Integer() inst1 = Stuff(num=5) @@ -86,6 +98,7 @@ def test_superclass_fields_are_inherited(self): Tests that fields defined on the super class are inherited properly """ class TestModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) text = columns.Text() class InheritedModel(TestModel): @@ -124,6 +137,7 @@ def test_partition_keys(self): Test compound partition key definition """ class ModelWithPartitionKeys(cqlengine.Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) c1 = cqlengine.Text(primary_key=True) p1 = cqlengine.Text(partition_key=True) p2 = cqlengine.Text(partition_key=True) @@ -144,6 +158,7 @@ class ModelWithPartitionKeys(cqlengine.Model): def test_del_attribute_is_assigned_properly(self): """ Tests that columns that can be deleted have the del attribute """ class DelModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) key = columns.Integer(primary_key=True) data = columns.Integer(required=False) @@ -156,9 +171,10 @@ def test_does_not_exist_exceptions_are_not_shared_between_model(self): """ Tests that DoesNotExist exceptions are not the same exception between models """ class Model1(Model): - pass + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + class Model2(Model): - pass + id = columns.UUID(primary_key=True, default=lambda:uuid4()) try: raise Model1.DoesNotExist @@ -171,7 +187,8 @@ class Model2(Model): def test_does_not_exist_inherits_from_superclass(self): """ Tests that a DoesNotExist exception can be caught by it's parent class DoesNotExist """ class Model1(Model): - pass + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + class Model2(Model1): pass @@ -184,14 +201,14 @@ class Model2(Model1): assert False, "Model2 exception should not be caught by Model1" class TestManualTableNaming(BaseCassEngTestCase): - + class RenamedTest(cqlengine.Model): keyspace = 'whatever' table_name = 'manual_name' - + id = cqlengine.UUID(primary_key=True) data = cqlengine.Text() - + def test_proper_table_naming(self): assert self.RenamedTest.column_family_name(include_keyspace=False) == 'manual_name' assert self.RenamedTest.column_family_name(include_keyspace=True) == 'whatever.manual_name' diff --git a/cqlengine/tests/model/test_equality_operations.py b/cqlengine/tests/model/test_equality_operations.py index 4d5b95219b..a3e592b3dd 100644 --- a/cqlengine/tests/model/test_equality_operations.py +++ b/cqlengine/tests/model/test_equality_operations.py @@ -1,4 +1,5 @@ from unittest import skip +from uuid import uuid4 from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import create_table @@ -7,6 +8,7 @@ from cqlengine import columns class TestModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 08e77ded36..08d41d8b27 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -8,10 +8,18 @@ from cqlengine import columns class TestModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) a_bool = columns.Boolean(default=False) +class TestModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + count = columns.Integer() + text = columns.Text(required=False) + a_bool = columns.Boolean(default=False) + + class TestModelIO(BaseCassEngTestCase): @classmethod @@ -34,6 +42,8 @@ def test_model_save_and_load(self): 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 From f254f4da7a4e93e7b757da272a79c59eb01b64ab Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 14 Jun 2013 17:53:00 -0700 Subject: [PATCH 0219/3961] updated changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index 3d94f5fb91..f17057b083 100644 --- a/changelog +++ b/changelog @@ -3,6 +3,7 @@ CHANGELOG 0.4.0 * removed default values from all column types * explicit primary key is required (automatic id removed) +* added validation on keyname types on .create() 0.3.3 * added abstract base class models From 23a59e85a2c2c1717417e7ce4f8ff29df117e82d Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 14 Jun 2013 17:53:15 -0700 Subject: [PATCH 0220/3961] changelog update --- changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog b/changelog index f17057b083..f16994fa22 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,6 @@ CHANGELOG -0.4.0 +0.4.0 (in progress) * removed default values from all column types * explicit primary key is required (automatic id removed) * added validation on keyname types on .create() From 30c59e5e8fa7e1cdc70cf0109690a46f44ed9074 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 14 Jun 2013 17:57:42 -0700 Subject: [PATCH 0221/3961] required is false by default #61 --- cqlengine/columns.py | 2 +- cqlengine/tests/columns/test_validation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 67112eb059..941d2419aa 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -80,7 +80,7 @@ def __init__(self, index=False, db_field=None, default=None, - required=True, + required=False, clustering_order=None): """ :param primary_key: bool flag, indicates this column is a primary key. The first primary key defined diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index acbf9099a9..627ccbd3e6 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -185,7 +185,7 @@ def test_type_checking(self): Text().validate(bytearray('bytearray')) with self.assertRaises(ValidationError): - Text().validate(None) + Text(required=True).validate(None) with self.assertRaises(ValidationError): Text().validate(5) From 0476a97a67c433843c0ad8dd2d713f1495648ac2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 10:06:55 -0700 Subject: [PATCH 0222/3961] adding query method, and expanding the column descriptor --- cqlengine/query.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cqlengine/query.py b/cqlengine/query.py index dd511d5939..88d16aa42a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -188,6 +188,13 @@ class NamedColumnDescriptor(AbstractColumnDescriptor): def __init__(self, name): self.name = name + @property + def cql(self): + return self.name + + def to_database(self, val): + return val + C = NamedColumnDescriptor class TableDescriptor(object): @@ -544,6 +551,12 @@ def filter(self, **kwargs): return clone + def query(self, *args): + """ + Same end result as filter, but uses the new comparator style args + ie: Model.column == val + """ + def get(self, **kwargs): """ Returns a single instance matching this query, optionally with additional filter kwargs. From c90da56021b2c047e3a294fbc17d211a2aa8c567 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 10:30:02 -0700 Subject: [PATCH 0223/3961] replacing property stuff with ColumnDescriptor instances --- cqlengine/models.py | 62 ++++++++++++++++--- .../tests/model/test_class_construction.py | 2 +- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 64faf6a4d1..57629260b9 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -1,9 +1,10 @@ from collections import OrderedDict import re +import cqlengine from cqlengine import columns from cqlengine.exceptions import ModelException, CQLEngineException -from cqlengine.query import QuerySet, DMLQuery +from cqlengine.query import QuerySet, DMLQuery, AbstractColumnDescriptor from cqlengine.query import DoesNotExist as _DoesNotExist from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned @@ -46,6 +47,56 @@ def __call__(self, *args, **kwargs): """ Just a hint to IDEs that it's ok to call this """ raise NotImplementedError +class ColumnDescriptor(AbstractColumnDescriptor): + """ + Handles the reading and writing of column values to and from + a model instance's value manager, as well as creating + comparator queries + """ + + def __init__(self, column): + """ + :param column: + :type column: columns.Column + :return: + """ + self.column = column + + def __get__(self, instance, owner): + """ + Returns either the value or column, depending + on if an instance is provided or not + + :param instance: the model instance + :type instance: Model + """ + + if instance: + return instance._values[self.column.column_name].getval() + else: + return self.column + + def __set__(self, instance, value): + """ + Sets the value on an instance, raises an exception with classes + TODO: use None instance to create update statements + """ + if instance: + return instance._values[self.column.column_name].setval(value) + else: + raise AttributeError('cannot reassign column values') + + def __delete__(self, instance): + """ + Sets the column value to None, if possible + """ + if instance: + if self.column.can_delete: + instance._values[self.column.column_name].delval() + else: + raise AttributeError('cannot delete {} columns'.format(self.column.column_name)) + + class BaseModel(object): """ The base model class, don't inherit from this, inherit from Model, defined below @@ -180,7 +231,6 @@ def _inst_batch(self, batch): batch = hybrid_classmethod(_class_batch, _inst_batch) - class ModelMetaClass(type): def __new__(cls, name, bases, attrs): @@ -207,13 +257,7 @@ def _transform_column(col_name, col_obj): primary_keys[col_name] = col_obj col_obj.set_column_name(col_name) #set properties - _get = lambda self: self._values[col_name].getval() - _set = lambda self, val: self._values[col_name].setval(val) - _del = lambda self: self._values[col_name].delval() - if col_obj.can_delete: - attrs[col_name] = property(_get, _set, _del) - else: - attrs[col_name] = property(_get, _set) + attrs[col_name] = ColumnDescriptor(col_obj) column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.Column)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index a37b6baa3c..0739fe467f 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -253,7 +253,7 @@ def test_attempting_query_on_abstract_model_fails(self): 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, property) + assert isinstance(ConcreteModelWithCol.pkey, columns.Column) assert isinstance(ConcreteModelWithCol._columns['pkey'], columns.Column) def test_concrete_class_table_creation_cycle(self): From 36082ee04a20fb250ad9e0217c507d8e1186eb89 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 10:34:36 -0700 Subject: [PATCH 0224/3961] updating changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index f16994fa22..a697b88734 100644 --- a/changelog +++ b/changelog @@ -4,6 +4,7 @@ CHANGELOG * removed default values from all column types * explicit primary key is required (automatic id removed) * added validation on keyname types on .create() +* changed internal implementation of model value get/set 0.3.3 * added abstract base class models From 4d21458264e239501c4aa284584050a8dc79cb58 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 11:22:12 -0700 Subject: [PATCH 0225/3961] 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 4875d0b3c2..cf070cd644 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -29,7 +29,9 @@ def __get__(self, instance, owner): 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 0226/3961] 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 cf070cd644..bfa66b871c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -53,6 +53,25 @@ def __call__(self, *args, **kwargs): :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 88d16aa42a..95b6fbc35b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -530,6 +530,13 @@ def _parse_filter_arg(self, arg): 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 @@ 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: + 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 0227/3961] 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 bfa66b871c..808938e067 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -74,7 +74,15 @@ def __call__(self, *args, **kwargs): """ 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 @@ def __init__(self, column): :return: """ self.column = column + self.query_evaluator = ColumnQueryEvaluator(self.column) def __get__(self, instance, owner): """ @@ -101,7 +110,7 @@ def __get__(self, instance, owner): 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 DoesNotExist(_DoesNotExist): pass 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 155fc17e1d..25cd590604 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -3,7 +3,7 @@ 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 @@ def test_attempting_query_on_abstract_model_fails(self): 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 0228/3961] 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 17954d184a..549eec6869 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -52,6 +52,22 @@ def test_query_filter_parsing(self): 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 0229/3961] 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 95b6fbc35b..efeb40e0ca 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -569,6 +569,8 @@ def query(self, *args): """ 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 549eec6869..7208075ea2 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -73,7 +73,21 @@ 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 """ 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 0230/3961] 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 7208075ea2..3d9cca8abb 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -89,7 +89,7 @@ def test_using_non_query_operators_in_query_args_raises_error(self): 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 @@ def test_where_clause_generation(self): 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 @@ def test_queryset_is_immutable(self): 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 0231/3961] adding additional tests around query method --- cqlengine/tests/query/test_queryset.py | 63 ++++++++++++++++++-------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 3d9cca8abb..230c1dd06f 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -218,12 +218,21 @@ def tearDownClass(cls): 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 @@ def test_iteration(self): 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 @@ def test_iteration(self): 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)]) + # 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.attempt_id, t.expected_result + val = t.test_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_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)): + #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 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 0232/3961] 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 808938e067..afc2678ddd 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -34,6 +34,7 @@ def __call__(self, *args, **kwargs): """ raise NotImplementedError + class QuerySetDescriptor(object): """ returns a fresh queryset for the given model @@ -54,25 +55,6 @@ def __call__(self, *args, **kwargs): """ 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 DoesNotExist(_DoesNotExist): pass 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 @@ def all(cls): 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 efeb40e0ca..47eabf9ade 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -316,8 +316,8 @@ def __unicode__(self): 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 @@ def _parse_filter_arg(self, arg): 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 @@ def filter(self, **kwargs): """ #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 @@ def filter(self, **kwargs): 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 230c1dd06f..0b68617869 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -54,14 +54,14 @@ def test_query_filter_parsing(self): 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 @@ 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) + 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 @@ def test_query_method_where_clause_generation(self): """ 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 @@ 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) + 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 @@ 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) + q = TestModel.objects(TestModel.test_id == 0) assert q.count() == 4 def test_iteration(self): @@ -254,7 +254,7 @@ def test_iteration(self): 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 @@ def test_iteration(self): 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 @@ def test_multiple_iterators_are_isolated(self): """ 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 0233/3961] 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 47eabf9ade..a56a8a33cc 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -570,7 +570,9 @@ def get(self, *args, **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(*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 0b68617869..921da1b559 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -103,7 +103,7 @@ def test_filter_method_where_clause_generation(self): 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 @@ def test_queryset_is_immutable(self): 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 @@ def test_count(self): 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 @@ def test_iteration(self): 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 @@ def test_multiple_iterators_are_isolated(self): """ 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 @@ def test_get_success_case(self): 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 0234/3961] 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 921da1b559..943efb2093 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -361,10 +361,14 @@ def test_order_by_success_case(self): 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 @@ def test_primary_key_or_index_must_be_specified(self): """ 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 @@ def test_primary_key_or_index_must_have_equal_relation_filter(self): """ 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 @@ def test_indexed_field_can_be_queried(self): 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 @@ def test_success_case(self): 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 @@ def test_success_case(self): 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 9f94df4b8eb15a9cd27d411e4105124613be3c88 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Sat, 15 Jun 2013 12:50:12 -0700 Subject: [PATCH 0235/3961] #58 - renamed model configuration attributes to use __double_underscore__ style --- changelog | 1 + cqlengine/management.py | 2 +- cqlengine/models.py | 15 ++++++++------- cqlengine/tests/model/test_class_construction.py | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/changelog b/changelog index f16994fa22..423f2783a7 100644 --- a/changelog +++ b/changelog @@ -4,6 +4,7 @@ CHANGELOG * removed default values from all column types * explicit primary key is required (automatic id removed) * added validation on keyname types on .create() +* changed table_name to __table_name__, read_repair_chance to __read_repair_chance__, keyspace to __keyspace__ 0.3.3 * added abstract base class models diff --git a/cqlengine/management.py b/cqlengine/management.py index 90005d2bc2..b3504b919e 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -83,7 +83,7 @@ def add_column(col): qs += ['({})'.format(', '.join(qtypes))] - with_qs = ['read_repair_chance = {}'.format(model.read_repair_chance)] + with_qs = ['read_repair_chance = {}'.format(model.__read_repair_chance__)] _order = ["%s %s" % (c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] if _order: diff --git a/cqlengine/models.py b/cqlengine/models.py index 7240bf16ca..5e8f6ef308 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -58,11 +58,12 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #table names will be generated automatically from it's model and package name #however, you can also define them manually here - table_name = None + __table_name__ = None #the keyspace for this model - keyspace = None - read_repair_chance = 0.1 + __keyspace__ = None + + __read_repair_chance__ = 0.1 def __init__(self, **values): self._values = {} @@ -96,7 +97,7 @@ def _can_update(self): @classmethod def _get_keyspace(cls): """ Returns the manual keyspace, if set, otherwise the default keyspace """ - return cls.keyspace or DEFAULT_KEYSPACE + return cls.__keyspace__ or DEFAULT_KEYSPACE def __eq__(self, other): return self.as_dict() == other.as_dict() @@ -111,8 +112,8 @@ def column_family_name(cls, include_keyspace=True): otherwise, it creates it from the module and class name """ cf_name = '' - if cls.table_name: - cf_name = cls.table_name.lower() + if cls.__table_name__: + cf_name = cls.__table_name__.lower() else: camelcase = re.compile(r'([a-z])([A-Z])') ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) @@ -279,7 +280,7 @@ def _transform_column(col_name, col_obj): db_map[col.db_field_name] = field_name #short circuit table_name inheritance - attrs['table_name'] = attrs.get('table_name') + attrs['table_name'] = attrs.get('__table_name__') #add management members to the class attrs['_columns'] = column_dict diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 3e249be2e4..a3dda94f7b 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -203,8 +203,8 @@ class Model2(Model1): class TestManualTableNaming(BaseCassEngTestCase): class RenamedTest(cqlengine.Model): - keyspace = 'whatever' - table_name = 'manual_name' + __keyspace__ = 'whatever' + __table_name__ = 'manual_name' id = cqlengine.UUID(primary_key=True) data = cqlengine.Text() From 09883d6b0408424e32b4190da41271e6673d8f85 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 16:46:34 -0700 Subject: [PATCH 0236/3961] 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 a56a8a33cc..8d7a6392de 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -163,12 +163,17 @@ class AbstractColumnDescriptor(object): def _get_column(self): raise NotImplementedError - def __eq__(self, other): - return EqualsOperator(self._get_column(), other) + def in_(self, item): + """ + Returns an in operator - def __contains__(self, item): + 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 __gt__(self, other): return GreaterThanOperator(self._get_column(), other) @@ -195,7 +200,6 @@ def cql(self): def to_database(self, val): return val -C = NamedColumnDescriptor class TableDescriptor(object): """ describes a cql table """ @@ -204,7 +208,6 @@ def __init__(self, keyspace, name): self.keyspace = keyspace self.name = name -T = TableDescriptor class KeyspaceDescriptor(object): """ Describes a cql keyspace """ @@ -219,7 +222,6 @@ def table(self, name): """ 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 943efb2093..117c87c18b 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -573,7 +573,7 @@ def test_kwarg_success_case(self): 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 From d50fdfab8b4fbe6fbdb732d69fe54e91af4a0ecc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 16:55:22 -0700 Subject: [PATCH 0237/3961] renaming queryable column --- cqlengine/models.py | 12 +++++++++--- cqlengine/query.py | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index afc2678ddd..55b03dc20a 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -1,10 +1,9 @@ from collections import OrderedDict import re -import cqlengine from cqlengine import columns from cqlengine.exceptions import ModelException, CQLEngineException, ValidationError -from cqlengine.query import QuerySet, DMLQuery, AbstractColumnDescriptor +from cqlengine.query import QuerySet, DMLQuery, AbstractQueryableColumn from cqlengine.query import DoesNotExist as _DoesNotExist from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned @@ -56,7 +55,14 @@ def __call__(self, *args, **kwargs): raise NotImplementedError -class ColumnQueryEvaluator(AbstractColumnDescriptor): +class ColumnQueryEvaluator(AbstractQueryableColumn): + """ + Wraps a column and allows it to be used in comparator + expressions, returning query operators + + ie: + Model.column == 5 + """ def __init__(self, column): self.column = column diff --git a/cqlengine/query.py b/cqlengine/query.py index 8d7a6392de..af3a01ddfe 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -154,7 +154,7 @@ class LessThanOrEqualOperator(QueryOperator): symbol = "LTE" cql_symbol = '<=' -class AbstractColumnDescriptor(object): +class AbstractQueryableColumn(object): """ exposes cql query operators through pythons builtin comparator symbols @@ -187,7 +187,7 @@ def __le__(self, other): return LessThanOrEqualOperator(self._get_column(), other) -class NamedColumnDescriptor(AbstractColumnDescriptor): +class NamedColumnDescriptor(AbstractQueryableColumn): """ describes a named cql column """ def __init__(self, name): From 005129e6e1fd581962ccb1b9a7892613ed1e7eb7 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 17:37:04 -0700 Subject: [PATCH 0238/3961] moving named classes into their own file, and renaming them --- cqlengine/named.py | 41 +++++++++++++++++++++++++++++++++++++++++ cqlengine/query.py | 36 ------------------------------------ 2 files changed, 41 insertions(+), 36 deletions(-) create mode 100644 cqlengine/named.py diff --git a/cqlengine/named.py b/cqlengine/named.py new file mode 100644 index 0000000000..f56032486b --- /dev/null +++ b/cqlengine/named.py @@ -0,0 +1,41 @@ +from cqlengine.query import AbstractQueryableColumn + + +class NamedColumn(AbstractQueryableColumn): + """ describes a named cql column """ + + def __init__(self, name): + self.name = name + + @property + def cql(self): + return self.name + + def to_database(self, val): + return val + + +class NamedTable(object): + """ describes a cql table """ + + def __init__(self, keyspace, name): + self.keyspace = keyspace + self.name = name + + def column(self, name): + return NamedColumn(name) + + +class NamedKeyspace(object): + """ Describes a cql keyspace """ + + def __init__(self, name): + self.name = name + + def table(self, name): + """ + returns a table descriptor with the given + name that belongs to this keyspace + """ + return NamedTable(self.name, name) + diff --git a/cqlengine/query.py b/cqlengine/query.py index af3a01ddfe..a626a817ed 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -187,42 +187,6 @@ def __le__(self, other): return LessThanOrEqualOperator(self._get_column(), other) -class NamedColumnDescriptor(AbstractQueryableColumn): - """ describes a named cql column """ - - def __init__(self, name): - self.name = name - - @property - def cql(self): - return self.name - - def to_database(self, val): - return val - - -class TableDescriptor(object): - """ describes a cql table """ - - def __init__(self, keyspace, name): - self.keyspace = keyspace - self.name = name - - -class KeyspaceDescriptor(object): - """ Describes a cql keyspace """ - - def __init__(self, name): - self.name = name - - def table(self, name): - """ - returns a table descriptor with the given - name that belongs to this keyspace - """ - return TableDescriptor(self.name, name) - - class BatchType(object): Unlogged = 'UNLOGGED' Counter = 'COUNTER' From 13075bc8a56fbbed36e416c3fda850a373be3357 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 17:40:18 -0700 Subject: [PATCH 0239/3961] adding tests around name object query creation --- cqlengine/tests/query/test_named.py | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 cqlengine/tests/query/test_named.py diff --git a/cqlengine/tests/query/test_named.py b/cqlengine/tests/query/test_named.py new file mode 100644 index 0000000000..a7379b0aaa --- /dev/null +++ b/cqlengine/tests/query/test_named.py @@ -0,0 +1,47 @@ +from cqlengine import query +from cqlengine.named import NamedKeyspace +from cqlengine.tests.query.test_queryset import TestModel +from cqlengine.tests.base import BaseCassEngTestCase + + +class TestQuerySetOperation(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestQuerySetOperation, cls).setUpClass() + cls.keyspace = NamedKeyspace('cqlengine_test') + cls.table = cls.keyspace.table('test_model') + + def test_query_filter_parsing(self): + """ + Tests the queryset filter method parses it's kwargs properly + """ + query1 = self.table.objects(test_id=5) + assert len(query1._where) == 1 + + op = query1._where[0] + assert isinstance(op, query.EqualsOperator) + assert op.value == 5 + + query2 = query1.filter(expected_result__gte=1) + assert len(query2._where) == 2 + + op = query2._where[1] + assert isinstance(op, query.GreaterThanOrEqualOperator) + assert op.value == 1 + + def test_query_expression_parsing(self): + """ Tests that query experessions are evaluated properly """ + query1 = self.table.filter(self.table.column('test_id') == 5) + assert len(query1._where) == 1 + + op = query1._where[0] + assert isinstance(op, query.EqualsOperator) + assert op.value == 5 + + query2 = query1.filter(self.table.column('expected_result') >= 1) + assert len(query2._where) == 2 + + op = query2._where[1] + assert isinstance(op, query.GreaterThanOrEqualOperator) + assert op.value == 1 From 50506f9a970966ea5544396f08385ffa0a2d0a0f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 17:42:40 -0700 Subject: [PATCH 0240/3961] adding methods to mimic regular models --- cqlengine/named.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cqlengine/named.py b/cqlengine/named.py index f56032486b..f2ab1d04d1 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -1,3 +1,4 @@ +from cqlengine.models import QuerySetDescriptor from cqlengine.query import AbstractQueryableColumn @@ -7,6 +8,9 @@ class NamedColumn(AbstractQueryableColumn): def __init__(self, name): self.name = name + def _get_column(self): + return self + @property def cql(self): return self.name @@ -25,6 +29,25 @@ def __init__(self, keyspace, name): def column(self, name): return NamedColumn(name) + __abstract__ = False + objects = QuerySetDescriptor() + + @classmethod + def create(cls, **kwargs): + return cls.objects.create(**kwargs) + + @classmethod + def all(cls): + return cls.objects.all() + + @classmethod + def filter(cls, *args, **kwargs): + return cls.objects.filter(*args, **kwargs) + + @classmethod + def get(cls, *args, **kwargs): + return cls.objects.get(*args, **kwargs) + class NamedKeyspace(object): """ Describes a cql keyspace """ From 58e38bca76169291aded8b73042c952929425683 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 17:51:14 -0700 Subject: [PATCH 0241/3961] adding fake _column --- cqlengine/named.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cqlengine/named.py b/cqlengine/named.py index f2ab1d04d1..abc19ac997 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -1,3 +1,5 @@ +from collections import defaultdict, namedtuple + from cqlengine.models import QuerySetDescriptor from cqlengine.query import AbstractQueryableColumn @@ -22,16 +24,25 @@ def to_database(self, val): class NamedTable(object): """ describes a cql table """ + __abstract__ = False + + class ColumnContainer(dict): + def __missing__(self, name): + column = NamedColumn(name) + self[name] = column + return column + _columns = ColumnContainer() + + objects = QuerySetDescriptor() + def __init__(self, keyspace, name): self.keyspace = keyspace self.name = name - def column(self, name): + @classmethod + def column(cls, name): return NamedColumn(name) - __abstract__ = False - objects = QuerySetDescriptor() - @classmethod def create(cls, **kwargs): return cls.objects.create(**kwargs) From df2a769c6aca4410c4efcf3e4082468f8997d0a5 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 15 Jun 2013 17:51:32 -0700 Subject: [PATCH 0242/3961] commenting out named table create, for now --- cqlengine/named.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/named.py b/cqlengine/named.py index abc19ac997..0072c5318a 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -43,9 +43,9 @@ def __init__(self, keyspace, name): def column(cls, name): return NamedColumn(name) - @classmethod - def create(cls, **kwargs): - return cls.objects.create(**kwargs) + # @classmethod + # def create(cls, **kwargs): + # return cls.objects.create(**kwargs) @classmethod def all(cls): From e6644239bb61981f7728cf37495967ed6a26ce73 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Sat, 15 Jun 2013 22:56:57 -0700 Subject: [PATCH 0243/3961] updated docs --- docs/topics/queryset.rst | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 48e26a7cba..c237ed6b59 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -29,9 +29,9 @@ Retrieving objects with filters That can be accomplished with the QuerySet's ``.filter(\*\*)`` method. For example, given the model definition: - + .. code-block:: python - + class Automobile(Model): manufacturer = columns.Text(primary_key=True) year = columns.Integer(primary_key=True) @@ -40,11 +40,17 @@ Retrieving objects with filters ...and assuming the Automobile table contains a record of every car model manufactured in the last 20 years or so, we can retrieve only the cars made by a single manufacturer like this: - + .. code-block:: python q = Automobile.objects.filter(manufacturer='Tesla') + You can also use the more convenient syntax: + + .. code-block:: python + + q = Automobile.objects(Automobile.manufacturer == 'Tesla') + We can then further filter our query with another call to **.filter** .. code-block:: python @@ -123,6 +129,7 @@ Filtering Operators q = Automobile.objects.filter(manufacturer='Tesla') q = q.filter(year__in=[2011, 2012]) + :attr:`> (__gt) ` .. code-block:: python @@ -130,6 +137,10 @@ Filtering Operators q = Automobile.objects.filter(manufacturer='Tesla') q = q.filter(year__gt=2010) # year > 2010 + # or the nicer syntax + + q.filter(Automobile.year > 2010) + :attr:`>= (__gte) ` .. code-block:: python @@ -137,6 +148,10 @@ Filtering Operators q = Automobile.objects.filter(manufacturer='Tesla') q = q.filter(year__gte=2010) # year >= 2010 + # or the nicer syntax + + Automobile.objects.filter(Automobile.manufacturer == 'Tesla') + :attr:`< (__lt) ` .. code-block:: python @@ -144,6 +159,10 @@ Filtering Operators q = Automobile.objects.filter(manufacturer='Tesla') q = q.filter(year__lt=2012) # year < 2012 + # or... + + q.filter(Automobile.year < 2012) + :attr:`<= (__lte) ` .. code-block:: python @@ -151,6 +170,8 @@ Filtering Operators q = Automobile.objects.filter(manufacturer='Tesla') q = q.filter(year__lte=2012) # year <= 2012 + q.filter(Automobile.year <= 2012) + TimeUUID Functions ================== @@ -220,7 +241,7 @@ Ordering QuerySets Since Cassandra is essentially a distributed hash table on steroids, the order you get records back in will not be particularly predictable. - However, you can set a column to order on with the ``.order_by(column_name)`` method. + However, you can set a column to order on with the ``.order_by(column_name)`` method. *Example* @@ -245,12 +266,12 @@ Values Lists Batch Queries =============== - cqlengine now supports batch queries using the BatchQuery class. Batch queries can be started and stopped manually, or within a context manager. To add queries to the batch object, you just need to precede the create/save/delete call with a call to batch, and pass in the batch object. - + cqlengine now supports batch queries using the BatchQuery class. Batch queries can be started and stopped manually, or within a context manager. To add queries to the batch object, you just need to precede the create/save/delete call with a call to batch, and pass in the batch object. + You can only create, update, and delete rows with a batch query, attempting to read rows out of the database with a batch query will fail. .. code-block:: python - + from cqlengine import BatchQuery #using a context manager @@ -298,7 +319,7 @@ QuerySet method reference .. method:: limit(num) Limits the number of results returned by Cassandra. - + *Note that CQL's default limit is 10,000, so all queries without a limit set explicitly will have an implicit limit of 10,000* .. method:: order_by(field_name) @@ -306,7 +327,7 @@ QuerySet method reference :param field_name: the name of the field to order on. *Note: the field_name must be a clustering key* :type field_name: string - Sets the field to order on. + Sets the field to order on. .. method:: allow_filtering() From fedb622fe426080d30aa58906aeae7ea9fad7cdb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:18:53 -0700 Subject: [PATCH 0244/3961] adding tests around where clause generation --- cqlengine/tests/query/test_named.py | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cqlengine/tests/query/test_named.py b/cqlengine/tests/query/test_named.py index a7379b0aaa..561083d4db 100644 --- a/cqlengine/tests/query/test_named.py +++ b/cqlengine/tests/query/test_named.py @@ -45,3 +45,34 @@ def test_query_expression_parsing(self): op = query2._where[1] assert isinstance(op, query.GreaterThanOrEqualOperator) assert op.value == 1 + + def test_filter_method_where_clause_generation(self): + """ + Tests the where clause creation + """ + query1 = self.table.objects(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.filter(expected_result__gte=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_query_expression_where_clause_generation(self): + """ + Tests the where clause creation + """ + query1 = self.table.objects(self.table.column('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.filter(self.table.column('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) + + + From e87d806e5b7ce022ecb64e2dd1d242e6aa194b06 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:21:04 -0700 Subject: [PATCH 0245/3961] renaming queryset --- cqlengine/models.py | 8 ++++---- cqlengine/query.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 55b03dc20a..c3ae852050 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -3,7 +3,7 @@ from cqlengine import columns from cqlengine.exceptions import ModelException, CQLEngineException, ValidationError -from cqlengine.query import QuerySet, DMLQuery, AbstractQueryableColumn +from cqlengine.query import ModelQuerySet, DMLQuery, AbstractQueryableColumn from cqlengine.query import DoesNotExist as _DoesNotExist from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned @@ -41,16 +41,16 @@ class QuerySetDescriptor(object): """ def __get__(self, obj, model): - """ :rtype: QuerySet """ + """ :rtype: ModelQuerySet """ if model.__abstract__: raise CQLEngineException('cannot execute queries against abstract models') - return QuerySet(model) + return ModelQuerySet(model) def __call__(self, *args, **kwargs): """ Just a hint to IDEs that it's ok to call this - :rtype: QuerySet + :rtype: ModelQuerySet """ raise NotImplementedError diff --git a/cqlengine/query.py b/cqlengine/query.py index a626a817ed..1a9fa7817a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -239,10 +239,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: return self.execute() -class QuerySet(object): +class ModelQuerySet(object): def __init__(self, model): - super(QuerySet, self).__init__() + super(ModelQuerySet, self).__init__() self.model = model #Where clause filters @@ -501,7 +501,7 @@ def filter(self, *args, **kwargs): #TODO: show examples - :rtype: QuerySet + :rtype: ModelQuerySet """ #add arguments to the where clause filters clone = copy.deepcopy(self) From 86202721ada6d1e5e8c153a9b8b038e4898363c6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:24:19 -0700 Subject: [PATCH 0246/3961] creating simple query set --- cqlengine/query.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 1a9fa7817a..cc735f6d88 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -239,10 +239,11 @@ def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: return self.execute() -class ModelQuerySet(object): + +class SimpleQuerySet(object): def __init__(self, model): - super(ModelQuerySet, self).__init__() + super(SimpleQuerySet, self).__init__() self.model = model #Where clause filters @@ -501,7 +502,7 @@ def filter(self, *args, **kwargs): #TODO: show examples - :rtype: ModelQuerySet + :rtype: SimpleQuerySet """ #add arguments to the where clause filters clone = copy.deepcopy(self) @@ -700,6 +701,13 @@ def __eq__(self, q): def __ne__(self, q): return not (self != q) + +class ModelQuerySet(SimpleQuerySet): + """ + + """ + + class DMLQuery(object): """ A query object used for queries performing inserts, updates, or deletes From 8aaae66254c82cadfc3f2042be8ab6171b88b43f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:27:02 -0700 Subject: [PATCH 0247/3961] moving where clause validation off of SimpleQuerySet --- cqlengine/query.py | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index cc735f6d88..9aef5a48a6 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -314,29 +314,8 @@ def __del__(self): #----query generation / execution---- - def _validate_where_syntax(self): - """ Checks that a filterset will not create invalid cql """ - - #check that there's either a = or IN relationship with a primary key or indexed field - equal_ops = [w for w in self._where if isinstance(w, EqualsOperator)] - token_ops = [w for w in self._where if isinstance(w.value, Token)] - if not any([w.column.primary_key or w.column.index for w in equal_ops]) and not token_ops: - raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') - - if not self._allow_filtering: - #if the query is not on an indexed field - if not any([w.column.index for w in equal_ops]): - if not any([w.column.partition_key for w in equal_ops]) and not token_ops: - raise QueryException('Filtering on a clustering key without a partition key is not allowed unless allow_filtering() is called on the querset') - if any(not w.column.partition_key for w in token_ops): - raise QueryException('The token() function is only supported on the partition key') - - - #TODO: abuse this to see if we can get cql to raise an exception - def _where_clause(self): """ Returns a where clause based on the given filter args """ - self._validate_where_syntax() return ' AND '.join([f.cql for f in self._where]) def _where_values(self): @@ -706,6 +685,30 @@ class ModelQuerySet(SimpleQuerySet): """ """ + def _validate_where_syntax(self): + """ Checks that a filterset will not create invalid cql """ + + #check that there's either a = or IN relationship with a primary key or indexed field + equal_ops = [w for w in self._where if isinstance(w, EqualsOperator)] + token_ops = [w for w in self._where if isinstance(w.value, Token)] + if not any([w.column.primary_key or w.column.index for w in equal_ops]) and not token_ops: + raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') + + if not self._allow_filtering: + #if the query is not on an indexed field + if not any([w.column.index for w in equal_ops]): + if not any([w.column.partition_key for w in equal_ops]) and not token_ops: + raise QueryException('Filtering on a clustering key without a partition key is not allowed unless allow_filtering() is called on the querset') + if any(not w.column.partition_key for w in token_ops): + raise QueryException('The token() function is only supported on the partition key') + + + #TODO: abuse this to see if we can get cql to raise an exception + + def _where_clause(self): + """ Returns a where clause based on the given filter args """ + self._validate_where_syntax() + return super(ModelQuerySet, self)._where_clause() class DMLQuery(object): From 83c11cb735dda575617e3875bffda207fab0e963 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:32:27 -0700 Subject: [PATCH 0248/3961] encapsulating queryset column retrieval --- cqlengine/models.py | 11 +++++++++++ cqlengine/query.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index c3ae852050..30a9261745 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -173,6 +173,17 @@ def _get_keyspace(cls): """ Returns the manual keyspace, if set, otherwise the default keyspace """ return cls.keyspace or DEFAULT_KEYSPACE + @classmethod + def _get_column(cls, name): + """ + Returns the column matching the given name, raising a key error if + it doesn't exist + + :param name: the name of the column to return + :rtype: Column + """ + return cls._columns[name] + def __eq__(self, other): return self.as_dict() == other.as_dict() diff --git a/cqlengine/query.py b/cqlengine/query.py index 9aef5a48a6..a846c8c0c6 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -494,7 +494,7 @@ def filter(self, *args, **kwargs): col_name, col_op = self._parse_filter_arg(arg) #resolve column and operator try: - column = self.model._columns[col_name] + column = self.model._get_column(col_name) except KeyError: if col_name == 'pk__token': column = columns._PartitionKeysToken(self.model) From 9c56f3181a10f79d7c8449dd58b949f67e1d98b0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:35:48 -0700 Subject: [PATCH 0249/3961] switching named columns from model queryset to simple queryset --- cqlengine/named.py | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/cqlengine/named.py b/cqlengine/named.py index 0072c5318a..fc0eb0e01f 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -1,7 +1,26 @@ -from collections import defaultdict, namedtuple +from cqlengine.exceptions import CQLEngineException +from cqlengine.query import AbstractQueryableColumn, SimpleQuerySet -from cqlengine.models import QuerySetDescriptor -from cqlengine.query import AbstractQueryableColumn + +class QuerySetDescriptor(object): + """ + returns a fresh queryset for the given model + it's declared on everytime it's accessed + """ + + def __get__(self, obj, model): + """ :rtype: ModelQuerySet """ + if model.__abstract__: + raise CQLEngineException('cannot execute queries against abstract models') + return SimpleQuerySet(model) + + def __call__(self, *args, **kwargs): + """ + Just a hint to IDEs that it's ok to call this + + :rtype: ModelQuerySet + """ + raise NotImplementedError class NamedColumn(AbstractQueryableColumn): @@ -26,13 +45,6 @@ class NamedTable(object): __abstract__ = False - class ColumnContainer(dict): - def __missing__(self, name): - column = NamedColumn(name) - self[name] = column - return column - _columns = ColumnContainer() - objects = QuerySetDescriptor() def __init__(self, keyspace, name): @@ -43,6 +55,15 @@ def __init__(self, keyspace, name): def column(cls, name): return NamedColumn(name) + @classmethod + def _get_column(cls, name): + """ + Returns the column matching the given name + + :rtype: Column + """ + return cls.column(name) + # @classmethod # def create(cls, **kwargs): # return cls.objects.create(**kwargs) From 23ec346b82a873f9919e6fb7f4602d1e77072960 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:37:03 -0700 Subject: [PATCH 0250/3961] updating the named object doc strings --- cqlengine/named.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cqlengine/named.py b/cqlengine/named.py index fc0eb0e01f..5e092156bf 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -24,7 +24,9 @@ def __call__(self, *args, **kwargs): class NamedColumn(AbstractQueryableColumn): - """ describes a named cql column """ + """ + A column that is not coupled to a model class, or type + """ def __init__(self, name): self.name = name @@ -41,7 +43,9 @@ def to_database(self, val): class NamedTable(object): - """ describes a cql table """ + """ + A Table that is not coupled to a model class + """ __abstract__ = False @@ -82,7 +86,9 @@ def get(cls, *args, **kwargs): class NamedKeyspace(object): - """ Describes a cql keyspace """ + """ + A keyspace + """ def __init__(self, name): self.name = name From 6d42ab38ed821bdf317fd42d15f4f4d1a5d20f1d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 08:40:36 -0700 Subject: [PATCH 0251/3961] expanding cql methods on named column --- cqlengine/named.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cqlengine/named.py b/cqlengine/named.py index 5e092156bf..94a3c05e90 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -36,7 +36,10 @@ def _get_column(self): @property def cql(self): - return self.name + return self.get_cql() + + def get_cql(self): + return '"{}"'.format(self.name) def to_database(self, val): return val From e9d951b671d6b7035bd68bec4f1e999cde943ba9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:14:37 -0700 Subject: [PATCH 0252/3961] making named table operations instance specific --- cqlengine/named.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cqlengine/named.py b/cqlengine/named.py index 94a3c05e90..9b62efcb39 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -12,7 +12,7 @@ def __get__(self, obj, model): """ :rtype: ModelQuerySet """ if model.__abstract__: raise CQLEngineException('cannot execute queries against abstract models') - return SimpleQuerySet(model) + return SimpleQuerySet(obj) def __call__(self, *args, **kwargs): """ @@ -58,11 +58,19 @@ def __init__(self, keyspace, name): self.keyspace = keyspace self.name = name - @classmethod def column(cls, name): return NamedColumn(name) - @classmethod + def column_family_name(self, include_keyspace=True): + """ + Returns the column family name if it's been defined + otherwise, it creates it from the module and class name + """ + if include_keyspace: + return '{}.{}'.format(self.keyspace, self.name) + else: + return self.name + def _get_column(cls, name): """ Returns the column matching the given name @@ -75,15 +83,12 @@ def _get_column(cls, name): # def create(cls, **kwargs): # return cls.objects.create(**kwargs) - @classmethod def all(cls): return cls.objects.all() - @classmethod def filter(cls, *args, **kwargs): return cls.objects.filter(*args, **kwargs) - @classmethod def get(cls, *args, **kwargs): return cls.objects.get(*args, **kwargs) From cc3b08bdd6e6a7d45bd8b4f895212dd2f1284345 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:17:13 -0700 Subject: [PATCH 0253/3961] renaming cls to self --- cqlengine/named.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/cqlengine/named.py b/cqlengine/named.py index 9b62efcb39..f2301bbb03 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -58,7 +58,7 @@ def __init__(self, keyspace, name): self.keyspace = keyspace self.name = name - def column(cls, name): + def column(self, name): return NamedColumn(name) def column_family_name(self, include_keyspace=True): @@ -71,26 +71,25 @@ def column_family_name(self, include_keyspace=True): else: return self.name - def _get_column(cls, name): + def _get_column(self, name): """ Returns the column matching the given name :rtype: Column """ - return cls.column(name) + return self.column(name) - # @classmethod - # def create(cls, **kwargs): - # return cls.objects.create(**kwargs) + # def create(self, **kwargs): + # return self.objects.create(**kwargs) - def all(cls): - return cls.objects.all() + def all(self): + return self.objects.all() - def filter(cls, *args, **kwargs): - return cls.objects.filter(*args, **kwargs) + def filter(self, *args, **kwargs): + return self.objects.filter(*args, **kwargs) - def get(cls, *args, **kwargs): - return cls.objects.get(*args, **kwargs) + def get(self, *args, **kwargs): + return self.objects.get(*args, **kwargs) class NamedKeyspace(object): From def580706533dd3814d4d108a12cbc3b95b53b22 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:22:59 -0700 Subject: [PATCH 0254/3961] =?UTF-8?q?adding=20tests=20around=20named=20tab?= =?UTF-8?q?le=20queries=E2=80=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cqlengine/tests/query/test_named.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/query/test_named.py b/cqlengine/tests/query/test_named.py index 561083d4db..6f1327f094 100644 --- a/cqlengine/tests/query/test_named.py +++ b/cqlengine/tests/query/test_named.py @@ -1,6 +1,6 @@ from cqlengine import query from cqlengine.named import NamedKeyspace -from cqlengine.tests.query.test_queryset import TestModel +from cqlengine.tests.query.test_queryset import TestModel, BaseQuerySetUsage from cqlengine.tests.base import BaseCassEngTestCase @@ -75,4 +75,28 @@ def test_query_expression_where_clause_generation(self): assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) +class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): + + @classmethod + def setUpClass(cls): + super(TestQuerySetCountSelectionAndIteration, cls).setUpClass() + ks,tn = TestModel.column_family_name().split('.') + cls.keyspace = NamedKeyspace(ks) + cls.table = cls.keyspace.table(tn) + + + def test_count(self): + """ Tests that adding filtering statements affects the count query as expected """ + assert self.table.objects.count() == 12 + + q = self.table.objects(test_id=0) + assert q.count() == 4 + + def test_query_expression_count(self): + """ Tests that adding query statements affects the count query as expected """ + assert self.table.objects.count() == 12 + + q = self.table.objects(self.table.column('test_id') == 0) + assert q.count() == 4 + From cc40938b592a8d95922e3e321adb07a3a9f92723 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:27:41 -0700 Subject: [PATCH 0255/3961] splitting the simple queryset into an abstract and simple queryset --- cqlengine/query.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a846c8c0c6..b54fcbf30a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -240,10 +240,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.execute() -class SimpleQuerySet(object): +class AbstractQuerySet(object): def __init__(self, model): - super(SimpleQuerySet, self).__init__() + super(AbstractQuerySet, self).__init__() self.model = model #Where clause filters @@ -325,16 +325,15 @@ def _where_values(self): values.update(where.get_dict()) return values + def _get_select_fields(self): + """ Returns the fields to be returned by the select query """ + raise NotImplementedError + def _select_query(self): """ Returns a select clause based on the given filter args """ - fields = self.model._columns.keys() - if self._defer_fields: - fields = [f for f in fields if f not in self._defer_fields] - elif self._only_fields: - fields = self._only_fields - db_fields = [self.model._columns[f].db_field_name for f in fields] + db_fields = self._get_select_fields() qs = ['SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields]))] qs += ['FROM {}'.format(self.column_family_name)] @@ -481,7 +480,7 @@ def filter(self, *args, **kwargs): #TODO: show examples - :rtype: SimpleQuerySet + :rtype: AbstractQuerySet """ #add arguments to the where clause filters clone = copy.deepcopy(self) @@ -680,8 +679,17 @@ def __eq__(self, q): def __ne__(self, q): return not (self != q) +class SimpleQuerySet(AbstractQuerySet): + """ + + """ + + def _get_select_fields(self): + """ Returns the fields to be returned by the select query """ + return ['*'] + -class ModelQuerySet(SimpleQuerySet): +class ModelQuerySet(AbstractQuerySet): """ """ @@ -710,6 +718,15 @@ def _where_clause(self): self._validate_where_syntax() return super(ModelQuerySet, self)._where_clause() + def _get_select_fields(self): + """ Returns the fields to be returned by the select query """ + fields = self.model._columns.keys() + if self._defer_fields: + fields = [f for f in fields if f not in self._defer_fields] + elif self._only_fields: + fields = self._only_fields + return [self.model._columns[f].db_field_name for f in fields] + class DMLQuery(object): """ From df964ec3a5543d670f775b96159e520d5505e572 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:37:42 -0700 Subject: [PATCH 0256/3961] moving the select statement into it's own method --- cqlengine/query.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index b54fcbf30a..5a87d94965 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -325,17 +325,15 @@ def _where_values(self): values.update(where.get_dict()) return values - def _get_select_fields(self): - """ Returns the fields to be returned by the select query """ + def _get_select_statement(self): + """ returns the select portion of this queryset's cql statement """ raise NotImplementedError def _select_query(self): """ Returns a select clause based on the given filter args """ - db_fields = self._get_select_fields() - - qs = ['SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields]))] + qs = [self._get_select_statement()] qs += ['FROM {}'.format(self.column_family_name)] if self._where: @@ -684,9 +682,9 @@ class SimpleQuerySet(AbstractQuerySet): """ - def _get_select_fields(self): + def _get_select_statement(self): """ Returns the fields to be returned by the select query """ - return ['*'] + return 'SELECT *' class ModelQuerySet(AbstractQuerySet): @@ -718,14 +716,15 @@ def _where_clause(self): self._validate_where_syntax() return super(ModelQuerySet, self)._where_clause() - def _get_select_fields(self): + def _get_select_statement(self): """ Returns the fields to be returned by the select query """ fields = self.model._columns.keys() if self._defer_fields: fields = [f for f in fields if f not in self._defer_fields] elif self._only_fields: fields = self._only_fields - return [self.model._columns[f].db_field_name for f in fields] + db_fields = [self.model._columns[f].db_field_name for f in fields] + return 'SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields])) class DMLQuery(object): From fbe3294554a1c1e2b498de4298829fd6d6d33cfa Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:48:07 -0700 Subject: [PATCH 0257/3961] creating different result constructor factory methods for each queryset type --- cqlengine/query.py | 71 +++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 5a87d94965..06d2f2bfe4 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -420,22 +420,7 @@ def _create_result_constructor(self, names): """ Returns a function that will be used to instantiate query results """ - model = self.model - db_map = model._db_map - if not self._values_list: - def _construct_instance(values): - field_dict = dict((db_map.get(k, k), v) for k, v in zip(names, values)) - instance = model(**field_dict) - instance._is_persisted = True - return instance - return _construct_instance - - columns = [model._columns[db_map[name]] for name in names] - if self._flat_values_list: - return (lambda values: columns[0].to_python(values[0])) - else: - # result_cls = namedtuple("{}Tuple".format(self.model.__name__), names) - return (lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values))) + raise NotImplementedError def batch(self, batch_obj): """ @@ -658,19 +643,6 @@ def delete(self, columns=[]): else: execute(qs, self._where_values()) - def values_list(self, *fields, **kwargs): - """ Instructs the query set to return tuples, not model instance """ - flat = kwargs.pop('flat', False) - if kwargs: - raise TypeError('Unexpected keyword arguments to values_list: %s' - % (kwargs.keys(),)) - if flat and len(fields) > 1: - raise TypeError("'flat' is not valid when values_list is called with more than one field.") - clone = self.only(fields) - clone._values_list = True - clone._flat_values_list = flat - return clone - def __eq__(self, q): return set(self._where) == set(q._where) @@ -686,6 +658,13 @@ def _get_select_statement(self): """ Returns the fields to be returned by the select query """ return 'SELECT *' + def _create_result_constructor(self, names): + """ + Returns a function that will be used to instantiate query results + """ + def _construct_instance(values): + return dict(zip(names, values)) + return _construct_instance class ModelQuerySet(AbstractQuerySet): """ @@ -726,6 +705,40 @@ def _get_select_statement(self): db_fields = [self.model._columns[f].db_field_name for f in fields] return 'SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields])) + def _create_result_constructor(self, names): + """ + Returns a function that will be used to instantiate query results + """ + model = self.model + db_map = model._db_map + if not self._values_list: + def _construct_instance(values): + field_dict = dict((db_map.get(k, k), v) for k, v in zip(names, values)) + instance = model(**field_dict) + instance._is_persisted = True + return instance + return _construct_instance + + columns = [model._columns[db_map[name]] for name in names] + if self._flat_values_list: + return (lambda values: columns[0].to_python(values[0])) + else: + # result_cls = namedtuple("{}Tuple".format(self.model.__name__), names) + return (lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values))) + + def values_list(self, *fields, **kwargs): + """ Instructs the query set to return tuples, not model instance """ + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' + % (kwargs.keys(),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is called with more than one field.") + clone = self.only(fields) + clone._values_list = True + clone._flat_values_list = flat + return clone + class DMLQuery(object): """ From d741790ec78b90dd66a19c328670aec84b60c01c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:52:37 -0700 Subject: [PATCH 0258/3961] changing named query results to Result Object instance --- cqlengine/query.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 06d2f2bfe4..d3b9d01023 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -649,6 +649,17 @@ def __eq__(self, q): def __ne__(self, q): return not (self != q) +class ResultObject(dict): + """ + adds attribute access to a dictionary + """ + + def __getattr__(self, item): + try: + return self[item] + except KeyError: + raise AttributeError + class SimpleQuerySet(AbstractQuerySet): """ @@ -663,7 +674,7 @@ def _create_result_constructor(self, names): Returns a function that will be used to instantiate query results """ def _construct_instance(values): - return dict(zip(names, values)) + return ResultObject(zip(names, values)) return _construct_instance class ModelQuerySet(AbstractQuerySet): From 5c1e203f099470d8d66ec44be8446b04d8461169 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 09:58:56 -0700 Subject: [PATCH 0259/3961] adding does not exist and multiple objects returns exceptions to named table --- cqlengine/named.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cqlengine/named.py b/cqlengine/named.py index f2301bbb03..c1d1263a82 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -1,6 +1,8 @@ from cqlengine.exceptions import CQLEngineException from cqlengine.query import AbstractQueryableColumn, SimpleQuerySet +from cqlengine.query import DoesNotExist as _DoesNotExist +from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned class QuerySetDescriptor(object): """ @@ -54,6 +56,9 @@ class NamedTable(object): objects = QuerySetDescriptor() + class DoesNotExist(_DoesNotExist): pass + class MultipleObjectsReturned(_MultipleObjectsReturned): pass + def __init__(self, keyspace, name): self.keyspace = keyspace self.name = name From 6ccc32084c208d612eca7f577962b79f5cf7714e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 10:07:57 -0700 Subject: [PATCH 0260/3961] moving order conditions into their own method and adding extra validation to the model queryset order condition method --- cqlengine/query.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index d3b9d01023..b01e1a9ea3 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -510,6 +510,12 @@ def get(self, *args, **kwargs): else: return self[0] + def _get_ordering_condition(self, colname): + order_type = 'DESC' if colname.startswith('-') else 'ASC' + colname = colname.replace('-', '') + + return colname, order_type + def order_by(self, *colnames): """ orders the result set. @@ -524,24 +530,7 @@ def order_by(self, *colnames): conditions = [] for colname in colnames: - order_type = 'DESC' if colname.startswith('-') else 'ASC' - colname = colname.replace('-', '') - - column = self.model._columns.get(colname) - if column is None: - raise QueryException("Can't resolve the column name: '{}'".format(colname)) - - #validate the column selection - if not column.primary_key: - raise QueryException( - "Can't order on '{}', can only order on (clustered) primary keys".format(colname)) - - pks = [v for k, v in self.model._columns.items() if v.primary_key] - if column == pks[0]: - raise QueryException( - "Can't order by the first primary key (partition key), clustering (secondary) keys only") - - conditions.append('"{}" {}'.format(column.db_field_name, order_type)) + conditions.append('"{}" {}'.format(*self._get_ordering_condition(colname))) clone = copy.deepcopy(self) clone._order.extend(conditions) @@ -737,6 +726,25 @@ def _construct_instance(values): # result_cls = namedtuple("{}Tuple".format(self.model.__name__), names) return (lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values))) + def _get_ordering_condition(self, colname): + colname, order_type = super(ModelQuerySet, self)._get_ordering_condition(colname) + + column = self.model._columns.get(colname) + if column is None: + raise QueryException("Can't resolve the column name: '{}'".format(colname)) + + #validate the column selection + if not column.primary_key: + raise QueryException( + "Can't order on '{}', can only order on (clustered) primary keys".format(colname)) + + pks = [v for k, v in self.model._columns.items() if v.primary_key] + if column == pks[0]: + raise QueryException( + "Can't order by the first primary key (partition key), clustering (secondary) keys only") + + return column.db_field_name, order_type + def values_list(self, *fields, **kwargs): """ Instructs the query set to return tuples, not model instance """ flat = kwargs.pop('flat', False) From d7a7d13f54661b9af781219ce2a1e6de65378ab8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 10:08:15 -0700 Subject: [PATCH 0261/3961] adding additional tests around named table queries --- cqlengine/tests/query/test_named.py | 128 +++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/query/test_named.py b/cqlengine/tests/query/test_named.py index 6f1327f094..cba46dcb59 100644 --- a/cqlengine/tests/query/test_named.py +++ b/cqlengine/tests/query/test_named.py @@ -1,6 +1,7 @@ from cqlengine import query from cqlengine.named import NamedKeyspace -from cqlengine.tests.query.test_queryset import TestModel, BaseQuerySetUsage +from cqlengine.query import ResultObject +from cqlengine.tests.query.test_queryset import BaseQuerySetUsage from cqlengine.tests.base import BaseCassEngTestCase @@ -80,6 +81,9 @@ class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): @classmethod def setUpClass(cls): super(TestQuerySetCountSelectionAndIteration, cls).setUpClass() + + from cqlengine.tests.query.test_queryset import TestModel + ks,tn = TestModel.column_family_name().split('.') cls.keyspace = NamedKeyspace(ks) cls.table = cls.keyspace.table(tn) @@ -99,4 +103,126 @@ def test_query_expression_count(self): q = self.table.objects(self.table.column('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 = self.table.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 regular filtering + q = self.table.objects(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 + + # test with query method + q = self.table.objects(self.table.column('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 """ + # test with both the filtering method and the query method + for q in (self.table.objects(test_id=0), self.table.objects(self.table.column('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 + + def test_multiple_iterators_are_isolated(self): + """ + tests that the use of one iterator does not affect the behavior of another + """ + for q in (self.table.objects(test_id=0), self.table.objects(self.table.column('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): + """ + Tests that the .get() method works on new and existing querysets + """ + m = self.table.objects.get(test_id=0, attempt_id=0) + assert isinstance(m, ResultObject) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = self.table.objects(test_id=0, attempt_id=0) + m = q.get() + assert isinstance(m, ResultObject) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = self.table.objects(test_id=0) + m = q.get(attempt_id=0) + assert isinstance(m, ResultObject) + 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 = self.table.get(self.table.column('test_id') == 0, self.table.column('attempt_id') == 0) + assert isinstance(m, ResultObject) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = self.table.objects(self.table.column('test_id') == 0, self.table.column('attempt_id') == 0) + m = q.get() + assert isinstance(m, ResultObject) + assert m.test_id == 0 + assert m.attempt_id == 0 + + q = self.table.objects(self.table.column('test_id') == 0) + m = q.get(self.table.column('attempt_id') == 0) + assert isinstance(m, ResultObject) + 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 + """ + with self.assertRaises(self.table.DoesNotExist): + self.table.objects.get(test_id=100) + + def test_get_multipleobjects_exception(self): + """ + Tests that get calls that return multiple results raise a MultipleObjectsReturned error + """ + with self.assertRaises(self.table.MultipleObjectsReturned): + self.table.objects.get(test_id=1) + From 423e26e6b4d53e48a6b0ad65e945be06d6546dc4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 16 Jun 2013 10:27:42 -0700 Subject: [PATCH 0262/3961] removing module name from auto column family name generation --- cqlengine/models.py | 4 ---- cqlengine/tests/model/test_class_construction.py | 8 ++++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 3dca106428..6b9536e960 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -187,10 +187,6 @@ def column_family_name(cls, include_keyspace=True): camelcase = re.compile(r'([a-z])([A-Z])') ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) - module = cls.__module__.split('.') - if module: - cf_name = ccase(module[-1]) + '_' - cf_name += ccase(cls.__name__) #trim to less than 48 characters or cassandra will complain cf_name = cf_name[-48:] diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 05e77bde9a..f0344c6bd3 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -107,6 +107,14 @@ class InheritedModel(TestModel): assert 'text' in InheritedModel._columns assert 'numbers' in InheritedModel._columns + def test_column_family_name_generation(self): + """ Tests that auto column family name generation works as expected """ + class TestModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + text = columns.Text() + + assert TestModel.column_family_name(include_keyspace=False) == 'test_model' + def test_normal_fields_can_be_defined_between_primary_keys(self): """ Tests tha non primary key fields can be defined between primary key fields From 61de63acc8863c0b2a46df0a18f871fc94fbb4bf Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 17 Jun 2013 11:45:37 -0700 Subject: [PATCH 0263/3961] updating changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index bb830d8efb..9084cdfc55 100644 --- a/changelog +++ b/changelog @@ -5,6 +5,7 @@ CHANGELOG * explicit primary key is required (automatic id removed) * added validation on keyname types on .create() * changed table_name to __table_name__, read_repair_chance to __read_repair_chance__, keyspace to __keyspace__ +* modified table name auto generator to ignore module name * changed internal implementation of model value get/set 0.3.3 From d9491aa5f5acc7c806dfdc7809d0a9aefa14e62d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 17 Jun 2013 11:48:58 -0700 Subject: [PATCH 0264/3961] adding breaking change link to readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0037b9465..10531060e5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ cqlengine =============== -cqlengine is a Cassandra CQL 3 Object Mapper for Python with an interface similar to the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 Object Mapper for Python + +**Users of versions < 0.4, please read this post: [Breaking Changes](https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU)** [Documentation](https://cqlengine.readthedocs.org/en/latest/) From fec88a847e8b52b4a0b46bc5dada002f4c6c4d14 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 17 Jun 2013 16:37:03 -0700 Subject: [PATCH 0265/3961] create uuid1 from datetime #65 --- cqlengine/columns.py | 48 ++++++++++++++++++++++ cqlengine/tests/columns/test_validation.py | 13 +++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 941d2419aa..0838195ddb 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -304,6 +304,9 @@ def to_python(self, value): def to_database(self, value): return self.validate(value) +_last_timestamp = None +from uuid import UUID as pyUUID, getnode + class TimeUUID(UUID): """ UUID containing timestamp @@ -311,6 +314,50 @@ class TimeUUID(UUID): db_type = 'timeuuid' + @classmethod + def from_datetime(self, dt): + """ + generates a UUID for a given datetime + + :param dt: datetime + :type dt: datetime + :return: + """ + global _last_timestamp + + epoch = datetime(1970, 1, 1, tzinfo=dt.tzinfo) + + offset = 0 + if epoch.tzinfo: + offset_delta = epoch.tzinfo.utcoffset(epoch) + offset = offset_delta.days*24*3600 + offset_delta.seconds + + timestamp = (dt - epoch).total_seconds() - offset + + node = None + clock_seq = None + + nanoseconds = int(timestamp * 1e9) + timestamp = int(nanoseconds // 100) + 0x01b21dd213814000L + + if _last_timestamp is not None and timestamp <= _last_timestamp: + timestamp = _last_timestamp + 1 + _last_timestamp = timestamp + if clock_seq is None: + import random + clock_seq = random.randrange(1 << 14L) # instead of stable storage + time_low = timestamp & 0xffffffffL + time_mid = (timestamp >> 32L) & 0xffffL + time_hi_version = (timestamp >> 48L) & 0x0fffL + clock_seq_low = clock_seq & 0xffL + clock_seq_hi_variant = (clock_seq >> 8L) & 0x3fL + if node is None: + node = getnode() + return pyUUID(fields=(time_low, time_mid, time_hi_version, + clock_seq_hi_variant, clock_seq_low, node), version=1) + + + class Boolean(Column): db_type = 'boolean' @@ -325,6 +372,7 @@ def to_python(self, value): def to_database(self, value): return self.Quoter(bool(value)) + class Float(Column): db_type = 'double' diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 627ccbd3e6..a68958bb23 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -3,6 +3,7 @@ from datetime import date from datetime import tzinfo from decimal import Decimal as D +from unittest import TestCase from uuid import uuid4, uuid1 from cqlengine import ValidationError @@ -204,10 +205,18 @@ def test_extra_field(self): self.TestModel.create(bacon=5000) +class TestTimeUUIDFromDatetime(TestCase): + def test_conversion_specific_date(self): + dt = datetime(1981, 7, 11, microsecond=555000) + uuid = TimeUUID.from_datetime(dt) + from uuid import UUID + assert isinstance(uuid, UUID) + ts = (uuid.time - 0x01b21dd213814000) / 1e7 # back to a timestamp + new_dt = datetime.utcfromtimestamp(ts) - - + # checks that we created a UUID1 with the proper timestamp + assert new_dt == dt From fed38658460871eaa1ff86b036c577406f20dc7b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 17 Jun 2013 16:39:26 -0700 Subject: [PATCH 0266/3961] updated changelog --- changelog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog b/changelog index 9084cdfc55..5f52491b32 100644 --- a/changelog +++ b/changelog @@ -7,6 +7,8 @@ CHANGELOG * changed table_name to __table_name__, read_repair_chance to __read_repair_chance__, keyspace to __keyspace__ * modified table name auto generator to ignore module name * changed internal implementation of model value get/set +* added TimeUUID.from_datetime(), used for generating UUID1's for a specific +time 0.3.3 * added abstract base class models From 071b54a76bafa1f62344a226f41c93fe2ab22142 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 18 Jun 2013 11:50:22 -0700 Subject: [PATCH 0267/3961] updating setup --- setup.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 512d55e902..fb2fc15630 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version = '0.3.3' long_desc = """ -cqlengine is a Cassandra CQL Object Mapper for Python in the style of the Django orm and mongoengine +Cassandra CQL 3 Object Mapper for Python [Documentation](https://cqlengine.readthedocs.org/en/latest/) @@ -21,8 +21,7 @@ setup( name='cqlengine', version=version, - description='Cassandra CQL ORM for Python in the style of the Django orm and mongoengine', - dependency_links = ['https://github.com/bdeggleston/cqlengine/archive/{0}.tar.gz#egg=cqlengine-{0}'.format(version)], + description='Cassandra CQL 3 Object Mapper for Python', long_description=long_desc, classifiers = [ "Environment :: Web Environment", @@ -38,7 +37,7 @@ install_requires = ['cql'], author='Blake Eggleston', author_email='bdeggleston@gmail.com', - url='https://github.com/bdeggleston/cqlengine', + url='https://github.com/cqlengine/cqlengine', license='BSD', packages=find_packages(), include_package_data=True, From 474bfb4c098a6240edf01c917a529a3cc3ca0e34 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 18 Jun 2013 11:51:21 -0700 Subject: [PATCH 0268/3961] version number bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index d103644c90..3e2d00b097 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.3.3' +__version__ = '0.4' diff --git a/docs/conf.py b/docs/conf.py index 5d797fee3e..50f1bd0129 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.3.3' +version = '0.4' # The full version, including alpha/beta/rc tags. -release = '0.3.3' +release = '0.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index fb2fc15630..1b6b8b8d5e 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.3.3' +version = '0.4' long_desc = """ Cassandra CQL 3 Object Mapper for Python From b5d0ddad8bcf8eef4da51348a49fe128b3fd49de Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 18 Jun 2013 11:57:35 -0700 Subject: [PATCH 0269/3961] removing alpha warning --- docs/index.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f93a8505c4..2901f2aff4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -88,8 +88,6 @@ Getting Started `Dev Mailing List `_ -**NOTE: cqlengine is in alpha and under development, some features may change. Make sure to check the changelog and test your app before upgrading** - Indices and tables ================== From cc1af16c1c80d4acb631e4769903c4a2d78b3d98 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 18 Jun 2013 17:50:30 -0700 Subject: [PATCH 0270/3961] fixed TS issue --- cqlengine/columns.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 0838195ddb..0210e8dbb8 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -304,7 +304,6 @@ def to_python(self, value): def to_database(self, value): return self.validate(value) -_last_timestamp = None from uuid import UUID as pyUUID, getnode class TimeUUID(UUID): @@ -340,9 +339,6 @@ def from_datetime(self, dt): nanoseconds = int(timestamp * 1e9) timestamp = int(nanoseconds // 100) + 0x01b21dd213814000L - if _last_timestamp is not None and timestamp <= _last_timestamp: - timestamp = _last_timestamp + 1 - _last_timestamp = timestamp if clock_seq is None: import random clock_seq = random.randrange(1 << 14L) # instead of stable storage From 1d4e9b0957070092e02eb4a659370255a896fb10 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 18 Jun 2013 17:53:53 -0700 Subject: [PATCH 0271/3961] bugfix for timeuuids --- cqlengine/__init__.py | 2 +- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 3e2d00b097..df941b0fff 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4' +__version__ = '0.4.1' diff --git a/docs/conf.py b/docs/conf.py index 50f1bd0129..5aff68540e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,7 +50,7 @@ # The short X.Y version. version = '0.4' # The full version, including alpha/beta/rc tags. -release = '0.4' +release = '0.4.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 1b6b8b8d5e..ff3dcddc4b 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4' +version = '0.4.1' long_desc = """ Cassandra CQL 3 Object Mapper for Python From dad5effe7b79d86f8c39444cc0c2b93ac9ed33ec Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 19 Jun 2013 09:55:54 -0700 Subject: [PATCH 0272/3961] adding change warning to all doc pages --- docs/index.rst | 4 ++++ docs/topics/columns.rst | 4 ++++ docs/topics/connection.rst | 4 ++++ docs/topics/manage_schemas.rst | 6 ++++++ docs/topics/models.rst | 6 ++++++ docs/topics/queryset.rst | 6 ++++++ 6 files changed, 30 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 2901f2aff4..9dff72fd56 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,10 @@ cqlengine documentation ======================= +**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ + +.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU + cqlengine is a Cassandra CQL 3 Object Mapper for Python with an interface similar to the Django orm and mongoengine :ref:`getting-started` diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 61252e7f97..987d7d7c4f 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -2,6 +2,10 @@ Columns ======= +**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ + +.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU + .. module:: cqlengine.columns .. class:: Bytes() diff --git a/docs/topics/connection.rst b/docs/topics/connection.rst index 48558f9c46..88f601fbd0 100644 --- a/docs/topics/connection.rst +++ b/docs/topics/connection.rst @@ -2,6 +2,10 @@ Connection ============== +**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ + +.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU + .. module:: cqlengine.connection The setup function in `cqlengine.connection` records the Cassandra servers to connect to. diff --git a/docs/topics/manage_schemas.rst b/docs/topics/manage_schemas.rst index 2174e56f6a..413b53c481 100644 --- a/docs/topics/manage_schemas.rst +++ b/docs/topics/manage_schemas.rst @@ -2,6 +2,12 @@ Managing Schmas =============== +**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ + +.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU + +.. module:: cqlengine.connection + .. module:: cqlengine.management Once a connection has been made to Cassandra, you can use the functions in ``cqlengine.management`` to create and delete keyspaces, as well as create and delete tables for defined models diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 5dc322f61b..f599f2cfb7 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -2,6 +2,12 @@ Models ====== +**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ + +.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU + +.. module:: cqlengine.connection + .. module:: cqlengine.models A model is a python class representing a CQL table. diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index c237ed6b59..0a481fbe9f 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -2,6 +2,12 @@ Making Queries ============== +**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ + +.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU + +.. module:: cqlengine.connection + .. module:: cqlengine.query Retrieving objects From 147158eb99c16530e8cf40d8324ba8059eb234cc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 19 Jun 2013 14:50:43 -0700 Subject: [PATCH 0273/3961] updating changelog --- changelog | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 5f52491b32..29a8b3f208 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,12 @@ CHANGELOG -0.4.0 (in progress) +0.4.2 +* added support for instantiating container columns with column instances + +0.4.1 +* fixed bug in TimeUUID from datetime method + +0.4.0 * removed default values from all column types * explicit primary key is required (automatic id removed) * added validation on keyname types on .create() From fb5aba9ba69318b90057f6bfad434a2d4459bae2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 19 Jun 2013 14:57:33 -0700 Subject: [PATCH 0274/3961] adding support for instantiating container columns with column instances, as well as classes --- cqlengine/columns.py | 14 +++++++++---- .../tests/columns/test_container_columns.py | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 0210e8dbb8..bc71e51ab4 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -424,15 +424,21 @@ def __init__(self, value_type, **kwargs): """ :param value_type: a column class indicating the types of the value """ - if not issubclass(value_type, Column): + inheritance_comparator = issubclass if isinstance(value_type, type) else isinstance + if not inheritance_comparator(value_type, Column): raise ValidationError('value_type must be a column class') - if issubclass(value_type, BaseContainerColumn): + if inheritance_comparator(value_type, BaseContainerColumn): raise ValidationError('container types cannot be nested') if value_type.db_type is None: raise ValidationError('value_type cannot be an abstract column type') - self.value_type = value_type - self.value_col = self.value_type() + if isinstance(value_type, type): + self.value_type = value_type + self.value_col = self.value_type() + else: + self.value_col = value_type + self.value_type = self.value_col.__class__ + super(BaseContainerColumn, self).__init__(**kwargs) def get_column_def(self): diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 3533aad3ef..989978ac2a 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -6,6 +6,27 @@ from cqlengine.management import create_table, delete_table from cqlengine.tests.base import BaseCassEngTestCase +class TestClassConstruction(BaseCassEngTestCase): + """ + Tests around the instantiation of container columns + """ + + def test_instantiation_with_column_class(self): + """ + Tests that columns instantiated with a column class work properly + and that the class is instantiated in the constructor + """ + column = columns.List(columns.Text) + assert isinstance(column.value_col, columns.Text) + + def test_instantiation_with_column_instance(self): + """ + Tests that columns instantiated with a column instance work properly + """ + column = columns.List(columns.Text(min_length=100)) + assert isinstance(column.value_col, columns.Text) + + class TestSetModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) int_set = columns.Set(columns.Integer, required=False) From c81d01d0f765087bc6ed97d4f860f33492d920c9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 19 Jun 2013 15:02:24 -0700 Subject: [PATCH 0275/3961] adding column instance support to map keys as well --- cqlengine/columns.py | 13 ++-- .../tests/columns/test_container_columns.py | 61 ++++++++++++++----- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index bc71e51ab4..2c74ed1028 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -653,15 +653,20 @@ def __init__(self, key_type, value_type, **kwargs): :param key_type: a column class indicating the types of the key :param value_type: a column class indicating the types of the value """ - if not issubclass(value_type, Column): + inheritance_comparator = issubclass if isinstance(key_type, type) else isinstance + if not inheritance_comparator(key_type, Column): raise ValidationError('key_type must be a column class') - if issubclass(value_type, BaseContainerColumn): + if inheritance_comparator(key_type, BaseContainerColumn): raise ValidationError('container types cannot be nested') if key_type.db_type is None: raise ValidationError('key_type cannot be an abstract column type') - self.key_type = key_type - self.key_col = self.key_type() + if isinstance(key_type, type): + self.key_type = key_type + self.key_col = self.key_type() + else: + self.key_col = key_type + self.key_type = self.key_col.__class__ super(Map, self).__init__(value_type, **kwargs) def get_column_def(self): diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 989978ac2a..6ba261d907 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -11,20 +11,6 @@ class TestClassConstruction(BaseCassEngTestCase): Tests around the instantiation of container columns """ - def test_instantiation_with_column_class(self): - """ - Tests that columns instantiated with a column class work properly - and that the class is instantiated in the constructor - """ - column = columns.List(columns.Text) - assert isinstance(column.value_col, columns.Text) - - def test_instantiation_with_column_instance(self): - """ - Tests that columns instantiated with a column instance work properly - """ - column = columns.List(columns.Text(min_length=100)) - assert isinstance(column.value_col, columns.Text) class TestSetModel(Model): @@ -93,6 +79,21 @@ def test_partial_update_creation(self): assert len([s for s in statements if '"TEST" = "TEST" -' in s]) == 1 assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 + def test_instantiation_with_column_class(self): + """ + Tests that columns instantiated with a column class work properly + and that the class is instantiated in the constructor + """ + column = columns.Set(columns.Text) + assert isinstance(column.value_col, columns.Text) + + def test_instantiation_with_column_instance(self): + """ + Tests that columns instantiated with a column instance work properly + """ + column = columns.Set(columns.Text(min_length=100)) + assert isinstance(column.value_col, columns.Text) + class TestListModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) int_list = columns.List(columns.Integer, required=False) @@ -164,6 +165,21 @@ def test_partial_update_creation(self): assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 assert len([s for s in statements if '+ "TEST"' in s]) == 1 + def test_instantiation_with_column_class(self): + """ + Tests that columns instantiated with a column class work properly + and that the class is instantiated in the constructor + """ + column = columns.List(columns.Text) + assert isinstance(column.value_col, columns.Text) + + def test_instantiation_with_column_instance(self): + """ + Tests that columns instantiated with a column instance work properly + """ + column = columns.List(columns.Text(min_length=100)) + assert isinstance(column.value_col, columns.Text) + class TestMapModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) int_map = columns.Map(columns.Integer, columns.UUID, required=False) @@ -251,6 +267,23 @@ def test_updates_to_none(self): m2 = TestMapModel.get(partition=m.partition) assert m2.int_map is None + def test_instantiation_with_column_class(self): + """ + Tests that columns instantiated with a column class work properly + and that the class is instantiated in the constructor + """ + column = columns.Map(columns.Text, columns.Integer) + assert isinstance(column.key_col, columns.Text) + assert isinstance(column.value_col, columns.Integer) + + def test_instantiation_with_column_instance(self): + """ + Tests that columns instantiated with a column instance work properly + """ + column = columns.Map(columns.Text(min_length=100), columns.Integer()) + assert isinstance(column.key_col, columns.Text) + assert isinstance(column.value_col, columns.Integer) + # def test_partial_update_creation(self): # """ # Tests that proper update statements are created for a partial list update From d78a452087dc1bf4a960420949e14e78dc58ba5b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 19 Jun 2013 15:03:10 -0700 Subject: [PATCH 0276/3961] removing now empty test class --- cqlengine/tests/columns/test_container_columns.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 6ba261d907..c27c03289d 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -6,18 +6,13 @@ from cqlengine.management import create_table, delete_table from cqlengine.tests.base import BaseCassEngTestCase -class TestClassConstruction(BaseCassEngTestCase): - """ - Tests around the instantiation of container columns - """ - - class TestSetModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) int_set = columns.Set(columns.Integer, required=False) text_set = columns.Set(columns.Text, required=False) + class TestSetColumn(BaseCassEngTestCase): @classmethod @@ -94,11 +89,13 @@ def test_instantiation_with_column_instance(self): column = columns.Set(columns.Text(min_length=100)) assert isinstance(column.value_col, columns.Text) + class TestListModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) int_list = columns.List(columns.Integer, required=False) text_list = columns.List(columns.Text, required=False) + class TestListColumn(BaseCassEngTestCase): @classmethod @@ -180,11 +177,13 @@ def test_instantiation_with_column_instance(self): column = columns.List(columns.Text(min_length=100)) assert isinstance(column.value_col, columns.Text) + class TestMapModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) int_map = columns.Map(columns.Integer, columns.UUID, required=False) text_map = columns.Map(columns.Text, columns.DateTime, required=False) + class TestMapColumn(BaseCassEngTestCase): @classmethod From 01cf5bf0b00622f8849d9289920edacab4436e28 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 19 Jun 2013 15:04:24 -0700 Subject: [PATCH 0277/3961] version number bump --- cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index df941b0fff..1bdc2ce5eb 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4.1' +__version__ = '0.4.2' diff --git a/docs/conf.py b/docs/conf.py index 5aff68540e..8dc1ccd58c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.4' +version = '0.4.2' # The full version, including alpha/beta/rc tags. -release = '0.4.1' +release = '0.4.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index ff3dcddc4b..494f2d7947 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4.1' +version = '0.4.2' long_desc = """ Cassandra CQL 3 Object Mapper for Python From 43b6eb3c8dca2d3d372ecd308ebe024b7777d707 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Jun 2013 10:30:06 -0700 Subject: [PATCH 0278/3961] fixing text min length calculation to work with new required=False default --- cqlengine/columns.py | 2 +- cqlengine/tests/columns/test_validation.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 2c74ed1028..8f52e3f885 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -197,7 +197,7 @@ class Text(Column): db_type = 'text' def __init__(self, *args, **kwargs): - self.min_length = kwargs.pop('min_length', 1 if kwargs.get('required', True) else None) + self.min_length = kwargs.pop('min_length', 1 if kwargs.get('required', False) else None) self.max_length = kwargs.pop('max_length', None) super(Text, self).__init__(*args, **kwargs) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index a68958bb23..46e8516364 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -194,6 +194,12 @@ def test_type_checking(self): with self.assertRaises(ValidationError): Text().validate(True) + def test_non_required_validation(self): + """ Tests that validation is ok on none and blank values if required is False """ + Text().validate('') + Text().validate(None) + + class TestExtraFieldsRaiseException(BaseCassEngTestCase): From 6587d383c27ab05a5c4a2bd0fda8e06e1beb2530 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Jun 2013 10:41:57 -0700 Subject: [PATCH 0279/3961] 0.4.3 version bump --- changelog | 3 +++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 29a8b3f208..1c147a6706 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.4.3 +* fixed bug with Text column validation + 0.4.2 * added support for instantiating container columns with column instances diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 1bdc2ce5eb..2ab73db5d1 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4.2' +__version__ = '0.4.3' diff --git a/docs/conf.py b/docs/conf.py index 8dc1ccd58c..3de8e4dd9f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.4.2' +version = '0.4.3' # The full version, including alpha/beta/rc tags. -release = '0.4.2' +release = '0.4.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 494f2d7947..99fa7b3f69 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4.2' +version = '0.4.3' long_desc = """ Cassandra CQL 3 Object Mapper for Python From 9634f47ea57c2122e9060ec6e94c445e1c941f45 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Jun 2013 10:48:10 -0700 Subject: [PATCH 0280/3961] updating failing test --- cqlengine/tests/columns/test_validation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 46e8516364..53cd738c02 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -158,9 +158,7 @@ class TestText(BaseCassEngTestCase): def test_min_length(self): #min len defaults to 1 col = Text() - - with self.assertRaises(ValidationError): - col.validate('') + col.validate('') col.validate('b') From 174aa4aa1b946f7958ac479821ed32879890f88f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 21 Jun 2013 10:41:34 -0700 Subject: [PATCH 0281/3961] updated batch docs --- docs/topics/queryset.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index c237ed6b59..47fc4ef3e0 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -291,6 +291,15 @@ Batch Queries em3 = ExampleModel.batch(b).create(example_type=0, description="3", created_at=now) b.execute() + # updating in a batch + + b = BatchQuery() + em1.description = "new description" + em1.batch(b).save() + em2.description = "another new description" + em2.batch(b).save() + b.execute() + QuerySet method reference ========================= From 7d62430cc849d1f38a7069e518574f3ec04133ec Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Mon, 24 Jun 2013 10:49:24 +0300 Subject: [PATCH 0282/3961] Restore CQL query logging at DEBUG level. --- cqlengine/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 5010aebc1a..4119d7f9bc 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -141,6 +141,7 @@ def execute(self, query, params): con = self.get() cur = con.cursor() cur.execute(query, params) + LOG.debug('{} {}'.format(query, repr(params))) self.put(con) return cur except cql.ProgrammingError as ex: From ad6d7b834ef228f522d2c80eee689ecac45eee93 Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Fri, 7 Jun 2013 23:17:31 +0300 Subject: [PATCH 0283/3961] Make ValueQuoter objects equal if their repr() are equal. This makes model instances which have quoted values to equal in an intuitive manner. --- cqlengine/columns.py | 5 +++++ cqlengine/tests/columns/test_value_io.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 8f52e3f885..8191c2abcc 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -65,6 +65,11 @@ def __str__(self): def __repr__(self): return self.__str__() + def __eq__(self, other): + if isinstance(other, self.__class__): + return repr(self) == repr(other) + return False + class Column(object): diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index fda632e965..64054279cc 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -7,7 +7,10 @@ from cqlengine.management import create_table from cqlengine.management import delete_table from cqlengine.models import Model +from cqlengine.columns import ValueQuoter from cqlengine import columns +import unittest + class BaseColumnIOTest(BaseCassEngTestCase): """ @@ -145,3 +148,11 @@ class TestDecimalIO(BaseColumnIOTest): def comparator_converter(self, val): return Decimal(val) + +class TestQuoter(unittest.TestCase): + + def test_equals(self): + assert ValueQuoter(False) == ValueQuoter(False) + assert ValueQuoter(1) == ValueQuoter(1) + assert ValueQuoter("foo") == ValueQuoter("foo") + assert ValueQuoter(1.55) == ValueQuoter(1.55) From ed46c0c5ab62f84a36e511326daacd88979174c3 Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Fri, 7 Jun 2013 19:18:07 +0300 Subject: [PATCH 0284/3961] Fixed a bug when NetworkTopologyStrategy is used. Although the Cassandra documentation implies that the `replication_factor` parameter would be ignored in this case its presence will cause an error when creating a keyspace using NetworkTopologyStrategy. --- cqlengine/management.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cqlengine/management.py b/cqlengine/management.py index b3504b919e..e8a692e0be 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -30,6 +30,12 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, } replication_map.update(replication_values) + if strategy_class.lower() != 'simplestrategy': + # Although the Cassandra documentation states for `replication_factor` + # that it is "Required if class is SimpleStrategy; otherwise, + # not used." we get an error if it is present. + replication_map.pop('replication_factor', None) + query = """ CREATE KEYSPACE {} WITH REPLICATION = {} From 934c8b34f31d2b5d031def97ae9fa4c15c664240 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 13:39:06 -0700 Subject: [PATCH 0285/3961] fixing bug that would cause failures when updating a model with an empty list --- cqlengine/columns.py | 10 ++++++ .../tests/columns/test_container_columns.py | 31 ++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 8f52e3f885..9c5199866f 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -585,10 +585,20 @@ def _insert(): if val is None or val == prev: return [] + elif prev is None: return _insert() + elif len(val) < len(prev): + # if elements have been removed, + # rewrite the whole list return _insert() + + elif len(prev) == 0: + # if we're updating from an empty + # list, do a complete insert + return _insert() + else: # the prepend and append lists, # if both of these are still None after looking diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index c27c03289d..03b152c3aa 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -146,10 +146,7 @@ def test_partial_updates(self): assert list(m2.int_list) == final def test_partial_update_creation(self): - """ - Tests that proper update statements are created for a partial list update - :return: - """ + """ Tests that proper update statements are created for a partial list update """ final = range(10) initial = final[3:7] @@ -162,6 +159,32 @@ def test_partial_update_creation(self): assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 assert len([s for s in statements if '+ "TEST"' in s]) == 1 + def test_update_from_none(self): + """ Tests that updating an 'None' list creates a straight insert statement """ + ctx = {} + col = columns.List(columns.Integer, db_field="TEST") + statements = col.get_update_statement([1, 2, 3], None, ctx) + + #only one variable /statement should be generated + assert len(ctx) == 1 + assert len(statements) == 1 + + assert str(ctx.values()[0]) == str([1, 2, 3]) + assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) + + def test_update_from_empty(self): + """ Tests that updating an empty list creates a straight insert statement """ + ctx = {} + col = columns.List(columns.Integer, db_field="TEST") + statements = col.get_update_statement([1, 2, 3], [], ctx) + + #only one variable /statement should be generated + assert len(ctx) == 1 + assert len(statements) == 1 + + assert str(ctx.values()[0]) == str([1,2,3]) + assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) + def test_instantiation_with_column_class(self): """ Tests that columns instantiated with a column class work properly From 27ba94a03a5df562eccd695dd235031d4d2490dc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 13:44:26 -0700 Subject: [PATCH 0286/3961] adding tests around updating empty and null sets --- .../tests/columns/test_container_columns.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 03b152c3aa..e14adc6971 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -74,6 +74,32 @@ def test_partial_update_creation(self): assert len([s for s in statements if '"TEST" = "TEST" -' in s]) == 1 assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 + def test_update_from_none(self): + """ Tests that updating an 'None' list creates a straight insert statement """ + ctx = {} + col = columns.Set(columns.Integer, db_field="TEST") + statements = col.get_update_statement({1,2,3,4}, None, ctx) + + #only one variable /statement should be generated + assert len(ctx) == 1 + assert len(statements) == 1 + + assert ctx.values()[0].value == {1,2,3,4} + assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) + + def test_update_from_empty(self): + """ Tests that updating an empty list creates a straight insert statement """ + ctx = {} + col = columns.Set(columns.Integer, db_field="TEST") + statements = col.get_update_statement({1,2,3,4}, set(), ctx) + + #only one variable /statement should be generated + assert len(ctx) == 1 + assert len(statements) == 1 + + assert ctx.values()[0].value == {1,2,3,4} + assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) + def test_instantiation_with_column_class(self): """ Tests that columns instantiated with a column class work properly @@ -169,7 +195,7 @@ def test_update_from_none(self): assert len(ctx) == 1 assert len(statements) == 1 - assert str(ctx.values()[0]) == str([1, 2, 3]) + assert ctx.values()[0].value == [1, 2, 3] assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) def test_update_from_empty(self): @@ -182,7 +208,7 @@ def test_update_from_empty(self): assert len(ctx) == 1 assert len(statements) == 1 - assert str(ctx.values()[0]) == str([1,2,3]) + assert ctx.values()[0].value == [1,2,3] assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) def test_instantiation_with_column_class(self): From 1aacad2673f5848bf9da0c955d4da343b705e06d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 13:45:14 -0700 Subject: [PATCH 0287/3961] reformatting --- .../tests/columns/test_container_columns.py | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index e14adc6971..227502b12a 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -8,13 +8,12 @@ class TestSetModel(Model): - partition = columns.UUID(primary_key=True, default=uuid4) - int_set = columns.Set(columns.Integer, required=False) - text_set = columns.Set(columns.Text, required=False) + partition = columns.UUID(primary_key=True, default=uuid4) + int_set = columns.Set(columns.Integer, required=False) + text_set = columns.Set(columns.Text, required=False) class TestSetColumn(BaseCassEngTestCase): - @classmethod def setUpClass(cls): super(TestSetColumn, cls).setUpClass() @@ -28,7 +27,7 @@ def tearDownClass(cls): def test_io_success(self): """ Tests that a basic usage works as expected """ - m1 = TestSetModel.create(int_set={1,2}, text_set={'kai', 'andreas'}) + m1 = TestSetModel.create(int_set={1, 2}, text_set={'kai', 'andreas'}) m2 = TestSetModel.get(partition=m1.partition) assert isinstance(m2.int_set, set) @@ -49,16 +48,16 @@ def test_type_validation(self): def test_partial_updates(self): """ Tests that partial udpates work as expected """ - m1 = TestSetModel.create(int_set={1,2,3,4}) + m1 = TestSetModel.create(int_set={1, 2, 3, 4}) m1.int_set.add(5) m1.int_set.remove(1) - assert m1.int_set == {2,3,4,5} + assert m1.int_set == {2, 3, 4, 5} m1.save() - m2 = TestSetModel.get(partition=m1.partition) - assert m2.int_set == {2,3,4,5} + m2 = TestSetModel.get(partition=m1.partition) + assert m2.int_set == {2, 3, 4, 5} def test_partial_update_creation(self): """ @@ -67,7 +66,7 @@ def test_partial_update_creation(self): """ ctx = {} col = columns.Set(columns.Integer, db_field="TEST") - statements = col.get_update_statement({1,2,3,4}, {2,3,4,5}, ctx) + statements = col.get_update_statement({1, 2, 3, 4}, {2, 3, 4, 5}, ctx) assert len([v for v in ctx.values() if {1} == v.value]) == 1 assert len([v for v in ctx.values() if {5} == v.value]) == 1 @@ -78,26 +77,26 @@ def test_update_from_none(self): """ Tests that updating an 'None' list creates a straight insert statement """ ctx = {} col = columns.Set(columns.Integer, db_field="TEST") - statements = col.get_update_statement({1,2,3,4}, None, ctx) + statements = col.get_update_statement({1, 2, 3, 4}, None, ctx) #only one variable /statement should be generated assert len(ctx) == 1 assert len(statements) == 1 - assert ctx.values()[0].value == {1,2,3,4} + assert ctx.values()[0].value == {1, 2, 3, 4} assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) def test_update_from_empty(self): """ Tests that updating an empty list creates a straight insert statement """ ctx = {} col = columns.Set(columns.Integer, db_field="TEST") - statements = col.get_update_statement({1,2,3,4}, set(), ctx) + statements = col.get_update_statement({1, 2, 3, 4}, set(), ctx) #only one variable /statement should be generated assert len(ctx) == 1 assert len(statements) == 1 - assert ctx.values()[0].value == {1,2,3,4} + assert ctx.values()[0].value == {1, 2, 3, 4} assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) def test_instantiation_with_column_class(self): @@ -117,13 +116,12 @@ def test_instantiation_with_column_instance(self): class TestListModel(Model): - partition = columns.UUID(primary_key=True, default=uuid4) - int_list = columns.List(columns.Integer, required=False) - text_list = columns.List(columns.Text, required=False) + partition = columns.UUID(primary_key=True, default=uuid4) + int_list = columns.List(columns.Integer, required=False) + text_list = columns.List(columns.Text, required=False) class TestListColumn(BaseCassEngTestCase): - @classmethod def setUpClass(cls): super(TestListColumn, cls).setUpClass() @@ -137,7 +135,7 @@ def tearDownClass(cls): def test_io_success(self): """ Tests that a basic usage works as expected """ - m1 = TestListModel.create(int_list=[1,2], text_list=['kai', 'andreas']) + m1 = TestListModel.create(int_list=[1, 2], text_list=['kai', 'andreas']) m2 = TestListModel.get(partition=m1.partition) assert isinstance(m2.int_list, list) @@ -168,7 +166,7 @@ def test_partial_updates(self): m1.int_list = final m1.save() - m2 = TestListModel.get(partition=m1.partition) + m2 = TestListModel.get(partition=m1.partition) assert list(m2.int_list) == final def test_partial_update_creation(self): @@ -180,8 +178,8 @@ def test_partial_update_creation(self): col = columns.List(columns.Integer, db_field="TEST") statements = col.get_update_statement(final, initial, ctx) - assert len([v for v in ctx.values() if [2,1,0] == v.value]) == 1 - assert len([v for v in ctx.values() if [7,8,9] == v.value]) == 1 + assert len([v for v in ctx.values() if [2, 1, 0] == v.value]) == 1 + assert len([v for v in ctx.values() if [7, 8, 9] == v.value]) == 1 assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 assert len([s for s in statements if '+ "TEST"' in s]) == 1 @@ -208,7 +206,7 @@ def test_update_from_empty(self): assert len(ctx) == 1 assert len(statements) == 1 - assert ctx.values()[0].value == [1,2,3] + assert ctx.values()[0].value == [1, 2, 3] assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) def test_instantiation_with_column_class(self): @@ -228,13 +226,12 @@ def test_instantiation_with_column_instance(self): class TestMapModel(Model): - partition = columns.UUID(primary_key=True, default=uuid4) - int_map = columns.Map(columns.Integer, columns.UUID, required=False) - text_map = columns.Map(columns.Text, columns.DateTime, required=False) + partition = columns.UUID(primary_key=True, default=uuid4) + int_map = columns.Map(columns.Integer, columns.UUID, required=False) + text_map = columns.Map(columns.Text, columns.DateTime, required=False) class TestMapColumn(BaseCassEngTestCase): - @classmethod def setUpClass(cls): super(TestMapColumn, cls).setUpClass() @@ -252,7 +249,7 @@ def test_io_success(self): k2 = uuid4() now = datetime.now() then = now + timedelta(days=1) - m1 = TestMapModel.create(int_map={1:k1,2:k2}, text_map={'now':now, 'then':then}) + m1 = TestMapModel.create(int_map={1: k1, 2: k2}, text_map={'now': now, 'then': then}) m2 = TestMapModel.get(partition=m1.partition) assert isinstance(m2.int_map, dict) @@ -273,7 +270,7 @@ def test_type_validation(self): Tests that attempting to use the wrong types will raise an exception """ with self.assertRaises(ValidationError): - TestMapModel.create(int_map={'key':2,uuid4():'val'}, text_map={2:5}) + TestMapModel.create(int_map={'key': 2, uuid4(): 'val'}, text_map={2: 5}) def test_partial_updates(self): """ Tests that partial udpates work as expected """ @@ -284,21 +281,21 @@ def test_partial_updates(self): earlier = early - timedelta(minutes=30) later = now + timedelta(minutes=30) - initial = {'now':now, 'early':earlier} - final = {'later':later, 'early':early} + initial = {'now': now, 'early': earlier} + final = {'later': later, 'early': early} m1 = TestMapModel.create(text_map=initial) m1.text_map = final m1.save() - m2 = TestMapModel.get(partition=m1.partition) + m2 = TestMapModel.get(partition=m1.partition) assert m2.text_map == final def test_updates_from_none(self): """ Tests that updates from None work as expected """ m = TestMapModel.create(int_map=None) - expected = {1:uuid4()} + expected = {1: uuid4()} m.int_map = expected m.save() @@ -308,7 +305,7 @@ def test_updates_from_none(self): def test_updates_to_none(self): """ Tests that setting the field to None works as expected """ - m = TestMapModel.create(int_map={1:uuid4()}) + m = TestMapModel.create(int_map={1: uuid4()}) m.int_map = None m.save() From f65b39ee2b6a679f1360498d9e7e07562b4d6162 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 13:48:21 -0700 Subject: [PATCH 0288/3961] changelog update and version bump --- changelog | 4 ++++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 1c147a6706..5c70db60b6 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.4.4 +* adding query logging back +* fixed bug updating an empty list column + 0.4.3 * fixed bug with Text column validation diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 2ab73db5d1..98c57ebebd 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4.3' +__version__ = '0.4.4' diff --git a/docs/conf.py b/docs/conf.py index 3de8e4dd9f..8bbd4c3575 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.4.3' +version = '0.4.4' # The full version, including alpha/beta/rc tags. -release = '0.4.3' +release = '0.4.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 99fa7b3f69..bedf371d88 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4.3' +version = '0.4.4' long_desc = """ Cassandra CQL 3 Object Mapper for Python From 4b97781a7bdd15d172adb8231d41f5412ff33c61 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 15:00:44 -0700 Subject: [PATCH 0289/3961] adding to_python call to value column in container column to_python methods --- cqlengine/columns.py | 9 +++- .../tests/columns/test_container_columns.py | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 9c5199866f..a4dc26d09f 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -489,6 +489,10 @@ def validate(self, value): return {self.value_col.validate(v) for v in val} + def to_python(self, value): + if value is None: return None + return {self.value_col.to_python(v) for v in value} + def to_database(self, value): if value is None: return None if isinstance(value, self.Quoter): return value @@ -560,7 +564,7 @@ def validate(self, value): def to_python(self, value): if value is None: return None - return list(value) + return [self.value_col.to_python(v) for v in value] def to_database(self, value): if value is None: return None @@ -643,6 +647,7 @@ def _insert(): return statements + class Map(BaseContainerColumn): """ Stores a key -> value map (dictionary) @@ -698,7 +703,7 @@ def validate(self, value): def to_python(self, value): if value is not None: - return {self.key_col.to_python(k):self.value_col.to_python(v) for k,v in value.items()} + return {self.key_col.to_python(k): self.value_col.to_python(v) for k,v in value.items()} def to_database(self, value): if value is None: return None diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 227502b12a..2134764dde 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +import json from uuid import uuid4 from cqlengine import Model, ValidationError @@ -13,6 +14,20 @@ class TestSetModel(Model): text_set = columns.Set(columns.Text, required=False) +class JsonTestColumn(columns.Column): + db_type = 'text' + + def to_python(self, value): + if value is None: return + if isinstance(value, basestring): + return json.loads(value) + else: + return value + + def to_database(self, value): + if value is None: return + return json.dumps(value) + class TestSetColumn(BaseCassEngTestCase): @classmethod def setUpClass(cls): @@ -114,6 +129,15 @@ def test_instantiation_with_column_instance(self): column = columns.Set(columns.Text(min_length=100)) assert isinstance(column.value_col, columns.Text) + def test_to_python(self): + """ Tests that to_python of value column is called """ + column = columns.Set(JsonTestColumn) + val = {1, 2, 3} + db_val = column.to_database(val) + assert db_val.value == {json.dumps(v) for v in val} + py_val = column.to_python(db_val.value) + assert py_val == val + class TestListModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) @@ -224,6 +248,14 @@ def test_instantiation_with_column_instance(self): column = columns.List(columns.Text(min_length=100)) assert isinstance(column.value_col, columns.Text) + def test_to_python(self): + """ Tests that to_python of value column is called """ + column = columns.List(JsonTestColumn) + val = [1, 2, 3] + db_val = column.to_database(val) + assert db_val.value == [json.dumps(v) for v in val] + py_val = column.to_python(db_val.value) + assert py_val == val class TestMapModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) @@ -329,6 +361,15 @@ def test_instantiation_with_column_instance(self): assert isinstance(column.key_col, columns.Text) assert isinstance(column.value_col, columns.Integer) + def test_to_python(self): + """ Tests that to_python of value column is called """ + column = columns.Map(JsonTestColumn, JsonTestColumn) + val = {1: 2, 3: 4, 5: 6} + db_val = column.to_database(val) + assert db_val.value == {json.dumps(k):json.dumps(v) for k,v in val.items()} + py_val = column.to_python(db_val.value) + assert py_val == val + # def test_partial_update_creation(self): # """ # Tests that proper update statements are created for a partial list update From 479224df322104776dda3c6f586f7339a8747ffe Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 15:02:33 -0700 Subject: [PATCH 0290/3961] updating changelog and version --- changelog | 3 +++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 5c70db60b6..7a68e4f84b 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.4.5 +* fixed bug where container columns would not call their child to_python method, this only really affected columns with special to_python logic + 0.4.4 * adding query logging back * fixed bug updating an empty list column diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 98c57ebebd..f246c6a70a 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4.4' +__version__ = '0.4.5' diff --git a/docs/conf.py b/docs/conf.py index 8bbd4c3575..cadd3038d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.4.4' +version = '0.4.5' # The full version, including alpha/beta/rc tags. -release = '0.4.4' +release = '0.4.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index bedf371d88..326913033d 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4.4' +version = '0.4.5' long_desc = """ Cassandra CQL 3 Object Mapper for Python From 044e9fb933a6e8f0af5b7dc583719ed6f6140f3f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 15:30:48 -0700 Subject: [PATCH 0291/3961] updating value quoter equal function to compare values not repr --- cqlengine/columns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 0c4e62467b..fe205ae884 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -67,7 +67,7 @@ def __repr__(self): def __eq__(self, other): if isinstance(other, self.__class__): - return repr(self) == repr(other) + return self.value == other.value return False From e722b505464e3f8c8a76fd564653aed97b69ebce Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 15:34:35 -0700 Subject: [PATCH 0292/3961] updating equality operations to stop relying on the to_database dictionary conversion --- cqlengine/models.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index a0c41be1ce..609e055933 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -186,7 +186,21 @@ def _get_column(cls, name): return cls._columns[name] def __eq__(self, other): - return self.as_dict() == other.as_dict() + if self.__class__ != other.__class__: + return False + + # check attribute keys + keys = set(self._columns.keys()) + other_keys = set(self._columns.keys()) + if keys != other_keys: + return False + + # check that all of the attributes match + for key in other_keys: + if getattr(self, key, None) != getattr(other, key, None): + return False + + return True def __ne__(self, other): return not self.__eq__(other) From 7a4c7235006da305273b49c6a28fe83497b9dec1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 15:46:38 -0700 Subject: [PATCH 0293/3961] renaming as_dict to _as_dict --- cqlengine/models.py | 2 +- cqlengine/query.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 609e055933..8849324943 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -232,7 +232,7 @@ def validate(self): val = col.validate(getattr(self, name)) setattr(self, name, val) - def as_dict(self): + def _as_dict(self): """ Returns a map of column names to cleaned values """ values = self._dynamic_columns or {} for name, col in self._columns.items(): diff --git a/cqlengine/query.py b/cqlengine/query.py index b01e1a9ea3..b8b4926dea 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -794,7 +794,7 @@ def save(self): #organize data value_pairs = [] - values = self.instance.as_dict() + values = self.instance._as_dict() #get defined fields and their column names for name, col in self.model._columns.items(): From ed0bb798962205b699da5c0ddf4e830ad6afcb2a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 15:47:41 -0700 Subject: [PATCH 0294/3961] updating changelog and version --- changelog | 3 +++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 7a68e4f84b..35981af32e 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.4.6 +* fixing the way models are compared + 0.4.5 * fixed bug where container columns would not call their child to_python method, this only really affected columns with special to_python logic diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index f246c6a70a..a07df1f2e7 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4.5' +__version__ = '0.4.6' diff --git a/docs/conf.py b/docs/conf.py index cadd3038d6..880cbeb40b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.4.5' +version = '0.4.6' # The full version, including alpha/beta/rc tags. -release = '0.4.5' +release = '0.4.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 326913033d..c00d3ab982 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4.5' +version = '0.4.6' long_desc = """ Cassandra CQL 3 Object Mapper for Python From f90cfb046a7e727a160e4e331d799064d1588c06 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 27 Jun 2013 13:10:16 -0700 Subject: [PATCH 0295/3961] adding support for setting a query's batch object to None --- cqlengine/query.py | 24 +++++++++++------------ cqlengine/tests/query/test_batch_query.py | 21 +++++++++++++++++++- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index b8b4926dea..d86b504bab 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -428,8 +428,8 @@ def batch(self, batch_obj): :param batch_obj: :return: """ - if not isinstance(batch_obj, BatchQuery): - raise CQLEngineException('batch_obj must be a BatchQuery instance') + if batch_obj is not None and not isinstance(batch_obj, BatchQuery): + raise CQLEngineException('batch_obj must be a BatchQuery instance or None') clone = copy.deepcopy(self) clone._batch = batch_obj return clone @@ -772,13 +772,13 @@ def __init__(self, model, instance=None, batch=None): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance - self.batch = batch + self._batch = batch pass def batch(self, batch_obj): - if not isinstance(batch_obj, BatchQuery): - raise CQLEngineException('batch_obj must be a BatchQuery instance') - self.batch = batch_obj + if batch_obj is not None and not isinstance(batch_obj, BatchQuery): + raise CQLEngineException('batch_obj must be a BatchQuery instance or None') + self._batch = batch_obj return self def save(self): @@ -852,8 +852,8 @@ def save(self): # skip query execution if it's empty # caused by pointless update queries if qs: - if self.batch: - self.batch.add_query(qs, query_values) + if self._batch: + self._batch.add_query(qs, query_values) else: execute(qs, query_values) @@ -885,8 +885,8 @@ def save(self): qs = ' '.join(qs) - if self.batch: - self.batch.add_query(qs, query_values) + if self._batch: + self._batch.add_query(qs, query_values) else: execute(qs, query_values) @@ -906,8 +906,8 @@ def delete(self): qs += [' AND '.join(where_statements)] qs = ' '.join(qs) - if self.batch: - self.batch.add_query(qs, field_values) + if self._batch: + self._batch.add_query(qs, field_values) else: execute(qs, field_values) diff --git a/cqlengine/tests/query/test_batch_query.py b/cqlengine/tests/query/test_batch_query.py index 5b31826a39..37e8d28d56 100644 --- a/cqlengine/tests/query/test_batch_query.py +++ b/cqlengine/tests/query/test_batch_query.py @@ -4,7 +4,7 @@ import random from cqlengine import Model, columns from cqlengine.management import delete_table, create_table -from cqlengine.query import BatchQuery +from cqlengine.query import BatchQuery, DMLQuery from cqlengine.tests.base import BaseCassEngTestCase class TestMultiKeyModel(Model): @@ -104,3 +104,22 @@ def test_bulk_delete_success_case(self): for m in TestMultiKeyModel.all(): m.delete() + def test_none_success_case(self): + """ Tests that passing None into the batch call clears any batch object """ + b = BatchQuery() + + q = TestMultiKeyModel.objects.batch(b) + assert q._batch == b + + q = q.batch(None) + assert q._batch is None + + def test_dml_none_success_case(self): + """ Tests that passing None into the batch call clears any batch object """ + b = BatchQuery() + + q = DMLQuery(TestMultiKeyModel, batch=b) + assert q._batch == b + + q.batch(None) + assert q._batch is None From 01e9747411452e889f2aebeda26c6c8ea2727930 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 27 Jun 2013 13:11:50 -0700 Subject: [PATCH 0296/3961] changelog and version update --- changelog | 3 +++ cqlengine/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 35981af32e..6d49d52889 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.4.7 +* adding support for passing None into query batch methods to clear any batch objects + 0.4.6 * fixing the way models are compared diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index a07df1f2e7..d9763109b4 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,5 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4.6' +__version__ = '0.4.7' diff --git a/docs/conf.py b/docs/conf.py index 880cbeb40b..2b29bbfe6d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.4.6' +version = '0.4.7' # The full version, including alpha/beta/rc tags. -release = '0.4.6' +release = '0.4.7' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index c00d3ab982..9865237f08 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4.6' +version = '0.4.7' long_desc = """ Cassandra CQL 3 Object Mapper for Python From 75d20b69200e99f8ff209530c6c824505b2f93f1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 27 Jun 2013 14:22:47 -0700 Subject: [PATCH 0297/3961] fixed empty sets, maps, lists --- cqlengine/columns.py | 15 +++++++++----- .../tests/columns/test_container_columns.py | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index fe205ae884..f8d262dfe5 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -473,14 +473,15 @@ def __str__(self): cq = cql_quote return '{' + ', '.join([cq(v) for v in self.value]) + '}' - def __init__(self, value_type, strict=True, **kwargs): + def __init__(self, value_type, strict=True, default=set, **kwargs): """ :param value_type: a column class indicating the types of the value :param strict: sets whether non set values will be coerced to set type on validation, or raise a validation error, defaults to True """ self.strict = strict - super(Set, self).__init__(value_type, **kwargs) + + super(Set, self).__init__(value_type, default=default, **kwargs) def validate(self, value): val = super(Set, self).validate(value) @@ -495,11 +496,12 @@ def validate(self, value): return {self.value_col.validate(v) for v in val} def to_python(self, value): - if value is None: return None + if value is None: return set() return {self.value_col.to_python(v) for v in value} def to_database(self, value): if value is None: return None + if isinstance(value, self.Quoter): return value return self.Quoter({self.value_col.to_database(v) for v in value}) @@ -560,6 +562,9 @@ def __str__(self): cq = cql_quote return '[' + ', '.join([cq(v) for v in self.value]) + ']' + def __init__(self, value_type, default=set, **kwargs): + return super(List, self).__init__(value_type=value_type, default=default, **kwargs) + def validate(self, value): val = super(List, self).validate(value) if val is None: return @@ -668,7 +673,7 @@ def __str__(self): cq = cql_quote return '{' + ', '.join([cq(k) + ':' + cq(v) for k,v in self.value.items()]) + '}' - def __init__(self, key_type, value_type, **kwargs): + def __init__(self, key_type, value_type, default=dict, **kwargs): """ :param key_type: a column class indicating the types of the key :param value_type: a column class indicating the types of the value @@ -687,7 +692,7 @@ def __init__(self, key_type, value_type, **kwargs): else: self.key_col = key_type self.key_type = self.key_col.__class__ - super(Map, self).__init__(value_type, **kwargs) + super(Map, self).__init__(value_type, default=default, **kwargs) def get_column_def(self): """ diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 2134764dde..180378dec0 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -40,6 +40,16 @@ def tearDownClass(cls): super(TestSetColumn, cls).tearDownClass() delete_table(TestSetModel) + + def test_empty_set_initial(self): + """ + tests that sets are set() by default, should never be none + :return: + """ + m = TestSetModel.create() + m.int_set.add(5) + m.save() + def test_io_success(self): """ Tests that a basic usage works as expected """ m1 = TestSetModel.create(int_set={1, 2}, text_set={'kai', 'andreas'}) @@ -129,6 +139,8 @@ def test_instantiation_with_column_instance(self): column = columns.Set(columns.Text(min_length=100)) assert isinstance(column.value_col, columns.Text) + + def test_to_python(self): """ Tests that to_python of value column is called """ column = columns.Set(JsonTestColumn) @@ -157,6 +169,10 @@ def tearDownClass(cls): super(TestListColumn, cls).tearDownClass() delete_table(TestListModel) + def test_initial(self): + tmp = TestListModel.create() + tmp.int_list.append(1) + def test_io_success(self): """ Tests that a basic usage works as expected """ m1 = TestListModel.create(int_list=[1, 2], text_list=['kai', 'andreas']) @@ -275,6 +291,10 @@ def tearDownClass(cls): super(TestMapColumn, cls).tearDownClass() delete_table(TestMapModel) + def test_empty_default(self): + tmp = TestMapModel.create() + tmp.int_map['blah'] = 1 + def test_io_success(self): """ Tests that a basic usage works as expected """ k1 = uuid4() From 5cc63e67cfb8aec15ff1d0a4fc8240cafd64cd71 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 27 Jun 2013 14:58:32 -0700 Subject: [PATCH 0298/3961] CQL version file instead of manually updating --- cqlengine/VERSION | 1 + cqlengine/__init__.py | 4 +++- docs/conf.py | 7 +++++-- setup.py | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 cqlengine/VERSION diff --git a/cqlengine/VERSION b/cqlengine/VERSION new file mode 100644 index 0000000000..cb498ab2c8 --- /dev/null +++ b/cqlengine/VERSION @@ -0,0 +1 @@ +0.4.8 diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index d9763109b4..bdcaa6e420 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -3,5 +3,7 @@ from cqlengine.models import Model from cqlengine.query import BatchQuery -__version__ = '0.4.7' +__cqlengine_version_path__ = os.path.realpath(__file__ + '/../VERSION') +__version__ = open(__cqlengine_version_path__, 'r').readline().strip() + diff --git a/docs/conf.py b/docs/conf.py index 2b29bbfe6d..847ef50dce 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,10 +47,13 @@ # |version| and |release|, also used in various other places throughout the # built documents. # +__cqlengine_version_path__ = os.path.realpath(__file__ + '/../VERSION') # The short X.Y version. -version = '0.4.7' +version = open(__cqlengine_version_path__, 'r').readline().strip() # The full version, including alpha/beta/rc tags. -release = '0.4.7' +release = version + + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 9865237f08..5fd441428a 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ #python setup.py register #python setup.py sdist upload -version = '0.4.7' +version = open('cqlengine/VERSION', 'r').readline().strip() long_desc = """ Cassandra CQL 3 Object Mapper for Python From 7ab977b59c540c2746cd1c2fe09e84b1b2016870 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 27 Jun 2013 15:01:15 -0700 Subject: [PATCH 0299/3961] fixed import --- cqlengine/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index bdcaa6e420..27ef399572 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,3 +1,5 @@ +import os + from cqlengine.columns import * from cqlengine.functions import * from cqlengine.models import Model From e2dafaa25caea0f137ad78bd4c33af8d65756ed5 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 27 Jun 2013 15:30:02 -0700 Subject: [PATCH 0300/3961] fixes for pulling data out of DB with empty sets, lists, dict --- cqlengine/columns.py | 4 +++- cqlengine/models.py | 3 ++- .../tests/columns/test_container_columns.py | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index f8d262dfe5..3b7a22d06e 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -573,7 +573,7 @@ def validate(self, value): return [self.value_col.validate(v) for v in val] def to_python(self, value): - if value is None: return None + if value is None: return [] return [self.value_col.to_python(v) for v in value] def to_database(self, value): @@ -712,6 +712,8 @@ def validate(self, value): return {self.key_col.validate(k):self.value_col.validate(v) for k,v in val.items()} def to_python(self, value): + if value is None: + return {} if value is not None: return {self.key_col.to_python(k): self.value_col.to_python(v) for k,v in value.items()} diff --git a/cqlengine/models.py b/cqlengine/models.py index 8849324943..a2d9b6abce 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -149,7 +149,8 @@ def __init__(self, **values): for name, column in self._columns.items(): value = values.get(name, None) - if value is not None: value = column.to_python(value) + if value is not None or isinstance(column, columns.BaseContainerColumn): + value = column.to_python(value) value_mngr = column.value_manager(self, column, value) self._values[name] = value_mngr diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 180378dec0..6cf218299c 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -50,6 +50,11 @@ def test_empty_set_initial(self): m.int_set.add(5) m.save() + def test_empty_set_retrieval(self): + m = TestSetModel.create() + m2 = TestSetModel.get(partition=m.partition) + m2.int_set.add(3) + def test_io_success(self): """ Tests that a basic usage works as expected """ m1 = TestSetModel.create(int_set={1, 2}, text_set={'kai', 'andreas'}) @@ -173,6 +178,11 @@ def test_initial(self): tmp = TestListModel.create() tmp.int_list.append(1) + def test_initial(self): + tmp = TestListModel.create() + tmp2 = TestListModel.get(partition=tmp.partition) + tmp2.int_list.append(1) + def test_io_success(self): """ Tests that a basic usage works as expected """ m1 = TestListModel.create(int_list=[1, 2], text_list=['kai', 'andreas']) @@ -295,6 +305,13 @@ def test_empty_default(self): tmp = TestMapModel.create() tmp.int_map['blah'] = 1 + def test_empty_retrieve(self): + tmp = TestMapModel.create() + tmp2 = TestMapModel.get(partition=tmp.partition) + tmp2.int_map['blah'] = 1 + + + def test_io_success(self): """ Tests that a basic usage works as expected """ k1 = uuid4() @@ -362,7 +379,7 @@ def test_updates_to_none(self): m.save() m2 = TestMapModel.get(partition=m.partition) - assert m2.int_map is None + assert m2.int_map == {} def test_instantiation_with_column_class(self): """ From 62fdaa5542d80be291f1826f108ebac1a4b426bc Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 27 Jun 2013 15:30:52 -0700 Subject: [PATCH 0301/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index cb498ab2c8..76914ddc02 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.4.8 +0.4.9 From db60c151817794dc11b3eb309ba5a22341b64eb2 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 27 Jun 2013 16:03:12 -0700 Subject: [PATCH 0302/3961] batch docs for deletion --- docs/topics/queryset.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 9248e1acb7..cb1fdbb836 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -306,6 +306,12 @@ Batch Queries em2.batch(b).save() b.execute() + # deleting in a batch + b = BatchQuery() + ExampleModel.objects(id=some_id).batch(b).delete() + ExampleModel.objects(id=some_id2).batch(b).delete() + b.execute() + QuerySet method reference ========================= From cd7a36665f94ee0b03bac8d9048f8f1e6f5a70f1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 30 Jun 2013 18:25:13 -0700 Subject: [PATCH 0303/3961] updating field placeholders to use uuid4 instead of uuid1 due to collision0 which appear in some Virtualbox systems fixes #82 --- cqlengine/query.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index d86b504bab..16f6e806b3 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,9 +1,6 @@ -from collections import namedtuple import copy from datetime import datetime -from hashlib import md5 -from time import time -from uuid import uuid1 +from uuid import uuid4 from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns from cqlengine.connection import connection_pool, connection_manager, execute @@ -122,7 +119,7 @@ class EqualsOperator(QueryOperator): class IterableQueryValue(QueryValue): def __init__(self, value): try: - super(IterableQueryValue, self).__init__(value, [uuid1().hex for i in value]) + super(IterableQueryValue, self).__init__(value, [uuid4().hex for i in value]) except TypeError: raise QueryException("in operator arguments must be iterable, {} found".format(value)) @@ -804,7 +801,7 @@ def save(self): #construct query string field_names = zip(*value_pairs)[0] - field_ids = {n:uuid1().hex for n in field_names} + field_ids = {n:uuid4().hex for n in field_names} field_values = dict(value_pairs) query_values = {field_ids[n]:field_values[n] for n in field_names} @@ -878,7 +875,7 @@ def save(self): qs += ['WHERE'] where_statements = [] for name, col in self.model._primary_keys.items(): - field_id = uuid1().hex + field_id = uuid4().hex query_values[field_id] = field_values[name] where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] qs += [' AND '.join(where_statements)] @@ -899,7 +896,7 @@ def delete(self): qs += ['WHERE'] where_statements = [] for name, col in self.model._primary_keys.items(): - field_id = uuid1().hex + field_id = uuid4().hex field_values[field_id] = col.to_database(getattr(self.instance, name)) where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] From 49acd4b271bc9510201c518904e4b14dec527865 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 30 Jun 2013 18:27:33 -0700 Subject: [PATCH 0304/3961] updating changelog and version --- changelog | 3 +++ cqlengine/VERSION | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 6d49d52889..d090b319c6 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.4.10 +* changing query parameter placeholders from uuid1 to uuid4 + 0.4.7 * adding support for passing None into query batch methods to clear any batch objects diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 76914ddc02..e8423da873 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.4.9 +0.4.10 From d417e09eb508e1b81133a0b28cbac9c4715a9fde Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 16:01:32 -0700 Subject: [PATCH 0305/3961] modifying execute function to just return the connection pool Conflicts: cqlengine/connection.py --- cqlengine/connection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 4119d7f9bc..03810c57ee 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -156,7 +156,8 @@ def execute(query, params={}): @contextmanager def connection_manager(): + """ :rtype: ConnectionPool """ global connection_pool - tmp = connection_pool.get() - yield tmp - connection_pool.put(tmp) + # tmp = connection_pool.get() + yield connection_pool + # connection_pool.put(tmp) From 85d75512537f3154d45c01a666d68885baf90374 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 16:02:56 -0700 Subject: [PATCH 0306/3961] updating row serialization Conflicts: cqlengine/connection.py cqlengine/query.py --- cqlengine/connection.py | 11 +++++++++++ cqlengine/management.py | 2 ++ cqlengine/query.py | 16 +++++++--------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 03810c57ee..cca31b118b 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -27,6 +27,17 @@ class CQLConnectionError(CQLEngineException): pass # global connection pool connection_pool = None + +class CQLConnectionError(CQLEngineException): pass + + +class RowResult(tuple): pass + + +def _column_tuple_factory(colnames, values): + return tuple(colnames), [RowResult(v) for v in values] + + def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None, consistency='ONE'): """ Records the hosts and connects to one of them diff --git a/cqlengine/management.py b/cqlengine/management.py index b3504b919e..dbe6a497ae 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -40,11 +40,13 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, execute(query) + def delete_keyspace(name): with connection_manager() as con: if name in [k.name for k in con.client.describe_keyspaces()]: execute("DROP KEYSPACE {}".format(name)) + def create_table(model, create_missing_keyspace=True): if model.__abstract__: diff --git a/cqlengine/query.py b/cqlengine/query.py index 16f6e806b3..4cf1a86711 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -3,7 +3,7 @@ from uuid import uuid4 from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns -from cqlengine.connection import connection_pool, connection_manager, execute +from cqlengine.connection import connection_manager, execute, RowResult from cqlengine.exceptions import CQLEngineException from cqlengine.functions import QueryValue, Token @@ -353,11 +353,8 @@ def _execute_query(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: - self._cur = execute(self._select_query(), self._where_values()) - self._result_cache = [None]*self._cur.rowcount - if self._cur.description: - names = [i[0] for i in self._cur.description] - self._construct_result = self._create_result_constructor(names) + columns, self._result_cache = execute(self._select_query(), self._where_values()) + self._construct_result = self._create_result_constructor(columns) def _fill_result_cache_to_idx(self, idx): self._execute_query() @@ -368,7 +365,9 @@ def _fill_result_cache_to_idx(self, idx): if qty < 1: return else: - for values in self._cur.fetchmany(qty): + start = max(0, self._result_idx) + stop = start + qty + for values in self._result_cache[start:stop]: self._result_idx += 1 self._result_cache[self._result_idx] = self._construct_result(values) @@ -382,7 +381,7 @@ def __iter__(self): for idx in range(len(self._result_cache)): instance = self._result_cache[idx] - if instance is None: + if isinstance(instance, RowResult): self._fill_result_cache_to_idx(idx) yield self._result_cache[idx] @@ -716,7 +715,6 @@ def _construct_instance(values): return instance return _construct_instance - columns = [model._columns[db_map[name]] for name in names] if self._flat_values_list: return (lambda values: columns[0].to_python(values[0])) else: From 0d1ae6e6ca28554e6305ff33ad7c4779683f5413 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 26 Jun 2013 08:04:15 -0700 Subject: [PATCH 0307/3961] fixing a problem with the result cache filler --- cqlengine/connection.py | 3 ++- cqlengine/query.py | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index cca31b118b..8b3a8f0479 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -31,7 +31,8 @@ class CQLConnectionError(CQLEngineException): pass class CQLConnectionError(CQLEngineException): pass -class RowResult(tuple): pass +class RowResult(tuple): + pass def _column_tuple_factory(colnames, values): diff --git a/cqlengine/query.py b/cqlengine/query.py index 4cf1a86711..ea81376f7c 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -365,11 +365,9 @@ def _fill_result_cache_to_idx(self, idx): if qty < 1: return else: - start = max(0, self._result_idx) - stop = start + qty - for values in self._result_cache[start:stop]: + for idx in range(qty): self._result_idx += 1 - self._result_cache[self._result_idx] = self._construct_result(values) + self._result_cache[self._result_idx] = self._construct_result(self._result_cache[self._result_idx]) #return the connection to the connection pool if we have all objects if self._result_cache and self._result_idx == (len(self._result_cache) - 1): From d3137dc321fa82aa848a968e406e12f51fdb8f52 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 16:04:15 -0700 Subject: [PATCH 0308/3961] fixing count method Conflicts: cqlengine/query.py --- cqlengine/query.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index ea81376f7c..7431bd8cb9 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,6 +1,9 @@ import copy from datetime import datetime from uuid import uuid4 +from hashlib import md5 +from time import time +from uuid import uuid1 from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns from cqlengine.connection import connection_manager, execute, RowResult @@ -545,8 +548,8 @@ def count(self): qs = ' '.join(qs) - cur = execute(qs, self._where_values()) - return cur.fetchone()[0] + result = execute(qs, self._where_values(), row_factory=decoder.tuple_factory) + return result[0][0] else: return len(self._result_cache) From 533d70b1be65ac5348720c24e7401871112397d4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 25 Jun 2013 10:12:12 -0700 Subject: [PATCH 0309/3961] updating management module to work with the native driver --- cqlengine/management.py | 83 ++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index dbe6a497ae..23f32795c6 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -14,36 +14,30 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, :param **replication_values: 1.2 only, additional values to ad to the replication data map """ with connection_manager() as con: - #TODO: check system tables instead of using cql thrifteries - if not any([name == k.name for k in con.client.describe_keyspaces()]): - # if name not in [k.name for k in con.con.client.describe_keyspaces()]: - try: - #Try the 1.1 method - execute("""CREATE KEYSPACE {} - WITH strategy_class = '{}' - AND strategy_options:replication_factor={};""".format(name, strategy_class, replication_factor)) - except CQLEngineException: - #try the 1.2 method - replication_map = { - 'class': strategy_class, - 'replication_factor':replication_factor - } - replication_map.update(replication_values) - - query = """ - CREATE KEYSPACE {} - WITH REPLICATION = {} - """.format(name, json.dumps(replication_map).replace('"', "'")) - - if strategy_class != 'SimpleStrategy': - query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false') - - execute(query) + keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) + if name not in [r['keyspace_name'] for r in keyspaces]: + #try the 1.2 method + replication_map = { + 'class': strategy_class, + 'replication_factor':replication_factor + } + replication_map.update(replication_values) + + query = """ + CREATE KEYSPACE {} + WITH REPLICATION = {} + """.format(name, json.dumps(replication_map).replace('"', "'")) + + if strategy_class != 'SimpleStrategy': + query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false') + + execute(query) def delete_keyspace(name): with connection_manager() as con: - if name in [k.name for k in con.client.describe_keyspaces()]: + keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) + if name in [r['keyspace_name'] for r in keyspaces]: execute("DROP KEYSPACE {}".format(name)) @@ -56,16 +50,17 @@ def create_table(model, create_missing_keyspace=True): cf_name = model.column_family_name() raw_cf_name = model.column_family_name(include_keyspace=False) + ks_name = model._get_keyspace() #create missing keyspace if create_missing_keyspace: - create_keyspace(model._get_keyspace()) + create_keyspace(ks_name) with connection_manager() as con: - ks_info = con.client.describe_keyspace(model._get_keyspace()) + tables = con.execute("SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = %s", [ks_name]) #check for an existing column family #TODO: check system tables instead of using cql thrifteries - if not any([raw_cf_name == cf.name for cf in ks_info.cf_defs]): + if raw_cf_name not in tables: qs = ['CREATE TABLE {}'.format(cf_name)] #add column types @@ -105,10 +100,9 @@ def add_column(col): #get existing index names, skip ones that already exist with connection_manager() as con: - ks_info = con.client.describe_keyspace(model._get_keyspace()) + idx_names = con.execute("SELECT index_name from system.\"IndexInfo\" WHERE table_name=%s", [raw_cf_name]) - cf_defs = [cf for cf in ks_info.cf_defs if cf.name == raw_cf_name] - idx_names = [i.index_name for i in cf_defs[0].column_metadata] if cf_defs else [] + idx_names = [i['index_name'] for i in idx_names] idx_names = filter(None, idx_names) indexes = [c for n,c in model._columns.items() if c.index] @@ -120,22 +114,19 @@ def add_column(col): qs += ['("{}")'.format(column.db_field_name)] qs = ' '.join(qs) - try: - execute(qs) - except CQLEngineException as ex: - # 1.2 doesn't return cf names, so we have to examine the exception - # and ignore if it says the index already exists - if "Index already exists" not in unicode(ex): - raise + execute(qs) def delete_table(model): - cf_name = model.column_family_name() - try: - execute('drop table {};'.format(cf_name)) - except CQLEngineException as ex: - #don't freak out if the table doesn't exist - if 'Cannot drop non existing column family' not in unicode(ex): - raise + # don't try to delete non existant tables + ks_name = model._get_keyspace() + with connection_manager() as con: + tables = con.execute("SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = %s", [ks_name]) + raw_cf_name = model.column_family_name(include_keyspace=False) + if raw_cf_name not in [t['columnfamily_name'] for t in tables]: + return + + cf_name = model.column_family_name() + execute('drop table {};'.format(cf_name)) From 5c6e8601ec0b73b14db76ab713f1b5fb0d8e8614 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 16:09:03 -0700 Subject: [PATCH 0310/3961] updating management functions to work with the cql driver --- cqlengine/management.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 23f32795c6..d66e808422 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -56,7 +56,10 @@ def create_table(model, create_missing_keyspace=True): create_keyspace(ks_name) with connection_manager() as con: - tables = con.execute("SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = %s", [ks_name]) + tables = con.execute( + "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", + {'ks_name': ks_name} + ) #check for an existing column family #TODO: check system tables instead of using cql thrifteries @@ -100,7 +103,10 @@ def add_column(col): #get existing index names, skip ones that already exist with connection_manager() as con: - idx_names = con.execute("SELECT index_name from system.\"IndexInfo\" WHERE table_name=%s", [raw_cf_name]) + idx_names = con.execute( + "SELECT index_name from system.\"IndexInfo\" WHERE table_name=:table_name", + {'table_name': raw_cf_name} + ) idx_names = [i['index_name'] for i in idx_names] idx_names = filter(None, idx_names) @@ -122,7 +128,10 @@ def delete_table(model): # don't try to delete non existant tables ks_name = model._get_keyspace() with connection_manager() as con: - tables = con.execute("SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = %s", [ks_name]) + tables = con.execute( + "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", + {'ks_name': ks_name} + ) raw_cf_name = model.column_family_name(include_keyspace=False) if raw_cf_name not in [t['columnfamily_name'] for t in tables]: return From 8aa4b04374539b95cbc4b744c77ea79d5f87704c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 16:18:38 -0700 Subject: [PATCH 0311/3961] updating connection to work with the cql driver --- cqlengine/connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 8b3a8f0479..588390b398 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -153,9 +153,11 @@ def execute(self, query, params): con = self.get() cur = con.cursor() cur.execute(query, params) + columns = [i[0] for i in cur.description or []] + results = [RowResult(r) for r in cur.fetchall()] LOG.debug('{} {}'.format(query, repr(params))) self.put(con) - return cur + return columns, results except cql.ProgrammingError as ex: raise CQLEngineException(unicode(ex)) except TTransportException: @@ -163,9 +165,11 @@ def execute(self, query, params): raise CQLEngineException("Could not execute query against the cluster") + def execute(query, params={}): return connection_pool.execute(query, params) + @contextmanager def connection_manager(): """ :rtype: ConnectionPool """ From 0590c5e0f6d7457bdafbff6f7f3a0122e938c804 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 16:19:07 -0700 Subject: [PATCH 0312/3961] updating management to work with with the cql driver --- cqlengine/management.py | 12 ++++++------ cqlengine/query.py | 6 ------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index d66e808422..293e2ff4c7 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -14,8 +14,8 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, :param **replication_values: 1.2 only, additional values to ad to the replication data map """ with connection_manager() as con: - keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) - if name not in [r['keyspace_name'] for r in keyspaces]: + _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) + if name not in [r[0] for r in keyspaces]: #try the 1.2 method replication_map = { 'class': strategy_class, @@ -103,12 +103,12 @@ def add_column(col): #get existing index names, skip ones that already exist with connection_manager() as con: - idx_names = con.execute( + _, idx_names = con.execute( "SELECT index_name from system.\"IndexInfo\" WHERE table_name=:table_name", {'table_name': raw_cf_name} ) - idx_names = [i['index_name'] for i in idx_names] + idx_names = [i[0] for i in idx_names] idx_names = filter(None, idx_names) indexes = [c for n,c in model._columns.items() if c.index] @@ -128,12 +128,12 @@ def delete_table(model): # don't try to delete non existant tables ks_name = model._get_keyspace() with connection_manager() as con: - tables = con.execute( + _, tables = con.execute( "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", {'ks_name': ks_name} ) raw_cf_name = model.column_family_name(include_keyspace=False) - if raw_cf_name not in [t['columnfamily_name'] for t in tables]: + if raw_cf_name not in [t[0] for t in tables]: return cf_name = model.column_family_name() diff --git a/cqlengine/query.py b/cqlengine/query.py index 7431bd8cb9..b8b05f2536 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -306,12 +306,6 @@ def __len__(self): self._execute_query() return len(self._result_cache) - def __del__(self): - if self._con: - self._con.close() - self._con = None - self._cur = None - #----query generation / execution---- def _where_clause(self): From 92ef6fce17c766ee8d3a3b8390ed473ac31810c4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 16:26:02 -0700 Subject: [PATCH 0313/3961] fixing some problems with the eager result loading --- cqlengine/management.py | 10 +++++++--- cqlengine/query.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 293e2ff4c7..46676fe31e 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -36,8 +36,8 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, def delete_keyspace(name): with connection_manager() as con: - keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) - if name in [r['keyspace_name'] for r in keyspaces]: + _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) + if name in [r[0] for r in keyspaces]: execute("DROP KEYSPACE {}".format(name)) @@ -120,7 +120,11 @@ def add_column(col): qs += ['("{}")'.format(column.db_field_name)] qs = ' '.join(qs) - execute(qs) + try: + execute(qs) + except CQLEngineException: + # index already exists + pass def delete_table(model): diff --git a/cqlengine/query.py b/cqlengine/query.py index b8b05f2536..a64a0fd8e8 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -542,7 +542,7 @@ def count(self): qs = ' '.join(qs) - result = execute(qs, self._where_values(), row_factory=decoder.tuple_factory) + _, result = execute(qs, self._where_values()) return result[0][0] else: return len(self._result_cache) @@ -710,6 +710,7 @@ def _construct_instance(values): return instance return _construct_instance + columns = [model._columns[n] for n in names] if self._flat_values_list: return (lambda values: columns[0].to_python(values[0])) else: From e9b055452af5c26b7e33e3fa006c2fbc09d69bc2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 17:39:21 -0700 Subject: [PATCH 0314/3961] wrapping query results in a named tuple --- cqlengine/connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 588390b398..97c96d4fab 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -34,6 +34,8 @@ class CQLConnectionError(CQLEngineException): pass class RowResult(tuple): pass +QueryResult = namedtuple('RowResult', ('columns', 'results')) + def _column_tuple_factory(colnames, values): return tuple(colnames), [RowResult(v) for v in values] @@ -157,7 +159,7 @@ def execute(self, query, params): results = [RowResult(r) for r in cur.fetchall()] LOG.debug('{} {}'.format(query, repr(params))) self.put(con) - return columns, results + return QueryResult(columns, results) except cql.ProgrammingError as ex: raise CQLEngineException(unicode(ex)) except TTransportException: From 779a960edb054dcc01f19c308d0fdfc0507e5fbe Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 1 Jul 2013 17:39:33 -0700 Subject: [PATCH 0315/3961] updating changelog and version --- changelog | 5 +++++ cqlengine/VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index d090b319c6..69582a2cd9 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,10 @@ CHANGELOG +0.5 +* eagerly loading results into the query result cache, the cql driver does this anyway, + and pulling them from the cursor was causing some problems with gevented queries, + this will cause some breaking changes for users calling execute directly + 0.4.10 * changing query parameter placeholders from uuid1 to uuid4 diff --git a/cqlengine/VERSION b/cqlengine/VERSION index e8423da873..2eb3c4fe4e 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.4.10 +0.5 From 8091278d6ab4a427da6582f02a4feec8f9194eb4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 5 Jul 2013 17:44:25 -0700 Subject: [PATCH 0316/3961] removing auto column from docs --- docs/topics/models.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index f599f2cfb7..16d95db76d 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -146,6 +146,3 @@ Model Attributes *Optional.* Sets the name of the keyspace used by this model. Defaulst to cqlengine -Automatic Primary Keys -====================== - CQL requires that all tables define at least one primary key. If a model definition does not include a primary key column, cqlengine will automatically add a uuid primary key column named ``id``. From 6ea6d178836291950ba9b39dd9a564f9fe7478e2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 6 Jul 2013 08:28:40 -0700 Subject: [PATCH 0317/3961] fixing doc version --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 847ef50dce..37422f20a5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,7 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -__cqlengine_version_path__ = os.path.realpath(__file__ + '/../VERSION') +__cqlengine_version_path__ = os.path.realpath(__file__ + '/../../cqlengine/VERSION') # The short X.Y version. version = open(__cqlengine_version_path__, 'r').readline().strip() # The full version, including alpha/beta/rc tags. From 714f1e0214620f9a491dd08508ee34cacea24b86 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 6 Jul 2013 08:28:50 -0700 Subject: [PATCH 0318/3961] updating columns docs --- docs/topics/columns.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 987d7d7c4f..d323ac216a 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -46,18 +46,30 @@ Columns Stores a datetime value. - Python's datetime.now callable is set as the default value for this column :: - columns.DateTime() .. class:: UUID() Stores a type 1 or type 4 UUID. - Python's uuid.uuid4 callable is set as the default value for this column. :: - columns.UUID() +.. class:: TimeUUID() + + Stores a UUID value as the cql type 'timeuuid' :: + + columns.TimeUUID() + + .. classmethod:: from_datetime(dt) + + generates a TimeUUID for the given datetime + + :param dt: the datetime to create a time uuid from + :type dt: datetime.datetime + + :returns: a time uuid created from the given datetime + :rtype: uuid1 + .. class:: Boolean() Stores a boolean True or False value :: @@ -91,6 +103,7 @@ Collection Type Columns .. code-block:: python class Person(Model): + id = columns.UUID(primary_key=True, default=uuid.uuid4) first_name = columns.Text() last_name = columns.Text() From 2a779643bd0534360e1a194aca4480c360214582 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 6 Jul 2013 08:32:23 -0700 Subject: [PATCH 0319/3961] fixing the default value on the example partition key --- README.md | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10531060e5..23516e78fe 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ from cqlengine.models import Model class ExampleModel(Model): read_repair_chance = 0.05 # optional - defaults to 0.1 - example_id = columns.UUID(primary_key=True) + example_id = columns.UUID(primary_key=True, default=uuid.uuid4) example_type = columns.Integer(index=True) created_at = columns.DateTime() description = columns.Text(required=False) diff --git a/docs/index.rst b/docs/index.rst index 9dff72fd56..2053e35934 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,7 +36,7 @@ Getting Started from cqlengine import Model class ExampleModel(Model): - example_id = columns.UUID(primary_key=True) + example_id = columns.UUID(primary_key=True, default=uuid.uuid4) example_type = columns.Integer(index=True) created_at = columns.DateTime() description = columns.Text(required=False) From 8e0cccf8f57c74808fbca6d7c2eecc0b682ff603 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 6 Jul 2013 08:34:49 -0700 Subject: [PATCH 0320/3961] removing other references to auto defaults and ids from the docs --- README.md | 2 -- docs/index.rst | 2 -- docs/topics/models.rst | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 23516e78fe..09e2014241 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,6 @@ class ExampleModel(Model): >>> em6 = ExampleModel.create(example_type=1, description="example6", created_at=datetime.now()) >>> em7 = ExampleModel.create(example_type=1, description="example7", created_at=datetime.now()) >>> em8 = ExampleModel.create(example_type=1, description="example8", created_at=datetime.now()) -# Note: the UUID and DateTime columns will create uuid4 and datetime.now -# values automatically if we don't specify them when creating new rows #and now we can run some queries against our table >>> ExampleModel.objects.count() diff --git a/docs/index.rst b/docs/index.rst index 2053e35934..243c365313 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,8 +58,6 @@ Getting Started >>> em6 = ExampleModel.create(example_type=1, description="example6", created_at=datetime.now()) >>> em7 = ExampleModel.create(example_type=1, description="example7", created_at=datetime.now()) >>> em8 = ExampleModel.create(example_type=1, description="example8", created_at=datetime.now()) - # Note: the UUID and DateTime columns will create uuid4 and datetime.now - # values automatically if we don't specify them when creating new rows #and now we can run some queries against our table >>> ExampleModel.objects.count() diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 16d95db76d..3360ba7fee 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -41,7 +41,7 @@ The Person model would create this CQL table: Columns ======= - Columns in your models map to columns in your CQL table. You define CQL columns by defining column attributes on your model classes. For a model to be valid it needs at least one primary key column (defined automatically if you don't define one) and one non-primary key column. + Columns in your models map to columns in your CQL table. You define CQL columns by defining column attributes on your model classes. For a model to be valid it needs at least one primary key column and one non-primary key column. Just as in CQL, the order you define your columns in is important, and is the same order they are defined in on a model's corresponding table. From 600c9175cd648cff965254b2c9aa317363851fa3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 6 Jul 2013 08:45:51 -0700 Subject: [PATCH 0321/3961] updating description --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 243c365313..1122599cca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,7 @@ cqlengine documentation .. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU -cqlengine is a Cassandra CQL 3 Object Mapper for Python with an interface similar to the Django orm and mongoengine +cqlengine is a Cassandra CQL 3 Object Mapper for Python :ref:`getting-started` From ce5da33e57f697922d430ec89e4d68a2c0295334 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 9 Jul 2013 21:25:07 -0700 Subject: [PATCH 0322/3961] added a retry until no servers can be contacted --- cqlengine/connection.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 97c96d4fab..dde5e3da6f 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -151,21 +151,23 @@ def _create_connection(self): raise CQLConnectionError("Could not connect to any server in cluster") def execute(self, query, params): - try: - con = self.get() - cur = con.cursor() - cur.execute(query, params) - columns = [i[0] for i in cur.description or []] - results = [RowResult(r) for r in cur.fetchall()] - LOG.debug('{} {}'.format(query, repr(params))) - self.put(con) - return QueryResult(columns, results) - except cql.ProgrammingError as ex: - raise CQLEngineException(unicode(ex)) - except TTransportException: - pass + while True: + try: + con = self.get() + cur = con.cursor() + cur.execute(query, params) + columns = [i[0] for i in cur.description or []] + results = [RowResult(r) for r in cur.fetchall()] + LOG.debug('{} {}'.format(query, repr(params))) + self.put(con) + return QueryResult(columns, results) + except CQLConnectionError as ex: + raise CQLEngineException("Could not execute query against the cluster") + except cql.ProgrammingError as ex: + raise CQLEngineException(unicode(ex)) + except TTransportException: + raise CQLEngineException("Could not execute query against the cluster") - raise CQLEngineException("Could not execute query against the cluster") def execute(query, params={}): From 21bb32c045ecd484986c324298483e3a5acfffbb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 13 Jul 2013 11:57:52 -0700 Subject: [PATCH 0323/3961] quoting clustering order column, fixes #88 --- cqlengine/management.py | 4 ++-- cqlengine/tests/management/test_management.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 46676fe31e..be96b79f80 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -85,9 +85,9 @@ def add_column(col): with_qs = ['read_repair_chance = {}'.format(model.__read_repair_chance__)] - _order = ["%s %s" % (c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] + _order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] if _order: - with_qs.append("clustering order by ({})".format(', '.join(_order))) + with_qs.append('clustering order by ({})'.format(', '.join(_order))) # add read_repair_chance qs += ['WITH {}'.format(' AND '.join(with_qs))] diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 4e271c58ce..5e00bb977d 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -7,6 +7,8 @@ from mock import Mock, MagicMock, MagicProxy, patch from cqlengine import management from cqlengine.tests.query.test_queryset import TestModel +from cqlengine.models import Model +from cqlengine import columns from cql.thrifteries import ThriftConnection @@ -65,3 +67,23 @@ def test_multiple_deletes_dont_fail(self): delete_table(TestModel) delete_table(TestModel) + +class LowercaseKeyModel(Model): + first_key = columns.Integer(primary_key=True) + second_key = columns.Integer(primary_key=True) + some_data = columns.Text() + +class CapitalizedKeyModel(Model): + firstKey = columns.Integer(primary_key=True) + secondKey = columns.Integer(primary_key=True) + someData = columns.Text() + +class CapitalizedKeyTest(BaseCassEngTestCase): + + def test_table_definition(self): + """ Tests that creating a table with capitalized column names succeedso """ + create_table(LowercaseKeyModel) + create_table(CapitalizedKeyModel) + + delete_table(LowercaseKeyModel) + delete_table(CapitalizedKeyModel) From 019f1435746a073cd799870cbab7ae4fcf7585b0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 13 Jul 2013 12:04:04 -0700 Subject: [PATCH 0324/3961] updating changelog and version# --- changelog | 4 ++++ cqlengine/VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 69582a2cd9..752a1eaa48 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.5.1 +* improving connection pooling +* fixing bug with clustering order columns not being quoted + 0.5 * eagerly loading results into the query result cache, the cql driver does this anyway, and pulling them from the cursor was causing some problems with gevented queries, diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 2eb3c4fe4e..4b9fcbec10 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.5 +0.5.1 From e11a0b0d7c93366a4875e2ef5b1b998941017014 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 13 Jul 2013 12:13:48 -0700 Subject: [PATCH 0325/3961] adding hex conversion to Bytes column --- cqlengine/columns.py | 5 +++++ cqlengine/tests/columns/test_value_io.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 3b7a22d06e..f82e468fe7 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -195,6 +195,11 @@ def get_cql(self): class Bytes(Column): db_type = 'blob' + def to_database(self, value): + val = super(Bytes, self).to_database(value) + if val is None: return + return val.encode('hex') + class Ascii(Column): db_type = 'ascii' diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 64054279cc..1c822a9e5d 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -77,6 +77,12 @@ def test_column_io(self): #delete self._generated_model.filter(pkey=pkey).delete() +class TestBlobIO(BaseColumnIOTest): + + column = columns.Bytes + pkey_val = 'blake', uuid4().bytes + data_val = 'eggleston', uuid4().bytes + class TestTextIO(BaseColumnIOTest): column = columns.Text From 69c288019733a8dd3fad95c627803ead81b995a2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 13 Jul 2013 12:14:15 -0700 Subject: [PATCH 0326/3961] updating version and changelog --- changelog | 3 +++ cqlengine/VERSION | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 752a1eaa48..0da8f3c7e9 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.5.2 +* adding hex conversion to Bytes column + 0.5.1 * improving connection pooling * fixing bug with clustering order columns not being quoted diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 4b9fcbec10..cb0c939a93 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.5.1 +0.5.2 From 49a6e16d27ba5eb6fc4fccd2259eba665505c764 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 23 Jul 2013 12:38:26 -0700 Subject: [PATCH 0327/3961] fixing a mutable kwarg --- cqlengine/connection.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index dde5e3da6f..b2d62023f3 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -169,11 +169,10 @@ def execute(self, query, params): raise CQLEngineException("Could not execute query against the cluster") - -def execute(query, params={}): +def execute(query, params=None): + params = params or {} return connection_pool.execute(query, params) - @contextmanager def connection_manager(): """ :rtype: ConnectionPool """ From c99903aa9aa8084d04d7d477263536b803d4a6c1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 23 Jul 2013 12:48:06 -0700 Subject: [PATCH 0328/3961] adding counter column --- cqlengine/columns.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index f82e468fe7..cd01cf40ee 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -243,6 +243,24 @@ def to_database(self, value): return self.validate(value) +class Counter(Integer): + db_type = 'counter' + + def get_update_statement(self, val, prev, ctx): + val = self.to_database(val) + prev = self.to_database(prev or 0) + + delta = val - prev + if delta == 0: + return [] + + field_id = uuid4().hex + sign = '+' if delta > 0 else '-' + delta = abs(delta) + ctx[field_id] = delta + return ['"{0}" = "{0}" {1} {2}'.format(self.db_field_name, sign, delta)] + + class DateTime(Column): db_type = 'timestamp' @@ -363,7 +381,6 @@ def from_datetime(self, dt): clock_seq_hi_variant, clock_seq_low, node), version=1) - class Boolean(Column): db_type = 'boolean' @@ -419,11 +436,6 @@ def to_python(self, value): def to_database(self, value): return self.validate(value) -class Counter(Column): - #TODO: counter field - def __init__(self, **kwargs): - super(Counter, self).__init__(**kwargs) - raise NotImplementedError class BaseContainerColumn(Column): """ From ceb487af88507aaa0df82cefbbf528d63c5248de Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 23 Jul 2013 12:48:34 -0700 Subject: [PATCH 0329/3961] adding counter support to update statement generator --- cqlengine/query.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a64a0fd8e8..4327ff1155 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -5,6 +5,7 @@ from time import time from uuid import uuid1 from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns +from cqlengine.columns import Counter from cqlengine.connection import connection_manager, execute, RowResult @@ -809,8 +810,9 @@ def save(self): for name, col in self.model._columns.items(): if not col.is_primary_key: val = values.get(name) - if val is None: continue - if isinstance(col, BaseContainerColumn): + if val is None: + continue + if isinstance(col, (BaseContainerColumn, Counter)): #remove value from query values, the column will handle it query_values.pop(field_ids.get(name), None) From 17b77ea9e4b03a6caa4611db3966610dfc491a42 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 23 Jul 2013 12:53:00 -0700 Subject: [PATCH 0330/3961] adding support for using set statements for counter columns if there is no previous value, reformatting --- cqlengine/columns.py | 18 ++++++++++++++++-- .../tests/columns/test_container_columns.py | 3 ++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index cd01cf40ee..0f505ccc02 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -192,6 +192,7 @@ def cql(self): def get_cql(self): return '"{}"'.format(self.db_field_name) + class Bytes(Column): db_type = 'blob' @@ -200,9 +201,11 @@ def to_database(self, value): if val is None: return return val.encode('hex') + class Ascii(Column): db_type = 'ascii' + class Text(Column): db_type = 'text' @@ -248,13 +251,19 @@ class Counter(Integer): def get_update_statement(self, val, prev, ctx): val = self.to_database(val) - prev = self.to_database(prev or 0) + prev = self.to_database(prev) + field_id = uuid4().hex + + # use a set statement if there is no + # previous value to compute a delta from + if prev is None: + ctx[field_id] = val + return ['"{}" = :{}'.format(self.db_field_name, field_id)] delta = val - prev if delta == 0: return [] - field_id = uuid4().hex sign = '+' if delta > 0 else '-' delta = abs(delta) ctx[field_id] = delta @@ -334,6 +343,7 @@ def to_database(self, value): from uuid import UUID as pyUUID, getnode + class TimeUUID(UUID): """ UUID containing timestamp @@ -417,6 +427,7 @@ def to_python(self, value): def to_database(self, value): return self.validate(value) + class Decimal(Column): db_type = 'decimal' @@ -476,6 +487,7 @@ def get_update_statement(self, val, prev, ctx): """ raise NotImplementedError + class Set(BaseContainerColumn): """ Stores a set of unordered, unique values @@ -565,6 +577,7 @@ def get_update_statement(self, val, prev, ctx): return statements + class List(BaseContainerColumn): """ Stores a list of ordered values @@ -789,6 +802,7 @@ def get_delete_statement(self, val, prev, ctx): return del_statements + class _PartitionKeysToken(Column): """ virtual column representing token of partition columns. diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 6cf218299c..cd11fb5abb 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -28,7 +28,9 @@ def to_database(self, value): if value is None: return return json.dumps(value) + class TestSetColumn(BaseCassEngTestCase): + @classmethod def setUpClass(cls): super(TestSetColumn, cls).setUpClass() @@ -40,7 +42,6 @@ def tearDownClass(cls): super(TestSetColumn, cls).tearDownClass() delete_table(TestSetModel) - def test_empty_set_initial(self): """ tests that sets are set() by default, should never be none From 19d6984e727d79af79c466b0c3842de7a4a46207 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 24 Jul 2013 09:49:15 -0700 Subject: [PATCH 0331/3961] updating the model and query classes to handle models with counter columns correctly --- cqlengine/columns.py | 26 +++++++++++++++----------- cqlengine/models.py | 10 ++++++++-- cqlengine/query.py | 9 +++++---- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 0f505ccc02..56221c2dd1 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -249,22 +249,26 @@ def to_database(self, value): class Counter(Integer): db_type = 'counter' + def __init__(self, + index=False, + db_field=None, + required=False): + super(Counter, self).__init__( + primary_key=False, + partition_key=False, + index=index, + db_field=db_field, + default=0, + required=required, + ) + def get_update_statement(self, val, prev, ctx): val = self.to_database(val) - prev = self.to_database(prev) + prev = self.to_database(prev or 0) field_id = uuid4().hex - # use a set statement if there is no - # previous value to compute a delta from - if prev is None: - ctx[field_id] = val - return ['"{}" = :{}'.format(self.db_field_name, field_id)] - delta = val - prev - if delta == 0: - return [] - - sign = '+' if delta > 0 else '-' + sign = '-' if delta < 0 else '+' delta = abs(delta) ctx[field_id] = delta return ['"{0}" = "{0}" {1} {2}'.format(self.db_field_name, sign, delta)] diff --git a/cqlengine/models.py b/cqlengine/models.py index a2d9b6abce..640e023bd5 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -70,6 +70,7 @@ def __init__(self, column): def _get_column(self): return self.column + class ColumnDescriptor(object): """ Handles the reading and writing of column values to and from @@ -127,6 +128,7 @@ class BaseModel(object): """ class DoesNotExist(_DoesNotExist): pass + class MultipleObjectsReturned(_MultipleObjectsReturned): pass objects = QuerySetDescriptor() @@ -316,14 +318,17 @@ def _transform_column(col_name, col_obj): column_definitions = inherited_columns.items() + column_definitions - #columns defined on model, excludes automatically - #defined columns defined_columns = OrderedDict(column_definitions) #prepend primary key if one hasn't been defined if not is_abstract and not any([v.primary_key for k,v in column_definitions]): raise ModelDefinitionException("At least 1 primary key is required.") + counter_columns = [c for c in defined_columns.values() if isinstance(c, columns.Counter)] + data_columns = [c for c in defined_columns.values() if not c.primary_key and not isinstance(c, columns.Counter)] + if counter_columns and data_columns: + raise ModelDefinitionException('counter models may not have data columns') + has_partition_keys = any(v.partition_key for (k, v) in column_definitions) #TODO: check that the defined columns don't conflict with any of the Model API's existing attributes/methods @@ -382,6 +387,7 @@ def _transform_column(col_name, col_obj): attrs['_partition_keys'] = partition_keys attrs['_clustering_keys'] = clustering_keys + attrs['_has_counter'] = len(counter_columns) > 0 #setup class exceptions DoesNotExistBase = None diff --git a/cqlengine/query.py b/cqlengine/query.py index 4327ff1155..194b4c9330 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -801,7 +801,7 @@ def save(self): query_values = {field_ids[n]:field_values[n] for n in field_names} qs = [] - if self.instance._can_update(): + if self.instance._has_counter or self.instance._can_update(): qs += ["UPDATE {}".format(self.column_family_name)] qs += ["SET"] @@ -818,7 +818,7 @@ def save(self): val_mgr = self.instance._values[name] set_statements += col.get_update_statement(val, val_mgr.previous_value, query_values) - pass + else: set_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] qs += [', '.join(set_statements)] @@ -831,8 +831,9 @@ def save(self): qs += [' AND '.join(where_statements)] - # clear the qs if there are not set statements - if not set_statements: qs = [] + # clear the qs if there are no set statements and this is not a counter model + if not set_statements and not self.instance._has_counter: + qs = [] else: qs += ["INSERT INTO {}".format(self.column_family_name)] From 3c24995481d29d145f99217290a2ca3f1cea4be6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 24 Jul 2013 09:49:41 -0700 Subject: [PATCH 0332/3961] adding tests around counter columns --- .../tests/columns/test_counter_column.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 cqlengine/tests/columns/test_counter_column.py diff --git a/cqlengine/tests/columns/test_counter_column.py b/cqlengine/tests/columns/test_counter_column.py new file mode 100644 index 0000000000..f92cb4f418 --- /dev/null +++ b/cqlengine/tests/columns/test_counter_column.py @@ -0,0 +1,64 @@ +from uuid import uuid4 + +from cqlengine import Model, ValidationError +from cqlengine import columns +from cqlengine.management import create_table, delete_table +from cqlengine.tests.base import BaseCassEngTestCase + + +class TestCounterModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + cluster = columns.UUID(primary_key=True, default=uuid4) + counter = columns.Counter() + + +class TestClassConstruction(BaseCassEngTestCase): + + def test_defining_a_non_counter_column_fails(self): + """ Tests that defining a non counter column field fails """ + + def test_defining_a_primary_key_counter_column_fails(self): + """ Tests that defining primary keys on counter columns fails """ + + +class TestCounterColumn(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestCounterColumn, cls).setUpClass() + delete_table(TestCounterModel) + create_table(TestCounterModel) + + @classmethod + def tearDownClass(cls): + super(TestCounterColumn, cls).tearDownClass() + delete_table(TestCounterModel) + + def test_updates(self): + """ Tests that counter updates work as intended """ + instance = TestCounterModel.create() + instance.counter += 5 + instance.save() + + actual = TestCounterModel.get(partition=instance.partition) + assert actual.counter == 5 + + def test_concurrent_updates(self): + """ Tests updates from multiple queries reaches the correct value """ + instance = TestCounterModel.create() + new1 = TestCounterModel.get(partition=instance.partition) + new2 = TestCounterModel.get(partition=instance.partition) + + new1.counter += 5 + new1.save() + new2.counter += 5 + new2.save() + + actual = TestCounterModel.get(partition=instance.partition) + assert actual.counter == 10 + + def test_update_from_none(self): + """ Tests that updating from None uses a create statement """ + + def test_multiple_inserts(self): + """ Tests inserting over existing data works as expected """ From 58223eab242d23f43cc67ee7a09ebe6ffbff184f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 31 Jul 2013 12:35:49 -0700 Subject: [PATCH 0333/3961] Only adds new columns. Will not remove non existing. --- cqlengine/connection.py | 2 + cqlengine/management.py | 41 ++++++++++++++-- cqlengine/tests/management/test_management.py | 48 +++++++++++++++++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index dde5e3da6f..12d37c8ea7 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -154,6 +154,8 @@ def execute(self, query, params): while True: try: con = self.get() + if not con: + raise CQLEngineException("Error calling execute without calling setup.") cur = con.cursor() cur.execute(query, params) columns = [i[0] for i in cur.description or []] diff --git a/cqlengine/management.py b/cqlengine/management.py index be96b79f80..ce72e778b9 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -3,6 +3,13 @@ from cqlengine.connection import connection_manager, execute from cqlengine.exceptions import CQLEngineException +import logging +from collections import namedtuple +Field = namedtuple('Field', ['name', 'type']) + +logger = logging.getLogger(__name__) + + def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, durable_writes=True, **replication_values): """ creates a keyspace @@ -60,6 +67,7 @@ def create_table(model, create_missing_keyspace=True): "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", {'ks_name': ks_name} ) + tables = [x[0] for x in tables.results] #check for an existing column family #TODO: check system tables instead of using cql thrifteries @@ -67,9 +75,9 @@ def create_table(model, create_missing_keyspace=True): qs = ['CREATE TABLE {}'.format(cf_name)] #add column types - pkeys = [] - ckeys = [] - qtypes = [] + pkeys = [] # primary keys + ckeys = [] # clustering keys + qtypes = [] # field types def add_column(col): s = col.get_column_def() if col.primary_key: @@ -100,6 +108,19 @@ def add_column(col): # and ignore if it says the column family already exists if "Cannot add already existing column family" not in unicode(ex): raise + else: + # see if we're missing any columns + fields = get_fields(model) + field_names = [x.name for x in fields] + for name, col in model._columns.items(): + if col.primary_key or col.partition_key: continue # we can't mess with the PK + if name in field_names: continue # skip columns already defined + + # add missing column using the column def + query = "ALTER TABLE {} add {}".format(cf_name, col.get_column_def()) + logger.debug(query) + execute(query) + #get existing index names, skip ones that already exist with connection_manager() as con: @@ -126,6 +147,20 @@ def add_column(col): # index already exists pass +def get_fields(model): + # returns all fields that aren't part of the PK + ks_name = model._get_keyspace() + col_family = model.column_family_name(include_keyspace=False) + + with connection_manager() as con: + query = "SELECT column_name, validator FROM system.schema_columns \ + WHERE keyspace_name = :ks_name AND columnfamily_name = :col_family" + + logger.debug("get_fields %s %s", ks_name, col_family) + + tmp = con.execute(query, {'ks_name':ks_name, 'col_family':col_family}) + return [Field(x[0], x[1]) for x in tmp.results] + # convert to Field named tuples def delete_table(model): diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 5e00bb977d..b99bd5a922 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,16 +1,15 @@ from cqlengine.exceptions import CQLEngineException -from cqlengine.management import create_table, delete_table +from cqlengine.management import create_table, delete_table, get_fields from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.connection import ConnectionPool, Host -from mock import Mock, MagicMock, MagicProxy, patch +from mock import MagicMock, patch from cqlengine import management from cqlengine.tests.query.test_queryset import TestModel from cqlengine.models import Model from cqlengine import columns -from cql.thrifteries import ThriftConnection class ConnectionPoolFailoverTestCase(BaseCassEngTestCase): """Test cassandra connection pooling.""" @@ -87,3 +86,46 @@ def test_table_definition(self): delete_table(LowercaseKeyModel) delete_table(CapitalizedKeyModel) + + +class FirstModel(Model): + __table_name__ = 'first_model' + first_key = columns.UUID(primary_key=True) + second_key = columns.UUID() + third_key = columns.Text() + +class SecondModel(Model): + __table_name__ = 'first_model' + first_key = columns.UUID(primary_key=True) + second_key = columns.UUID() + third_key = columns.Text() + fourth_key = columns.Text() + +class ThirdModel(Model): + __table_name__ = 'first_model' + first_key = columns.UUID(primary_key=True) + second_key = columns.UUID() + third_key = columns.Text() + # removed fourth key, but it should stay in the DB + blah = columns.Map(columns.Text, columns.Text) + +class AddColumnTest(BaseCassEngTestCase): + def setUp(self): + delete_table(FirstModel) + + def test_add_column(self): + create_table(FirstModel) + fields = get_fields(FirstModel) + + # this should contain the second key + self.assertEqual(len(fields), 2) + # get schema + create_table(SecondModel) + + fields = get_fields(FirstModel) + self.assertEqual(len(fields), 3) + + create_table(ThirdModel) + fields = get_fields(FirstModel) + self.assertEqual(len(fields), 4) + From 121c04d9758c38c63e5f6f91393d0f8127a26e49 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 31 Jul 2013 15:50:46 -0700 Subject: [PATCH 0334/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index cb0c939a93..be14282b7f 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.5.2 +0.5.3 From 1b4430fa85b2b24d647d2c9380e6f6e3fba312fa Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 31 Jul 2013 16:56:03 -0700 Subject: [PATCH 0335/3961] fixed db field name issue --- cqlengine/management.py | 2 +- cqlengine/tests/management/test_management.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index ce72e778b9..a06a10fb97 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -114,7 +114,7 @@ def add_column(col): field_names = [x.name for x in fields] for name, col in model._columns.items(): if col.primary_key or col.partition_key: continue # we can't mess with the PK - if name in field_names: continue # skip columns already defined + if col.db_field_name in field_names: continue # skip columns already defined # add missing column using the column def query = "ALTER TABLE {} add {}".format(cf_name, col.get_column_def()) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index b99bd5a922..9df6a31507 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -109,6 +109,14 @@ class ThirdModel(Model): # removed fourth key, but it should stay in the DB blah = columns.Map(columns.Text, columns.Text) +class FourthModel(Model): + __table_name__ = 'first_model' + first_key = columns.UUID(primary_key=True) + second_key = columns.UUID() + third_key = columns.Text() + # removed fourth key, but it should stay in the DB + renamed = columns.Map(columns.Text, columns.Text, db_field='blah') + class AddColumnTest(BaseCassEngTestCase): def setUp(self): delete_table(FirstModel) @@ -129,3 +137,7 @@ def test_add_column(self): fields = get_fields(FirstModel) self.assertEqual(len(fields), 4) + create_table(FourthModel) + fields = get_fields(FirstModel) + self.assertEqual(len(fields), 4) + From 368bfb642a7015bea8841d65554851f6957c8ca5 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 31 Jul 2013 16:57:13 -0700 Subject: [PATCH 0336/3961] fixed bug in alter --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index be14282b7f..a918a2aa18 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.5.3 +0.6.0 From 3ee88ecf93ffaad8eb753021b1e811a27c8d513f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 14 Aug 2013 15:48:24 -0700 Subject: [PATCH 0337/3961] setting counter column default instantiation value to 0 (not None) --- cqlengine/columns.py | 9 +++++++++ cqlengine/tests/columns/test_counter_column.py | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 56221c2dd1..9940f1aa18 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -246,9 +246,18 @@ def to_database(self, value): return self.validate(value) +class CounterValueManager(BaseValueManager): + def __init__(self, instance, column, value): + super(CounterValueManager, self).__init__(instance, column, value) + self.value = self.value or 0 + self.previous_value = self.previous_value or 0 + + class Counter(Integer): db_type = 'counter' + value_manager = CounterValueManager + def __init__(self, index=False, db_field=None, diff --git a/cqlengine/tests/columns/test_counter_column.py b/cqlengine/tests/columns/test_counter_column.py index f92cb4f418..f8dc0096e1 100644 --- a/cqlengine/tests/columns/test_counter_column.py +++ b/cqlengine/tests/columns/test_counter_column.py @@ -59,6 +59,15 @@ def test_concurrent_updates(self): def test_update_from_none(self): """ Tests that updating from None uses a create statement """ + instance = TestCounterModel() + instance.counter += 1 + instance.save() + + new = TestCounterModel.get(partition=instance.partition) + assert new.counter == 1 + + def test_new_instance_defaults_to_zero(self): + """ Tests that instantiating a new model instance will set the counter column to zero """ + instance = TestCounterModel() + assert instance.counter == 0 - def test_multiple_inserts(self): - """ Tests inserting over existing data works as expected """ From a13d6cf88ba099f615555f5c085aabc8f10a283d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 14 Aug 2013 15:56:23 -0700 Subject: [PATCH 0338/3961] adding tests and checks to model definition constraints --- cqlengine/models.py | 4 ++++ .../tests/columns/test_counter_column.py | 24 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 640e023bd5..d03a00415d 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -334,6 +334,10 @@ def _transform_column(col_name, col_obj): #TODO: check that the defined columns don't conflict with any of the Model API's existing attributes/methods #transform column definitions for k,v in column_definitions: + # counter column primary keys are not allowed + if (v.primary_key or v.partition_key) and isinstance(v, (columns.Counter, columns.BaseContainerColumn)): + raise ModelDefinitionException('counter columns and container columns cannot be used as primary keys') + # this will mark the first primary key column as a partition # key, if one hasn't been set already if not has_partition_keys and v.primary_key: diff --git a/cqlengine/tests/columns/test_counter_column.py b/cqlengine/tests/columns/test_counter_column.py index f8dc0096e1..c260f38ed0 100644 --- a/cqlengine/tests/columns/test_counter_column.py +++ b/cqlengine/tests/columns/test_counter_column.py @@ -1,8 +1,9 @@ from uuid import uuid4 -from cqlengine import Model, ValidationError +from cqlengine import Model from cqlengine import columns from cqlengine.management import create_table, delete_table +from cqlengine.models import ModelDefinitionException from cqlengine.tests.base import BaseCassEngTestCase @@ -15,10 +16,29 @@ class TestCounterModel(Model): class TestClassConstruction(BaseCassEngTestCase): def test_defining_a_non_counter_column_fails(self): - """ Tests that defining a non counter column field fails """ + """ Tests that defining a non counter column field in a model with a counter column fails """ + with self.assertRaises(ModelDefinitionException): + class model(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + counter = columns.Counter() + text = columns.Text() + def test_defining_a_primary_key_counter_column_fails(self): """ Tests that defining primary keys on counter columns fails """ + with self.assertRaises(TypeError): + class model(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + cluster = columns.Counter(primary_ley=True) + counter = columns.Counter() + + # force it + with self.assertRaises(ModelDefinitionException): + class model(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + cluster = columns.Counter() + cluster.primary_key = True + counter = columns.Counter() class TestCounterColumn(BaseCassEngTestCase): From 7a3b6a1bbabfacf77e7e09c6ab4da640d36d3d36 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 14 Aug 2013 18:07:30 -0700 Subject: [PATCH 0339/3961] renamed create_table to sync_table (added an alias), getting compaction started --- cqlengine/__init__.py | 3 +++ cqlengine/management.py | 4 +++- cqlengine/models.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 27ef399572..639b56b0bd 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -8,4 +8,7 @@ __cqlengine_version_path__ = os.path.realpath(__file__ + '/../VERSION') __version__ = open(__cqlengine_version_path__, 'r').readline().strip() +# compaction +SizeTieredCompactionStrategy = "SizeTieredCompactionStrategy" +LeveledCompactionStrategy = "LeveledCompactionStrategy" diff --git a/cqlengine/management.py b/cqlengine/management.py index a06a10fb97..740f78e335 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -47,8 +47,10 @@ def delete_keyspace(name): if name in [r[0] for r in keyspaces]: execute("DROP KEYSPACE {}".format(name)) - def create_table(model, create_missing_keyspace=True): + sync_table(model, create_missing_keyspace) + +def sync_table(model, create_missing_keyspace=True): if model.__abstract__: raise CQLEngineException("cannot create table from abstract model") diff --git a/cqlengine/models.py b/cqlengine/models.py index d03a00415d..7eb88e1728 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -140,6 +140,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #the keyspace for this model __keyspace__ = None + + __compaction__ = None __read_repair_chance__ = 0.1 def __init__(self, **values): From 23afb880d976cfb47c4b04d2a6436b5f068ef734 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 13:20:22 -0700 Subject: [PATCH 0340/3961] test classes for different scenarios in place, working to test failure cases --- cqlengine/management.py | 32 +++++++++++ cqlengine/models.py | 16 ++++++ cqlengine/tests/management/test_management.py | 57 ++++++++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 740f78e335..d2ecbca9b9 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,4 +1,5 @@ import json +from cqlengine import SizeTieredCompactionStrategy from cqlengine.connection import connection_manager, execute from cqlengine.exceptions import CQLEngineException @@ -99,6 +100,7 @@ def add_column(col): if _order: with_qs.append('clustering order by ({})'.format(', '.join(_order))) + # add read_repair_chance qs += ['WITH {}'.format(' AND '.join(with_qs))] qs = ' '.join(qs) @@ -149,6 +151,36 @@ def add_column(col): # index already exists pass +def get_compaction_options(model): + """ + Generates dictionary (later converted to a string) for creating and altering + tables with compaction strategy + + :param model: + :return: + """ + if not model.__compaction__: + return None + + result = {'class':model.__compaction__} + + def setter(key, limited_to_strategy = None): + mkey = "__compaction_{}__".format(key) + tmp = getattr(model, mkey) + if tmp and limited_to_strategy and limited_to_strategy != model.__compaction__: + raise CQLEngineException("{} is limited to {}".format(key, limited_to_strategy)) + + if tmp: + result[key] = tmp + + setter('min_threshold') + setter('tombstone_compaction_interval') + setter('bucket_high', SizeTieredCompactionStrategy) + setter('bucket_low', SizeTieredCompactionStrategy) + + return result + + def get_fields(model): # returns all fields that aren't part of the PK ks_name = model._get_keyspace() diff --git a/cqlengine/models.py b/cqlengine/models.py index 7eb88e1728..3abf1abaf1 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -141,7 +141,23 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass __keyspace__ = None + # compaction options __compaction__ = None + __compaction_tombstone_compaction_interval__ = None + __compaction_tombstone_threshold = None + + # compaction - size tiered options + __compaction_bucket_high__ = None + __compaction_bucket_low__ = None + __compaction_max_threshold__ = None + __compaction_min_threshold__ = None + __compaction_min_sstable_size__ = None + + # compaction - leveled options + __compaction_sstable_size_in_mb__ = None # only works with Leveled + + # end compaction + __read_repair_chance__ = 0.1 def __init__(self, **values): diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 9df6a31507..02973c0485 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,11 +1,11 @@ from cqlengine.exceptions import CQLEngineException -from cqlengine.management import create_table, delete_table, get_fields +from cqlengine.management import create_table, delete_table, get_fields, get_compaction_options from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.connection import ConnectionPool, Host from mock import MagicMock, patch -from cqlengine import management +from cqlengine import management, SizeTieredCompactionStrategy, LeveledCompactionStrategy from cqlengine.tests.query.test_queryset import TestModel from cqlengine.models import Model from cqlengine import columns @@ -141,3 +141,56 @@ def test_add_column(self): fields = get_fields(FirstModel) self.assertEqual(len(fields), 4) +class CompactionModel(Model): + __compaction__ = None + cid = columns.UUID(primary_key=True) + name = columns.Text() + +class CompactionSizeTieredModel(Model): + __compaction__ = SizeTieredCompactionStrategy + cid = columns.UUID(primary_key=True) + name = columns.Text() + +class CompactionLeveledStrategyModel(Model): + __compaction__ = LeveledCompactionStrategy + cid = columns.UUID(primary_key=True) + name = columns.Text() + +import copy + +class EmptyCompactionTest(BaseCassEngTestCase): + def test_empty_compaction(self): + self.model = copy.deepcopy(CompactionModel) + result = get_compaction_options(self.model) + self.assertIsNone(result) + + +class SizeTieredCompactionTest(BaseCassEngTestCase): + + def setUp(self): + self.model = copy.deepcopy(CompactionModel) + self.model.__compaction__ = SizeTieredCompactionStrategy + + def test_size_tiered(self): + result = get_compaction_options(self.model) + assert result['class'] == SizeTieredCompactionStrategy + + def test_min_threshold(self): + self.model.__compaction_min_threshold__ = 2 + + result = get_compaction_options(self.model) + assert result['min_threshold'] == 2 + +class LeveledCompactionTest(BaseCassEngTestCase): + def setUp(self): + self.model = copy.deepcopy(CompactionLeveledStrategyModel) + + def test_simple_leveled(self): + result = get_compaction_options(self.model) + assert result['class'] == LeveledCompactionStrategy + + def test_bucket_high_fails(self): + with patch.object(self.model, '__compaction_bucket_high__', 10), \ + self.assertRaises(CQLEngineException): + result = get_compaction_options(self.model) + From 15f6e613c382f72b8e9333b21d10ca0154a27260 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 13:27:53 -0700 Subject: [PATCH 0341/3961] more size tiered options --- cqlengine/management.py | 4 ++++ cqlengine/tests/management/test_management.py | 24 ++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index d2ecbca9b9..f6b5f176ef 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -175,8 +175,12 @@ def setter(key, limited_to_strategy = None): setter('min_threshold') setter('tombstone_compaction_interval') + setter('bucket_high', SizeTieredCompactionStrategy) setter('bucket_low', SizeTieredCompactionStrategy) + setter('max_threshold', SizeTieredCompactionStrategy) + setter('min_threshold', SizeTieredCompactionStrategy) + setter('min_sstable_size', SizeTieredCompactionStrategy) return result diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 02973c0485..5caa337a52 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -185,12 +185,30 @@ class LeveledCompactionTest(BaseCassEngTestCase): def setUp(self): self.model = copy.deepcopy(CompactionLeveledStrategyModel) + def assert_option_fails(self, key): + # key is a normal_key, converted to + # __compaction_key__ + key = "__compaction_{}__".format(key) + + with patch.object(self.model, key, 10), \ + self.assertRaises(CQLEngineException): + get_compaction_options(self.model) + def test_simple_leveled(self): result = get_compaction_options(self.model) assert result['class'] == LeveledCompactionStrategy def test_bucket_high_fails(self): - with patch.object(self.model, '__compaction_bucket_high__', 10), \ - self.assertRaises(CQLEngineException): - result = get_compaction_options(self.model) + self.assert_option_fails('bucket_high') + + def test_bucket_low_fails(self): + self.assert_option_fails('bucket_low') + + def test_max_threshold_fails(self): + self.assert_option_fails('max_threshold') + + def test_min_threshold_fails(self): + self.assert_option_fails('min_threshold') + def test_min_sstable_size_fails(self): + self.assert_option_fails('min_sstable_size') From 8c38d7a6eb08ba76e7d014e33aaffbda99484634 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 14:02:48 -0700 Subject: [PATCH 0342/3961] finishing out available options --- cqlengine/management.py | 4 ++- cqlengine/tests/management/test_management.py | 25 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index f6b5f176ef..b0c24b829c 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,5 +1,5 @@ import json -from cqlengine import SizeTieredCompactionStrategy +from cqlengine import SizeTieredCompactionStrategy, LeveledCompactionStrategy from cqlengine.connection import connection_manager, execute from cqlengine.exceptions import CQLEngineException @@ -182,6 +182,8 @@ def setter(key, limited_to_strategy = None): setter('min_threshold', SizeTieredCompactionStrategy) setter('min_sstable_size', SizeTieredCompactionStrategy) + setter("sstable_size_in_mb", LeveledCompactionStrategy) + return result diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 5caa337a52..028fe9a5cb 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -164,8 +164,18 @@ def test_empty_compaction(self): result = get_compaction_options(self.model) self.assertIsNone(result) +class BaseCompactionTest(BaseCassEngTestCase): + def assert_option_fails(self, key): + # key is a normal_key, converted to + # __compaction_key__ + + key = "__compaction_{}__".format(key) -class SizeTieredCompactionTest(BaseCassEngTestCase): + with patch.object(self.model, key, 10), \ + self.assertRaises(CQLEngineException): + get_compaction_options(self.model) + +class SizeTieredCompactionTest(BaseCompactionTest): def setUp(self): self.model = copy.deepcopy(CompactionModel) @@ -177,23 +187,13 @@ def test_size_tiered(self): def test_min_threshold(self): self.model.__compaction_min_threshold__ = 2 - result = get_compaction_options(self.model) assert result['min_threshold'] == 2 -class LeveledCompactionTest(BaseCassEngTestCase): +class LeveledCompactionTest(BaseCompactionTest): def setUp(self): self.model = copy.deepcopy(CompactionLeveledStrategyModel) - def assert_option_fails(self, key): - # key is a normal_key, converted to - # __compaction_key__ - key = "__compaction_{}__".format(key) - - with patch.object(self.model, key, 10), \ - self.assertRaises(CQLEngineException): - get_compaction_options(self.model) - def test_simple_leveled(self): result = get_compaction_options(self.model) assert result['class'] == LeveledCompactionStrategy @@ -212,3 +212,4 @@ def test_min_threshold_fails(self): def test_min_sstable_size_fails(self): self.assert_option_fails('min_sstable_size') + From 95f851863b409cdc2c3185ce339ccaf8dc5cd667 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 14:05:05 -0700 Subject: [PATCH 0343/3961] check for sstable size in MB ok in leveled compaction --- cqlengine/tests/management/test_management.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 028fe9a5cb..d308bb5038 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -213,3 +213,7 @@ def test_min_threshold_fails(self): def test_min_sstable_size_fails(self): self.assert_option_fails('min_sstable_size') + def test_sstable_size_in_mb(self): + with patch.object(self.model, '__compaction_sstable_size_in_mb__', 32): + result = get_compaction_options(self.model) + assert result['sstable_size_in_mb'] == 32 From a9de7843cfd788efc8f4736413ec492134d03e29 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 14:49:16 -0700 Subject: [PATCH 0344/3961] breaking apart sync table --- cqlengine/management.py | 64 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index b0c24b829c..306d330cf3 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -75,35 +75,7 @@ def sync_table(model, create_missing_keyspace=True): #check for an existing column family #TODO: check system tables instead of using cql thrifteries if raw_cf_name not in tables: - qs = ['CREATE TABLE {}'.format(cf_name)] - - #add column types - pkeys = [] # primary keys - ckeys = [] # clustering keys - qtypes = [] # field types - def add_column(col): - s = col.get_column_def() - if col.primary_key: - keys = (pkeys if col.partition_key else ckeys) - keys.append('"{}"'.format(col.db_field_name)) - qtypes.append(s) - for name, col in model._columns.items(): - add_column(col) - - qtypes.append('PRIMARY KEY (({}){})'.format(', '.join(pkeys), ckeys and ', ' + ', '.join(ckeys) or '')) - - qs += ['({})'.format(', '.join(qtypes))] - - with_qs = ['read_repair_chance = {}'.format(model.__read_repair_chance__)] - - _order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] - if _order: - with_qs.append('clustering order by ({})'.format(', '.join(_order))) - - - # add read_repair_chance - qs += ['WITH {}'.format(' AND '.join(with_qs))] - qs = ' '.join(qs) + qs = get_create_table(model) try: execute(qs) @@ -151,6 +123,40 @@ def add_column(col): # index already exists pass +def get_create_table(model): + cf_name = model.column_family_name() + qs = ['CREATE TABLE {}'.format(cf_name)] + + #add column types + pkeys = [] # primary keys + ckeys = [] # clustering keys + qtypes = [] # field types + def add_column(col): + s = col.get_column_def() + if col.primary_key: + keys = (pkeys if col.partition_key else ckeys) + keys.append('"{}"'.format(col.db_field_name)) + qtypes.append(s) + for name, col in model._columns.items(): + add_column(col) + + qtypes.append('PRIMARY KEY (({}){})'.format(', '.join(pkeys), ckeys and ', ' + ', '.join(ckeys) or '')) + + qs += ['({})'.format(', '.join(qtypes))] + + with_qs = ['read_repair_chance = {}'.format(model.__read_repair_chance__)] + + _order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] + if _order: + with_qs.append('clustering order by ({})'.format(', '.join(_order))) + + + # add read_repair_chance + qs += ['WITH {}'.format(' AND '.join(with_qs))] + qs = ' '.join(qs) + return qs + + def get_compaction_options(model): """ Generates dictionary (later converted to a string) for creating and altering From 9ea1c7859e507d5767e33297b2bab31c4bc2fbfd Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 15:47:52 -0700 Subject: [PATCH 0345/3961] options added to create string --- cqlengine/management.py | 8 ++++++++ cqlengine/tests/management/test_management.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 306d330cf3..5c9350a326 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -147,12 +147,20 @@ def add_column(col): with_qs = ['read_repair_chance = {}'.format(model.__read_repair_chance__)] _order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] + if _order: with_qs.append('clustering order by ({})'.format(', '.join(_order))) + compaction_options = get_compaction_options(model) + + if compaction_options: + compaction_options = json.dumps(compaction_options).replace('"', "'") + with_qs.append("compaction = {}".format(compaction_options)) # add read_repair_chance qs += ['WITH {}'.format(' AND '.join(with_qs))] + + qs = ' '.join(qs) return qs diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index d308bb5038..ab27b132cc 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,5 +1,5 @@ from cqlengine.exceptions import CQLEngineException -from cqlengine.management import create_table, delete_table, get_fields, get_compaction_options +from cqlengine.management import create_table, delete_table, get_fields, get_compaction_options, get_create_table from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.connection import ConnectionPool, Host @@ -216,4 +216,8 @@ def test_min_sstable_size_fails(self): def test_sstable_size_in_mb(self): with patch.object(self.model, '__compaction_sstable_size_in_mb__', 32): result = get_compaction_options(self.model) + assert result['sstable_size_in_mb'] == 32 + + + From f7b9953e0a035bf7f21222c35f6d02f61c01a84b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 16:11:18 -0700 Subject: [PATCH 0346/3961] test for leveled compaction working --- changelog | 9 +++++++++ cqlengine/management.py | 6 ++++++ cqlengine/tests/management/test_management.py | 11 ++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 0da8f3c7e9..2ed482986a 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,14 @@ CHANGELOG +0.7.0 +* added counter columns +* added support for compaction settings at the model level +* deprecated delete_table in favor of drop_table +* deprecated create_table in favor of sync_table + +0.6.0 +* added table sync + 0.5.2 * adding hex conversion to Bytes column diff --git a/cqlengine/management.py b/cqlengine/management.py index 5c9350a326..eae327a6ae 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,4 +1,5 @@ import json +import warnings from cqlengine import SizeTieredCompactionStrategy, LeveledCompactionStrategy from cqlengine.connection import connection_manager, execute @@ -49,6 +50,7 @@ def delete_keyspace(name): execute("DROP KEYSPACE {}".format(name)) def create_table(model, create_missing_keyspace=True): + warnings.warn("create_table has been deprecated in favor of sync_table and will be removed in a future release", DeprecationWarning) sync_table(model, create_missing_keyspace) def sync_table(model, create_missing_keyspace=True): @@ -217,6 +219,10 @@ def get_fields(model): # convert to Field named tuples def delete_table(model): + warnings.warn("delete_table has been deprecated in favor of drop_table()", DeprecationWarning) + return drop_table(model) + +def drop_table(model): # don't try to delete non existant tables ks_name = model._get_keyspace() diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index ab27b132cc..a89c0b4084 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,5 +1,5 @@ from cqlengine.exceptions import CQLEngineException -from cqlengine.management import create_table, delete_table, get_fields, get_compaction_options, get_create_table +from cqlengine.management import create_table, delete_table, get_fields, get_compaction_options, get_create_table, sync_table, drop_table from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.connection import ConnectionPool, Host @@ -219,5 +219,14 @@ def test_sstable_size_in_mb(self): assert result['sstable_size_in_mb'] == 32 + def test_create_table(self): + class LeveledcompactionTestTable(Model): + __compaction__ = LeveledCompactionStrategy + __compaction_sstable_size_in_mb__ = 64 + user_id = columns.UUID(primary_key=True) + name = columns.Text() + + drop_table(LeveledcompactionTestTable) + sync_table(LeveledcompactionTestTable) From 6148b9edbf0c77d4d77e75d6654033d356e2d6a8 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 16:44:31 -0700 Subject: [PATCH 0347/3961] working on alters --- cqlengine/management.py | 15 ++++++++++++++- cqlengine/tests/management/test_management.py | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index eae327a6ae..ddcbdaa213 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -75,7 +75,6 @@ def sync_table(model, create_missing_keyspace=True): tables = [x[0] for x in tables.results] #check for an existing column family - #TODO: check system tables instead of using cql thrifteries if raw_cf_name not in tables: qs = get_create_table(model) @@ -218,6 +217,20 @@ def get_fields(model): return [Field(x[0], x[1]) for x in tmp.results] # convert to Field named tuples +def get_compaction_settings(model): + # returns a dictionary of compaction settings in an existing table + ks_name = model._get_keyspace() + col_family = model.column_family_name(include_keyspace=False) + with connection_manager() as con: + query = "SELECT , validator FROM system.schema_columns \ + WHERE keyspace_name = :ks_name AND columnfamily_name = :col_family" + + logger.debug("get_fields %s %s", ks_name, col_family) + + tmp = con.execute(query, {'ks_name':ks_name, 'col_family':col_family}) + import ipdb; ipdb.set_trace() + + def delete_table(model): warnings.warn("delete_table has been deprecated in favor of drop_table()", DeprecationWarning) return drop_table(model) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index a89c0b4084..6f1a833749 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -229,4 +229,9 @@ class LeveledcompactionTestTable(Model): drop_table(LeveledcompactionTestTable) sync_table(LeveledcompactionTestTable) + LeveledcompactionTestTable.__compaction__ = SizeTieredCompactionStrategy + LeveledcompactionTestTable.__compaction_sstable_size_in_mb__ = None + + sync_table(LeveledcompactionTestTable) + From a78e4ee54ffb592c92c938b4d82297ca5e2288bb Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 15 Aug 2013 18:54:56 -0700 Subject: [PATCH 0348/3961] moved compaction settings to it's own test module --- cqlengine/management.py | 23 ++-- .../management/test_compaction_settings.py | 104 ++++++++++++++++++ cqlengine/tests/management/test_management.py | 102 +---------------- 3 files changed, 123 insertions(+), 106 deletions(-) create mode 100644 cqlengine/tests/management/test_compaction_settings.py diff --git a/cqlengine/management.py b/cqlengine/management.py index ddcbdaa213..0267a719ca 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,6 +1,7 @@ import json import warnings from cqlengine import SizeTieredCompactionStrategy, LeveledCompactionStrategy +from cqlengine.named import NamedTable from cqlengine.connection import connection_manager, execute from cqlengine.exceptions import CQLEngineException @@ -12,6 +13,10 @@ logger = logging.getLogger(__name__) +# system keyspaces +schema_columnfamilies = NamedTable('system', 'schema_columnfamilies') + + def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, durable_writes=True, **replication_values): """ creates a keyspace @@ -98,6 +103,10 @@ def sync_table(model, create_missing_keyspace=True): logger.debug(query) execute(query) + update_compaction(model) + # update compaction + + #get existing index names, skip ones that already exist with connection_manager() as con: @@ -217,18 +226,16 @@ def get_fields(model): return [Field(x[0], x[1]) for x in tmp.results] # convert to Field named tuples -def get_compaction_settings(model): - # returns a dictionary of compaction settings in an existing table + +def update_compaction(model): ks_name = model._get_keyspace() col_family = model.column_family_name(include_keyspace=False) - with connection_manager() as con: - query = "SELECT , validator FROM system.schema_columns \ - WHERE keyspace_name = :ks_name AND columnfamily_name = :col_family" - logger.debug("get_fields %s %s", ks_name, col_family) + row = schema_columnfamilies.get(keyspace_name=ks_name, + columnfamily_name=col_family) + # check compaction_strategy_class + # check compaction_strategy_options - tmp = con.execute(query, {'ks_name':ks_name, 'col_family':col_family}) - import ipdb; ipdb.set_trace() def delete_table(model): diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py new file mode 100644 index 0000000000..6242be654c --- /dev/null +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -0,0 +1,104 @@ +import copy +from mock import patch +from cqlengine import Model, columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy +from cqlengine.exceptions import CQLEngineException +from cqlengine.management import get_compaction_options, drop_table, sync_table +from cqlengine.tests.base import BaseCassEngTestCase + + +class CompactionModel(Model): + __compaction__ = None + cid = columns.UUID(primary_key=True) + name = columns.Text() + + +class BaseCompactionTest(BaseCassEngTestCase): + def assert_option_fails(self, key): + # key is a normal_key, converted to + # __compaction_key__ + + key = "__compaction_{}__".format(key) + + with patch.object(self.model, key, 10), \ + self.assertRaises(CQLEngineException): + get_compaction_options(self.model) + + +class SizeTieredCompactionTest(BaseCompactionTest): + + def setUp(self): + self.model = copy.deepcopy(CompactionModel) + self.model.__compaction__ = SizeTieredCompactionStrategy + + def test_size_tiered(self): + result = get_compaction_options(self.model) + assert result['class'] == SizeTieredCompactionStrategy + + def test_min_threshold(self): + self.model.__compaction_min_threshold__ = 2 + result = get_compaction_options(self.model) + assert result['min_threshold'] == 2 + + +class LeveledCompactionTest(BaseCompactionTest): + def setUp(self): + self.model = copy.deepcopy(CompactionLeveledStrategyModel) + + def test_simple_leveled(self): + result = get_compaction_options(self.model) + assert result['class'] == LeveledCompactionStrategy + + def test_bucket_high_fails(self): + self.assert_option_fails('bucket_high') + + def test_bucket_low_fails(self): + self.assert_option_fails('bucket_low') + + def test_max_threshold_fails(self): + self.assert_option_fails('max_threshold') + + def test_min_threshold_fails(self): + self.assert_option_fails('min_threshold') + + def test_min_sstable_size_fails(self): + self.assert_option_fails('min_sstable_size') + + def test_sstable_size_in_mb(self): + with patch.object(self.model, '__compaction_sstable_size_in_mb__', 32): + result = get_compaction_options(self.model) + + assert result['sstable_size_in_mb'] == 32 + + def test_create_table(self): + class LeveledcompactionTestTable(Model): + __compaction__ = LeveledCompactionStrategy + __compaction_sstable_size_in_mb__ = 64 + user_id = columns.UUID(primary_key=True) + name = columns.Text() + + drop_table(LeveledcompactionTestTable) + sync_table(LeveledcompactionTestTable) + + LeveledcompactionTestTable.__compaction__ = SizeTieredCompactionStrategy + LeveledcompactionTestTable.__compaction_sstable_size_in_mb__ = None + + sync_table(LeveledcompactionTestTable) + + +class EmptyCompactionTest(BaseCassEngTestCase): + def test_empty_compaction(self): + self.model = copy.deepcopy(CompactionModel) + result = get_compaction_options(self.model) + self.assertIsNone(result) + + +class CompactionLeveledStrategyModel(Model): + __compaction__ = LeveledCompactionStrategy + cid = columns.UUID(primary_key=True) + name = columns.Text() + + +class CompactionSizeTieredModel(Model): + __compaction__ = SizeTieredCompactionStrategy + cid = columns.UUID(primary_key=True) + name = columns.Text() diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 6f1a833749..88405b1e40 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,11 +1,10 @@ +from mock import MagicMock, patch + from cqlengine.exceptions import CQLEngineException -from cqlengine.management import create_table, delete_table, get_fields, get_compaction_options, get_create_table, sync_table, drop_table +from cqlengine.management import create_table, delete_table, get_fields from cqlengine.tests.base import BaseCassEngTestCase - from cqlengine.connection import ConnectionPool, Host - -from mock import MagicMock, patch -from cqlengine import management, SizeTieredCompactionStrategy, LeveledCompactionStrategy +from cqlengine import management from cqlengine.tests.query.test_queryset import TestModel from cqlengine.models import Model from cqlengine import columns @@ -141,97 +140,4 @@ def test_add_column(self): fields = get_fields(FirstModel) self.assertEqual(len(fields), 4) -class CompactionModel(Model): - __compaction__ = None - cid = columns.UUID(primary_key=True) - name = columns.Text() - -class CompactionSizeTieredModel(Model): - __compaction__ = SizeTieredCompactionStrategy - cid = columns.UUID(primary_key=True) - name = columns.Text() - -class CompactionLeveledStrategyModel(Model): - __compaction__ = LeveledCompactionStrategy - cid = columns.UUID(primary_key=True) - name = columns.Text() - -import copy - -class EmptyCompactionTest(BaseCassEngTestCase): - def test_empty_compaction(self): - self.model = copy.deepcopy(CompactionModel) - result = get_compaction_options(self.model) - self.assertIsNone(result) - -class BaseCompactionTest(BaseCassEngTestCase): - def assert_option_fails(self, key): - # key is a normal_key, converted to - # __compaction_key__ - - key = "__compaction_{}__".format(key) - - with patch.object(self.model, key, 10), \ - self.assertRaises(CQLEngineException): - get_compaction_options(self.model) - -class SizeTieredCompactionTest(BaseCompactionTest): - - def setUp(self): - self.model = copy.deepcopy(CompactionModel) - self.model.__compaction__ = SizeTieredCompactionStrategy - - def test_size_tiered(self): - result = get_compaction_options(self.model) - assert result['class'] == SizeTieredCompactionStrategy - - def test_min_threshold(self): - self.model.__compaction_min_threshold__ = 2 - result = get_compaction_options(self.model) - assert result['min_threshold'] == 2 - -class LeveledCompactionTest(BaseCompactionTest): - def setUp(self): - self.model = copy.deepcopy(CompactionLeveledStrategyModel) - - def test_simple_leveled(self): - result = get_compaction_options(self.model) - assert result['class'] == LeveledCompactionStrategy - - def test_bucket_high_fails(self): - self.assert_option_fails('bucket_high') - - def test_bucket_low_fails(self): - self.assert_option_fails('bucket_low') - - def test_max_threshold_fails(self): - self.assert_option_fails('max_threshold') - - def test_min_threshold_fails(self): - self.assert_option_fails('min_threshold') - - def test_min_sstable_size_fails(self): - self.assert_option_fails('min_sstable_size') - - def test_sstable_size_in_mb(self): - with patch.object(self.model, '__compaction_sstable_size_in_mb__', 32): - result = get_compaction_options(self.model) - - assert result['sstable_size_in_mb'] == 32 - - def test_create_table(self): - class LeveledcompactionTestTable(Model): - __compaction__ = LeveledCompactionStrategy - __compaction_sstable_size_in_mb__ = 64 - user_id = columns.UUID(primary_key=True) - name = columns.Text() - - drop_table(LeveledcompactionTestTable) - sync_table(LeveledcompactionTestTable) - - LeveledcompactionTestTable.__compaction__ = SizeTieredCompactionStrategy - LeveledcompactionTestTable.__compaction_sstable_size_in_mb__ = None - - sync_table(LeveledcompactionTestTable) - From f3c6d32d08b5b394604ba0bbd83f30d8c6faa93d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 16 Aug 2013 08:29:02 -0700 Subject: [PATCH 0349/3961] fixing tablename and keyspace name class args --- docs/topics/models.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 3360ba7fee..c030c76445 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -138,11 +138,11 @@ Model Attributes *Optional.* Indicates that this model is only intended to be used as a base class for other models. You can't create tables for abstract models, but checks around schema validity are skipped during class construction. - .. attribute:: Model.table_name + .. attribute:: Model.__table_name__ *Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix. Manually defined table names are not inherited. - .. attribute:: Model.keyspace + .. attribute:: Model.__keyspace__ *Optional.* Sets the name of the keyspace used by this model. Defaulst to cqlengine From f33627c9791a53aa5efeb9a330d761fc8ae2e7eb Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 16 Aug 2013 11:26:06 -0700 Subject: [PATCH 0350/3961] making sure update table is called --- cqlengine/management.py | 5 ++-- .../management/test_compaction_settings.py | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 0267a719ca..820d8236e2 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -103,9 +103,7 @@ def sync_table(model, create_missing_keyspace=True): logger.debug(query) execute(query) - update_compaction(model) - # update compaction - + update_compaction(model) #get existing index names, skip ones that already exist @@ -228,6 +226,7 @@ def get_fields(model): def update_compaction(model): + logger.debug("Checking %s for compaction differences", model) ks_name = model._get_keyspace() col_family = model.column_family_name(include_keyspace=False) diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index 6242be654c..4ef3cc170c 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -1,5 +1,6 @@ import copy -from mock import patch +from time import sleep +from mock import patch, MagicMock from cqlengine import Model, columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy from cqlengine.exceptions import CQLEngineException from cqlengine.management import get_compaction_options, drop_table, sync_table @@ -69,20 +70,26 @@ def test_sstable_size_in_mb(self): assert result['sstable_size_in_mb'] == 32 - def test_create_table(self): - class LeveledcompactionTestTable(Model): - __compaction__ = LeveledCompactionStrategy - __compaction_sstable_size_in_mb__ = 64 - user_id = columns.UUID(primary_key=True) - name = columns.Text() +class LeveledcompactionTestTable(Model): + __compaction__ = LeveledCompactionStrategy + __compaction_sstable_size_in_mb__ = 64 + + user_id = columns.UUID(primary_key=True) + name = columns.Text() + + +class AlterTableTest(BaseCassEngTestCase): + + def test_alter_is_called_table(self): drop_table(LeveledcompactionTestTable) sync_table(LeveledcompactionTestTable) + with patch('cqlengine.management.update_compaction') as mock: + mock.return_value = True + sync_table(LeveledcompactionTestTable) + assert mock.called == 1 - LeveledcompactionTestTable.__compaction__ = SizeTieredCompactionStrategy - LeveledcompactionTestTable.__compaction_sstable_size_in_mb__ = None - sync_table(LeveledcompactionTestTable) class EmptyCompactionTest(BaseCassEngTestCase): From edf0dc06b7b18df61ad3a778c7e059eaceefc1b1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 16 Aug 2013 11:26:58 -0700 Subject: [PATCH 0351/3961] removed unnecessary return value --- cqlengine/tests/management/test_compaction_settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index 4ef3cc170c..cc568201f0 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -85,7 +85,6 @@ def test_alter_is_called_table(self): drop_table(LeveledcompactionTestTable) sync_table(LeveledcompactionTestTable) with patch('cqlengine.management.update_compaction') as mock: - mock.return_value = True sync_table(LeveledcompactionTestTable) assert mock.called == 1 From d43bf1c9559420a7ca04511764ba98692c4166fa Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 16 Aug 2013 12:07:29 -0700 Subject: [PATCH 0352/3961] working on updating compaction strategy --- cqlengine/management.py | 8 +++++++- .../tests/management/test_compaction_settings.py | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 820d8236e2..8fb447c2aa 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -187,6 +187,12 @@ def get_compaction_options(model): result = {'class':model.__compaction__} def setter(key, limited_to_strategy = None): + """ + sets key in result, checking if the key is limited to either SizeTiered or Leveled + :param key: one of the compaction options, like "bucket_high" + :param limited_to_strategy: SizeTieredCompactionStrategy, LeveledCompactionStrategy + :return: + """ mkey = "__compaction_{}__".format(key) tmp = getattr(model, mkey) if tmp and limited_to_strategy and limited_to_strategy != model.__compaction__: @@ -234,7 +240,7 @@ def update_compaction(model): columnfamily_name=col_family) # check compaction_strategy_class # check compaction_strategy_options - + return True def delete_table(model): diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index cc568201f0..d6d96ad53c 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -78,6 +78,7 @@ class LeveledcompactionTestTable(Model): user_id = columns.UUID(primary_key=True) name = columns.Text() +from cqlengine.management import schema_columnfamilies class AlterTableTest(BaseCassEngTestCase): @@ -88,6 +89,18 @@ def test_alter_is_called_table(self): sync_table(LeveledcompactionTestTable) assert mock.called == 1 + def test_alter_actually_alters(self): + tmp = copy.deepcopy(LeveledcompactionTestTable) + drop_table(tmp) + sync_table(tmp) + tmp.__compaction__ = SizeTieredCompactionStrategy + tmp.__compaction_sstable_size_in_mb__ = None + sync_table(tmp) + + table_settings = schema_columnfamilies.get(keyspace_name=tmp._get_keyspace(), + columnfamily_name=tmp.column_family_name(include_keyspace=False)) + self.assertRegexpMatches(table_settings['compaction_strategy_class'], '.*SizeTieredCompactionStrategy$') + From b5d69d0906b7080f1933df22f36fed75ccab6d4d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 16 Aug 2013 16:00:06 -0700 Subject: [PATCH 0353/3961] updating column option docs --- docs/topics/models.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index c030c76445..86e0840d73 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -67,15 +67,19 @@ Column Types Column Options -------------- - Each column can be defined with optional arguments to modify the way they behave. While some column types may define additional column options, these are the options that are available on all columns: + Each column can be defined with optional arguments to modify the way they behave. While some column types may + define additional column options, these are the options that are available on all columns: :attr:`~cqlengine.columns.BaseColumn.primary_key` If True, this column is created as a primary key field. A model can have multiple primary keys. Defaults to False. - *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys, unless partition keys are specified manually using* :attr:`~cqlengine.columns.BaseColumn.partition_key` + *In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first + primary key is the partition key, and all others are clustering keys, unless partition keys are specified + manually using* :attr:`~cqlengine.columns.BaseColumn.partition_key` :attr:`~cqlengine.columns.BaseColumn.partition_key` - If True, this column is created as partition primary key. There may be many partition keys defined, forming *composite partition key* + If True, this column is created as partition primary key. There may be many partition keys defined, + forming a *composite partition key* :attr:`~cqlengine.columns.BaseColumn.index` If True, an index will be created for this column. Defaults to False. @@ -83,13 +87,16 @@ Column Options *Note: Indexes can only be created on models with one primary key* :attr:`~cqlengine.columns.BaseColumn.db_field` - Explicitly sets the name of the column in the database table. If this is left blank, the column name will be the same as the name of the column attribute. Defaults to None. + Explicitly sets the name of the column in the database table. If this is left blank, the column name will be + the same as the name of the column attribute. Defaults to None. :attr:`~cqlengine.columns.BaseColumn.default` - The default value for this column. If a model instance is saved without a value for this column having been defined, the default value will be used. This can be either a value or a callable object (ie: datetime.now is a valid default argument). + The default value for this column. If a model instance is saved without a value for this column having been + defined, the default value will be used. This can be either a value or a callable object (ie: datetime.now is a valid default argument). + Callable defaults will be called each time a default is assigned to a None value :attr:`~cqlengine.columns.BaseColumn.required` - If True, this model cannot be saved without a value defined for this column. Defaults to True. Primary key fields cannot have their required fields set to False. + If True, this model cannot be saved without a value defined for this column. Defaults to False. Primary key fields always require values. Model Methods ============= From b2eace8bd49825421d12291dfb441fa32542e1f2 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 19 Aug 2013 12:02:51 -0700 Subject: [PATCH 0354/3961] altering correctly on incorrect compaction strategy --- cqlengine/management.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 8fb447c2aa..3ac4dd1f04 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -239,8 +239,14 @@ def update_compaction(model): row = schema_columnfamilies.get(keyspace_name=ks_name, columnfamily_name=col_family) # check compaction_strategy_class + do_update = not row['compaction_strategy_class'].endswith(model.__compaction__) + # check compaction_strategy_options - return True + if do_update: + options = get_compaction_options(model) + options = json.dumps(options).replace('"', "'") + cf_name = model.column_family_name() + execute("ALTER TABLE {} with compaction = {}".format(cf_name, options)) def delete_table(model): From cb925d342cb8cd3db92f4fc2bf47c67c37e40ad0 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 19 Aug 2013 13:12:19 -0700 Subject: [PATCH 0355/3961] verifying alter table with compaction" --- cqlengine/management.py | 20 +++++++++++++++---- .../management/test_compaction_settings.py | 15 +++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 3ac4dd1f04..8aeb55276c 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -182,7 +182,7 @@ def get_compaction_options(model): :return: """ if not model.__compaction__: - return None + return {} result = {'class':model.__compaction__} @@ -239,12 +239,24 @@ def update_compaction(model): row = schema_columnfamilies.get(keyspace_name=ks_name, columnfamily_name=col_family) # check compaction_strategy_class - do_update = not row['compaction_strategy_class'].endswith(model.__compaction__) + if model.__compaction__: + do_update = not row['compaction_strategy_class'].endswith(model.__compaction__) + else: + do_update = False + + existing_options = row['compaction_strategy_options'] + existing_options = json.loads(existing_options) + + desired_options = get_compaction_options(model) + desired_options.pop('class', None) + + for k,v in desired_options.items(): + if existing_options[k] != v: + do_update = True # check compaction_strategy_options if do_update: - options = get_compaction_options(model) - options = json.dumps(options).replace('"', "'") + options = json.dumps(get_compaction_options(model)).replace('"', "'") cf_name = model.column_family_name() execute("ALTER TABLE {} with compaction = {}".format(cf_name, options)) diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index d6d96ad53c..4a958f901d 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -101,6 +101,19 @@ def test_alter_actually_alters(self): columnfamily_name=tmp.column_family_name(include_keyspace=False)) self.assertRegexpMatches(table_settings['compaction_strategy_class'], '.*SizeTieredCompactionStrategy$') + def test_alter_options(self): + + class AlterTable(Model): + __compaction__ = LeveledCompactionStrategy + __compaction_sstable_size_in_mb__ = 64 + + user_id = columns.UUID(primary_key=True) + name = columns.Text() + + drop_table(AlterTable) + sync_table(AlterTable) + AlterTable.__compaction_sstable_size_in_mb__ = 128 + sync_table(AlterTable) @@ -108,7 +121,7 @@ class EmptyCompactionTest(BaseCassEngTestCase): def test_empty_compaction(self): self.model = copy.deepcopy(CompactionModel) result = get_compaction_options(self.model) - self.assertIsNone(result) + self.assertEqual({}, result) class CompactionLeveledStrategyModel(Model): From 32b005edb1883d179ebc2bf9ef3c6e89760b1167 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 19 Aug 2013 13:45:32 -0700 Subject: [PATCH 0356/3961] fixed broken test due to weird copying issue --- cqlengine/tests/management/test_compaction_settings.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index 4a958f901d..07b72a8170 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -119,8 +119,12 @@ class AlterTable(Model): class EmptyCompactionTest(BaseCassEngTestCase): def test_empty_compaction(self): - self.model = copy.deepcopy(CompactionModel) - result = get_compaction_options(self.model) + class EmptyCompactionModel(Model): + __compaction__ = None + cid = columns.UUID(primary_key=True) + name = columns.Text() + + result = get_compaction_options(EmptyCompactionModel) self.assertEqual({}, result) From e7c83a0d69933d418b01c8202cc8c9e87b52f7c5 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 19 Aug 2013 15:49:26 -0700 Subject: [PATCH 0357/3961] adding support for using different queryset and dmlquery classes on a model --- cqlengine/models.py | 12 ++++-- .../tests/model/test_class_construction.py | 38 ++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index d03a00415d..2c011b8e62 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -7,10 +7,12 @@ from cqlengine.query import DoesNotExist as _DoesNotExist from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned + class ModelDefinitionException(ModelException): pass DEFAULT_KEYSPACE = 'cqlengine' + class hybrid_classmethod(object): """ Allows a method to behave as both a class method and @@ -44,7 +46,7 @@ def __get__(self, obj, model): """ :rtype: ModelQuerySet """ if model.__abstract__: raise CQLEngineException('cannot execute queries against abstract models') - return ModelQuerySet(model) + return model.__queryset__(model) def __call__(self, *args, **kwargs): """ @@ -140,6 +142,10 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #the keyspace for this model __keyspace__ = None + # the queryset class used for this class + __queryset__ = ModelQuerySet + __dmlquery__ = DMLQuery + __read_repair_chance__ = 0.1 def __init__(self, **values): @@ -261,7 +267,7 @@ def get(cls, *args, **kwargs): def save(self): is_new = self.pk is None self.validate() - DMLQuery(self.__class__, self, batch=self._batch).save() + self.__dmlquery__(self.__class__, self, batch=self._batch).save() #reset the value managers for v in self._values.values(): @@ -272,7 +278,7 @@ def save(self): def delete(self): """ Deletes this instance """ - DMLQuery(self.__class__, self, batch=self._batch).delete() + self.__dmlquery__(self.__class__, self, batch=self._batch).delete() @classmethod def _class_batch(cls, batch): diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index f0344c6bd3..6653946bea 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -1,5 +1,5 @@ from uuid import uuid4 -from cqlengine.query import QueryException +from cqlengine.query import QueryException, ModelQuerySet, DMLQuery from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException, CQLEngineException @@ -300,6 +300,42 @@ def test_concrete_class_table_creation_cycle(self): delete_table(ConcreteModelWithCol) +class TestCustomQuerySet(BaseCassEngTestCase): + """ Tests overriding the default queryset class """ + + class TestException(Exception): pass + + def test_overriding_queryset(self): + + class QSet(ModelQuerySet): + def create(iself, **kwargs): + raise self.TestException + + class CQModel(Model): + __queryset__ = QSet + part = columns.UUID(primary_key=True) + data = columns.Text() + + with self.assertRaises(self.TestException): + CQModel.create(part=uuid4(), data='s') + + def test_overriding_dmlqueryset(self): + + class DMLQ(DMLQuery): + def save(iself): + raise self.TestException + + class CDQModel(Model): + __dmlquery__ = DMLQ + part = columns.UUID(primary_key=True) + data = columns.Text() + + with self.assertRaises(self.TestException): + CDQModel().save() + + + + From 29c8561049ab6333684ce56c398cbe95b849d3dc Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 11:43:41 -0700 Subject: [PATCH 0358/3961] updated docs with options, added tests for compaction options --- cqlengine/management.py | 32 ++++++----- .../management/test_compaction_settings.py | 54 +++++++++++++++++-- docs/topics/models.rst | 50 ++++++++++++++--- 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 8aeb55276c..e05c7fc265 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -201,7 +201,6 @@ def setter(key, limited_to_strategy = None): if tmp: result[key] = tmp - setter('min_threshold') setter('tombstone_compaction_interval') setter('bucket_high', SizeTieredCompactionStrategy) @@ -231,18 +230,19 @@ def get_fields(model): # convert to Field named tuples +def get_table_settings(model): + return schema_columnfamilies.get(keyspace_name=model._get_keyspace(), + columnfamily_name=model.column_family_name(include_keyspace=False)) + + def update_compaction(model): logger.debug("Checking %s for compaction differences", model) - ks_name = model._get_keyspace() - col_family = model.column_family_name(include_keyspace=False) - - row = schema_columnfamilies.get(keyspace_name=ks_name, - columnfamily_name=col_family) + row = get_table_settings(model) # check compaction_strategy_class - if model.__compaction__: - do_update = not row['compaction_strategy_class'].endswith(model.__compaction__) - else: - do_update = False + if not model.__compaction__: + return + + do_update = not row['compaction_strategy_class'].endswith(model.__compaction__) existing_options = row['compaction_strategy_options'] existing_options = json.loads(existing_options) @@ -251,14 +251,19 @@ def update_compaction(model): desired_options.pop('class', None) for k,v in desired_options.items(): - if existing_options[k] != v: + val = existing_options.pop(k, None) + if val != v: do_update = True # check compaction_strategy_options if do_update: - options = json.dumps(get_compaction_options(model)).replace('"', "'") + options = get_compaction_options(model) + # jsonify + options = json.dumps(options).replace('"', "'") cf_name = model.column_family_name() - execute("ALTER TABLE {} with compaction = {}".format(cf_name, options)) + query = "ALTER TABLE {} with compaction = {}".format(cf_name, options) + logger.debug(query) + execute(query) def delete_table(model): @@ -281,3 +286,4 @@ def drop_table(model): cf_name = model.column_family_name() execute('drop table {};'.format(cf_name)) + diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index 07b72a8170..014abcb2db 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -1,9 +1,10 @@ import copy +import json from time import sleep from mock import patch, MagicMock from cqlengine import Model, columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy from cqlengine.exceptions import CQLEngineException -from cqlengine.management import get_compaction_options, drop_table, sync_table +from cqlengine.management import get_compaction_options, drop_table, sync_table, get_table_settings from cqlengine.tests.base import BaseCassEngTestCase @@ -97,10 +98,11 @@ def test_alter_actually_alters(self): tmp.__compaction_sstable_size_in_mb__ = None sync_table(tmp) - table_settings = schema_columnfamilies.get(keyspace_name=tmp._get_keyspace(), - columnfamily_name=tmp.column_family_name(include_keyspace=False)) + table_settings = get_table_settings(tmp) + self.assertRegexpMatches(table_settings['compaction_strategy_class'], '.*SizeTieredCompactionStrategy$') + def test_alter_options(self): class AlterTable(Model): @@ -138,3 +140,49 @@ class CompactionSizeTieredModel(Model): __compaction__ = SizeTieredCompactionStrategy cid = columns.UUID(primary_key=True) name = columns.Text() + + + +class OptionsTest(BaseCassEngTestCase): + + def test_all_size_tiered_options(self): + class AllSizeTieredOptionsModel(Model): + __compaction__ = SizeTieredCompactionStrategy + __compaction_bucket_low__ = .3 + __compaction_bucket_high__ = 2 + __compaction_min_threshold__ = 2 + __compaction_max_threshold__ = 64 + __compaction_tombstone_compaction_interval__ = 86400 + + cid = columns.UUID(primary_key=True) + name = columns.Text() + + drop_table(AllSizeTieredOptionsModel) + sync_table(AllSizeTieredOptionsModel) + + settings = get_table_settings(AllSizeTieredOptionsModel) + options = json.loads(settings['compaction_strategy_options']) + expected = {u'min_threshold': u'2', + u'bucket_low': u'0.3', + u'tombstone_compaction_interval': u'86400', + u'bucket_high': u'2', + u'max_threshold': u'64'} + self.assertDictEqual(options, expected) + + + def test_all_leveled_options(self): + + class AllLeveledOptionsModel(Model): + __compaction__ = LeveledCompactionStrategy + __compaction_sstable_size_in_mb__ = 64 + + cid = columns.UUID(primary_key=True) + name = columns.Text() + + drop_table(AllLeveledOptionsModel) + sync_table(AllLeveledOptionsModel) + + settings = get_table_settings(AllLeveledOptionsModel) + options = json.loads(settings['compaction_strategy_options']) + self.assertDictEqual(options, {u'sstable_size_in_mb': u'64'}) + diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 86e0840d73..4d9ab98a55 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -21,11 +21,11 @@ This example defines a Person table, with the columns ``first_name`` and ``last_ from cqlengine import columns from cqlengine.models import Model - + class Person(Model): first_name = columns.Text() last_name = columns.Text() - + The Person model would create this CQL table: @@ -83,7 +83,7 @@ Column Options :attr:`~cqlengine.columns.BaseColumn.index` If True, an index will be created for this column. Defaults to False. - + *Note: Indexes can only be created on models with one primary key* :attr:`~cqlengine.columns.BaseColumn.db_field` @@ -109,7 +109,7 @@ Model Methods *Example* .. code-block:: python - + #using the person model from earlier: class Person(Model): first_name = columns.Text() @@ -118,7 +118,7 @@ Model Methods person = Person(first_name='Blake', last_name='Eggleston') person.first_name #returns 'Blake' person.last_name #returns 'Eggleston' - + .. method:: save() @@ -135,7 +135,7 @@ Model Methods .. method:: delete() - + Deletes the object from the database. Model Attributes @@ -153,3 +153,41 @@ Model Attributes *Optional.* Sets the name of the keyspace used by this model. Defaulst to cqlengine + +Compaction Options +==================== + + As of cqlengine 0.6 we've added support for specifying compaction options. cqlengine will only use your compaction options if you have a strategy set. When a table is synced, it will be altered to match the compaction options set on your table. This means that if you are changing settings manually they will be changed back on resync. Do not use the compaction settings of cqlengine if you want to manage your compaction settings manually. + + cqlengine supports all compaction options as of Cassandra 1.2.8. + + Tables may either be + .. attribute:: Model.__compaction_bucket_high__ + + .. attribute:: Model.__compaction_bucket_low__ + + .. attribute:: Model.__compaction_max_compaction_threshold__ + + .. attribute:: Model.__compaction_min_compaction_threshold__ + + .. attribute:: Model.__compaction_min_sstable_size__ + + .. attribute:: Model.__compaction_sstable_size_in_mb__ + + .. attribute:: Model.__compaction_tombstone_compaction_interval__ + + .. attribute:: Model.__compaction_tombstone_threshold__ + + For example: + + .. code-block::python + + class User(Model): + __compaction__ = LeveledCompactionStrategy + __compaction_sstable_size_in_mb__ = 64 + __compaction_tombstone_threshold__ = .2 + + user_id = columns.UUID(primary_key=True) + name = columns.Text() + + From 4c53e3e2e40697e390d8b6d5ce73249d85687708 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 13:23:52 -0700 Subject: [PATCH 0359/3961] updated version --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index a918a2aa18..faef31a435 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.6.0 +0.7.0 From f2f9230b5821990330003d5ed0757fdba8ccbb66 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 13:32:55 -0700 Subject: [PATCH 0360/3961] fixed docs --- docs/topics/models.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 4d9ab98a55..093f1cdd95 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -157,7 +157,7 @@ Model Attributes Compaction Options ==================== - As of cqlengine 0.6 we've added support for specifying compaction options. cqlengine will only use your compaction options if you have a strategy set. When a table is synced, it will be altered to match the compaction options set on your table. This means that if you are changing settings manually they will be changed back on resync. Do not use the compaction settings of cqlengine if you want to manage your compaction settings manually. + As of cqlengine 0.7 we've added support for specifying compaction options. cqlengine will only use your compaction options if you have a strategy set. When a table is synced, it will be altered to match the compaction options set on your table. This means that if you are changing settings manually they will be changed back on resync. Do not use the compaction settings of cqlengine if you want to manage your compaction settings manually. cqlengine supports all compaction options as of Cassandra 1.2.8. @@ -183,7 +183,7 @@ Compaction Options .. code-block::python class User(Model): - __compaction__ = LeveledCompactionStrategy + __compaction__ = cqlengine.LeveledCompactionStrategy __compaction_sstable_size_in_mb__ = 64 __compaction_tombstone_threshold__ = .2 @@ -191,3 +191,4 @@ Compaction Options name = columns.Text() + Tables may use LeveledCompactionStrategy or SizeTieredCompactionStrategy. Both options are available in the top level cqlengine module. From 705b18968f6d02c066b4126b0b8ef6a62fd2efb6 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 13:35:08 -0700 Subject: [PATCH 0361/3961] fixing more docs errors --- docs/topics/models.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 093f1cdd95..551056b55d 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -180,7 +180,7 @@ Compaction Options For example: - .. code-block::python + .. code-block:: python class User(Model): __compaction__ = cqlengine.LeveledCompactionStrategy From 2abdbb52454013c3a6ef7795eedbf252da97a0f1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 13:38:41 -0700 Subject: [PATCH 0362/3961] improving docs --- docs/topics/models.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 551056b55d..17e7cbdcfc 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -190,5 +190,16 @@ Compaction Options user_id = columns.UUID(primary_key=True) name = columns.Text() + or for SizeTieredCompaction: - Tables may use LeveledCompactionStrategy or SizeTieredCompactionStrategy. Both options are available in the top level cqlengine module. + .. code-block:: python + + class TimeData(Model): + __compaction__ = SizeTieredCompactionStrategy + __compaction_bucket_low__ = .3 + __compaction_bucket_high__ = 2 + __compaction_min_threshold__ = 2 + __compaction_max_threshold__ = 64 + __compaction_tombstone_compaction_interval__ = 86400 + + Tables may use `LeveledCompactionStrategy` or `SizeTieredCompactionStrategy`. Both options are available in the top level cqlengine module. To reiterate, you will need to set your `__compaction__` option explicitly in order for cqlengine to handle any of your settings. From dc869c4916d636d93aebf3863dee13f13672f106 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 13:40:37 -0700 Subject: [PATCH 0363/3961] updated readme and index to use sync_table rather than create_table --- README.md | 4 ++-- docs/index.rst | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 09e2014241..a1242e34fc 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ class ExampleModel(Model): >>> connection.setup(['127.0.0.1:9160']) #...and create your CQL table ->>> from cqlengine.management import create_table ->>> create_table(ExampleModel) +>>> from cqlengine.management import sync_table +>>> sync_table(ExampleModel) #now we can create some rows: >>> em1 = ExampleModel.create(example_type=0, description="example1", created_at=datetime.now()) diff --git a/docs/index.rst b/docs/index.rst index 1122599cca..b97360ebe6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,7 +17,7 @@ Contents: .. toctree:: :maxdepth: 2 - + topics/models topics/queryset topics/columns @@ -46,8 +46,8 @@ Getting Started >>> connection.setup(['127.0.0.1:9160']) #...and create your CQL table - >>> from cqlengine.management import create_table - >>> create_table(ExampleModel) + >>> from cqlengine.management import sync_table + >>> sync_table(ExampleModel) #now we can create some rows: >>> em1 = ExampleModel.create(example_type=0, description="example1", created_at=datetime.now()) From 66930a00d96c0a9fee624efc00dee82036c3f4bb Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 13:44:15 -0700 Subject: [PATCH 0364/3961] fixed author --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5fd441428a..944d41191f 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ ], keywords='cassandra,cql,orm', install_requires = ['cql'], - author='Blake Eggleston', + author='Blake Eggleston, Jon Haddad', author_email='bdeggleston@gmail.com', url='https://github.com/cqlengine/cqlengine', license='BSD', From 9c5a0104f56beacee91e953173e246d1710f4c2b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 14:25:58 -0700 Subject: [PATCH 0365/3961] fixing docs --- docs/topics/models.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 17e7cbdcfc..961a866be9 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -161,7 +161,8 @@ Compaction Options cqlengine supports all compaction options as of Cassandra 1.2.8. - Tables may either be + Available Options: + .. attribute:: Model.__compaction_bucket_high__ .. attribute:: Model.__compaction_bucket_low__ From e6cb306771defd290ca1f21562f27fce29b1d0e4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 22:16:45 -0700 Subject: [PATCH 0366/3961] column docs --- docs/topics/columns.rst | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index d323ac216a..daffa28a50 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -18,9 +18,9 @@ Columns .. class:: Ascii() Stores a US-ASCII character string :: - + columns.Ascii() - + .. class:: Text() @@ -93,15 +93,24 @@ Columns columns.Decimal() +.. class:: Counter() + + Counters can be incremented and decremented. + + ..code:: python + + columns.Counter() + + Collection Type Columns ---------------------------- CQLEngine also supports container column types. Each container column requires a column class argument to specify what type of objects it will hold. The Map column requires 2, one for the key, and the other for the value - + *Example* .. code-block:: python - + class Person(Model): id = columns.UUID(primary_key=True, default=uuid.uuid4) first_name = columns.Text() @@ -111,7 +120,7 @@ Collection Type Columns enemies = columns.Set(columns.Text) todo_list = columns.List(columns.Text) birthdays = columns.Map(columns.Text, columns.DateTime) - + .. class:: Set() From 9cbe8d7268797fd7c34495eaf9e9aec2fb0e2384 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 20 Aug 2013 22:17:34 -0700 Subject: [PATCH 0367/3961] typo --- docs/topics/columns.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index daffa28a50..5ee9a18686 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -95,11 +95,9 @@ Columns .. class:: Counter() - Counters can be incremented and decremented. + Counters can be incremented and decremented :: - ..code:: python - - columns.Counter() + columns.Counter() Collection Type Columns From 7354b5d646b3eb31dfc9a7173250c76472fdec3c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 22 Aug 2013 15:26:41 -0700 Subject: [PATCH 0368/3961] adding dev requirements file --- requirements-dev.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000000..2773a99fe4 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +nose +nose-progressive +profilestats +pycallgraph +ipdbplugin==1.2 +ipdb==0.7 From 70f2c95c90e03ed04e5380190e394e9c8b8791c1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 22 Aug 2013 15:29:29 -0700 Subject: [PATCH 0369/3961] splitting up instance constructor methods to make defining custom instantiation methods easier --- cqlengine/query.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 194b4c9330..4ad557d59a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -352,7 +352,7 @@ def _execute_query(self): raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: columns, self._result_cache = execute(self._select_query(), self._where_values()) - self._construct_result = self._create_result_constructor(columns) + self._construct_result = self._get_result_constructor(columns) def _fill_result_cache_to_idx(self, idx): self._execute_query() @@ -408,7 +408,7 @@ def __getitem__(self, s): self._fill_result_cache_to_idx(s) return self._result_cache[s] - def _create_result_constructor(self, names): + def _get_result_constructor(self, names): """ Returns a function that will be used to instantiate query results """ @@ -650,7 +650,7 @@ def _get_select_statement(self): """ Returns the fields to be returned by the select query """ return 'SELECT *' - def _create_result_constructor(self, names): + def _get_result_constructor(self, names): """ Returns a function that will be used to instantiate query results """ @@ -697,26 +697,27 @@ def _get_select_statement(self): db_fields = [self.model._columns[f].db_field_name for f in fields] return 'SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields])) - def _create_result_constructor(self, names): - """ - Returns a function that will be used to instantiate query results - """ + def _get_instance_constructor(self, names): + """ returns a function used to construct model instances """ model = self.model db_map = model._db_map + def _construct_instance(values): + field_dict = dict((db_map.get(k, k), v) for k, v in zip(names, values)) + instance = model(**field_dict) + instance._is_persisted = True + return instance + return _construct_instance + + def _get_result_constructor(self, names): + """ Returns a function that will be used to instantiate query results """ if not self._values_list: - def _construct_instance(values): - field_dict = dict((db_map.get(k, k), v) for k, v in zip(names, values)) - instance = model(**field_dict) - instance._is_persisted = True - return instance - return _construct_instance - - columns = [model._columns[n] for n in names] - if self._flat_values_list: - return (lambda values: columns[0].to_python(values[0])) + return self._get_instance_constructor(names) else: - # result_cls = namedtuple("{}Tuple".format(self.model.__name__), names) - return (lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values))) + columns = [self.model._columns[n] for n in names] + if self._flat_values_list: + return lambda values: columns[0].to_python(values[0]) + else: + return lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values)) def _get_ordering_condition(self, colname): colname, order_type = super(ModelQuerySet, self)._get_ordering_condition(colname) From f70973a90a07b007c8038756dad32446b5439eef Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 22 Aug 2013 15:32:23 -0700 Subject: [PATCH 0370/3961] updating --- changelog | 3 +++ cqlengine/VERSION | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 6a72c4bd18..8dd2fe8608 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.7.1 +* refactoring query class to make defining custom model instantiation logic easier + 0.7.0 * added counter columns * added support for compaction settings at the model level diff --git a/cqlengine/VERSION b/cqlengine/VERSION index faef31a435..39e898a4f9 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.7.0 +0.7.1 From d2b86bb31aefc23a37ab310c7e79e17943beb573 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 22 Aug 2013 15:44:18 -0700 Subject: [PATCH 0371/3961] removing table_name inheritance short circuit, which wasn't working anyway --- cqlengine/models.py | 3 --- cqlengine/tests/model/test_class_construction.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 18a49f7f7b..3212d67244 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -401,9 +401,6 @@ def _transform_column(col_name, col_obj): for field_name, col in column_dict.items(): db_map[col.db_field_name] = field_name - #short circuit table_name inheritance - attrs['table_name'] = attrs.get('__table_name__') - #add management members to the class attrs['_columns'] = column_dict attrs['_primary_keys'] = primary_keys diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 6653946bea..6ae46753ec 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -221,10 +221,6 @@ def test_proper_table_naming(self): assert self.RenamedTest.column_family_name(include_keyspace=False) == 'manual_name' assert self.RenamedTest.column_family_name(include_keyspace=True) == 'whatever.manual_name' - def test_manual_table_name_is_not_inherited(self): - class InheritedTest(self.RenamedTest): pass - assert InheritedTest.table_name is None - class AbstractModel(Model): __abstract__ = True From 4c0648343c161a54d0133968328d18087c3b927b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 30 Aug 2013 15:22:34 -0700 Subject: [PATCH 0372/3961] adding polymorphic_key arg to base column --- cqlengine/columns.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 9940f1aa18..8834b1db7f 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -86,7 +86,8 @@ def __init__(self, db_field=None, default=None, required=False, - clustering_order=None): + clustering_order=None, + polymorphic_key=False): """ :param primary_key: bool flag, indicates this column is a primary key. The first primary key defined on a model is the partition key (unless partition keys are set), all others are cluster keys @@ -99,6 +100,8 @@ def __init__(self, exception if required is set to True and there is a None value assigned :param clustering_order: only applicable on clustering keys (primary keys that are not partition keys) determines the order that the clustering keys are sorted on disk + :param polymorphic_key: boolean, if set to True, this column will be used for saving and loading instances + of polymorphic tables """ self.partition_key = partition_key self.primary_key = partition_key or primary_key @@ -107,6 +110,7 @@ def __init__(self, self.default = default self.required = required self.clustering_order = clustering_order + self.polymorphic_key = polymorphic_key #the column name in the model definition self.column_name = None From 54d610ec20d867f19c61918c315ca6fee421e625 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 10:25:53 -0700 Subject: [PATCH 0373/3961] adding polymorphic config to model metaclass --- cqlengine/models.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 3212d67244..f43f107428 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -10,6 +10,9 @@ class ModelDefinitionException(ModelException): pass + +class PolyMorphicModelException(ModelException): pass + DEFAULT_KEYSPACE = 'cqlengine' @@ -142,6 +145,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #the keyspace for this model __keyspace__ = None + #polymorphism options + __polymorphic_key__ = None # compaction options __compaction__ = None @@ -282,6 +287,14 @@ def get(cls, *args, **kwargs): return cls.objects.get(*args, **kwargs) def save(self): + + # handle polymorphic models + if self._is_polymorphic: + if self._is_polymorphic_base: + raise PolyMorphicModelException('cannot save polymorphic base model') + else: + setattr(self, self._polymorphic_column_name, self.__polymorphic_key__) + is_new = self.pk is None self.validate() self.__dmlquery__(self.__class__, self, batch=self._batch).save() @@ -328,6 +341,9 @@ def __new__(cls, name, bases, attrs): #short circuit __abstract__ inheritance is_abstract = attrs['__abstract__'] = attrs.get('__abstract__', False) + #short circuit __polymorphic_key__ inheritance + attrs['__polymorphic_key__'] = attrs.get('__polymorphic_key__', None) + def _transform_column(col_name, col_obj): column_dict[col_name] = col_obj if col_obj.primary_key: @@ -339,11 +355,20 @@ def _transform_column(col_name, col_obj): column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.Column)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) + polymorphic_base = any([c[1].polymorphic_key for c in column_definitions]) + column_definitions = inherited_columns.items() + column_definitions + polymorphic_columns = [c for c in column_definitions if c[1].polymorphic_key] + is_polymorphic = len(polymorphic_columns) > 0 + if len(polymorphic_columns) > 1: + raise ModelDefinitionException('only one polymorphic_key can be defined in a model, {} found'.format(len(polymorphic_columns))) + + polymorphic_column_name, polymorphic_column = polymorphic_columns[0] if polymorphic_columns else (None, None) + defined_columns = OrderedDict(column_definitions) - #prepend primary key if one hasn't been defined + # check for primary key if not is_abstract and not any([v.primary_key for k,v in column_definitions]): raise ModelDefinitionException("At least 1 primary key is required.") @@ -356,7 +381,7 @@ def _transform_column(col_name, col_obj): #TODO: check that the defined columns don't conflict with any of the Model API's existing attributes/methods #transform column definitions - for k,v in column_definitions: + for k, v in column_definitions: # counter column primary keys are not allowed if (v.primary_key or v.partition_key) and isinstance(v, (columns.Counter, columns.BaseContainerColumn)): raise ModelDefinitionException('counter columns and container columns cannot be used as primary keys') @@ -413,6 +438,12 @@ def _transform_column(col_name, col_obj): attrs['_clustering_keys'] = clustering_keys attrs['_has_counter'] = len(counter_columns) > 0 + # add polymorphic management attributes + attrs['_is_polymorphic_base'] = polymorphic_base + attrs['_is_polymorphic'] = is_polymorphic + attrs['_polymorphic_column'] = polymorphic_column + attrs['_polymorphic_column_name'] = polymorphic_column_name + #setup class exceptions DoesNotExistBase = None for base in bases: From 0c380402954df642d4462749bb6dfb5cfbce38ab Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 10:26:05 -0700 Subject: [PATCH 0374/3961] adding tests around polymorphic models --- cqlengine/tests/model/test_polymorphism.py | 110 +++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 cqlengine/tests/model/test_polymorphism.py diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py new file mode 100644 index 0000000000..2b42f556f5 --- /dev/null +++ b/cqlengine/tests/model/test_polymorphism.py @@ -0,0 +1,110 @@ +import uuid + +from cqlengine import columns +from cqlengine import models +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine import management + + +class TestPolymorphicClassConstruction(BaseCassEngTestCase): + + def test_multiple_polymorphic_key_failure(self): + """ Tests that defining a model with more than one polymorphic key fails """ + with self.assertRaises(models.ModelDefinitionException): + class M(models.Model): + partition = columns.Integer(primary_key=True) + type1 = columns.Integer(polymorphic_key=True) + type2 = columns.Integer(polymorphic_key=True) + + def test_polymorphic_key_inheritance(self): + """ Tests that polymorphic_key attribute is not inherited """ + class Base(models.Model): + partition = columns.Integer(primary_key=True) + type1 = columns.Integer(polymorphic_key=True) + + class M1(Base): + __polymorphic_key__ = 1 + + class M2(M1): + pass + + assert M2.__polymorphic_key__ is None + + def test_polymorphic_metaclass(self): + """ Tests that the model meta class configures polymorphic models properly """ + class Base(models.Model): + partition = columns.Integer(primary_key=True) + type1 = columns.Integer(polymorphic_key=True) + + class M1(Base): + __polymorphic_key__ = 1 + + assert Base._is_polymorphic + assert M1._is_polymorphic + + assert Base._is_polymorphic_base + assert not M1._is_polymorphic_base + + assert Base._polymorphic_column == Base.type1 + assert M1._polymorphic_column == M1.type1 + + assert Base._polymorphic_column_name == 'type1' + assert M1._polymorphic_column_name == 'type1' + + +class PolyBase(models.Model): + partition = columns.UUID(primary_key=True, default=uuid.uuid4) + row_type = columns.Integer(polymorphic_key=True) + + +class Poly1(PolyBase): + __polymorphic_key__ = 1 + data1 = columns.Text() + + +class Poly2(PolyBase): + __polymorphic_key__ = 2 + data2 = columns.Text() + + +class TestPolymorphicModel(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestPolymorphicModel, cls).setUpClass() + management.sync_table(Poly1) + management.sync_table(Poly2) + + @classmethod + def tearDownClass(cls): + super(TestPolymorphicModel, cls).tearDownClass() + management.drop_table(Poly1) + management.drop_table(Poly2) + + def test_saving_base_model_fails(self): + with self.assertRaises(models.PolyMorphicModelException): + PolyBase.create(partition=1) + + def test_saving_subclass_saves_poly_key(self): + pass + + +class TestIndexedPolymorphicQuery(BaseCassEngTestCase): + + def test_success_case(self): + pass + + def test_polymorphic_key_is_added_to_queries(self): + pass + + +class TestUnindexedPolymorphicQuery(BaseCassEngTestCase): + + def test_non_conflicting_type_results_work(self): + pass + + def test_conflicting_type_results(self): + pass + + def test_allow_filtering_filters_types(self): + pass From 94c7578b0b2c1dc180a5041fa4b847df55fa4ede Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 12:00:11 -0700 Subject: [PATCH 0375/3961] moving instance construction onto the model --- cqlengine/models.py | 29 +++++++++++++++++++++++++++++ cqlengine/query.py | 16 +--------------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index f43f107428..7d84a3c889 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -189,6 +189,21 @@ def __init__(self, **values): self._is_persisted = False self._batch = None + @classmethod + def _construct_instance(cls, names, values): + """ + method used to construct instances from query results + + :param cls: + :param names: + :param values: + :return: + """ + field_dict = dict((cls._db_map.get(k, k), v) for k, v in zip(names, values)) + instance = cls(**field_dict) + instance._is_persisted = True + return instance + def _can_update(self): """ Called by the save function to check if this should be @@ -216,6 +231,16 @@ def _get_column(cls, name): """ return cls._columns[name] + @classmethod + def _get_polymorphic_base(cls): + if cls._is_polymorphic: + if cls._is_polymorphic_base: + return cls + for base in cls.__bases__: + klass = base._get_polymorphic_base() + if klass is not None: + return klass + def __eq__(self, other): if self.__class__ != other.__class__: return False @@ -246,6 +271,10 @@ def column_family_name(cls, include_keyspace=True): if cls.__table_name__: cf_name = cls.__table_name__.lower() else: + # get polymorphic base table names if model is polymorphic + if cls._is_polymorphic and not cls._is_polymorphic_base: + return cls._get_polymorphic_base().column_family_name(include_keyspace=include_keyspace) + camelcase = re.compile(r'([a-z])([A-Z])') ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) diff --git a/cqlengine/query.py b/cqlengine/query.py index 4ad557d59a..9bb1a31a33 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -679,9 +679,6 @@ def _validate_where_syntax(self): if any(not w.column.partition_key for w in token_ops): raise QueryException('The token() function is only supported on the partition key') - - #TODO: abuse this to see if we can get cql to raise an exception - def _where_clause(self): """ Returns a where clause based on the given filter args """ self._validate_where_syntax() @@ -697,21 +694,10 @@ def _get_select_statement(self): db_fields = [self.model._columns[f].db_field_name for f in fields] return 'SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields])) - def _get_instance_constructor(self, names): - """ returns a function used to construct model instances """ - model = self.model - db_map = model._db_map - def _construct_instance(values): - field_dict = dict((db_map.get(k, k), v) for k, v in zip(names, values)) - instance = model(**field_dict) - instance._is_persisted = True - return instance - return _construct_instance - def _get_result_constructor(self, names): """ Returns a function that will be used to instantiate query results """ if not self._values_list: - return self._get_instance_constructor(names) + return lambda values: self.model._construct_instance(names, values) else: columns = [self.model._columns[n] for n in names] if self._flat_values_list: From ba476f8d6dce244d2f0cb5581c027140582f48ce Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 12:09:17 -0700 Subject: [PATCH 0376/3961] moving polymorphic base discovery into model meta class --- cqlengine/models.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 7d84a3c889..70ecf05a2c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -231,16 +231,6 @@ def _get_column(cls, name): """ return cls._columns[name] - @classmethod - def _get_polymorphic_base(cls): - if cls._is_polymorphic: - if cls._is_polymorphic_base: - return cls - for base in cls.__bases__: - klass = base._get_polymorphic_base() - if klass is not None: - return klass - def __eq__(self, other): if self.__class__ != other.__class__: return False @@ -273,7 +263,7 @@ def column_family_name(cls, include_keyspace=True): else: # get polymorphic base table names if model is polymorphic if cls._is_polymorphic and not cls._is_polymorphic_base: - return cls._get_polymorphic_base().column_family_name(include_keyspace=include_keyspace) + return cls._polymorphic_base.column_family_name(include_keyspace=include_keyspace) camelcase = re.compile(r'([a-z])([A-Z])') ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s) @@ -384,7 +374,7 @@ def _transform_column(col_name, col_obj): column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.Column)] column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position)) - polymorphic_base = any([c[1].polymorphic_key for c in column_definitions]) + is_polymorphic_base = any([c[1].polymorphic_key for c in column_definitions]) column_definitions = inherited_columns.items() + column_definitions @@ -395,6 +385,18 @@ def _transform_column(col_name, col_obj): polymorphic_column_name, polymorphic_column = polymorphic_columns[0] if polymorphic_columns else (None, None) + # find polymorphic base class + polymorphic_base = None + if is_polymorphic and not is_polymorphic_base: + def _get_polymorphic_base(bases): + for base in bases: + if getattr(base, '_is_polymorphic_base', False): + return base + klass = _get_polymorphic_base(base.__bases__) + if klass: + return klass + polymorphic_base = _get_polymorphic_base(bases) + defined_columns = OrderedDict(column_definitions) # check for primary key @@ -468,8 +470,9 @@ def _transform_column(col_name, col_obj): attrs['_has_counter'] = len(counter_columns) > 0 # add polymorphic management attributes - attrs['_is_polymorphic_base'] = polymorphic_base + attrs['_is_polymorphic_base'] = is_polymorphic_base attrs['_is_polymorphic'] = is_polymorphic + attrs['_polymorphic_base'] = polymorphic_base attrs['_polymorphic_column'] = polymorphic_column attrs['_polymorphic_column_name'] = polymorphic_column_name From 2fc5350289c87b6db8e9ea313d505c238c348181 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 12:25:06 -0700 Subject: [PATCH 0377/3961] adding support for polymorphic model deserialization --- cqlengine/models.py | 53 +++++++++++++++++++--- cqlengine/tests/model/test_polymorphism.py | 28 +++++++++++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 70ecf05a2c..57f922ed41 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -189,18 +189,58 @@ def __init__(self, **values): self._is_persisted = False self._batch = None + @classmethod + def _discover_polymorphic_submodels(cls): + if not cls._is_polymorphic_base: + raise ModelException('_discover_polymorphic_submodels can only be called on polymorphic base classes') + def _discover(klass): + if not klass._is_polymorphic_base and klass.__polymorphic_key__ is not None: + cls._polymorphic_map[klass.__polymorphic_key__] = klass + for subklass in klass.__subclasses__(): + _discover(subklass) + _discover(cls) + + @classmethod + def _get_model_by_polymorphic_key(cls, key): + if not cls._is_polymorphic_base: + raise ModelException('_get_model_by_polymorphic_key can only be called on polymorphic base classes') + return cls._polymorphic_map.get(key) + @classmethod def _construct_instance(cls, names, values): """ method used to construct instances from query results - - :param cls: - :param names: - :param values: - :return: + this is where polymorphic deserialization occurs """ field_dict = dict((cls._db_map.get(k, k), v) for k, v in zip(names, values)) - instance = cls(**field_dict) + if cls._is_polymorphic: + poly_key = field_dict.get(cls._polymorphic_column_name) + + if poly_key is None: + raise PolyMorphicModelException('polymorphic key was not found in values') + + poly_base = cls if cls._is_polymorphic_base else cls._polymorphic_base + + klass = poly_base._get_model_by_polymorphic_key(poly_key) + if klass is None: + poly_base._discover_polymorphic_submodels() + klass = poly_base._get_model_by_polymorphic_key(poly_key) + if klass is None: + raise PolyMorphicModelException( + 'unrecognized polymorphic key {} for class {}'.format(poly_key, poly_base.__name__) + ) + + if not issubclass(klass, poly_base): + raise PolyMorphicModelException( + '{} is not a subclass of {}'.format(klass.__name__, poly_base.__name__) + ) + + field_dict = {k: v for k, v in field_dict.items() if k in klass._columns.keys()} + + else: + klass = cls + + instance = klass(**field_dict) instance._is_persisted = True return instance @@ -475,6 +515,7 @@ def _get_polymorphic_base(bases): attrs['_polymorphic_base'] = polymorphic_base attrs['_polymorphic_column'] = polymorphic_column attrs['_polymorphic_column_name'] = polymorphic_column_name + attrs['_polymorphic_map'] = {} if is_polymorphic_base else None #setup class exceptions DoesNotExistBase = None diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 2b42f556f5..7fc5275ae3 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -51,6 +51,16 @@ class M1(Base): assert Base._polymorphic_column_name == 'type1' assert M1._polymorphic_column_name == 'type1' + def test_table_names_are_inherited_from_poly_base(self): + class Base(models.Model): + partition = columns.Integer(primary_key=True) + type1 = columns.Integer(polymorphic_key=True) + + class M1(Base): + __polymorphic_key__ = 1 + + assert Base.column_family_name() == M1.column_family_name() + class PolyBase(models.Model): partition = columns.UUID(primary_key=True, default=uuid.uuid4) @@ -83,10 +93,24 @@ def tearDownClass(cls): def test_saving_base_model_fails(self): with self.assertRaises(models.PolyMorphicModelException): - PolyBase.create(partition=1) + PolyBase.create() def test_saving_subclass_saves_poly_key(self): - pass + p1 = Poly1.create(data1='pickle') + p2 = Poly2.create(data2='bacon') + + assert p1.row_type == Poly1.__polymorphic_key__ + assert p2.row_type == Poly2.__polymorphic_key__ + + def test_query_deserialization(self): + p1 = Poly1.create(data1='pickle') + p2 = Poly2.create(data2='bacon') + + p1r = PolyBase.get(partition=p1.partition) + p2r = PolyBase.get(partition=p2.partition) + + assert isinstance(p1r, Poly1) + assert isinstance(p2r, Poly2) class TestIndexedPolymorphicQuery(BaseCassEngTestCase): From 2984ba71634e5c3d4b23bb42a977401ca60ffc01 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 15:35:28 -0700 Subject: [PATCH 0378/3961] adding tests around unindexed polymorphic columns --- cqlengine/models.py | 2 +- cqlengine/tests/model/test_polymorphism.py | 65 +++++++++++++++++++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 57f922ed41..8a7d58f8ea 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -230,7 +230,7 @@ def _construct_instance(cls, names, values): 'unrecognized polymorphic key {} for class {}'.format(poly_key, poly_base.__name__) ) - if not issubclass(klass, poly_base): + if not issubclass(klass, cls): raise PolyMorphicModelException( '{} is not a subclass of {}'.format(klass.__name__, poly_base.__name__) ) diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 7fc5275ae3..6dece75fd8 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -113,22 +113,73 @@ def test_query_deserialization(self): assert isinstance(p2r, Poly2) -class TestIndexedPolymorphicQuery(BaseCassEngTestCase): +class UnindexedPolyBase(models.Model): + partition = columns.UUID(primary_key=True, default=uuid.uuid4) + cluster = columns.UUID(primary_key=True, default=uuid.uuid4) + row_type = columns.Integer(polymorphic_key=True) - def test_success_case(self): - pass - def test_polymorphic_key_is_added_to_queries(self): - pass +class UnindexedPoly1(UnindexedPolyBase): + __polymorphic_key__ = 1 + data1 = columns.Text() + + +class UnindexedPoly2(UnindexedPolyBase): + __polymorphic_key__ = 2 + data2 = columns.Text() class TestUnindexedPolymorphicQuery(BaseCassEngTestCase): + @classmethod + def setUpClass(cls): + super(TestUnindexedPolymorphicQuery, cls).setUpClass() + management.sync_table(UnindexedPoly1) + management.sync_table(UnindexedPoly2) + + cls.p1 = UnindexedPoly1.create(data1='pickle') + cls.p2 = UnindexedPoly2.create(partition=cls.p1.partition, data2='bacon') + + @classmethod + def tearDownClass(cls): + super(TestUnindexedPolymorphicQuery, cls).tearDownClass() + management.drop_table(UnindexedPoly1) + management.drop_table(UnindexedPoly2) + def test_non_conflicting_type_results_work(self): - pass + assert len([p for p in UnindexedPoly1.objects(partition=self.p1.partition, cluster=self.p1.cluster)]) == 1 + assert len([p for p in UnindexedPoly2.objects(partition=self.p1partition, cluster=self.p2.cluster)]) == 1 def test_conflicting_type_results(self): - pass + with self.assertRaises(models.PolyMorphicModelException): + [p for p in UnindexedPoly1.objects(partition=self.p1.partition)] + with self.assertRaises(models.PolyMorphicModelException): + [p for p in UnindexedPoly2.objects(partition=self.p1.partition)] def test_allow_filtering_filters_types(self): pass + + +class IndexedPolyBase(models.Model): + partition = columns.UUID(primary_key=True, default=uuid.uuid4) + row_type = columns.Integer(polymorphic_key=True, index=True) + + +class IndexedPoly1(IndexedPolyBase): + __polymorphic_key__ = 1 + data1 = columns.Text() + + +class IndexedPoly2(IndexedPolyBase): + __polymorphic_key__ = 2 + data2 = columns.Text() + + +class TestIndexedPolymorphicQuery(BaseCassEngTestCase): + + def test_success_case(self): + pass + + def test_polymorphic_key_is_added_to_queries(self): + pass + From 5dc9e971267c2072c60ae9271c6e230813f72e15 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 16:29:38 -0700 Subject: [PATCH 0379/3961] finishing up the unindexed polymorphic key tests --- cqlengine/models.py | 2 +- cqlengine/tests/model/test_polymorphism.py | 32 +++++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 8a7d58f8ea..0d94e08cbf 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -232,7 +232,7 @@ def _construct_instance(cls, names, values): if not issubclass(klass, cls): raise PolyMorphicModelException( - '{} is not a subclass of {}'.format(klass.__name__, poly_base.__name__) + '{} is not a subclass of {}'.format(klass.__name__, cls.__name__) ) field_dict = {k: v for k, v in field_dict.items() if k in klass._columns.keys()} diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 6dece75fd8..d24ac34044 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -61,6 +61,9 @@ class M1(Base): assert Base.column_family_name() == M1.column_family_name() + def test_collection_columns_cant_be_polymorphic_keys(self): + pass + class PolyBase(models.Model): partition = columns.UUID(primary_key=True, default=uuid.uuid4) @@ -129,6 +132,11 @@ class UnindexedPoly2(UnindexedPolyBase): data2 = columns.Text() +class UnindexedPoly3(UnindexedPoly2): + __polymorphic_key__ = 3 + data3 = columns.Text() + + class TestUnindexedPolymorphicQuery(BaseCassEngTestCase): @classmethod @@ -136,28 +144,33 @@ def setUpClass(cls): super(TestUnindexedPolymorphicQuery, cls).setUpClass() management.sync_table(UnindexedPoly1) management.sync_table(UnindexedPoly2) + management.sync_table(UnindexedPoly3) cls.p1 = UnindexedPoly1.create(data1='pickle') cls.p2 = UnindexedPoly2.create(partition=cls.p1.partition, data2='bacon') + cls.p3 = UnindexedPoly3.create(partition=cls.p1.partition, data3='bacon') @classmethod def tearDownClass(cls): super(TestUnindexedPolymorphicQuery, cls).tearDownClass() management.drop_table(UnindexedPoly1) management.drop_table(UnindexedPoly2) + management.drop_table(UnindexedPoly3) def test_non_conflicting_type_results_work(self): - assert len([p for p in UnindexedPoly1.objects(partition=self.p1.partition, cluster=self.p1.cluster)]) == 1 - assert len([p for p in UnindexedPoly2.objects(partition=self.p1partition, cluster=self.p2.cluster)]) == 1 + p1, p2, p3 = self.p1, self.p2, self.p3 + assert len(list(UnindexedPoly1.objects(partition=p1.partition, cluster=p1.cluster))) == 1 + assert len(list(UnindexedPoly2.objects(partition=p1.partition, cluster=p2.cluster))) == 1 + + def test_subclassed_model_results_work_properly(self): + p1, p2, p3 = self.p1, self.p2, self.p3 + assert len(list(UnindexedPoly2.objects(partition=p1.partition, cluster__in=[p2.cluster, p3.cluster]))) == 2 def test_conflicting_type_results(self): with self.assertRaises(models.PolyMorphicModelException): - [p for p in UnindexedPoly1.objects(partition=self.p1.partition)] + list(UnindexedPoly1.objects(partition=self.p1.partition)) with self.assertRaises(models.PolyMorphicModelException): - [p for p in UnindexedPoly2.objects(partition=self.p1.partition)] - - def test_allow_filtering_filters_types(self): - pass + list(UnindexedPoly2.objects(partition=self.p1.partition)) class IndexedPolyBase(models.Model): @@ -175,6 +188,11 @@ class IndexedPoly2(IndexedPolyBase): data2 = columns.Text() +class IndexedPoly3(IndexedPoly2): + __polymorphic_key__ = 3 + data3 = columns.Text() + + class TestIndexedPolymorphicQuery(BaseCassEngTestCase): def test_success_case(self): From 8aa59edb36deb7f985dea556251cf66c9242be1b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 16:33:22 -0700 Subject: [PATCH 0380/3961] switching select query to all columns when defer and only fields aren't defined --- cqlengine/query.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 9bb1a31a33..02a5d7d577 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -686,13 +686,16 @@ def _where_clause(self): def _get_select_statement(self): """ Returns the fields to be returned by the select query """ - fields = self.model._columns.keys() - if self._defer_fields: - fields = [f for f in fields if f not in self._defer_fields] - elif self._only_fields: - fields = self._only_fields - db_fields = [self.model._columns[f].db_field_name for f in fields] - return 'SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields])) + if self._defer_fields or self._only_fields: + fields = self.model._columns.keys() + if self._defer_fields: + fields = [f for f in fields if f not in self._defer_fields] + elif self._only_fields: + fields = self._only_fields + db_fields = [self.model._columns[f].db_field_name for f in fields] + return 'SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields])) + else: + return 'SELECT *' def _get_result_constructor(self, names): """ Returns a function that will be used to instantiate query results """ From 880600924ae33998b7dddc6b36de00618b089511 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 16:53:59 -0700 Subject: [PATCH 0381/3961] adding support for auto filtering on polymorphic subclass models with and indexed polymorphic key --- cqlengine/models.py | 13 ++++++++++- cqlengine/tests/model/test_polymorphism.py | 26 +++++++++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 0d94e08cbf..e55ed5ae45 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -49,7 +49,18 @@ def __get__(self, obj, model): """ :rtype: ModelQuerySet """ if model.__abstract__: raise CQLEngineException('cannot execute queries against abstract models') - return model.__queryset__(model) + queryset = model.__queryset__(model) + + # if this is a concrete polymorphic model, and the polymorphic + # key is an indexed column, add a filter clause to only return + # logical rows of the proper type + if model._is_polymorphic and not model._is_polymorphic_base: + name, column = model._polymorphic_column_name, model._polymorphic_column + if column.partition_key or column.index: + # look for existing poly types + return queryset.filter(**{name: model.__polymorphic_key__}) + + return queryset def __call__(self, *args, **kwargs): """ diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index d24ac34044..d608aae1b7 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -148,7 +148,7 @@ def setUpClass(cls): cls.p1 = UnindexedPoly1.create(data1='pickle') cls.p2 = UnindexedPoly2.create(partition=cls.p1.partition, data2='bacon') - cls.p3 = UnindexedPoly3.create(partition=cls.p1.partition, data3='bacon') + cls.p3 = UnindexedPoly3.create(partition=cls.p1.partition, data3='turkey') @classmethod def tearDownClass(cls): @@ -175,6 +175,7 @@ def test_conflicting_type_results(self): class IndexedPolyBase(models.Model): partition = columns.UUID(primary_key=True, default=uuid.uuid4) + cluster = columns.UUID(primary_key=True, default=uuid.uuid4) row_type = columns.Integer(polymorphic_key=True, index=True) @@ -188,16 +189,25 @@ class IndexedPoly2(IndexedPolyBase): data2 = columns.Text() -class IndexedPoly3(IndexedPoly2): - __polymorphic_key__ = 3 - data3 = columns.Text() +class TestIndexedPolymorphicQuery(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestIndexedPolymorphicQuery, cls).setUpClass() + management.sync_table(IndexedPoly1) + management.sync_table(IndexedPoly2) + cls.p1 = IndexedPoly1.create(data1='pickle') + cls.p2 = IndexedPoly2.create(partition=cls.p1.partition, data2='bacon') -class TestIndexedPolymorphicQuery(BaseCassEngTestCase): + @classmethod + def tearDownClass(cls): + super(TestIndexedPolymorphicQuery, cls).tearDownClass() + management.drop_table(IndexedPoly1) + management.drop_table(IndexedPoly2) def test_success_case(self): - pass + assert len(list(IndexedPoly1.objects(partition=self.p1.partition))) == 1 + assert len(list(IndexedPoly2.objects(partition=self.p1.partition))) == 1 - def test_polymorphic_key_is_added_to_queries(self): - pass From f0b6966d41f16a608bfcd25c30d10cbab57919c2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 17:00:15 -0700 Subject: [PATCH 0382/3961] adding restriction around defining counter or container columns as polymorphic keys --- cqlengine/models.py | 3 +++ cqlengine/tests/model/test_polymorphism.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index e55ed5ae45..d01208ea09 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -436,6 +436,9 @@ def _transform_column(col_name, col_obj): polymorphic_column_name, polymorphic_column = polymorphic_columns[0] if polymorphic_columns else (None, None) + if isinstance(polymorphic_column, (columns.BaseContainerColumn, columns.Counter)): + raise ModelDefinitionException('counter and container columns cannot be used for polymorphic keys') + # find polymorphic base class polymorphic_base = None if is_polymorphic and not is_polymorphic_base: diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index d608aae1b7..00078bcb50 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -62,7 +62,10 @@ class M1(Base): assert Base.column_family_name() == M1.column_family_name() def test_collection_columns_cant_be_polymorphic_keys(self): - pass + with self.assertRaises(models.ModelDefinitionException): + class Base(models.Model): + partition = columns.Integer(primary_key=True) + type1 = columns.Set(columns.Integer, polymorphic_key=True) class PolyBase(models.Model): From cdb88e4fa4897769011c0181ef48c0ef6cdd4621 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 3 Sep 2013 18:32:06 -0700 Subject: [PATCH 0383/3961] adding custom validation --- cqlengine/models.py | 6 ++++ cqlengine/tests/model/test_validation.py | 46 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index d01208ea09..202234af5e 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -331,6 +331,12 @@ def validate(self): """ Cleans and validates the field values """ for name, col in self._columns.items(): val = col.validate(getattr(self, name)) + + # handle custom validation + validation_function = getattr(self, 'validate_{}'.format(name), None) + if validation_function: + val = validation_function(val) + setattr(self, name, val) def _as_dict(self): diff --git a/cqlengine/tests/model/test_validation.py b/cqlengine/tests/model/test_validation.py index e69de29bb2..9a59872731 100644 --- a/cqlengine/tests/model/test_validation.py +++ b/cqlengine/tests/model/test_validation.py @@ -0,0 +1,46 @@ +from uuid import uuid4 + +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.management import sync_table +from cqlengine.management import drop_table +from cqlengine.models import Model +from cqlengine import columns +from cqlengine import ValidationError + + +class CustomValidationTestModel(Model): + id = columns.UUID(primary_key=True, default=lambda: uuid4()) + count = columns.Integer() + text = columns.Text(required=False) + a_bool = columns.Boolean(default=False) + + def validate_a_bool(self, val): + if val: + raise ValidationError('False only!') + return val + + def validate_text(self, val): + if val.lower() == 'jon': + raise ValidationError('no one likes jon') + return val + + +class TestCustomValidation(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestCustomValidation, cls).setUpClass() + sync_table(CustomValidationTestModel) + + @classmethod + def tearDownClass(cls): + super(TestCustomValidation, cls).tearDownClass() + drop_table(CustomValidationTestModel) + + def test_custom_validation(self): + # sanity check + CustomValidationTestModel.create(count=5, text='txt', a_bool=False) + + with self.assertRaises(ValidationError): + CustomValidationTestModel.create(count=5, text='txt', a_bool=True) + From 3b3d00051816f567348a57e1d11cf8e0db15dd97 Mon Sep 17 00:00:00 2001 From: Tommaso Barbugli Date: Wed, 4 Sep 2013 15:23:51 +0200 Subject: [PATCH 0384/3961] get cassandra host from the environment (CASSANDRA_TEST_HOST) or default to localhost --- cqlengine/tests/base.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index 126049a860..98e34eaa7e 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -1,13 +1,20 @@ from unittest import TestCase from cqlengine import connection +import os + + +if os.environ.get('CASSANDRA_TEST_HOST'): + CASSANDRA_TEST_HOST = os.environ['CASSANDRA_TEST_HOST'] +else: + CASSANDRA_TEST_HOST = 'localhost:9160' + class BaseCassEngTestCase(TestCase): @classmethod def setUpClass(cls): super(BaseCassEngTestCase, cls).setUpClass() - # todo fix - connection.setup(['localhost:9160'], default_keyspace='cqlengine_test') + connection.setup([CASSANDRA_TEST_HOST], default_keyspace='cqlengine_test') def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), From fb6543c973b8e445a96c110780fdecd0d8ebf7e1 Mon Sep 17 00:00:00 2001 From: Tommaso Barbugli Date: Wed, 4 Sep 2013 15:56:09 +0200 Subject: [PATCH 0385/3961] add VarInt column --- cqlengine/columns.py | 20 ++++++++++++++++ cqlengine/tests/columns/test_validation.py | 28 +++++++++++++++++++++- docs/topics/columns.rst | 6 +++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 9940f1aa18..be3e193110 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -246,6 +246,26 @@ def to_database(self, value): return self.validate(value) +class VarInt(Column): + db_type = 'varint' + + def validate(self, value): + val = super(VarInt, self).validate(value) + if val is None: + return + try: + return long(val) + except (TypeError, ValueError): + raise ValidationError( + "{} can't be converted to integral value".format(value)) + + def to_python(self, value): + return self.validate(value) + + def to_database(self, value): + return self.validate(value) + + class CounterValueManager(BaseValueManager): def __init__(self, instance, column, value): super(CounterValueManager, self).__init__(instance, column, value) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 53cd738c02..e2393d5b34 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -14,6 +14,7 @@ from cqlengine.columns import Ascii from cqlengine.columns import Text from cqlengine.columns import Integer +from cqlengine.columns import VarInt from cqlengine.columns import DateTime from cqlengine.columns import Date from cqlengine.columns import UUID @@ -24,6 +25,9 @@ from cqlengine.management import create_table, delete_table from cqlengine.models import Model +import sys + + class TestDatetime(BaseCassEngTestCase): class DatetimeTest(Model): test_id = Integer(primary_key=True) @@ -64,6 +68,28 @@ def test_datetime_date_support(self): assert dt2.created_at.isoformat() == datetime(today.year, today.month, today.day).isoformat() +class TestVarInt(BaseCassEngTestCase): + class VarIntTest(Model): + test_id = Integer(primary_key=True) + bignum = VarInt(primary_key=True) + + @classmethod + def setUpClass(cls): + super(TestVarInt, cls).setUpClass() + create_table(cls.VarIntTest) + + @classmethod + def tearDownClass(cls): + super(TestVarInt, cls).tearDownClass() + delete_table(cls.VarIntTest) + + def test_varint_io(self): + long_int = sys.maxint + 1 + int1 = self.VarIntTest.objects.create(test_id=0, bignum=long_int) + int2 = self.VarIntTest.objects(test_id=0).first() + assert int1.bignum == int2.bignum + + class TestDate(BaseCassEngTestCase): class DateTest(Model): test_id = Integer(primary_key=True) @@ -109,7 +135,7 @@ def tearDownClass(cls): super(TestDecimal, cls).tearDownClass() delete_table(cls.DecimalTest) - def test_datetime_io(self): + def test_decimal_io(self): dt = self.DecimalTest.objects.create(test_id=0, dec_val=D('0.00')) dt2 = self.DecimalTest.objects(test_id=0).first() assert dt2.dec_val == dt.dec_val diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 5ee9a18686..0418147f18 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -42,6 +42,12 @@ Columns columns.Integer() +.. class:: VarInt() + + Stores an arbitrary-precision integer :: + + columns.VarInt() + .. class:: DateTime() Stores a datetime value. From b35f915044fd697f2ad9a1f9acbd4aec520b0d66 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 4 Sep 2013 11:19:06 -0700 Subject: [PATCH 0386/3961] reverting custom validation changes --- cqlengine/models.py | 6 ---- cqlengine/tests/model/test_validation.py | 45 ------------------------ 2 files changed, 51 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 202234af5e..d01208ea09 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -331,12 +331,6 @@ def validate(self): """ Cleans and validates the field values """ for name, col in self._columns.items(): val = col.validate(getattr(self, name)) - - # handle custom validation - validation_function = getattr(self, 'validate_{}'.format(name), None) - if validation_function: - val = validation_function(val) - setattr(self, name, val) def _as_dict(self): diff --git a/cqlengine/tests/model/test_validation.py b/cqlengine/tests/model/test_validation.py index 9a59872731..8b13789179 100644 --- a/cqlengine/tests/model/test_validation.py +++ b/cqlengine/tests/model/test_validation.py @@ -1,46 +1 @@ -from uuid import uuid4 - -from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.management import sync_table -from cqlengine.management import drop_table -from cqlengine.models import Model -from cqlengine import columns -from cqlengine import ValidationError - - -class CustomValidationTestModel(Model): - id = columns.UUID(primary_key=True, default=lambda: uuid4()) - count = columns.Integer() - text = columns.Text(required=False) - a_bool = columns.Boolean(default=False) - - def validate_a_bool(self, val): - if val: - raise ValidationError('False only!') - return val - - def validate_text(self, val): - if val.lower() == 'jon': - raise ValidationError('no one likes jon') - return val - - -class TestCustomValidation(BaseCassEngTestCase): - - @classmethod - def setUpClass(cls): - super(TestCustomValidation, cls).setUpClass() - sync_table(CustomValidationTestModel) - - @classmethod - def tearDownClass(cls): - super(TestCustomValidation, cls).tearDownClass() - drop_table(CustomValidationTestModel) - - def test_custom_validation(self): - # sanity check - CustomValidationTestModel.create(count=5, text='txt', a_bool=False) - - with self.assertRaises(ValidationError): - CustomValidationTestModel.create(count=5, text='txt', a_bool=True) From 031ea5e8ad1540aed356f597dc72935c591f551b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 4 Sep 2013 14:05:34 -0700 Subject: [PATCH 0387/3961] adding docs for polymorphic models and user defined validation --- docs/topics/models.rst | 86 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 961a866be9..183fa27b12 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -154,6 +154,92 @@ Model Attributes *Optional.* Sets the name of the keyspace used by this model. Defaulst to cqlengine +Table Polymorphism +================== + + As of cqlengine 0.8, it is possible to save and load different model classes using a single CQL table. + This is useful in situations where you have different object types that you want to store in a single cassandra row. + + For instance, suppose you want a table that stores rows of pets owned by an owner: + + .. code-block:: python + + class Pet(Model): + __table_name__ = 'pet' + owner_id = UUID(primary_key=True) + pet_id = UUID(primary_key=True) + pet_type = Text(polymorphic_key=True) + name = Text() + + def eat(self, food): + pass + + def sleep(self, time): + pass + + class Cat(Pet): + __polymorphic_key__ = 'cat' + cuteness = Float() + + def tear_up_couch(self): + pass + + class Dog(Pet): + __polymorphic_key__ = 'dog' + fierceness = Float() + + def bark_all_night(self): + pass + + After calling ``sync_table`` on each of these tables, the columns defined in each model will be added to the + ``pet`` table. Additionally, saving ``Cat`` and ``Dog`` models will save the meta data needed to identify each row + as either a cat or dog. + + To setup a polymorphic model structure, follow these steps + + 1. Create a base model with a column set as the polymorphic_key (set ``polymorphic_key=True`` in the column definition) + 2. Create subclass models, and define a unique ``__polymorphic_key__`` value on each + 3. Run ``sync_table`` on each of the sub tables + + **About the polymorphic key** + + The polymorphic key is what cqlengine uses under the covers to map logical cql rows to the appropriate model type. The + base model maintains a map of polymorphic keys to subclasses. When a polymorphic model is saved, this value is automatically + saved into the polymorphic key column. You can set the polymorphic key column to any column type that you like, with + the exception of container and counter columns, although ``Integer`` columns make the most sense. Additionally, if you + set ``index=True`` on your polymorphic key column, you can execute queries against polymorphic subclasses, and a + ``WHERE`` clause will be automatically added to your query, returning only rows of that type. Note that you must + define a unique ``__polymorphic_key__`` value to each subclass, and that you can only assign a single polymorphic + key column per model + + +Extending Model Validation +========================== + + Each time you save a model instance in cqlengine, the data in the model is validated against the schema you've defined + for your model. Most of the validation is fairly straightforward, it basically checks that you're not trying to do + something like save text into an integer column, and it enforces the ``required`` flag set on column definitions. + It also performs any transformations needed to save the data properly. + + However, there are often additional constraints or transformations you want to impose on your data, beyond simply + making sure that Cassandra won't complain when you try to insert it. To define additional validation on a model, + extend the model's validation method: + + .. code-block:: python + + class Member(Model): + person_id = UUID(primary_key=True) + name = Text(required=True) + + def validate(self): + super(Member, self).validate() + if self.name == 'jon': + raise ValidationError('no jon\'s allowed') + + *Note*: while not required, the convention is to raise a ``ValidationError`` (``from cqlengine import ValidationError``) + if validation fails + + Compaction Options ==================== From 712814432ab013d753e7e68f534e71a8886e6386 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 4 Sep 2013 17:14:26 -0700 Subject: [PATCH 0388/3961] fixing typo in docs --- docs/topics/models.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 183fa27b12..06b720d4de 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -151,7 +151,7 @@ Model Attributes .. attribute:: Model.__keyspace__ - *Optional.* Sets the name of the keyspace used by this model. Defaulst to cqlengine + *Optional.* Sets the name of the keyspace used by this model. Defaults to cqlengine Table Polymorphism From 95246fc13421ab01758a13094878ed3339436346 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 4 Sep 2013 17:18:08 -0700 Subject: [PATCH 0389/3961] updating the doc index with download links --- docs/index.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index b97360ebe6..e4e8c394fd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,15 @@ cqlengine is a Cassandra CQL 3 Object Mapper for Python :ref:`getting-started` +Download +======== + +`Github `_ + +`PyPi `_ + Contents: +========= .. toctree:: :maxdepth: 2 From 41b9284c7129fad9f8d28c3f7cfbee2e01330f98 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 4 Sep 2013 21:04:34 -0700 Subject: [PATCH 0390/3961] savage version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 39e898a4f9..aec258df73 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.7.1 +0.8 From e72cde5f0e7de393e2b9494062245e0eaf1f7392 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Sep 2013 17:53:26 -0700 Subject: [PATCH 0391/3961] validation around not throwing an exception when updating a keyspace --- cqlengine/models.py | 7 +++---- cqlengine/tests/columns/test_validation.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index d01208ea09..2969c228aa 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -184,10 +184,6 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass def __init__(self, **values): self._values = {} - extra_columns = set(values.keys()) - set(self._columns.keys()) - if extra_columns: - raise ValidationError("Incorrect columns passed: {}".format(extra_columns)) - for name, column in self._columns.items(): value = values.get(name, None) if value is not None or isinstance(column, columns.BaseContainerColumn): @@ -342,6 +338,9 @@ def _as_dict(self): @classmethod def create(cls, **kwargs): + extra_columns = set(kwargs.keys()) - set(cls._columns.keys()) + if extra_columns: + raise ValidationError("Incorrect columns passed: {}".format(extra_columns)) return cls.objects.create(**kwargs) @classmethod diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index e2393d5b34..6362375ed4 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -6,6 +6,7 @@ from unittest import TestCase from uuid import uuid4, uuid1 from cqlengine import ValidationError +from cqlengine.connection import execute from cqlengine.tests.base import BaseCassEngTestCase @@ -22,7 +23,7 @@ from cqlengine.columns import Float from cqlengine.columns import Decimal -from cqlengine.management import create_table, delete_table +from cqlengine.management import create_table, delete_table, sync_table from cqlengine.models import Model import sys @@ -234,6 +235,16 @@ def test_extra_field(self): with self.assertRaises(ValidationError): self.TestModel.create(bacon=5000) +class TestPythonDoesntDieWhenExtraFieldIsInCassandra(BaseCassEngTestCase): + class TestModel(Model): + __table_name__ = 'alter_doesnt_break_running_app' + id = UUID(primary_key=True, default=uuid4) + + def test_extra_field(self): + sync_table(self.TestModel) + self.TestModel.create() + execute("ALTER TABLE {} add blah int".format(self.TestModel.column_family_name(include_keyspace=True))) + self.TestModel.objects().all() class TestTimeUUIDFromDatetime(TestCase): def test_conversion_specific_date(self): From 3bd1ee58551d4faf21a69a7a82b56f41c94c9c9a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Sep 2013 17:57:39 -0700 Subject: [PATCH 0392/3961] removed python 2.6, fixed issue with model creation --- cqlengine/VERSION | 2 +- cqlengine/tests/columns/test_validation.py | 3 ++- setup.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 39e898a4f9..100435be13 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.7.1 +0.8.2 diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 6362375ed4..3c6c665614 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -23,7 +23,7 @@ from cqlengine.columns import Float from cqlengine.columns import Decimal -from cqlengine.management import create_table, delete_table, sync_table +from cqlengine.management import create_table, delete_table, sync_table, drop_table from cqlengine.models import Model import sys @@ -241,6 +241,7 @@ class TestModel(Model): id = UUID(primary_key=True, default=uuid4) def test_extra_field(self): + drop_table(self.TestModel) sync_table(self.TestModel) self.TestModel.create() execute("ALTER TABLE {} add blah int".format(self.TestModel.column_family_name(include_keyspace=True))) diff --git a/setup.py b/setup.py index 944d41191f..3bb09c9c2c 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ "Environment :: Plugins", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules", From 96d4b45effcdf6cda5bfe7778f1347a384b5a260 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Sep 2013 17:57:53 -0700 Subject: [PATCH 0393/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 100435be13..6f4eebdf6f 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.8.2 +0.8.1 From 5c16b58a48c493322d515a98b774efb128a50320 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 24 Sep 2013 13:30:56 -0700 Subject: [PATCH 0394/3961] allow attempt to requery on connection failure --- cqlengine/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index fabf197a31..dc0d73a5a4 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -168,7 +168,7 @@ def execute(self, query, params): except cql.ProgrammingError as ex: raise CQLEngineException(unicode(ex)) except TTransportException: - raise CQLEngineException("Could not execute query against the cluster") + pass def execute(query, params=None): From 8c9487864f43df9d54b95cb22cf1ee8905271467 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 24 Sep 2013 13:32:53 -0700 Subject: [PATCH 0395/3961] version bumb --- changelog | 10 ++++++++++ cqlengine/VERSION | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 8dd2fe8608..75814f1386 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,15 @@ CHANGELOG +0.8.2 +* fix for connection failover + +0.8.1 +* fix for models not exactly matching schema + +0.8.0 +* support for table polymorphism +* var int type + 0.7.1 * refactoring query class to make defining custom model instantiation logic easier diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 6f4eebdf6f..100435be13 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.8.1 +0.8.2 From 639fef10cf659c573892aa00a022dfe7edb5d3f6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 25 Sep 2013 13:35:26 -0700 Subject: [PATCH 0396/3961] fixing documentation inconsistency --- docs/topics/columns.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 0418147f18..6d04df6452 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -197,7 +197,7 @@ Column Options .. attribute:: BaseColumn.required - If True, this model cannot be saved without a value defined for this column. Defaults to True. Primary key fields cannot have their required fields set to False. + If True, this model cannot be saved without a value defined for this column. Defaults to False. Primary key fields cannot have their required fields set to False. .. attribute:: BaseColumn.clustering_order From 523d99364a47e1a396e99dc3ed856ec0ec4d0669 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Sep 2013 13:35:19 -0700 Subject: [PATCH 0397/3961] better logging on operational errors --- cqlengine/connection.py | 6 ++++++ .../tests/connections/test_connection_pool.py | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index dc0d73a5a4..0092e1f538 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -12,6 +12,8 @@ from copy import copy from cqlengine.exceptions import CQLEngineException +from cql import OperationalError + from contextlib import contextmanager from thrift.transport.TTransport import TTransportException @@ -28,6 +30,7 @@ class CQLConnectionError(CQLEngineException): pass connection_pool = None + class CQLConnectionError(CQLEngineException): pass @@ -169,6 +172,9 @@ def execute(self, query, params): raise CQLEngineException(unicode(ex)) except TTransportException: pass + except OperationalError as ex: + LOG.exception("Operational Error %s on %s:%s", ex, con.host, con.port) + raise ex def execute(query, params=None): diff --git a/cqlengine/tests/connections/test_connection_pool.py b/cqlengine/tests/connections/test_connection_pool.py index e69de29bb2..29b3ea50a9 100644 --- a/cqlengine/tests/connections/test_connection_pool.py +++ b/cqlengine/tests/connections/test_connection_pool.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from cql import OperationalError +from mock import MagicMock, patch, Mock + +from cqlengine.connection import ConnectionPool, Host + + +class OperationalErrorLoggingTest(TestCase): + def test_logging(self): + p = ConnectionPool([Host('127.0.0.1', '9160')]) + + class MockConnection(object): + host = 'localhost' + port = 6379 + def cursor(self): + raise OperationalError('test') + + + with patch.object(p, 'get', return_value=MockConnection()): + with self.assertRaises(OperationalError): + p.execute("select * from system.peers", {}) From f6e37e96ce90a35a7a38124c7a451bab3c4b3c9b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Sep 2013 13:36:00 -0700 Subject: [PATCH 0398/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 100435be13..ee94dd834b 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.8.2 +0.8.3 From b8f9b336e8fba3e1404f59aa5d9c784f151db75a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Sep 2013 13:36:19 -0700 Subject: [PATCH 0399/3961] updated changelog --- changelog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog b/changelog index 75814f1386..9c8be6538e 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.8.3 +* better logging for operational errors + 0.8.2 * fix for connection failover From 5003e608c6593e98b82479ae9db876fa0ef0eccc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 2 Oct 2013 13:40:36 -0700 Subject: [PATCH 0400/3961] updating value manager to use deepcopy instead of copy --- changelog | 3 +++ cqlengine/VERSION | 2 +- cqlengine/columns.py | 8 +++++--- cqlengine/tests/columns/test_container_columns.py | 2 -- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/changelog b/changelog index 9c8be6538e..ac3d1a6486 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.8.4 +* changing value manager previous value copying to deepcopy + 0.8.3 * better logging for operational errors diff --git a/cqlengine/VERSION b/cqlengine/VERSION index ee94dd834b..b60d71966a 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.8.3 +0.8.4 diff --git a/cqlengine/columns.py b/cqlengine/columns.py index e40ddc28c0..10a5e1b956 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -1,5 +1,5 @@ #column field types -from copy import copy +from copy import deepcopy from datetime import datetime from datetime import date import re @@ -8,12 +8,13 @@ from cqlengine.exceptions import ValidationError + class BaseValueManager(object): def __init__(self, instance, column, value): self.instance = instance self.column = column - self.previous_value = copy(value) + self.previous_value = deepcopy(value) self.value = value @property @@ -31,7 +32,7 @@ def changed(self): return self.value != self.previous_value def reset_previous_value(self): - self.previous_value = copy(self.value) + self.previous_value = deepcopy(self.value) def getval(self): return self.value @@ -52,6 +53,7 @@ def get_property(self): else: return property(_get, _set) + class ValueQuoter(object): """ contains a single value, which will quote itself for CQL insertion statements diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index cd11fb5abb..ba4df808c8 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -145,8 +145,6 @@ def test_instantiation_with_column_instance(self): column = columns.Set(columns.Text(min_length=100)) assert isinstance(column.value_col, columns.Text) - - def test_to_python(self): """ Tests that to_python of value column is called """ column = columns.Set(JsonTestColumn) From 10e4d4290405083979ff7d1ce272ed97efb6f759 Mon Sep 17 00:00:00 2001 From: Eric Scrivner Date: Thu, 3 Oct 2013 16:22:34 -0700 Subject: [PATCH 0401/3961] Transport Creation --- cqlengine/connection.py | 57 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 0092e1f538..f6e25e4e76 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -44,11 +44,32 @@ def _column_tuple_factory(colnames, values): return tuple(colnames), [RowResult(v) for v in values] -def setup(hosts, username=None, password=None, max_connections=10, default_keyspace=None, consistency='ONE'): +def setup( + hosts, + username=None, + password=None, + max_connections=10, + default_keyspace=None, + consistency='ONE', + timeout=None): """ Records the hosts and connects to one of them :param hosts: list of hosts, strings in the :, or just + :type hosts: list + :param username: The cassandra username + :type username: str + :param password: The cassandra password + :type password: str + :param max_connections: The maximum number of connections to service + :type max_connections: int or long + :param default_keyspace: The default keyspace to use + :type default_keyspace: str + :param consistency: The global consistency level + :type consistency: str + :param timeout: The connection timeout in milliseconds + :type timeout: int or long + """ global _max_connections global connection_pool @@ -72,17 +93,24 @@ def setup(hosts, username=None, password=None, max_connections=10, default_keysp if not _hosts: raise CQLConnectionError("At least one host required") - connection_pool = ConnectionPool(_hosts, username, password, consistency) + connection_pool = ConnectionPool(_hosts, username, password, consistency, timeout) class ConnectionPool(object): """Handles pooling of database connections.""" - def __init__(self, hosts, username=None, password=None, consistency=None): + def __init__( + self, + hosts, + username=None, + password=None, + consistency=None, + timeout=None): self._hosts = hosts self._username = username self._password = password self._consistency = consistency + self._timeout = timeout self._queue = Queue.Queue(maxsize=_max_connections) @@ -124,6 +152,25 @@ def put(self, conn): else: self._queue.put(conn) + def _create_transport(self, host): + """ + Create a new Thrift transport for the given host. + + :param host: The host object + :type host: Host + + :rtype: thrift.TTransport.* + + """ + from thrift.transport import TSocket, TTransport + + thrift_socket = TSocket.TSocket(host.name, host.port) + + if self._timeout is not None: + thrift_socket.setTimeout(self._timeout) + + return TTransport.TFramedTransport(thrift_socket) + def _create_connection(self): """ Creates a new connection for the connection pool. @@ -138,12 +185,14 @@ def _create_connection(self): for host in hosts: try: + transport = self._create_transport(host) new_conn = cql.connect( host.name, host.port, user=self._username, password=self._password, - consistency_level=self._consistency + consistency_level=self._consistency, + transport=transport ) new_conn.set_cql_version('3.0.0') return new_conn From f4bf649709f8daefc153e91d9ffba31e4f0f4777 Mon Sep 17 00:00:00 2001 From: Eric Scrivner Date: Thu, 3 Oct 2013 16:29:31 -0700 Subject: [PATCH 0402/3961] Update documentation --- cqlengine/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index f6e25e4e76..ecd3edd82b 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -67,7 +67,7 @@ def setup( :type default_keyspace: str :param consistency: The global consistency level :type consistency: str - :param timeout: The connection timeout in milliseconds + :param timeout: The connection timeout in seconds :type timeout: int or long """ From f23d310767a6c506574cc124b9cbfc5312c6887d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 3 Oct 2013 16:46:23 -0700 Subject: [PATCH 0403/3961] updating version number and docs --- changelog | 3 +++ cqlengine/VERSION | 2 +- docs/topics/connection.rst | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index ac3d1a6486..21d31aa7c8 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,8 @@ CHANGELOG +0.8.5 +* adding support for timeouts + 0.8.4 * changing value manager previous value copying to deepcopy diff --git a/cqlengine/VERSION b/cqlengine/VERSION index b60d71966a..7ada0d303f 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.8.4 +0.8.5 diff --git a/docs/topics/connection.rst b/docs/topics/connection.rst index 88f601fbd0..c956b58fda 100644 --- a/docs/topics/connection.rst +++ b/docs/topics/connection.rst @@ -25,6 +25,9 @@ If there is a problem with one of the servers, cqlengine will try to connect to :param consistency: the consistency level of the connection, defaults to 'ONE' :type consistency: str + :param timeout: the connection timeout in seconds + :type timeout: int or long + Records the hosts and connects to one of them See the example at :ref:`getting-started` From d568185c3aa7ad4593d37f638a24cf1759ede6bd Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 3 Oct 2013 16:49:38 -0700 Subject: [PATCH 0404/3961] updating timeout docs --- cqlengine/connection.py | 2 +- docs/topics/connection.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index ecd3edd82b..f6e25e4e76 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -67,7 +67,7 @@ def setup( :type default_keyspace: str :param consistency: The global consistency level :type consistency: str - :param timeout: The connection timeout in seconds + :param timeout: The connection timeout in milliseconds :type timeout: int or long """ diff --git a/docs/topics/connection.rst b/docs/topics/connection.rst index c956b58fda..6140584704 100644 --- a/docs/topics/connection.rst +++ b/docs/topics/connection.rst @@ -25,7 +25,7 @@ If there is a problem with one of the servers, cqlengine will try to connect to :param consistency: the consistency level of the connection, defaults to 'ONE' :type consistency: str - :param timeout: the connection timeout in seconds + :param timeout: the connection timeout in milliseconds :type timeout: int or long Records the hosts and connects to one of them From 2b8424c74895c6a8dbd3d7f2cc789dd1027d6e89 Mon Sep 17 00:00:00 2001 From: Pandu Rao Date: Thu, 3 Oct 2013 18:34:32 -0700 Subject: [PATCH 0405/3961] Added guards to handle None value in Date and DateTime column to_python conversion --- cqlengine/columns.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 10a5e1b956..a361eb0d43 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -313,6 +313,7 @@ class DateTime(Column): db_type = 'timestamp' def to_python(self, value): + if value is None: return if isinstance(value, datetime): return value elif isinstance(value, date): @@ -340,6 +341,7 @@ class Date(Column): def to_python(self, value): + if value is None: return if isinstance(value, datetime): return value.date() elif isinstance(value, date): From 2a7a11dde4484bf4f0a77152e3ad1615f1235eaf Mon Sep 17 00:00:00 2001 From: Pandu Rao Date: Wed, 16 Oct 2013 18:30:47 -0700 Subject: [PATCH 0406/3961] Added guard to handle None value in Date column to_database conversion --- cqlengine/columns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index a361eb0d43..d82dd636fc 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -351,6 +351,7 @@ def to_python(self, value): def to_database(self, value): value = super(Date, self).to_database(value) + if value is None: return if isinstance(value, datetime): value = value.date() if not isinstance(value, date): From c9358733f60353f9ef583a166875ccd608792119 Mon Sep 17 00:00:00 2001 From: Pandu Rao Date: Wed, 16 Oct 2013 18:32:05 -0700 Subject: [PATCH 0407/3961] Add testcases for guards for None in date, datetime conversion --- cqlengine/tests/columns/test_validation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 3c6c665614..158bda4a52 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -68,6 +68,11 @@ def test_datetime_date_support(self): dt2 = self.DatetimeTest.objects(test_id=0).first() assert dt2.created_at.isoformat() == datetime(today.year, today.month, today.day).isoformat() + def test_datetime_none(self): + dt = self.DatetimeTest.objects.create(test_id=0, created_at=None) + dt2 = self.DatetimeTest.objects(test_id=0).first() + assert dt2.created_at is None + class TestVarInt(BaseCassEngTestCase): class VarIntTest(Model): @@ -120,6 +125,11 @@ def test_date_io_using_datetime(self): assert isinstance(dt2.created_at, date) assert dt2.created_at.isoformat() == now.date().isoformat() + def test_date_none(self): + dt = self.DateTest.objects.create(test_id=0, created_at=None) + dt2 = self.DateTest.objects(test_id=0).first() + assert dt2.created_at is None + class TestDecimal(BaseCassEngTestCase): class DecimalTest(Model): From b52e9a9b34411b67d264d4c0a6d4d2c3f2ef76d9 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Thu, 17 Oct 2013 11:17:22 -0400 Subject: [PATCH 0408/3961] add BigInt column --- cqlengine/columns.py | 4 ++++ cqlengine/tests/columns/test_validation.py | 11 +++++++++++ cqlengine/tests/columns/test_value_io.py | 6 ++++++ docs/topics/columns.rst | 8 +++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 10a5e1b956..8781e62fc8 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -252,6 +252,10 @@ def to_database(self, value): return self.validate(value) +class BigInt(Integer): + db_type = 'bigint' + + class VarInt(Column): db_type = 'varint' diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 3c6c665614..b89e4a9172 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -15,6 +15,7 @@ from cqlengine.columns import Ascii from cqlengine.columns import Text from cqlengine.columns import Integer +from cqlengine.columns import BigInt from cqlengine.columns import VarInt from cqlengine.columns import DateTime from cqlengine.columns import Date @@ -180,6 +181,16 @@ def test_default_zero_fields_validate(self): it = self.IntegerTest() it.validate() +class TestBigInt(BaseCassEngTestCase): + class BigIntTest(Model): + test_id = UUID(primary_key=True, default=lambda:uuid4()) + value = BigInt(default=0, required=True) + + def test_default_zero_fields_validate(self): + """ Tests that bigint columns with a default value of 0 validate """ + it = self.BigIntTest() + it.validate() + class TestText(BaseCassEngTestCase): def test_min_length(self): diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 1c822a9e5d..54d8f5d5c5 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -95,6 +95,12 @@ class TestInteger(BaseColumnIOTest): pkey_val = 5 data_val = 6 +class TestBigInt(BaseColumnIOTest): + + column = columns.BigInt + pkey_val = 6 + data_val = pow(2, 63) - 1 + class TestDateTime(BaseColumnIOTest): column = columns.DateTime diff --git a/docs/topics/columns.rst b/docs/topics/columns.rst index 6d04df6452..0f71e3c7ad 100644 --- a/docs/topics/columns.rst +++ b/docs/topics/columns.rst @@ -38,10 +38,16 @@ Columns .. class:: Integer() - Stores an integer value :: + Stores a 32-bit signed integer value :: columns.Integer() +.. class:: BigInt() + + Stores a 64-bit signed long value :: + + columns.BigInt() + .. class:: VarInt() Stores an arbitrary-precision integer :: From 6abf31f2cc0a9437807e7a6f4b585aa8f3a4f425 Mon Sep 17 00:00:00 2001 From: Pandu Rao Date: Thu, 17 Oct 2013 12:22:33 -0700 Subject: [PATCH 0409/3961] Fixed bug in test case and expanded test case to include values_list --- cqlengine/tests/columns/test_validation.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 158bda4a52..1a72046b2a 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -69,10 +69,13 @@ def test_datetime_date_support(self): assert dt2.created_at.isoformat() == datetime(today.year, today.month, today.day).isoformat() def test_datetime_none(self): - dt = self.DatetimeTest.objects.create(test_id=0, created_at=None) - dt2 = self.DatetimeTest.objects(test_id=0).first() + dt = self.DatetimeTest.objects.create(test_id=1, created_at=None) + dt2 = self.DatetimeTest.objects(test_id=1).first() assert dt2.created_at is None + dts = self.DatetimeTest.objects.filter(test_id=1).values_list('created_at') + assert dts[0][0] is None + class TestVarInt(BaseCassEngTestCase): class VarIntTest(Model): @@ -126,10 +129,13 @@ def test_date_io_using_datetime(self): assert dt2.created_at.isoformat() == now.date().isoformat() def test_date_none(self): - dt = self.DateTest.objects.create(test_id=0, created_at=None) - dt2 = self.DateTest.objects(test_id=0).first() + self.DateTest.objects.create(test_id=1, created_at=None) + dt2 = self.DateTest.objects(test_id=1).first() assert dt2.created_at is None + dts = self.DateTest.objects(test_id=1).values_list('created_at') + assert dts[0][0] is None + class TestDecimal(BaseCassEngTestCase): class DecimalTest(Model): From 442872f6a861bcc0686c21bb813f04e23ada6fbb Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Fri, 18 Oct 2013 15:40:12 +0100 Subject: [PATCH 0410/3961] Allow use of timezone aware datetimes in {Max,Min}TimeUUID Convert the given timezone aware datetimes to UTC in a similar manner as the DateTime column type does instead of resulting in an error. --- cqlengine/columns.py | 15 ++----- cqlengine/functions.py | 12 +++-- cqlengine/tests/query/test_queryset.py | 62 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 10a5e1b956..dd7b3ba325 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -328,11 +328,9 @@ def to_database(self, value): else: raise ValidationError("'{}' is not a datetime object".format(value)) epoch = datetime(1970, 1, 1, tzinfo=value.tzinfo) - offset = 0 - if epoch.tzinfo: - offset_delta = epoch.tzinfo.utcoffset(epoch) - offset = offset_delta.days*24*3600 + offset_delta.seconds - return long(((value - epoch).total_seconds() - offset) * 1000) + offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 + + return long(((value - epoch).total_seconds() - offset) * 1000) class Date(Column): @@ -402,12 +400,7 @@ def from_datetime(self, dt): global _last_timestamp epoch = datetime(1970, 1, 1, tzinfo=dt.tzinfo) - - offset = 0 - if epoch.tzinfo: - offset_delta = epoch.tzinfo.utcoffset(epoch) - offset = offset_delta.days*24*3600 + offset_delta.seconds - + offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 timestamp = (dt - epoch).total_seconds() - offset node = None diff --git a/cqlengine/functions.py b/cqlengine/functions.py index eee1fcab5e..136618453c 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -54,8 +54,10 @@ def __init__(self, value): super(MinTimeUUID, self).__init__(value) def get_value(self): - epoch = datetime(1970, 1, 1) - return long((self.value - epoch).total_seconds() * 1000) + epoch = datetime(1970, 1, 1, tzinfo=self.value.tzinfo) + offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 + + return long(((self.value - epoch).total_seconds() - offset) * 1000) def get_dict(self, column): return {self.identifier: self.get_value()} @@ -79,8 +81,10 @@ def __init__(self, value): super(MaxTimeUUID, self).__init__(value) def get_value(self): - epoch = datetime(1970, 1, 1) - return long((self.value - epoch).total_seconds() * 1000) + epoch = datetime(1970, 1, 1, tzinfo=self.value.tzinfo) + offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 + + return long(((self.value - epoch).total_seconds() - offset) * 1000) def get_dict(self, column): return {self.identifier: self.get_value()} diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 117c87c18b..9004a28bf1 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -11,6 +11,25 @@ from cqlengine.models import Model from cqlengine import columns from cqlengine import query +from datetime import timedelta +from datetime import tzinfo + + +class TzOffset(tzinfo): + """Minimal implementation of a timezone offset to help testing with timezone + aware datetimes. + """ + def __init__(self, offset): + self._offset = timedelta(hours=offset) + + def utcoffset(self, dt): + return self._offset + + def tzname(self, dt): + return 'TzOffset: {}'.format(self._offset.hours) + + def dst(self, dt): + return timedelta(0) class TestModel(Model): test_id = columns.Integer(primary_key=True) @@ -515,6 +534,49 @@ def tearDownClass(cls): super(TestMinMaxTimeUUIDFunctions, cls).tearDownClass() delete_table(TimeUUIDQueryModel) + def test_tzaware_datetime_support(self): + """Test that using timezone aware datetime instances works with the + MinTimeUUID/MaxTimeUUID functions. + """ + pk = uuid4() + midpoint_utc = datetime.utcnow().replace(tzinfo=TzOffset(0)) + midpoint_helsinki = midpoint_utc.astimezone(TzOffset(3)) + + # Assert pre-condition that we have the same logical point in time + assert midpoint_utc.utctimetuple() == midpoint_helsinki.utctimetuple() + assert midpoint_utc.timetuple() != midpoint_helsinki.timetuple() + + TimeUUIDQueryModel.create( + partition=pk, + time=columns.TimeUUID.from_datetime(midpoint_utc - timedelta(minutes=1)), + data='1') + + TimeUUIDQueryModel.create( + partition=pk, + time=columns.TimeUUID.from_datetime(midpoint_utc), + data='2') + + TimeUUIDQueryModel.create( + partition=pk, + time=columns.TimeUUID.from_datetime(midpoint_utc + timedelta(minutes=1)), + data='3') + + assert ['1', '2'] == [o.data for o in TimeUUIDQueryModel.filter( + TimeUUIDQueryModel.partition == pk, + TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint_utc))] + + assert ['1', '2'] == [o.data for o in TimeUUIDQueryModel.filter( + TimeUUIDQueryModel.partition == pk, + TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint_helsinki))] + + assert ['2', '3'] == [o.data for o in TimeUUIDQueryModel.filter( + TimeUUIDQueryModel.partition == pk, + TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint_utc))] + + assert ['2', '3'] == [o.data for o in TimeUUIDQueryModel.filter( + TimeUUIDQueryModel.partition == pk, + TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint_helsinki))] + def test_success_case(self): """ Test that the min and max time uuid functions work as expected """ pk = uuid4() From 72959da7db8a1fd479fbe8d1bea406ca49f1c8f1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 23 Oct 2013 16:49:58 -0700 Subject: [PATCH 0411/3961] fixed authors --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index b361bf0de4..a220cb26d6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,9 +1,10 @@ PRIMARY AUTHORS Blake Eggleston +Jon Haddad CONTRIBUTORS Eric Scrivner - test environment, connection pooling -Jon Haddad - helped hash out some of the architecture + From 284a69d7f27ade34074a98f582cf92a8088d7d9c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 11:55:15 -0700 Subject: [PATCH 0412/3961] adding stubbed ttl query tests --- cqlengine/tests/test_ttl.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 cqlengine/tests/test_ttl.py diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py new file mode 100644 index 0000000000..2274cf34c5 --- /dev/null +++ b/cqlengine/tests/test_ttl.py @@ -0,0 +1,16 @@ +from cqlengine.tests.base import BaseCassEngTestCase + + +class TTLQueryTests(BaseCassEngTestCase): + + def test_update_queryset_ttl_success_case(self): + """ tests that ttls on querysets work as expected """ + + def test_select_ttl_failure(self): + """ tests that ttls on select queries raise an exception """ + + +class TTLModelTests(BaseCassEngTestCase): + + def test_model_ttl_success_case(self): + """ tests that ttls on models work as expected """ \ No newline at end of file From 7b34285ec296b298d674943899415c4a9dad3b11 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 11:56:20 -0700 Subject: [PATCH 0413/3961] adding batch and ttl to docs --- docs/topics/models.rst | 8 ++++++++ docs/topics/queryset.rst | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 06b720d4de..e63c5dc791 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -138,6 +138,14 @@ Model Methods Deletes the object from the database. + .. method:: batch(batch_object) + + Sets the batch object to run instance updates and inserts queries with. + + .. method:: ttl(batch_object) + + Sets the ttl values to run instance updates and inserts queries with. + Model Attributes ================ diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index cb1fdbb836..08c46a2796 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -353,3 +353,12 @@ QuerySet method reference .. method:: allow_filtering() Enables the (usually) unwise practive of querying on a clustering key without also defining a partition key + + .. method:: batch(batch_object) + + Sets the batch object to run the query on. Note that running a select query with a batch object will raise an exception + + .. method:: ttl(batch_object) + + Sets the ttl to run the query query with. Note that running a select query with a ttl value will raise an exception + From c62b7519fd613042c1a46041926c176bedd52f6b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 12:17:17 -0700 Subject: [PATCH 0414/3961] adding failing tests for empty container column saving --- .../tests/columns/test_container_columns.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index ba4df808c8..2e1d7cb844 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -154,6 +154,10 @@ def test_to_python(self): py_val = column.to_python(db_val.value) assert py_val == val + def test_default_empty_container_saving(self): + """ tests that the default empty container is not saved if it hasn't been updated """ + self.fail("implement") + class TestListModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) @@ -282,6 +286,11 @@ def test_to_python(self): py_val = column.to_python(db_val.value) assert py_val == val + def test_default_empty_container_saving(self): + """ tests that the default empty container is not saved if it hasn't been updated """ + self.fail("implement") + + class TestMapModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) int_map = columns.Map(columns.Integer, columns.UUID, required=False) @@ -309,8 +318,6 @@ def test_empty_retrieve(self): tmp2 = TestMapModel.get(partition=tmp.partition) tmp2.int_map['blah'] = 1 - - def test_io_success(self): """ Tests that a basic usage works as expected """ k1 = uuid4() @@ -370,7 +377,6 @@ def test_updates_from_none(self): m2 = TestMapModel.get(partition=m.partition) assert m2.int_map == expected - def test_updates_to_none(self): """ Tests that setting the field to None works as expected """ m = TestMapModel.create(int_map={1: uuid4()}) @@ -406,6 +412,10 @@ def test_to_python(self): py_val = column.to_python(db_val.value) assert py_val == val + def test_default_empty_container_saving(self): + """ tests that the default empty container is not saved if it hasn't been updated """ + self.fail("implement") + # def test_partial_update_creation(self): # """ # Tests that proper update statements are created for a partial list update From 52e29a57597d9b5bcb1f0ccdd8de3ad0fd589308 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 12:17:52 -0700 Subject: [PATCH 0415/3961] adding update method to model docs --- docs/topics/models.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 06b720d4de..e665e6c5ca 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -138,6 +138,13 @@ Model Methods Deletes the object from the database. + -- method:: update(**values) + + Performs an update on the model instance. You can pass in values to set on the model + for updating, or you can call without values to execute an update against any modified + fields. If no fields on the model have been modified since loading, no query will be + performed. Model validation is performed normally. + Model Attributes ================ From a57d3730f3ea5180a130c56141f308a28c919492 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 13:08:52 -0700 Subject: [PATCH 0416/3961] separating inserts, updates, column deletes and query value logic --- cqlengine/query.py | 197 +++++++++++++++++++++++++++++---------------- 1 file changed, 129 insertions(+), 68 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 02a5d7d577..00e9db0c83 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -22,6 +22,7 @@ class MultipleObjectsReturned(QueryException): pass class QueryOperatorException(QueryException): pass + class QueryOperator(object): # The symbol that identifies this operator in filter kwargs # ie: colname__ @@ -116,10 +117,12 @@ def __ne__(self, op): def __hash__(self): return hash(self.column.db_field_name) ^ hash(self.value) + class EqualsOperator(QueryOperator): symbol = 'EQ' cql_symbol = '=' + class IterableQueryValue(QueryValue): def __init__(self, value): try: @@ -133,28 +136,34 @@ def get_dict(self, column): def get_cql(self): return '({})'.format(', '.join(':{}'.format(i) for i in self.identifier)) + class InOperator(EqualsOperator): symbol = 'IN' cql_symbol = 'IN' QUERY_VALUE_WRAPPER = IterableQueryValue + class GreaterThanOperator(QueryOperator): symbol = "GT" cql_symbol = '>' + class GreaterThanOrEqualOperator(QueryOperator): symbol = "GTE" cql_symbol = '>=' + class LessThanOperator(QueryOperator): symbol = "LT" cql_symbol = '<' + class LessThanOrEqualOperator(QueryOperator): symbol = "LTE" cql_symbol = '<=' + class AbstractQueryableColumn(object): """ exposes cql query operators through pythons @@ -192,6 +201,7 @@ class BatchType(object): Unlogged = 'UNLOGGED' Counter = 'COUNTER' + class BatchQuery(object): """ Handles the batching of queries @@ -624,12 +634,20 @@ def delete(self, columns=[]): else: execute(qs, self._where_values()) + def update(self, **values): + """ + updates the contents of the query + """ + qs = ['UPDATE {}'.format(self.column_family_name)] + qs += ['SET'] + def __eq__(self, q): return set(self._where) == set(q._where) def __ne__(self, q): return not (self != q) + class ResultObject(dict): """ adds attribute access to a dictionary @@ -641,6 +659,7 @@ def __getattr__(self, item): except KeyError: raise AttributeError + class SimpleQuerySet(AbstractQuerySet): """ @@ -658,6 +677,7 @@ def _construct_instance(values): return ResultObject(zip(names, values)) return _construct_instance + class ModelQuerySet(AbstractQuerySet): """ @@ -763,10 +783,48 @@ def batch(self, batch_obj): self._batch = batch_obj return self - def save(self): + def _delete_null_columns(self): """ - Creates / updates a row. - This is a blind insert call. + executes a delete query to remove null columns + """ + values, field_names, field_ids, field_values, query_values = self._get_query_values() + + # delete nulled columns and removed map keys + qs = ['DELETE'] + query_values = {} + + del_statements = [] + for k,v in self.instance._values.items(): + col = v.column + if v.deleted: + del_statements += ['"{}"'.format(col.db_field_name)] + elif isinstance(col, Map): + del_statements += col.get_delete_statement(v.value, v.previous_value, query_values) + + if del_statements: + qs += [', '.join(del_statements)] + + qs += ['FROM {}'.format(self.column_family_name)] + + qs += ['WHERE'] + where_statements = [] + for name, col in self.model._primary_keys.items(): + field_id = uuid4().hex + query_values[field_id] = field_values[name] + where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] + qs += [' AND '.join(where_statements)] + + qs = ' '.join(qs) + + if self._batch: + self._batch.add_query(qs, query_values) + else: + execute(qs, query_values) + + def update(self): + """ + updates a row. + This is a blind update call. All validation and cleaning needs to happen prior to calling this. """ @@ -774,6 +832,57 @@ def save(self): raise CQLEngineException("DML Query intance attribute is None") assert type(self.instance) == self.model + values, field_names, field_ids, field_values, query_values = self._get_query_values() + + qs = [] + qs += ["UPDATE {}".format(self.column_family_name)] + qs += ["SET"] + + set_statements = [] + #get defined fields and their column names + for name, col in self.model._columns.items(): + if not col.is_primary_key: + val = values.get(name) + if val is None: + continue + if isinstance(col, (BaseContainerColumn, Counter)): + #remove value from query values, the column will handle it + query_values.pop(field_ids.get(name), None) + + val_mgr = self.instance._values[name] + set_statements += col.get_update_statement(val, val_mgr.previous_value, query_values) + + else: + set_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] + qs += [', '.join(set_statements)] + + qs += ['WHERE'] + + where_statements = [] + for name, col in self.model._primary_keys.items(): + where_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] + + qs += [' AND '.join(where_statements)] + + # clear the qs if there are no set statements and this is not a counter model + if not set_statements and not self.instance._has_counter: + qs = [] + + qs = ' '.join(qs) + # skip query execution if it's empty + # caused by pointless update queries + if qs: + if self._batch: + self._batch.add_query(qs, query_values) + else: + execute(qs, query_values) + + self._delete_null_columns() + + def _get_query_values(self): + """ + returns all the data needed to do queries + """ #organize data value_pairs = [] values = self.instance._as_dict() @@ -789,42 +898,24 @@ def save(self): field_ids = {n:uuid4().hex for n in field_names} field_values = dict(value_pairs) query_values = {field_ids[n]:field_values[n] for n in field_names} + return values, field_names, field_ids, field_values, query_values - qs = [] - if self.instance._has_counter or self.instance._can_update(): - qs += ["UPDATE {}".format(self.column_family_name)] - qs += ["SET"] - - set_statements = [] - #get defined fields and their column names - for name, col in self.model._columns.items(): - if not col.is_primary_key: - val = values.get(name) - if val is None: - continue - if isinstance(col, (BaseContainerColumn, Counter)): - #remove value from query values, the column will handle it - query_values.pop(field_ids.get(name), None) - - val_mgr = self.instance._values[name] - set_statements += col.get_update_statement(val, val_mgr.previous_value, query_values) - - else: - set_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] - qs += [', '.join(set_statements)] - - qs += ['WHERE'] - - where_statements = [] - for name, col in self.model._primary_keys.items(): - where_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] - - qs += [' AND '.join(where_statements)] + def save(self): + """ + Creates / updates a row. + This is a blind insert call. + All validation and cleaning needs to happen + prior to calling this. + """ + if self.instance is None: + raise CQLEngineException("DML Query intance attribute is None") + assert type(self.instance) == self.model - # clear the qs if there are no set statements and this is not a counter model - if not set_statements and not self.instance._has_counter: - qs = [] + values, field_names, field_ids, field_values, query_values = self._get_query_values() + qs = [] + if self.instance._has_counter or self.instance._can_update(): + return self.update() else: qs += ["INSERT INTO {}".format(self.column_family_name)] qs += ["({})".format(', '.join(['"{}"'.format(f) for f in field_names]))] @@ -841,38 +932,8 @@ def save(self): else: execute(qs, query_values) - - # delete nulled columns and removed map keys - qs = ['DELETE'] - query_values = {} - - del_statements = [] - for k,v in self.instance._values.items(): - col = v.column - if v.deleted: - del_statements += ['"{}"'.format(col.db_field_name)] - elif isinstance(col, Map): - del_statements += col.get_delete_statement(v.value, v.previous_value, query_values) - - if del_statements: - qs += [', '.join(del_statements)] - - qs += ['FROM {}'.format(self.column_family_name)] - - qs += ['WHERE'] - where_statements = [] - for name, col in self.model._primary_keys.items(): - field_id = uuid4().hex - query_values[field_id] = field_values[name] - where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] - qs += [' AND '.join(where_statements)] - - qs = ' '.join(qs) - - if self._batch: - self._batch.add_query(qs, query_values) - else: - execute(qs, query_values) + # delete any nulled columns + self._delete_null_columns() def delete(self): """ Deletes one instance """ From dcd5a5a1acd80d1f21e9a6c0b8b89bb2e3138993 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 13:17:50 -0700 Subject: [PATCH 0417/3961] ttls are working off the Class and instance --- cqlengine/models.py | 50 ++++++++++++++++++++++++ cqlengine/query.py | 6 ++- cqlengine/tests/query/test_queryset.py | 8 ++-- cqlengine/tests/test_ttl.py | 54 ++++++++++++++++++++++++-- 4 files changed, 110 insertions(+), 8 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 2969c228aa..006e0b855f 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -70,6 +70,51 @@ def __call__(self, *args, **kwargs): """ raise NotImplementedError +class TTLDescriptor(object): + """ + returns a query set descriptor + """ + def __get__(self, instance, model): + if instance: + def ttl_setter(ts): + instance._ttl = ts + return instance + return ttl_setter + + qs = model.__queryset__(model) + + def ttl_setter(ts): + qs._ttl = ts + return qs + + return ttl_setter + + def __call__(self, *args, **kwargs): + raise NotImplementedError + + +class ConsistencyDescriptor(object): + """ + returns a query set descriptor if called on Class, instance if it was an instance call + """ + def __get__(self, instance, model): + if instance: + def consistency_setter(consistency): + instance._consistency = consistency + return instance + return consistency_setter + + qs = model.__queryset__(model) + + def consistency_setter(ts): + qs._consistency = ts + return qs + + return consistency_setter + + def __call__(self, *args, **kwargs): + raise NotImplementedError + class ColumnQueryEvaluator(AbstractQueryableColumn): """ @@ -148,6 +193,8 @@ class DoesNotExist(_DoesNotExist): pass class MultipleObjectsReturned(_MultipleObjectsReturned): pass objects = QuerySetDescriptor() + ttl = TTLDescriptor() + consistency = ConsistencyDescriptor() #table names will be generated automatically from it's model and package name #however, you can also define them manually here @@ -179,10 +226,13 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass __queryset__ = ModelQuerySet __dmlquery__ = DMLQuery + __ttl__ = None + __read_repair_chance__ = 0.1 def __init__(self, **values): self._values = {} + self._ttl = None for name, column in self._columns.items(): value = values.get(name, None) diff --git a/cqlengine/query.py b/cqlengine/query.py index 02a5d7d577..804650fa13 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -273,6 +273,7 @@ def __init__(self, model): self._result_idx = None self._batch = None + self._ttl = None @property def column_family_name(self): @@ -727,6 +728,10 @@ def _get_ordering_condition(self, colname): return column.db_field_name, order_type + def _get_ttl_statement(self): + return "" + + def values_list(self, *fields, **kwargs): """ Instructs the query set to return tuples, not model instance """ flat = kwargs.pop('flat', False) @@ -755,7 +760,6 @@ def __init__(self, model, instance=None, batch=None): self.column_family_name = self.model.column_family_name() self.instance = instance self._batch = batch - pass def batch(self, batch_obj): if batch_obj is not None and not isinstance(batch_obj, BatchQuery): diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 117c87c18b..cda6831c2e 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -6,7 +6,7 @@ from cqlengine.exceptions import ModelException from cqlengine import functions -from cqlengine.management import create_table +from cqlengine.management import create_table, drop_table from cqlengine.management import delete_table from cqlengine.models import Model from cqlengine import columns @@ -200,9 +200,9 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): super(BaseQuerySetUsage, cls).tearDownClass() - delete_table(TestModel) - delete_table(IndexedTestModel) - delete_table(TestMultiClusteringModel) + drop_table(TestModel) + drop_table(IndexedTestModel) + drop_table(TestMultiClusteringModel) class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 2274cf34c5..a0c0a6dfb9 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -1,7 +1,31 @@ +from cqlengine.management import sync_table, drop_table from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.models import Model +from uuid import uuid4 +from cqlengine import columns -class TTLQueryTests(BaseCassEngTestCase): +class TestTTLModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + count = columns.Integer() + text = columns.Text(required=False) + + +class BaseTTLTest(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(BaseTTLTest, cls).setUpClass() + sync_table(TestTTLModel) + + @classmethod + def tearDownClass(cls): + super(BaseTTLTest, cls).tearDownClass() + drop_table(TestTTLModel) + + + +class TTLQueryTests(BaseTTLTest): def test_update_queryset_ttl_success_case(self): """ tests that ttls on querysets work as expected """ @@ -10,7 +34,31 @@ def test_select_ttl_failure(self): """ tests that ttls on select queries raise an exception """ -class TTLModelTests(BaseCassEngTestCase): +class TTLModelTests(BaseTTLTest): def test_model_ttl_success_case(self): - """ tests that ttls on models work as expected """ \ No newline at end of file + """ tests that ttls on models work as expected """ + + def test_queryset_is_returned_on_class(self): + """ + ensures we get a queryset descriptor back + """ + qs = TestTTLModel.ttl(60) + self.assertTrue(isinstance(qs, TestTTLModel.__queryset__), type(qs)) + +class TTLInstanceTest(BaseTTLTest): + def test_instance_is_returned(self): + """ + ensures that we properly handle the instance.ttl(60).save() scenario + :return: + """ + o = TestTTLModel.create(text="whatever") + o.text = "new stuff" + o.ttl(60) + self.assertEqual(60, o._ttl) + o.save() + + + +class QuerySetTTLFragmentTest(BaseTTLTest): + pass From 982031672e7af39320018d64805e4c30c4e6e1e8 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 13:20:17 -0700 Subject: [PATCH 0418/3961] ttl statement fragment --- cqlengine/query.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 804650fa13..8ff33a6ef1 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -729,8 +729,9 @@ def _get_ordering_condition(self, colname): return column.db_field_name, order_type def _get_ttl_statement(self): - return "" - + if not self._ttl: + return "" + return "USING TTL {}".format(self._ttl) def values_list(self, *fields, **kwargs): """ Instructs the query set to return tuples, not model instance """ From 308a5be608e4ecdd51a4b03b5ae67174bfb8b77c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 13:25:07 -0700 Subject: [PATCH 0419/3961] updating update method to only issue set statements for modified rows and counters --- cqlengine/query.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cqlengine/query.py b/cqlengine/query.py index 00e9db0c83..dc7a18724d 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -843,8 +843,16 @@ def update(self): for name, col in self.model._columns.items(): if not col.is_primary_key: val = values.get(name) + + # don't update something that is null if val is None: continue + + # don't update something if it hasn't changed + if not self.instance._values[name].changed and not isinstance(col, Counter): + continue + + # add the update statements if isinstance(col, (BaseContainerColumn, Counter)): #remove value from query values, the column will handle it query_values.pop(field_ids.get(name), None) From f68a5924a6dceeb3ff854bb63bdd70f5cbfe0cfd Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 13:36:43 -0700 Subject: [PATCH 0420/3961] fixing container column bug that would write insert empty container columns --- cqlengine/columns.py | 22 +++++++++++++++++++--- cqlengine/query.py | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 10a5e1b956..adccf8f90f 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -198,6 +198,10 @@ def cql(self): def get_cql(self): return '"{}"'.format(self.db_field_name) + def _val_is_null(self, val): + """ determines if the given value equates to a null value for the given column type """ + return val is None + class Bytes(Column): db_type = 'blob' @@ -526,6 +530,15 @@ def get_update_statement(self, val, prev, ctx): """ raise NotImplementedError + def _val_is_null(self, val): + return not val + + +class BaseContainerQuoter(ValueQuoter): + + def __nonzero__(self): + return bool(self.value) + class Set(BaseContainerColumn): """ @@ -535,7 +548,7 @@ class Set(BaseContainerColumn): """ db_type = 'set<{}>' - class Quoter(ValueQuoter): + class Quoter(BaseContainerQuoter): def __str__(self): cq = cql_quote @@ -625,12 +638,15 @@ class List(BaseContainerColumn): """ db_type = 'list<{}>' - class Quoter(ValueQuoter): + class Quoter(BaseContainerQuoter): def __str__(self): cq = cql_quote return '[' + ', '.join([cq(v) for v in self.value]) + ']' + def __nonzero__(self): + return bool(self.value) + def __init__(self, value_type, default=set, **kwargs): return super(List, self).__init__(value_type=value_type, default=default, **kwargs) @@ -736,7 +752,7 @@ class Map(BaseContainerColumn): db_type = 'map<{}, {}>' - class Quoter(ValueQuoter): + class Quoter(BaseContainerQuoter): def __str__(self): cq = cql_quote diff --git a/cqlengine/query.py b/cqlengine/query.py index dc7a18724d..377accb4fd 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -898,7 +898,7 @@ def _get_query_values(self): #get defined fields and their column names for name, col in self.model._columns.items(): val = values.get(name) - if val is None: continue + if col._val_is_null(val): continue value_pairs += [(col.db_field_name, val)] #construct query string From 38df187625d07dd45fca299b0e0f07c68901f9e2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 13:42:20 -0700 Subject: [PATCH 0421/3961] adding tests around empty container insert bug fix --- .../tests/columns/test_container_columns.py | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 2e1d7cb844..0d3dc524b2 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -156,7 +156,14 @@ def test_to_python(self): def test_default_empty_container_saving(self): """ tests that the default empty container is not saved if it hasn't been updated """ - self.fail("implement") + pkey = uuid4() + # create a row with set data + TestSetModel.create(partition=pkey, int_set={3, 4}) + # create another with no set data + TestSetModel.create(partition=pkey) + + m = TestSetModel.get(partition=pkey) + self.assertEqual(m.int_set, {3, 4}) class TestListModel(Model): @@ -288,7 +295,14 @@ def test_to_python(self): def test_default_empty_container_saving(self): """ tests that the default empty container is not saved if it hasn't been updated """ - self.fail("implement") + pkey = uuid4() + # create a row with list data + TestListModel.create(partition=pkey, int_list=[1,2,3,4]) + # create another with no list data + TestListModel.create(partition=pkey) + + m = TestListModel.get(partition=pkey) + self.assertEqual(m.int_list, [1,2,3,4]) class TestMapModel(Model): @@ -414,7 +428,15 @@ def test_to_python(self): def test_default_empty_container_saving(self): """ tests that the default empty container is not saved if it hasn't been updated """ - self.fail("implement") + pkey = uuid4() + tmap = {1: uuid4(), 2: uuid4()} + # create a row with set data + TestMapModel.create(partition=pkey, int_map=tmap) + # create another with no set data + TestMapModel.create(partition=pkey) + + m = TestMapModel.get(partition=pkey) + self.assertEqual(m.int_map, tmap) # def test_partial_update_creation(self): # """ From 783f77222c27ec5b4268e5fdb7875b98e44c1754 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 13:48:38 -0700 Subject: [PATCH 0422/3961] added consistency constants to top level --- cqlengine/__init__.py | 10 ++++++++++ cqlengine/models.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 639b56b0bd..51b3fc2f22 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -12,3 +12,13 @@ SizeTieredCompactionStrategy = "SizeTieredCompactionStrategy" LeveledCompactionStrategy = "LeveledCompactionStrategy" + +ANY = "ANY" +ONE = "ONE" +TWO = "TWO" +THREE = "THREE" +QUORUM = "QUORUM" +LOCAL_QUORUM = "LOCAL_QUORUM" +EACH_QUORUM = "EACH_QUORUM" +ALL = "ALL" + diff --git a/cqlengine/models.py b/cqlengine/models.py index 006e0b855f..5c365c9a56 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -7,7 +7,6 @@ from cqlengine.query import DoesNotExist as _DoesNotExist from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned - class ModelDefinitionException(ModelException): pass @@ -76,6 +75,7 @@ class TTLDescriptor(object): """ def __get__(self, instance, model): if instance: + # instance method def ttl_setter(ts): instance._ttl = ts return instance From 8355e3bb5f276af6c6d0b9f6c5517e2b1dabfd70 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 14:14:31 -0700 Subject: [PATCH 0423/3961] test that ensures TTL is called on update --- cqlengine/query.py | 5 ++++- cqlengine/tests/test_ttl.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 19b90cd3b8..1e32acda0a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -284,6 +284,7 @@ def __init__(self, model): self._batch = None self._ttl = None + self._consistency = None @property def column_family_name(self): @@ -615,7 +616,7 @@ def defer(self, fields): return self._only_or_defer('defer', fields) def create(self, **kwargs): - return self.model(**kwargs).batch(self._batch).save() + return self.model(**kwargs).batch(self._batch).ttl(self._ttl).consistency(self._consistency).save() #----delete--- def delete(self, columns=[]): @@ -935,8 +936,10 @@ def save(self): qs += ['VALUES'] qs += ["({})".format(', '.join([':'+field_ids[f] for f in field_names]))] + qs = ' '.join(qs) + # skip query execution if it's empty # caused by pointless update queries if qs: diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index a0c0a6dfb9..92313b96d8 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -3,7 +3,8 @@ from cqlengine.models import Model from uuid import uuid4 from cqlengine import columns - +import mock +from cqlengine.connection import ConnectionPool class TestTTLModel(Model): id = columns.UUID(primary_key=True, default=lambda:uuid4()) @@ -56,7 +57,16 @@ def test_instance_is_returned(self): o.text = "new stuff" o.ttl(60) self.assertEqual(60, o._ttl) - o.save() + + def test_ttl_is_include_with_query(self): + o = TestTTLModel.create(text="whatever") + o.text = "new stuff" + o.ttl(60) + + with mock.patch.object(ConnectionPool, 'execute') as m: + o.save() + query = m.call_args[0][0] + self.assertIn("USING TTL", query) From 343367dbf7a57af85e6ea9eaecc4202bdb626bdb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 14:19:41 -0700 Subject: [PATCH 0424/3961] adding update method to model --- cqlengine/models.py | 32 +++++++++++++++++++++++++++++++- cqlengine/query.py | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 2969c228aa..c1730b3cc0 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -356,7 +356,6 @@ def get(cls, *args, **kwargs): return cls.objects.get(*args, **kwargs) def save(self): - # handle polymorphic models if self._is_polymorphic: if self._is_polymorphic_base: @@ -375,6 +374,37 @@ def save(self): return self + def update(self, **values): + for k, v in values.items(): + col = self._columns.get(k) + + # check for nonexistant columns + if col is None: + raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.__name__, k)) + + # check for primary key update attempts + if col.is_primary_key: + raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(k, self.__module__, self.__name__)) + + setattr(self, k, v) + + # handle polymorphic models + if self._is_polymorphic: + if self._is_polymorphic_base: + raise PolyMorphicModelException('cannot update polymorphic base model') + else: + setattr(self, self._polymorphic_column_name, self.__polymorphic_key__) + + self.validate() + self.__dmlquery__(self.__class__, self, batch=self._batch).update() + + #reset the value managers + for v in self._values.values(): + v.reset_previous_value() + self._is_persisted = True + + return self + def delete(self): """ Deletes this instance """ self.__dmlquery__(self.__class__, self, batch=self._batch).delete() diff --git a/cqlengine/query.py b/cqlengine/query.py index 377accb4fd..75d2b7a848 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -785,7 +785,7 @@ def batch(self, batch_obj): def _delete_null_columns(self): """ - executes a delete query to remove null columns + executes a delete query to remove columns that have changed to null """ values, field_names, field_ids, field_values, query_values = self._get_query_values() From eea1e7e1898ce1cdb5d6c19bb0d7d45cf7482958 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 14:49:25 -0700 Subject: [PATCH 0425/3961] TTL working for save on an update --- cqlengine/models.py | 2 +- cqlengine/query.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 5c365c9a56..cfeb724efc 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -416,7 +416,7 @@ def save(self): is_new = self.pk is None self.validate() - self.__dmlquery__(self.__class__, self, batch=self._batch).save() + self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl).save() #reset the value managers for v in self._values.values(): diff --git a/cqlengine/query.py b/cqlengine/query.py index 1e32acda0a..a2ce6d20d8 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -776,12 +776,14 @@ class DMLQuery(object): unlike the read query object, this is mutable """ + _ttl = None - def __init__(self, model, instance=None, batch=None): + def __init__(self, model, instance=None, batch=None, ttl=None): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance self._batch = batch + self._ttl = ttl def batch(self, batch_obj): if batch_obj is not None and not isinstance(batch_obj, BatchQuery): @@ -878,6 +880,9 @@ def update(self): qs += [' AND '.join(where_statements)] + if self._ttl: + qs += ["USING TTL {}".format(self._ttl)] + # clear the qs if there are no set statements and this is not a counter model if not set_statements and not self.instance._has_counter: qs = [] @@ -936,7 +941,10 @@ def save(self): qs += ['VALUES'] qs += ["({})".format(', '.join([':'+field_ids[f] for f in field_names]))] + if self._ttl: + qs += ["USING TTL {}".format(self._ttl)] + qs += [] qs = ' '.join(qs) From cbc07a9fc88fb28200da8d4a69aed5cb2bf8a2e9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 14:50:05 -0700 Subject: [PATCH 0426/3961] updating changelog --- changelog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog b/changelog index 21d31aa7c8..4e7d121ec1 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.9 +* adding update method +* only saving collection fields on insert if they've been modified + 0.8.5 * adding support for timeouts From 4996f05563814312101ce2fe8905615f841f33f6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 14:50:30 -0700 Subject: [PATCH 0427/3961] fixing update errors --- cqlengine/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index c1730b3cc0..93a9af984e 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -380,11 +380,11 @@ def update(self, **values): # check for nonexistant columns if col is None: - raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.__name__, k)) + raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.__class__.__name__, k)) # check for primary key update attempts if col.is_primary_key: - raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(k, self.__module__, self.__name__)) + raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(k, self.__module__, self.__class__.__name__)) setattr(self, k, v) From c906c77773c1f726f56d8315d593c14c945eec2b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 14:50:42 -0700 Subject: [PATCH 0428/3961] adding tests around model updates --- cqlengine/tests/model/test_updates.py | 91 +++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 cqlengine/tests/model/test_updates.py diff --git a/cqlengine/tests/model/test_updates.py b/cqlengine/tests/model/test_updates.py new file mode 100644 index 0000000000..c9ab45d9de --- /dev/null +++ b/cqlengine/tests/model/test_updates.py @@ -0,0 +1,91 @@ +from uuid import uuid4 + +from mock import patch +from cqlengine.exceptions import ValidationError + +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.models import Model +from cqlengine import columns +from cqlengine.management import sync_table, drop_table +from cqlengine.connection import ConnectionPool + + +class TestUpdateModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + cluster = columns.UUID(primary_key=True, default=uuid4) + count = columns.Integer(required=False) + text = columns.Text(required=False, index=True) + + +class ModelUpdateTests(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(ModelUpdateTests, cls).setUpClass() + sync_table(TestUpdateModel) + + @classmethod + def tearDownClass(cls): + super(ModelUpdateTests, cls).tearDownClass() + drop_table(TestUpdateModel) + + def test_update_model(self): + """ tests calling udpate on models with no values passed in """ + m0 = TestUpdateModel.create(count=5, text='monkey') + + # independently save over a new count value, unknown to original instance + m1 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster) + m1.count = 6 + m1.save() + + # update the text, and call update + m0.text = 'monkey land' + m0.update() + + # database should reflect both updates + m2 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster) + self.assertEqual(m2.count, m1.count) + self.assertEqual(m2.text, m0.text) + + def test_update_values(self): + """ tests calling update on models with values passed in """ + m0 = TestUpdateModel.create(count=5, text='monkey') + + # independently save over a new count value, unknown to original instance + m1 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster) + m1.count = 6 + m1.save() + + # update the text, and call update + m0.update(text='monkey land') + self.assertEqual(m0.text, 'monkey land') + + # database should reflect both updates + m2 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster) + self.assertEqual(m2.count, m1.count) + self.assertEqual(m2.text, m0.text) + + def test_noop_model_update(self): + """ tests that calling update on a model with no changes will do nothing. """ + m0 = TestUpdateModel.create(count=5, text='monkey') + + with patch.object(ConnectionPool, 'execute') as execute: + m0.update() + assert execute.call_count == 0 + + with patch.object(ConnectionPool, 'execute') as execute: + m0.update(count=5) + assert execute.call_count == 0 + + def test_invalid_update_kwarg(self): + """ tests that passing in a kwarg to the update method that isn't a column will fail """ + m0 = TestUpdateModel.create(count=5, text='monkey') + with self.assertRaises(ValidationError): + m0.update(numbers=20) + + def test_primary_key_update_failure(self): + """ tests that attempting to update the value of a primary key will fail """ + m0 = TestUpdateModel.create(count=5, text='monkey') + with self.assertRaises(ValidationError): + m0.update(partition=uuid4()) + From 2601354448f95348f35e0374717c430780cc7bbe Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 14:52:38 -0700 Subject: [PATCH 0429/3961] TTL working on inserts --- cqlengine/tests/test_ttl.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 92313b96d8..6093b76545 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -37,8 +37,13 @@ def test_select_ttl_failure(self): class TTLModelTests(BaseTTLTest): - def test_model_ttl_success_case(self): + def test_ttl_included_on_create(self): """ tests that ttls on models work as expected """ + with mock.patch.object(ConnectionPool, 'execute') as m: + TestTTLModel.ttl(60).create(text="hello blake") + + query = m.call_args[0][0] + self.assertIn("USING TTL", query) def test_queryset_is_returned_on_class(self): """ @@ -47,6 +52,8 @@ def test_queryset_is_returned_on_class(self): qs = TestTTLModel.ttl(60) self.assertTrue(isinstance(qs, TestTTLModel.__queryset__), type(qs)) + + class TTLInstanceTest(BaseTTLTest): def test_instance_is_returned(self): """ @@ -58,7 +65,7 @@ def test_instance_is_returned(self): o.ttl(60) self.assertEqual(60, o._ttl) - def test_ttl_is_include_with_query(self): + def test_ttl_is_include_with_query_on_update(self): o = TestTTLModel.create(text="whatever") o.text = "new stuff" o.ttl(60) From ca67541c5a689e7aac4670e11e4727ea1f986fe2 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 14:55:44 -0700 Subject: [PATCH 0430/3961] test for updates --- cqlengine/tests/test_ttl.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 6093b76545..11a868a32e 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -53,6 +53,17 @@ def test_queryset_is_returned_on_class(self): self.assertTrue(isinstance(qs, TestTTLModel.__queryset__), type(qs)) +class TTLInstanceUpdateTest(BaseTTLTest): + def test_update_includes_ttl(self): + model = TestTTLModel.create(text="goodbye blake") + with mock.patch.object(ConnectionPool, 'execute') as m: + model.ttl(60).update(text="goodbye forever") + + query = m.call_args[0][0] + self.assertIn("USING TTL", query) + + + class TTLInstanceTest(BaseTTLTest): def test_instance_is_returned(self): From e22dea631dc1541b7ebc3cde34f94f36c16ecf37 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 14:57:55 -0700 Subject: [PATCH 0431/3961] update with TTL work --- cqlengine/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 88abd85a51..16d7b01eb2 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -446,7 +446,7 @@ def update(self, **values): setattr(self, self._polymorphic_column_name, self.__polymorphic_key__) self.validate() - self.__dmlquery__(self.__class__, self, batch=self._batch).update() + self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl).update() #reset the value managers for v in self._values.values(): From f1fb9da47c867f4edd57327de0f4a3a773fd0068 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 15:36:20 -0700 Subject: [PATCH 0432/3961] ensure consistency is called with the right param --- cqlengine/connection.py | 11 +++++---- cqlengine/tests/test_consistency.py | 35 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 cqlengine/tests/test_consistency.py diff --git a/cqlengine/connection.py b/cqlengine/connection.py index f6e25e4e76..df89e430f2 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -165,10 +165,10 @@ def _create_transport(self, host): from thrift.transport import TSocket, TTransport thrift_socket = TSocket.TSocket(host.name, host.port) - + if self._timeout is not None: thrift_socket.setTimeout(self._timeout) - + return TTransport.TFramedTransport(thrift_socket) def _create_connection(self): @@ -202,14 +202,17 @@ def _create_connection(self): raise CQLConnectionError("Could not connect to any server in cluster") - def execute(self, query, params): + def execute(self, query, params, consistency_level=None): + if not consistency_level: + consistency_level = self._consistency + while True: try: con = self.get() if not con: raise CQLEngineException("Error calling execute without calling setup.") cur = con.cursor() - cur.execute(query, params) + cur.execute(query, params, consistency_level=consistency_level) columns = [i[0] for i in cur.description or []] results = [RowResult(r) for r in cur.fetchall()] LOG.debug('{} {}'.format(query, repr(params))) diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py new file mode 100644 index 0000000000..cf1673ed9c --- /dev/null +++ b/cqlengine/tests/test_consistency.py @@ -0,0 +1,35 @@ +from cqlengine.management import sync_table, drop_table +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.models import Model +from uuid import uuid4 +from cqlengine import columns +import mock +from cqlengine.connection import ConnectionPool +from cqlengine import ALL + +class TestConsistencyModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + count = columns.Integer() + text = columns.Text(required=False) + +class BaseConsistencyTest(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(BaseConsistencyTest, cls).setUpClass() + sync_table(TestConsistencyModel) + + @classmethod + def tearDownClass(cls): + super(BaseConsistencyTest, cls).tearDownClass() + drop_table(TestConsistencyModel) + + +class TestConsistency(BaseConsistencyTest): + def test_create_uses_consistency(self): + + with mock.patch.object(ConnectionPool, 'execute') as m: + TestConsistencyModel.consistency(ALL).create(text="i am not fault tolerant this way") + + args = m.call_args + self.assertEqual(ALL, args[2]) From a7c7a85a3522f38ae6184a4c524bd2487b22dbfc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 15:58:14 -0700 Subject: [PATCH 0433/3961] adding queryset updates and supporting tests --- cqlengine/query.py | 57 +++++++++++-- cqlengine/tests/query/test_updates.py | 118 ++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 cqlengine/tests/query/test_updates.py diff --git a/cqlengine/query.py b/cqlengine/query.py index 75d2b7a848..b24c67a06b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -9,7 +9,7 @@ from cqlengine.connection import connection_manager, execute, RowResult -from cqlengine.exceptions import CQLEngineException +from cqlengine.exceptions import CQLEngineException, ValidationError from cqlengine.functions import QueryValue, Token #CQL 3 reference: @@ -634,13 +634,6 @@ def delete(self, columns=[]): else: execute(qs, self._where_values()) - def update(self, **values): - """ - updates the contents of the query - """ - qs = ['UPDATE {}'.format(self.column_family_name)] - qs += ['SET'] - def __eq__(self, q): return set(self._where) == set(q._where) @@ -760,6 +753,54 @@ def values_list(self, *fields, **kwargs): clone._flat_values_list = flat return clone + def update(self, **values): + """ Updates the rows in this queryset """ + if not values: + raise ValidationError("At least one column needs to be updated") + + set_statements = [] + ctx = {} + nulled_columns = set() + for name, val in values.items(): + col = self.model._columns.get(name) + # check for nonexistant columns + if col is None: + raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.model.__name__, name)) + # check for primary key update attempts + if col.is_primary_key: + raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(name, self.__module__, self.model.__name__)) + + val = col.validate(val) + if val is None: + nulled_columns.add(name) + continue + # add the update statements + if isinstance(col, (BaseContainerColumn, Counter)): + val_mgr = self.instance._values[name] + set_statements += col.get_update_statement(val, val_mgr.previous_value, ctx) + + else: + field_id = uuid4().hex + set_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] + ctx[field_id] = val + + if set_statements: + qs = "UPDATE {} SET {} WHERE {}".format( + self.column_family_name, + ', '.join(set_statements), + self._where_clause() + ) + ctx.update(self._where_values()) + execute(qs, ctx) + + if nulled_columns: + qs = "DELETE {} FROM {} WHERE {}".format( + ', '.join(nulled_columns), + self.column_family_name, + self._where_clause() + ) + execute(qs, self._where_values()) + class DMLQuery(object): """ diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py new file mode 100644 index 0000000000..56d5cd5ef0 --- /dev/null +++ b/cqlengine/tests/query/test_updates.py @@ -0,0 +1,118 @@ +from uuid import uuid4 +from cqlengine.exceptions import ValidationError + +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.models import Model +from cqlengine.management import sync_table, drop_table +from cqlengine import columns + + +class TestQueryUpdateModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + cluster = columns.Integer(primary_key=True) + count = columns.Integer(required=False) + text = columns.Text(required=False, index=True) + + +class QueryUpdateTests(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(QueryUpdateTests, cls).setUpClass() + sync_table(TestQueryUpdateModel) + + @classmethod + def tearDownClass(cls): + super(QueryUpdateTests, cls).tearDownClass() + drop_table(TestQueryUpdateModel) + + def test_update_values(self): + """ tests calling udpate on a queryset """ + partition = uuid4() + for i in range(5): + TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i)) + + # sanity check + for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): + assert row.cluster == i + assert row.count == i + assert row.text == str(i) + + # perform update + TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6) + + for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): + assert row.cluster == i + assert row.count == (6 if i == 3 else i) + assert row.text == str(i) + + def test_update_values_validation(self): + """ tests calling udpate on models with values passed in """ + partition = uuid4() + for i in range(5): + TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i)) + + # sanity check + for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): + assert row.cluster == i + assert row.count == i + assert row.text == str(i) + + # perform update + with self.assertRaises(ValidationError): + TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count='asdf') + + def test_update_with_no_values_failure(self): + """ tests calling update on models with no values passed in """ + with self.assertRaises(ValidationError): + TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update() + + def test_invalid_update_kwarg(self): + """ tests that passing in a kwarg to the update method that isn't a column will fail """ + with self.assertRaises(ValidationError): + TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(bacon=5000) + + def test_primary_key_update_failure(self): + """ tests that attempting to update the value of a primary key will fail """ + with self.assertRaises(ValidationError): + TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(cluster=5000) + + def test_null_update_deletes_column(self): + """ setting a field to null in the update should issue a delete statement """ + partition = uuid4() + for i in range(5): + TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i)) + + # sanity check + for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): + assert row.cluster == i + assert row.count == i + assert row.text == str(i) + + # perform update + TestQueryUpdateModel.objects(partition=partition, cluster=3).update(text=None) + + for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): + assert row.cluster == i + assert row.count == i + assert row.text == (None if i == 3 else str(i)) + + def test_mixed_value_and_null_update(self): + """ tests that updating a columns value, and removing another works properly """ + partition = uuid4() + for i in range(5): + TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i)) + + # sanity check + for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): + assert row.cluster == i + assert row.count == i + assert row.text == str(i) + + # perform update + TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6, text=None) + + for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): + assert row.cluster == i + assert row.count == (6 if i == 3 else i) + assert row.text == (None if i == 3 else str(i)) From 1aa65768f066bfa6e22f422d0be7aa3b54b2861d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 17:14:00 -0700 Subject: [PATCH 0434/3961] removing empty update exception --- cqlengine/query.py | 2 +- cqlengine/tests/query/test_updates.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index b24c67a06b..6619c5a2bb 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -756,7 +756,7 @@ def values_list(self, *fields, **kwargs): def update(self, **values): """ Updates the rows in this queryset """ if not values: - raise ValidationError("At least one column needs to be updated") + return set_statements = [] ctx = {} diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 56d5cd5ef0..0c9c88cf21 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -1,5 +1,6 @@ from uuid import uuid4 from cqlengine.exceptions import ValidationError +from cqlengine.query import QueryException from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.models import Model @@ -62,11 +63,6 @@ def test_update_values_validation(self): with self.assertRaises(ValidationError): TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count='asdf') - def test_update_with_no_values_failure(self): - """ tests calling update on models with no values passed in """ - with self.assertRaises(ValidationError): - TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update() - def test_invalid_update_kwarg(self): """ tests that passing in a kwarg to the update method that isn't a column will fail """ with self.assertRaises(ValidationError): From 3ef6c10ac670e8c866c959ab8443065f820b687a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 17:14:34 -0700 Subject: [PATCH 0435/3961] adding docs for queryset update --- docs/topics/models.rst | 2 +- docs/topics/queryset.rst | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index e665e6c5ca..42d447f9d7 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -137,9 +137,9 @@ Model Methods .. method:: delete() Deletes the object from the database. - -- method:: update(**values) + Performs an update on the model instance. You can pass in values to set on the model for updating, or you can call without values to execute an update against any modified fields. If no fields on the model have been modified since loading, no query will be diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index cb1fdbb836..5632e41043 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -353,3 +353,16 @@ QuerySet method reference .. method:: allow_filtering() Enables the (usually) unwise practive of querying on a clustering key without also defining a partition key + + -- method:: update(**values) + + Performs an update on the row selected by the queryset. Include values to update in the + update like so: + + .. code-block:: python + Model.objects(key=n).update(value='x') + + Passing in updates for columns which are not part of the model will raise a ValidationError. + Per column validation will be performed, but instance level validation will not + (`Model.validate` is not called). + From bfe2ebd31ba8ddee6eb95b13b73d29bc469ed57d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 17:23:25 -0700 Subject: [PATCH 0436/3961] adding BigInt to changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index 4e7d121ec1..9ab05e8e79 100644 --- a/changelog +++ b/changelog @@ -2,6 +2,7 @@ CHANGELOG 0.9 * adding update method +* adding BigInt column (thanks @Lifto) * only saving collection fields on insert if they've been modified 0.8.5 From 73756b247682f299bf8c3aa2511e10cc36195d84 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 17:28:45 -0700 Subject: [PATCH 0437/3961] adding dokai's time uuid update to changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index 9ab05e8e79..57f6fc2c6d 100644 --- a/changelog +++ b/changelog @@ -3,6 +3,7 @@ CHANGELOG 0.9 * adding update method * adding BigInt column (thanks @Lifto) +* adding support for timezone aware time uuid functions (thanks @dokai) * only saving collection fields on insert if they've been modified 0.8.5 From 8b238734e66137f465cb12dc0a9faff15d54a107 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 17:33:52 -0700 Subject: [PATCH 0438/3961] making sure we send the consistency_level explicitly --- cqlengine/connection.py | 6 ++++-- cqlengine/models.py | 10 +++++++--- cqlengine/query.py | 12 +++++++----- cqlengine/tests/test_consistency.py | 8 +++++++- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index df89e430f2..6fec0d3d64 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -229,9 +229,11 @@ def execute(self, query, params, consistency_level=None): raise ex -def execute(query, params=None): +def execute(query, params=None, consistency_level=None): params = params or {} - return connection_pool.execute(query, params) + if consistency_level is None: + consistency_level = connection_pool._consistency + return connection_pool.execute(query, params, consistency_level) @contextmanager def connection_manager(): diff --git a/cqlengine/models.py b/cqlengine/models.py index 16d7b01eb2..143320a31b 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -106,8 +106,8 @@ def consistency_setter(consistency): qs = model.__queryset__(model) - def consistency_setter(ts): - qs._consistency = ts + def consistency_setter(consistency): + qs._consistency = consistency return qs return consistency_setter @@ -227,6 +227,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass __dmlquery__ = DMLQuery __ttl__ = None + __consistency__ = None # can be set per query __read_repair_chance__ = 0.1 @@ -446,7 +447,10 @@ def update(self, **values): setattr(self, self._polymorphic_column_name, self.__polymorphic_key__) self.validate() - self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl).update() + self.__dmlquery__(self.__class__, self, + batch=self._batch, + ttl=self._ttl, + consistency=self.consistency).update() #reset the value managers for v in self._values.values(): diff --git a/cqlengine/query.py b/cqlengine/query.py index 361f42ef44..d67b45d4a9 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -363,7 +363,7 @@ def _execute_query(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: - columns, self._result_cache = execute(self._select_query(), self._where_values()) + columns, self._result_cache = execute(self._select_query(), self._where_values(), self._consistency) self._construct_result = self._get_result_constructor(columns) def _fill_result_cache_to_idx(self, idx): @@ -777,13 +777,15 @@ class DMLQuery(object): unlike the read query object, this is mutable """ _ttl = None + _consistency = None - def __init__(self, model, instance=None, batch=None, ttl=None): + def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance self._batch = batch self._ttl = ttl + self._consistency = consistency def batch(self, batch_obj): if batch_obj is not None and not isinstance(batch_obj, BatchQuery): @@ -894,7 +896,7 @@ def update(self): if self._batch: self._batch.add_query(qs, query_values) else: - execute(qs, query_values) + execute(qs, query_values, consistency_level=self._consistency) self._delete_null_columns() @@ -954,7 +956,7 @@ def save(self): if self._batch: self._batch.add_query(qs, query_values) else: - execute(qs, query_values) + execute(qs, query_values, self._consistency) # delete any nulled columns self._delete_null_columns() @@ -978,6 +980,6 @@ def delete(self): if self._batch: self._batch.add_query(qs, field_values) else: - execute(qs, field_values) + execute(qs, field_values, self._consistency) diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index cf1673ed9c..7ab49d7867 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -32,4 +32,10 @@ def test_create_uses_consistency(self): TestConsistencyModel.consistency(ALL).create(text="i am not fault tolerant this way") args = m.call_args - self.assertEqual(ALL, args[2]) + self.assertEqual(ALL, args[0][2]) + + def test_queryset_is_returned_on_create(self): + qs = TestConsistencyModel.consistency(ALL) + self.assertTrue(isinstance(qs, TestConsistencyModel.__queryset__), type(qs)) + + From da58f8b2fbb83b535751b6503ba5dd37a8b0da2a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 17:54:57 -0700 Subject: [PATCH 0439/3961] updates can use consistency levels --- cqlengine/models.py | 9 ++++++--- cqlengine/tests/test_consistency.py | 13 ++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 143320a31b..ced3a3fa08 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -100,7 +100,7 @@ class ConsistencyDescriptor(object): def __get__(self, instance, model): if instance: def consistency_setter(consistency): - instance._consistency = consistency + instance.__consistency__ = consistency return instance return consistency_setter @@ -416,7 +416,10 @@ def save(self): is_new = self.pk is None self.validate() - self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl).save() + self.__dmlquery__(self.__class__, self, + batch=self._batch, + ttl=self._ttl, + consistency=self.__consistency__).save() #reset the value managers for v in self._values.values(): @@ -450,7 +453,7 @@ def update(self, **values): self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl, - consistency=self.consistency).update() + consistency=self.__consistency__).update() #reset the value managers for v in self._values.values(): diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 7ab49d7867..ec60450420 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -28,8 +28,9 @@ def tearDownClass(cls): class TestConsistency(BaseConsistencyTest): def test_create_uses_consistency(self): + qs = TestConsistencyModel.consistency(ALL) with mock.patch.object(ConnectionPool, 'execute') as m: - TestConsistencyModel.consistency(ALL).create(text="i am not fault tolerant this way") + qs.create(text="i am not fault tolerant this way") args = m.call_args self.assertEqual(ALL, args[0][2]) @@ -38,4 +39,14 @@ def test_queryset_is_returned_on_create(self): qs = TestConsistencyModel.consistency(ALL) self.assertTrue(isinstance(qs, TestConsistencyModel.__queryset__), type(qs)) + def test_update_uses_consistency(self): + t = TestConsistencyModel.create(text="bacon and eggs") + t.text = "ham sandwich" + + with mock.patch.object(ConnectionPool, 'execute') as m: + t.consistency(ALL).save() + + args = m.call_args + self.assertEqual(ALL, args[0][2]) + From 3f6d293af21a37bf4c3d116d70e9f65dfdb62c55 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 17:56:46 -0700 Subject: [PATCH 0440/3961] update test works --- cqlengine/tests/test_consistency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index ec60450420..27c455ef7d 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -45,7 +45,7 @@ def test_update_uses_consistency(self): with mock.patch.object(ConnectionPool, 'execute') as m: t.consistency(ALL).save() - + args = m.call_args self.assertEqual(ALL, args[0][2]) From 6b1391749edaccbbbd83767a16b8cacb8903a92c Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 18:19:04 -0700 Subject: [PATCH 0441/3961] consistency with batch verified --- cqlengine/query.py | 9 +++++++-- cqlengine/tests/test_consistency.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index d67b45d4a9..0e9395de6a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -208,17 +208,22 @@ class BatchQuery(object): http://www.datastax.com/docs/1.2/cql_cli/cql/BATCH """ + _consistency = None - def __init__(self, batch_type=None, timestamp=None): + def __init__(self, batch_type=None, timestamp=None, consistency=None): self.queries = [] self.batch_type = batch_type if timestamp is not None and not isinstance(timestamp, datetime): raise CQLEngineException('timestamp object must be an instance of datetime') self.timestamp = timestamp + self._consistency = consistency def add_query(self, query, params): self.queries.append((query, params)) + def consistency(self, consistency): + self._consistency = consistency + def execute(self): if len(self.queries) == 0: # Empty batch is a no-op @@ -238,7 +243,7 @@ def execute(self): query_list.append('APPLY BATCH;') - execute('\n'.join(query_list), parameters) + execute('\n'.join(query_list), parameters, self._consistency) self.queries = [] diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 27c455ef7d..f4c5986b80 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -5,7 +5,7 @@ from cqlengine import columns import mock from cqlengine.connection import ConnectionPool -from cqlengine import ALL +from cqlengine import ALL, BatchQuery class TestConsistencyModel(Model): id = columns.UUID(primary_key=True, default=lambda:uuid4()) @@ -50,3 +50,18 @@ def test_update_uses_consistency(self): self.assertEqual(ALL, args[0][2]) + def test_batch_consistency(self): + + with mock.patch.object(ConnectionPool, 'execute') as m: + with BatchQuery(consistency=ALL) as b: + TestConsistencyModel.batch(b).create(text="monkey") + + args = m.call_args + self.assertEqual(ALL, args[0][2]) + + with mock.patch.object(ConnectionPool, 'execute') as m: + with BatchQuery() as b: + TestConsistencyModel.batch(b).create(text="monkey") + + args = m.call_args + self.assertNotEqual(ALL, args[0][2]) From 691ddb31718d03b13c38f85797cec062b1294c5c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 18:20:30 -0700 Subject: [PATCH 0442/3961] beginning queryset refactor with some basic statement and operator classes and a bunch of empty test files --- cqlengine/operators.py | 71 +++++++++++++++++++ cqlengine/statements.py | 51 +++++++++++++ cqlengine/tests/operators/__init__.py | 1 + .../operators/test_assignment_operators.py | 0 .../tests/operators/test_base_operator.py | 9 +++ .../tests/operators/test_where_operators.py | 30 ++++++++ cqlengine/tests/statements/__init__.py | 1 + .../tests/statements/test_base_statement.py | 0 .../tests/statements/test_delete_statement.py | 0 .../tests/statements/test_dml_statement.py | 0 .../tests/statements/test_insert_statement.py | 0 .../tests/statements/test_select_statement.py | 0 .../tests/statements/test_update_statement.py | 0 .../tests/statements/test_where_clause.py | 13 ++++ 14 files changed, 176 insertions(+) create mode 100644 cqlengine/operators.py create mode 100644 cqlengine/statements.py create mode 100644 cqlengine/tests/operators/__init__.py create mode 100644 cqlengine/tests/operators/test_assignment_operators.py create mode 100644 cqlengine/tests/operators/test_base_operator.py create mode 100644 cqlengine/tests/operators/test_where_operators.py create mode 100644 cqlengine/tests/statements/__init__.py create mode 100644 cqlengine/tests/statements/test_base_statement.py create mode 100644 cqlengine/tests/statements/test_delete_statement.py create mode 100644 cqlengine/tests/statements/test_dml_statement.py create mode 100644 cqlengine/tests/statements/test_insert_statement.py create mode 100644 cqlengine/tests/statements/test_select_statement.py create mode 100644 cqlengine/tests/statements/test_update_statement.py create mode 100644 cqlengine/tests/statements/test_where_clause.py diff --git a/cqlengine/operators.py b/cqlengine/operators.py new file mode 100644 index 0000000000..dae52ee4e5 --- /dev/null +++ b/cqlengine/operators.py @@ -0,0 +1,71 @@ +class QueryOperatorException(Exception): pass + + +class BaseQueryOperator(object): + # The symbol that identifies this operator in kwargs + # ie: colname__ + symbol = None + + # The comparator symbol this operator uses in cql + cql_symbol = None + + def __unicode__(self): + if self.cql_symbol is None: + raise QueryOperatorException("cql symbol is None") + return self.cql_symbol + + @classmethod + def get_operator(cls, symbol): + if cls == BaseQueryOperator: + raise QueryOperatorException("get_operator can only be called from a BaseQueryOperator subclass") + if not hasattr(cls, 'opmap'): + cls.opmap = {} + def _recurse(klass): + if klass.symbol: + cls.opmap[klass.symbol.upper()] = klass + for subklass in klass.__subclasses__(): + _recurse(subklass) + pass + _recurse(cls) + try: + return cls.opmap[symbol.upper()] + except KeyError: + raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) + + +class BaseWhereOperator(BaseQueryOperator): + """ base operator used for where clauses """ + + +class EqualsOperator(BaseWhereOperator): + symbol = 'EQ' + cql_symbol = '=' + + +class InOperator(EqualsOperator): + symbol = 'IN' + cql_symbol = 'IN' + + +class GreaterThanOperator(BaseWhereOperator): + symbol = "GT" + cql_symbol = '>' + + +class GreaterThanOrEqualOperator(BaseWhereOperator): + symbol = "GTE" + cql_symbol = '>=' + + +class LessThanOperator(BaseWhereOperator): + symbol = "LT" + cql_symbol = '<' + + +class LessThanOrEqualOperator(BaseWhereOperator): + symbol = "LTE" + cql_symbol = '<=' + + +class BaseAssignmentOperator(BaseQueryOperator): + """ base operator used for insert and delete statements """ \ No newline at end of file diff --git a/cqlengine/statements.py b/cqlengine/statements.py new file mode 100644 index 0000000000..e1f5c2deee --- /dev/null +++ b/cqlengine/statements.py @@ -0,0 +1,51 @@ +from cqlengine.operators import BaseWhereOperator + + +class StatementException(Exception): pass + + +class WhereClause(object): + """ a single where statement used in queries """ + + def __init__(self, field, operator, value): + super(WhereClause, self).__init__() + if not isinstance(operator, BaseWhereOperator): + raise StatementException( + "operator must be of type {}, got {}".format(BaseWhereOperator, type(operator)) + ) + + self.field = field + self.operator = operator + self.value = value + + +class BaseCQLStatement(object): + """ The base cql statement class """ + + def __init__(self, table, consistency=None): + super(BaseCQLStatement, self).__init__() + self.table = table + self.consistency = None + self.where_clauses = [] + + + + +class SelectStatement(BaseCQLStatement): + """ a cql select statement """ + + +class DMLStatement(BaseCQLStatement): + """ mutation statements """ + + +class InsertStatement(BaseCQLStatement): + """ an cql insert select statement """ + + +class UpdateStatement(BaseCQLStatement): + """ an cql update select statement """ + + +class DeleteStatement(BaseCQLStatement): + """ a cql delete statement """ diff --git a/cqlengine/tests/operators/__init__.py b/cqlengine/tests/operators/__init__.py new file mode 100644 index 0000000000..f6150a6d76 --- /dev/null +++ b/cqlengine/tests/operators/__init__.py @@ -0,0 +1 @@ +__author__ = 'bdeggleston' diff --git a/cqlengine/tests/operators/test_assignment_operators.py b/cqlengine/tests/operators/test_assignment_operators.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/operators/test_base_operator.py b/cqlengine/tests/operators/test_base_operator.py new file mode 100644 index 0000000000..af13fdb115 --- /dev/null +++ b/cqlengine/tests/operators/test_base_operator.py @@ -0,0 +1,9 @@ +from unittest import TestCase +from cqlengine.operators import BaseQueryOperator, QueryOperatorException + + +class BaseOperatorTest(TestCase): + + def test_get_operator_cannot_be_called_from_base_class(self): + with self.assertRaises(QueryOperatorException): + BaseQueryOperator.get_operator('*') \ No newline at end of file diff --git a/cqlengine/tests/operators/test_where_operators.py b/cqlengine/tests/operators/test_where_operators.py new file mode 100644 index 0000000000..f8f0e8fad8 --- /dev/null +++ b/cqlengine/tests/operators/test_where_operators.py @@ -0,0 +1,30 @@ +from unittest import TestCase +from cqlengine.operators import * + + +class TestWhereOperators(TestCase): + + def test_symbol_lookup(self): + """ tests where symbols are looked up properly """ + + def check_lookup(symbol, expected): + op = BaseWhereOperator.get_operator(symbol) + self.assertEqual(op, expected) + + check_lookup('EQ', EqualsOperator) + check_lookup('IN', InOperator) + check_lookup('GT', GreaterThanOperator) + check_lookup('GTE', GreaterThanOrEqualOperator) + check_lookup('LT', LessThanOperator) + check_lookup('LTE', LessThanOrEqualOperator) + + def test_operator_rendering(self): + """ tests symbols are rendered properly """ + self.assertEqual("=", unicode(EqualsOperator())) + self.assertEqual("IN", unicode(InOperator())) + self.assertEqual(">", unicode(GreaterThanOperator())) + self.assertEqual(">=", unicode(GreaterThanOrEqualOperator())) + self.assertEqual("<", unicode(LessThanOperator())) + self.assertEqual("<=", unicode(LessThanOrEqualOperator())) + + diff --git a/cqlengine/tests/statements/__init__.py b/cqlengine/tests/statements/__init__.py new file mode 100644 index 0000000000..f6150a6d76 --- /dev/null +++ b/cqlengine/tests/statements/__init__.py @@ -0,0 +1 @@ +__author__ = 'bdeggleston' diff --git a/cqlengine/tests/statements/test_base_statement.py b/cqlengine/tests/statements/test_base_statement.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/statements/test_dml_statement.py b/cqlengine/tests/statements/test_dml_statement.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/statements/test_where_clause.py b/cqlengine/tests/statements/test_where_clause.py new file mode 100644 index 0000000000..65300a97aa --- /dev/null +++ b/cqlengine/tests/statements/test_where_clause.py @@ -0,0 +1,13 @@ +from unittest import TestCase +from cqlengine.statements import StatementException, WhereClause + + +class TestWhereClause(TestCase): + + def test_operator_check(self): + """ tests that creating a where statement with a non BaseWhereOperator object fails """ + with self.assertRaises(StatementException): + WhereClause('a', 'b', 'c') + + def test_where_clause_rendering(self): + """ tests that where clauses are rendered properly """ \ No newline at end of file From 308fd2fbd92c14386bc9026da83335d5c8a49d12 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 18:25:22 -0700 Subject: [PATCH 0443/3961] adding where clause rendering --- cqlengine/operators.py | 3 +++ cqlengine/statements.py | 6 ++++++ cqlengine/tests/statements/test_where_clause.py | 6 +++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cqlengine/operators.py b/cqlengine/operators.py index dae52ee4e5..c5ad4a5669 100644 --- a/cqlengine/operators.py +++ b/cqlengine/operators.py @@ -14,6 +14,9 @@ def __unicode__(self): raise QueryOperatorException("cql symbol is None") return self.cql_symbol + def __str__(self): + return str(unicode(self)) + @classmethod def get_operator(cls, symbol): if cls == BaseQueryOperator: diff --git a/cqlengine/statements.py b/cqlengine/statements.py index e1f5c2deee..e3ab61bb73 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -18,6 +18,12 @@ def __init__(self, field, operator, value): self.operator = operator self.value = value + def __unicode__(self): + return u"{} {} {}".format(self.field, self.operator, self.value) + + def __str__(self): + return str(unicode(self)) + class BaseCQLStatement(object): """ The base cql statement class """ diff --git a/cqlengine/tests/statements/test_where_clause.py b/cqlengine/tests/statements/test_where_clause.py index 65300a97aa..a455203642 100644 --- a/cqlengine/tests/statements/test_where_clause.py +++ b/cqlengine/tests/statements/test_where_clause.py @@ -1,4 +1,5 @@ from unittest import TestCase +from cqlengine.operators import EqualsOperator from cqlengine.statements import StatementException, WhereClause @@ -10,4 +11,7 @@ def test_operator_check(self): WhereClause('a', 'b', 'c') def test_where_clause_rendering(self): - """ tests that where clauses are rendered properly """ \ No newline at end of file + """ tests that where clauses are rendered properly """ + wc = WhereClause('a', EqualsOperator(), 'c') + self.assertEqual("a = c", unicode(wc)) + self.assertEqual("a = c", str(wc)) From 3bbf307ca9f7ddb928c67cb547dbbe0aa5d38148 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 18:28:56 -0700 Subject: [PATCH 0444/3961] adding field listification --- cqlengine/statements.py | 8 ++++++-- cqlengine/tests/statements/test_select_statement.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index e3ab61bb73..f448f7ae87 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -35,11 +35,15 @@ def __init__(self, table, consistency=None): self.where_clauses = [] - - class SelectStatement(BaseCQLStatement): """ a cql select statement """ + def __init__(self, table, fields, consistency=None): + super(SelectStatement, self).__init__(table, consistency) + if isinstance(fields, basestring): + fields = [fields] + self.fields = fields + class DMLStatement(BaseCQLStatement): """ mutation statements """ diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index e69de29bb2..81f800d328 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -0,0 +1,10 @@ +from unittest import TestCase +from cqlengine.statements import SelectStatement + + +class SelectStatementTests(TestCase): + + def test_single_field_is_listified(self): + """ tests that passing a string field into the constructor puts it into a list """ + ss = SelectStatement('table', 'field') + self.assertEqual(ss.fields, ['field']) \ No newline at end of file From c3cf18bbdb5d28b4db1fc2988293a2a20685c3ed Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 24 Oct 2013 18:32:29 -0700 Subject: [PATCH 0445/3961] test for blind updates --- cqlengine/tests/test_consistency.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index f4c5986b80..8eadd9c8d6 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -65,3 +65,14 @@ def test_batch_consistency(self): args = m.call_args self.assertNotEqual(ALL, args[0][2]) + + def test_blind_update(self): + t = TestConsistencyModel.create(text="bacon and eggs") + t.text = "ham sandwich" + uid = t.id + + with mock.patch.object(ConnectionPool, 'execute') as m: + TestConsistencyModel.objects(id=uid).update(text="grilled cheese") + + args = m.call_args + self.assertEqual(ALL, args[0][2]) From 0d708730bbd9750e5608c053d5337bcce1255b67 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 18:34:16 -0700 Subject: [PATCH 0446/3961] adding select field rendering --- cqlengine/statements.py | 7 ++++++- cqlengine/tests/statements/test_select_statement.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index f448f7ae87..b68afcd42b 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -31,7 +31,7 @@ class BaseCQLStatement(object): def __init__(self, table, consistency=None): super(BaseCQLStatement, self).__init__() self.table = table - self.consistency = None + self.consistency = consistency self.where_clauses = [] @@ -44,6 +44,11 @@ def __init__(self, table, fields, consistency=None): fields = [fields] self.fields = fields + def __unicode__(self): + qs = ['SELECT'] + qs += [', '.join(self.fields) if self.fields else '*'] + return ' '.join(qs) + class DMLStatement(BaseCQLStatement): """ mutation statements """ diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 81f800d328..9cf1e9db1e 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -7,4 +7,14 @@ class SelectStatementTests(TestCase): def test_single_field_is_listified(self): """ tests that passing a string field into the constructor puts it into a list """ ss = SelectStatement('table', 'field') - self.assertEqual(ss.fields, ['field']) \ No newline at end of file + self.assertEqual(ss.fields, ['field']) + + def test_field_rendering(self): + """ tests that fields are properly added to the select statement """ + ss = SelectStatement('table', ['f1', 'f2']) + self.assertTrue(unicode(ss).startswith('SELECT f1, f2')) + + def test_none_fields_rendering(self): + """ tests that a '*' is added if no fields are passed in """ + ss = SelectStatement('table', None) + self.assertTrue(unicode(ss).startswith('SELECT *')) From 39e5c08880eeb156ba2c8115b0895208a1768c3b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 19:05:09 -0700 Subject: [PATCH 0447/3961] adding more select rendering and beginning dml statements --- cqlengine/statements.py | 75 ++++++++++++++++--- .../tests/statements/test_insert_statement.py | 11 +++ .../tests/statements/test_select_statement.py | 26 ++++++- 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index b68afcd42b..96581599e3 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -19,7 +19,7 @@ def __init__(self, field, operator, value): self.value = value def __unicode__(self): - return u"{} {} {}".format(self.field, self.operator, self.value) + return u'"{}" {} {}'.format(self.field, self.operator, self.value) def __str__(self): return str(unicode(self)) @@ -28,39 +28,92 @@ def __str__(self): class BaseCQLStatement(object): """ The base cql statement class """ - def __init__(self, table, consistency=None): + def __init__(self, table, consistency=None, where=None): super(BaseCQLStatement, self).__init__() self.table = table self.consistency = consistency - self.where_clauses = [] + self.where_clauses = where or [] + + def add_where_clause(self, clause): + if not isinstance(clause, WhereClause): + raise StatementException("only instances of WhereClause can be added to statements") + self.where_clauses.append(clause) class SelectStatement(BaseCQLStatement): """ a cql select statement """ - def __init__(self, table, fields, consistency=None): - super(SelectStatement, self).__init__(table, consistency) - if isinstance(fields, basestring): - fields = [fields] - self.fields = fields + def __init__(self, + table, + fields, + consistency=None, + where=None, + order_by=None, + limit=None, + allow_filtering=False + ): + super(SelectStatement, self).__init__( + table, + consistency=consistency, + where=where + ) + + self.fields = [fields] if isinstance(fields, basestring) else fields + self.order_by = [order_by] if isinstance(order_by, basestring) else order_by + self.limit = limit + self.allow_filtering = allow_filtering def __unicode__(self): qs = ['SELECT'] - qs += [', '.join(self.fields) if self.fields else '*'] + qs += [', '.join(['"{}"'.format(f) for f in self.fields]) if self.fields else '*'] + qs += ['FROM', self.table] + + if self.where_clauses: + qs += ['WHERE', ' AND '.join([unicode(c) for c in self.where_clauses])] + + if self.order_by: + qs += ['ORDER BY {}'.format(', '.join(unicode(o) for o in self.order_by))] + + if self.limit: + qs += ['LIMIT {}'.format(self.limit)] + + if self.allow_filtering: + qs += ['ALLOW FILTERING'] + return ' '.join(qs) class DMLStatement(BaseCQLStatement): """ mutation statements """ + def __init__(self, table, consistency=None, where=None, ttl=None): + super(DMLStatement, self).__init__( + table, + consistency=consistency, + where=where) + self.ttl = ttl + -class InsertStatement(BaseCQLStatement): +class InsertStatement(DMLStatement): """ an cql insert select statement """ + def __init__(self, table, values, consistency=None): + super(InsertStatement, self).__init__( + table, + consistency=consistency, + where=None + ) + + def add_where_clause(self, clause): + raise StatementException("Cannot add where clauses to insert statements") -class UpdateStatement(BaseCQLStatement): + +class UpdateStatement(DMLStatement): """ an cql update select statement """ + def __init__(self, table, consistency=None, where=None): + super(UpdateStatement, self).__init__(table, consistency, where) + class DeleteStatement(BaseCQLStatement): """ a cql delete statement """ diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py index e69de29bb2..6b88ef343c 100644 --- a/cqlengine/tests/statements/test_insert_statement.py +++ b/cqlengine/tests/statements/test_insert_statement.py @@ -0,0 +1,11 @@ +from unittest import TestCase +from cqlengine.statements import InsertStatement, StatementException + + +class InsertStatementTests(TestCase): + + def test_where_clause_failure(self): + """ tests that where clauses cannot be added to Insert statements """ + ist = InsertStatement('table') + with self.assertRaises(StatementException): + ist.add_where_clause('s') \ No newline at end of file diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 9cf1e9db1e..bdaa3d8471 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -1,5 +1,6 @@ from unittest import TestCase -from cqlengine.statements import SelectStatement +from cqlengine.statements import SelectStatement, WhereClause +from cqlengine.operators import * class SelectStatementTests(TestCase): @@ -12,9 +13,30 @@ def test_single_field_is_listified(self): def test_field_rendering(self): """ tests that fields are properly added to the select statement """ ss = SelectStatement('table', ['f1', 'f2']) - self.assertTrue(unicode(ss).startswith('SELECT f1, f2')) + self.assertTrue(unicode(ss).startswith('SELECT "f1", "f2"')) + self.assertTrue(str(ss).startswith('SELECT "f1", "f2"')) def test_none_fields_rendering(self): """ tests that a '*' is added if no fields are passed in """ ss = SelectStatement('table', None) self.assertTrue(unicode(ss).startswith('SELECT *')) + self.assertTrue(str(ss).startswith('SELECT *')) + + def test_table_rendering(self): + ss = SelectStatement('table', None) + self.assertTrue(unicode(ss).startswith('SELECT * FROM table')) + self.assertTrue(str(ss).startswith('SELECT * FROM table')) + + def test_where_clause_rendering(self): + ss = SelectStatement('table', None) + ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) + self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = b') + + def test_order_by_rendering(self): + pass + + def test_limit_rendering(self): + pass + + def test_allow_filtering_rendering(self): + pass From 136339d9d3ce1bdb180a2881bd1d686197a76bc8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 19:10:46 -0700 Subject: [PATCH 0448/3961] moving where rendering into it's own property --- cqlengine/statements.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 96581599e3..6dc9a75ed9 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -39,6 +39,10 @@ def add_where_clause(self, clause): raise StatementException("only instances of WhereClause can be added to statements") self.where_clauses.append(clause) + @property + def _where(self): + return 'WHERE {}'.format(' AND '.join([unicode(c) for c in self.where_clauses)) + class SelectStatement(BaseCQLStatement): """ a cql select statement """ @@ -69,7 +73,7 @@ def __unicode__(self): qs += ['FROM', self.table] if self.where_clauses: - qs += ['WHERE', ' AND '.join([unicode(c) for c in self.where_clauses])] + qs += [self._where] if self.order_by: qs += ['ORDER BY {}'.format(', '.join(unicode(o) for o in self.order_by))] @@ -103,6 +107,7 @@ def __init__(self, table, values, consistency=None): consistency=consistency, where=None ) + self.values = values def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") From b23cb13ad8751cc34fcf51ef1a5be496ce8a71f2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 21:21:47 -0700 Subject: [PATCH 0449/3961] fixing str tests --- cqlengine/statements.py | 5 ++++- .../tests/statements/test_select_statement.py | 14 +++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 6dc9a75ed9..ee289b44d0 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -39,9 +39,12 @@ def add_where_clause(self, clause): raise StatementException("only instances of WhereClause can be added to statements") self.where_clauses.append(clause) + def __str__(self): + return str(unicode(self)) + @property def _where(self): - return 'WHERE {}'.format(' AND '.join([unicode(c) for c in self.where_clauses)) + return 'WHERE {}'.format(' AND '.join([unicode(c) for c in self.where_clauses])) class SelectStatement(BaseCQLStatement): diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index bdaa3d8471..d69f77bad8 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -13,24 +13,24 @@ def test_single_field_is_listified(self): def test_field_rendering(self): """ tests that fields are properly added to the select statement """ ss = SelectStatement('table', ['f1', 'f2']) - self.assertTrue(unicode(ss).startswith('SELECT "f1", "f2"')) - self.assertTrue(str(ss).startswith('SELECT "f1", "f2"')) + self.assertTrue(unicode(ss).startswith('SELECT "f1", "f2"'), unicode(ss)) + self.assertTrue(str(ss).startswith('SELECT "f1", "f2"'), str(ss)) def test_none_fields_rendering(self): """ tests that a '*' is added if no fields are passed in """ ss = SelectStatement('table', None) - self.assertTrue(unicode(ss).startswith('SELECT *')) - self.assertTrue(str(ss).startswith('SELECT *')) + self.assertTrue(unicode(ss).startswith('SELECT *'), unicode(ss)) + self.assertTrue(str(ss).startswith('SELECT *'), str(ss)) def test_table_rendering(self): ss = SelectStatement('table', None) - self.assertTrue(unicode(ss).startswith('SELECT * FROM table')) - self.assertTrue(str(ss).startswith('SELECT * FROM table')) + self.assertTrue(unicode(ss).startswith('SELECT * FROM table'), unicode(ss)) + self.assertTrue(str(ss).startswith('SELECT * FROM table'), str(ss)) def test_where_clause_rendering(self): ss = SelectStatement('table', None) ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = b') + self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = b', unicode(ss)) def test_order_by_rendering(self): pass From 1207c5fea5e41ef5b92875b30b161223bf5fa3ad Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 21:23:15 -0700 Subject: [PATCH 0450/3961] removing the dev mailing list from the docs --- README.md | 2 -- docs/index.rst | 2 -- setup.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/README.md b/README.md index a1242e34fc..15c591cb8b 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ cqlengine is a Cassandra CQL 3 Object Mapper for Python [Users Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users) -[Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) - ## Installation ``` pip install cqlengine diff --git a/docs/index.rst b/docs/index.rst index e4e8c394fd..d748f39fd1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -96,8 +96,6 @@ Getting Started `Users Mailing List `_ -`Dev Mailing List `_ - Indices and tables ================== diff --git a/setup.py b/setup.py index 3bb09c9c2c..2965a6d73a 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,6 @@ [Report a Bug](https://github.com/bdeggleston/cqlengine/issues) [Users Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users) - -[Dev Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-dev) """ setup( From 5733a05684cce03915dcbea7d157cb3560b49257 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 21:33:50 -0700 Subject: [PATCH 0451/3961] adding tests around misc select statement rendering --- .../tests/statements/test_select_statement.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index d69f77bad8..ec7673cd29 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -32,11 +32,15 @@ def test_where_clause_rendering(self): ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = b', unicode(ss)) - def test_order_by_rendering(self): - pass - - def test_limit_rendering(self): - pass - - def test_allow_filtering_rendering(self): - pass + def test_additional_rendering(self): + ss = SelectStatement( + 'table', + None, + order_by=['x', 'y'], + limit=15, + allow_filtering=True + ) + qstr = unicode(ss) + self.assertIn('LIMIT 15', qstr) + self.assertIn('ORDER BY x, y', qstr) + self.assertIn('ALLOW FILTERING', qstr) From e59d75977f683ca135d2a02f9907cdfc0cb9b95c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 21:34:51 -0700 Subject: [PATCH 0452/3961] fixing statement tests --- cqlengine/tests/statements/test_insert_statement.py | 2 +- cqlengine/tests/statements/test_where_clause.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py index 6b88ef343c..f81d8ea68d 100644 --- a/cqlengine/tests/statements/test_insert_statement.py +++ b/cqlengine/tests/statements/test_insert_statement.py @@ -6,6 +6,6 @@ class InsertStatementTests(TestCase): def test_where_clause_failure(self): """ tests that where clauses cannot be added to Insert statements """ - ist = InsertStatement('table') + ist = InsertStatement('table', {}) with self.assertRaises(StatementException): ist.add_where_clause('s') \ No newline at end of file diff --git a/cqlengine/tests/statements/test_where_clause.py b/cqlengine/tests/statements/test_where_clause.py index a455203642..353e506fd7 100644 --- a/cqlengine/tests/statements/test_where_clause.py +++ b/cqlengine/tests/statements/test_where_clause.py @@ -13,5 +13,5 @@ def test_operator_check(self): def test_where_clause_rendering(self): """ tests that where clauses are rendered properly """ wc = WhereClause('a', EqualsOperator(), 'c') - self.assertEqual("a = c", unicode(wc)) - self.assertEqual("a = c", str(wc)) + self.assertEqual('"a" = c', unicode(wc)) + self.assertEqual('"a" = c', str(wc)) From e6a411e3fc7e2920a62e0fd04e4e2b472879ecf7 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 21:42:20 -0700 Subject: [PATCH 0453/3961] adding a set clause --- cqlengine/statements.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index ee289b44d0..4535cc151c 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -1,19 +1,12 @@ -from cqlengine.operators import BaseWhereOperator +from cqlengine.operators import BaseWhereOperator, BaseAssignmentOperator class StatementException(Exception): pass -class WhereClause(object): - """ a single where statement used in queries """ +class BaseClause(object): def __init__(self, field, operator, value): - super(WhereClause, self).__init__() - if not isinstance(operator, BaseWhereOperator): - raise StatementException( - "operator must be of type {}, got {}".format(BaseWhereOperator, type(operator)) - ) - self.field = field self.operator = operator self.value = value @@ -25,6 +18,28 @@ def __str__(self): return str(unicode(self)) +class WhereClause(BaseClause): + """ a single where statement used in queries """ + + def __init__(self, field, operator, value): + if not isinstance(operator, BaseWhereOperator): + raise StatementException( + "operator must be of type {}, got {}".format(BaseWhereOperator, type(operator)) + ) + super(WhereClause, self).__init__(field, operator, value) + + +class SetClause(BaseClause): + """ a single variable st statement """ + + def __init__(self, field, operator, value): + if not isinstance(operator, BaseAssignmentOperator): + raise StatementException( + "operator must be of type {}, got {}".format(BaseAssignmentOperator, type(operator)) + ) + super(SetClause, self).__init__(field, operator, value) + + class BaseCQLStatement(object): """ The base cql statement class """ From 1d816dd07c5099898cf1be40523269adcd2332d4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 21:50:45 -0700 Subject: [PATCH 0454/3961] adding assignment statement base class --- cqlengine/statements.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 4535cc151c..d308d6dc72 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -29,7 +29,7 @@ def __init__(self, field, operator, value): super(WhereClause, self).__init__(field, operator, value) -class SetClause(BaseClause): +class AssignmentClause(BaseClause): """ a single variable st statement """ def __init__(self, field, operator, value): @@ -37,7 +37,7 @@ def __init__(self, field, operator, value): raise StatementException( "operator must be of type {}, got {}".format(BaseAssignmentOperator, type(operator)) ) - super(SetClause, self).__init__(field, operator, value) + super(AssignmentClause, self).__init__(field, operator, value) class BaseCQLStatement(object): @@ -72,8 +72,8 @@ def __init__(self, where=None, order_by=None, limit=None, - allow_filtering=False - ): + allow_filtering=False): + super(SelectStatement, self).__init__( table, consistency=consistency, @@ -106,7 +106,7 @@ def __unicode__(self): class DMLStatement(BaseCQLStatement): - """ mutation statements """ + """ mutation statements with ttls """ def __init__(self, table, consistency=None, where=None, ttl=None): super(DMLStatement, self).__init__( @@ -116,7 +116,30 @@ def __init__(self, table, consistency=None, where=None, ttl=None): self.ttl = ttl -class InsertStatement(DMLStatement): +class AssignmentStatement(DMLStatement): + """ value assignment statements """ + + def __init__(self, + table, + assignments, + consistency=None, + where=None, + ttl=None): + super(AssignmentStatement, self).__init__( + table, + consistency=consistency, + where=where, + ttl=ttl + ) + self.assignments = assignments or [] + + def add_assignment_clause(self, clause): + if not isinstance(clause, AssignmentClause): + raise StatementException("only instances of AssignmentClause can be added to statements") + self.assignments.append(clause) + + +class InsertStatement(AssignmentStatement): """ an cql insert select statement """ def __init__(self, table, values, consistency=None): @@ -131,12 +154,12 @@ def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") -class UpdateStatement(DMLStatement): +class UpdateStatement(AssignmentStatement): """ an cql update select statement """ def __init__(self, table, consistency=None, where=None): super(UpdateStatement, self).__init__(table, consistency, where) -class DeleteStatement(BaseCQLStatement): +class DeleteStatement(DMLStatement): """ a cql delete statement """ From 822f6f3c2d47c76ec9e2b7c7eb195a85a0a45171 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 24 Oct 2013 21:58:02 -0700 Subject: [PATCH 0455/3961] setting up constructors for other statements --- cqlengine/statements.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index d308d6dc72..45f8a49b00 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -80,7 +80,7 @@ def __init__(self, where=where ) - self.fields = [fields] if isinstance(fields, basestring) else fields + self.fields = [fields] if isinstance(fields, basestring) else (fields or []) self.order_by = [order_by] if isinstance(order_by, basestring) else order_by self.limit = limit self.allow_filtering = allow_filtering @@ -157,9 +157,26 @@ def add_where_clause(self, clause): class UpdateStatement(AssignmentStatement): """ an cql update select statement """ - def __init__(self, table, consistency=None, where=None): - super(UpdateStatement, self).__init__(table, consistency, where) + def __init__(self, table, assignments, consistency=None, where=None, ttl=None): + super(UpdateStatement, self).__init__( + table, + assignments, + consistency=consistency, + where=where, + ttl=ttl + ) class DeleteStatement(DMLStatement): """ a cql delete statement """ + + def __init__(self, table, fields, consistency=None, where=None, ttl=None): + super(DeleteStatement, self).__init__( + table, + consistency=consistency, + where=where, + ttl=ttl + ) + self.fields = [fields] if isinstance(fields, basestring) else (fields or []) + + From 58253b4ed73945b8ad5d6b81b5f5fb541ab82665 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 07:06:40 -0700 Subject: [PATCH 0456/3961] adding deterministic context ids --- cqlengine/operators.py | 10 +++++++++- cqlengine/statements.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/cqlengine/operators.py b/cqlengine/operators.py index c5ad4a5669..cbbfe4ad66 100644 --- a/cqlengine/operators.py +++ b/cqlengine/operators.py @@ -71,4 +71,12 @@ class LessThanOrEqualOperator(BaseWhereOperator): class BaseAssignmentOperator(BaseQueryOperator): - """ base operator used for insert and delete statements """ \ No newline at end of file + """ base operator used for insert and delete statements """ + + +class AssignmentOperator(BaseAssignmentOperator): + cql_symbol = "=" + + +class AddSymbol(BaseAssignmentOperator): + cql_symbol = "+" \ No newline at end of file diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 45f8a49b00..1a7593d7df 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -10,13 +10,27 @@ def __init__(self, field, operator, value): self.field = field self.operator = operator self.value = value + self.context_id = None def __unicode__(self): - return u'"{}" {} {}'.format(self.field, self.operator, self.value) + return u'"{}" {} {}'.format(self.field, self.operator, self.context_id) def __str__(self): return str(unicode(self)) + def get_context_size(self): + """ returns the number of entries this clause will add to the query context """ + return 1 + + def set_context_id(self, i): + """ sets the value placeholder that will be used in the query """ + self.context_id = i + + def update_context(self, ctx): + """ updates the query context with this clauses values """ + assert isinstance(ctx, dict) + ctx[self.context_id] = self.value + class WhereClause(BaseClause): """ a single where statement used in queries """ @@ -47,11 +61,17 @@ def __init__(self, table, consistency=None, where=None): super(BaseCQLStatement, self).__init__() self.table = table self.consistency = consistency - self.where_clauses = where or [] + self.context_counter = 0 + + self.where_clauses = [] + for clause in where or []: + self.add_where_clause(clause) def add_where_clause(self, clause): if not isinstance(clause, WhereClause): raise StatementException("only instances of WhereClause can be added to statements") + clause.set_context_id(self.context_counter) + self.context_counter += clause.get_context_size() self.where_clauses.append(clause) def __str__(self): @@ -131,11 +151,17 @@ def __init__(self, where=where, ttl=ttl ) - self.assignments = assignments or [] + + # add assignments + self.assignments = [] + for assignment in assignments or []: + self.add_assignment_clause(assignment) def add_assignment_clause(self, clause): if not isinstance(clause, AssignmentClause): raise StatementException("only instances of AssignmentClause can be added to statements") + clause.set_context_id(self.context_counter) + self.context_counter += clause.get_context_size() self.assignments.append(clause) From 317b68815583c9e795a1fbddfa41366c7921f789 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 07:10:36 -0700 Subject: [PATCH 0457/3961] adding test around adding assignments --- .../tests/statements/test_assignment_statement.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 cqlengine/tests/statements/test_assignment_statement.py diff --git a/cqlengine/tests/statements/test_assignment_statement.py b/cqlengine/tests/statements/test_assignment_statement.py new file mode 100644 index 0000000000..695c7cebc8 --- /dev/null +++ b/cqlengine/tests/statements/test_assignment_statement.py @@ -0,0 +1,11 @@ +from unittest import TestCase +from cqlengine.statements import AssignmentStatement, StatementException + + +class AssignmentStatementTest(TestCase): + + def test_add_assignment_type_checking(self): + """ tests that only assignment clauses can be added to queries """ + stmt = AssignmentStatement('table', []) + with self.assertRaises(StatementException): + stmt.add_assignment_clause('x=5') \ No newline at end of file From 6bfd7536acfde03594c53a115a44a45ecc2922e3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 07:12:11 -0700 Subject: [PATCH 0458/3961] adding test around adding where clauses --- cqlengine/tests/statements/test_base_statement.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cqlengine/tests/statements/test_base_statement.py b/cqlengine/tests/statements/test_base_statement.py index e69de29bb2..4acc1b926f 100644 --- a/cqlengine/tests/statements/test_base_statement.py +++ b/cqlengine/tests/statements/test_base_statement.py @@ -0,0 +1,11 @@ +from unittest import TestCase +from cqlengine.statements import BaseCQLStatement, StatementException + + +class BaseStatementTest(TestCase): + + def test_where_clause_type_checking(self): + """ tests that only assignment clauses can be added to queries """ + stmt = BaseCQLStatement('table', []) + with self.assertRaises(StatementException): + stmt.add_where_clause('x=5') From 66c625027c62f2ba3d5d6df3ceecb2bafc7beff4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 07:19:51 -0700 Subject: [PATCH 0459/3961] adding initial unicode rendering for insert statement --- cqlengine/statements.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 1a7593d7df..cbdd55bec4 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -53,6 +53,9 @@ def __init__(self, field, operator, value): ) super(AssignmentClause, self).__init__(field, operator, value) + def insert_tuple(self): + return self.field, self.context_id + class BaseCQLStatement(object): """ The base cql statement class """ @@ -179,6 +182,19 @@ def __init__(self, table, values, consistency=None): def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") + def __unicode__(self): + qs = ['INSERT INTO {}'.format(self.table)] + + # get column names and context placeholders + fields = [a.insert_tuple() for a in self.assignments] + columns, values = zip(*fields) + + qs += ["({})".format(', '.join(['"{}"'.format(c) for c in columns]))] + qs += ['VALUES'] + qs += ["({})".format(', '.join([':{}'.format(v) for v in values]))] + + return ' '.join(qs) + class UpdateStatement(AssignmentStatement): """ an cql update select statement """ From edcb766893b6e93f417fabef2aca5598d4cf970c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 07:27:31 -0700 Subject: [PATCH 0460/3961] adding test around clause context updating --- cqlengine/tests/statements/test_base_clause.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 cqlengine/tests/statements/test_base_clause.py diff --git a/cqlengine/tests/statements/test_base_clause.py b/cqlengine/tests/statements/test_base_clause.py new file mode 100644 index 0000000000..04d7d52845 --- /dev/null +++ b/cqlengine/tests/statements/test_base_clause.py @@ -0,0 +1,16 @@ +from unittest import TestCase +from cqlengine.statements import BaseClause + + +class BaseClauseTests(TestCase): + + def test_context_updating(self): + ss = BaseClause('a', 'b') + assert ss.get_context_size() == 1 + + ctx = {} + ss.set_context_id(10) + ss.update_context(ctx) + assert ctx == {10: 'b'} + + From 188fdd54aa20442beb74850a69df9c8aa2e5c91e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 07:30:13 -0700 Subject: [PATCH 0461/3961] reworkign clause construction and test around assignment clauses --- cqlengine/statements.py | 19 ++++++------------- .../statements/test_assignment_clause.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 cqlengine/tests/statements/test_assignment_clause.py diff --git a/cqlengine/statements.py b/cqlengine/statements.py index cbdd55bec4..3c49127cc7 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -6,15 +6,11 @@ class StatementException(Exception): pass class BaseClause(object): - def __init__(self, field, operator, value): + def __init__(self, field, value): self.field = field - self.operator = operator self.value = value self.context_id = None - def __unicode__(self): - return u'"{}" {} {}'.format(self.field, self.operator, self.context_id) - def __str__(self): return str(unicode(self)) @@ -40,19 +36,16 @@ def __init__(self, field, operator, value): raise StatementException( "operator must be of type {}, got {}".format(BaseWhereOperator, type(operator)) ) - super(WhereClause, self).__init__(field, operator, value) + super(WhereClause, self).__init__(field, value) + self.operator = operator + + def __unicode__(self): + return u'"{}" {} {}'.format(self.field, self.operator, self.context_id) class AssignmentClause(BaseClause): """ a single variable st statement """ - def __init__(self, field, operator, value): - if not isinstance(operator, BaseAssignmentOperator): - raise StatementException( - "operator must be of type {}, got {}".format(BaseAssignmentOperator, type(operator)) - ) - super(AssignmentClause, self).__init__(field, operator, value) - def insert_tuple(self): return self.field, self.context_id diff --git a/cqlengine/tests/statements/test_assignment_clause.py b/cqlengine/tests/statements/test_assignment_clause.py new file mode 100644 index 0000000000..b778d4b4a8 --- /dev/null +++ b/cqlengine/tests/statements/test_assignment_clause.py @@ -0,0 +1,13 @@ +from unittest import TestCase +from cqlengine.statements import AssignmentClause + + +class AssignmentClauseTests(TestCase): + + def test_rendering(self): + pass + + def test_insert_tuple(self): + ac = AssignmentClause('a', 'b') + ac.set_context_id(10) + self.assertEqual(ac.insert_tuple(), ('a', 10)) From 0a4eebfcd5447b91d55ed3b57dfa0b92dcaf294e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 07:41:15 -0700 Subject: [PATCH 0462/3961] adding tests around insert statements and fixing a few things --- cqlengine/statements.py | 4 ++-- .../tests/statements/test_insert_statement.py | 19 ++++++++++++++++--- .../tests/statements/test_select_statement.py | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 3c49127cc7..70db919773 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -164,13 +164,13 @@ def add_assignment_clause(self, clause): class InsertStatement(AssignmentStatement): """ an cql insert select statement """ - def __init__(self, table, values, consistency=None): + def __init__(self, table, assignments, consistency=None): super(InsertStatement, self).__init__( table, + assignments, consistency=consistency, where=None ) - self.values = values def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py index f81d8ea68d..093e799923 100644 --- a/cqlengine/tests/statements/test_insert_statement.py +++ b/cqlengine/tests/statements/test_insert_statement.py @@ -1,11 +1,24 @@ from unittest import TestCase -from cqlengine.statements import InsertStatement, StatementException +from cqlengine.statements import InsertStatement, StatementException, AssignmentClause class InsertStatementTests(TestCase): def test_where_clause_failure(self): """ tests that where clauses cannot be added to Insert statements """ - ist = InsertStatement('table', {}) + ist = InsertStatement('table', None) with self.assertRaises(StatementException): - ist.add_where_clause('s') \ No newline at end of file + ist.add_where_clause('s') + + def test_statement(self): + ist = InsertStatement('table', None) + ist.add_assignment_clause(AssignmentClause('a', 'b')) + ist.add_assignment_clause(AssignmentClause('c', 'd')) + + self.assertEqual( + unicode(ist), + 'INSERT INTO table ("a", "c") VALUES (:0, :1)' + ) + + def test_additional_rendering(self): + self.fail("Implement ttl and consistency") diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index ec7673cd29..13ed3d5005 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -44,3 +44,6 @@ def test_additional_rendering(self): self.assertIn('LIMIT 15', qstr) self.assertIn('ORDER BY x, y', qstr) self.assertIn('ALLOW FILTERING', qstr) + + self.fail("Implement ttl and consistency") + From 9413595f10b04dd5d29aa90a5c62733f88c17b21 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 10:57:53 -0700 Subject: [PATCH 0463/3961] fixed ttl bug on updates --- cqlengine/query.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 0e8d9ce1e0..a88c744be4 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -805,10 +805,12 @@ def update(self, **values): ctx[field_id] = val if set_statements: - qs = "UPDATE {} SET {} WHERE {}".format( + ttl_stmt = "USING TTL {}".format(self._ttl) if self._ttl else "" + qs = "UPDATE {} SET {} WHERE {} {}".format( self.column_family_name, ', '.join(set_statements), - self._where_clause() + self._where_clause(), + ttl_stmt ) ctx.update(self._where_values()) execute(qs, ctx, self._consistency) From 735b05f6ef7213c0f8e26e0959f6bfb0af0c5ff6 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 11:53:08 -0700 Subject: [PATCH 0464/3961] clone instead of modify self --- cqlengine/query.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a88c744be4..a542c303d2 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -307,7 +307,7 @@ def __call__(self, *args, **kwargs): def __deepcopy__(self, memo): clone = self.__class__(self.model) for k,v in self.__dict__.items(): - if k in ['_con', '_cur', '_result_cache', '_result_idx']: + if k in ['_con', '_cur', '_result_cache', '_result_idx']: # don't clone these clone.__dict__[k] = None elif k == '_batch': # we need to keep the same batch instance across @@ -766,12 +766,14 @@ def values_list(self, *fields, **kwargs): return clone def consistency(self, consistency): - self._consistency = consistency - return self + clone = copy.deepcopy(self) + clone._consistency = consistency + return clone def ttl(self, ttl): - self._ttl = ttl - return self + clone = copy.deepcopy(self) + clone._ttl = ttl + return clone def update(self, **values): """ Updates the rows in this queryset """ From de041cd08d5281b993f20981d14195e83900ed96 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 11:57:23 -0700 Subject: [PATCH 0465/3961] adding method for determining which column values have changed --- cqlengine/models.py | 4 ++++ cqlengine/tests/model/test_model_io.py | 12 +++++++++--- docs/topics/models.rst | 5 +++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 93a9af984e..7012fc4471 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -409,6 +409,10 @@ def delete(self): """ Deletes this instance """ self.__dmlquery__(self.__class__, self, batch=self._batch).delete() + def get_changed_columns(self): + """ returns a list of the columns that have been updated since instantiation or save """ + return [k for k,v in self._values.items() if v.changed] + @classmethod def _class_batch(cls, batch): return cls.objects.batch(batch) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 08d41d8b27..91d4591246 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -42,8 +42,6 @@ def test_model_save_and_load(self): 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 @@ -78,7 +76,6 @@ def test_column_deleting_works_properly(self): assert tm2.text is None assert tm2._values['text'].previous_value is None - def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): """ """ @@ -92,6 +89,7 @@ class TestMultiKeyModel(Model): count = columns.Integer(required=False) text = columns.Text(required=False) + class TestDeleting(BaseCassEngTestCase): @classmethod @@ -158,6 +156,14 @@ def test_deleting_only(self): assert check.count is None assert check.text is None + def test_get_changed_columns(self): + assert self.instance.get_changed_columns() == [] + self.instance.count = 1 + changes = self.instance.get_changed_columns() + assert len(changes) == 1 + assert changes == ['count'] + self.instance.save() + assert self.instance.get_changed_columns() == [] class TestCanUpdate(BaseCassEngTestCase): diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 42d447f9d7..3e776e5be6 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -137,6 +137,7 @@ Model Methods .. method:: delete() Deletes the object from the database. + -- method:: update(**values) @@ -145,6 +146,10 @@ Model Methods fields. If no fields on the model have been modified since loading, no query will be performed. Model validation is performed normally. + -- method:: get_changed_columns() + + Returns a list of column names that have changed since the model was instantiated or saved + Model Attributes ================ From dfb062ec85c555c5b0bea2d490c0d61bcf7c93be Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 12:01:39 -0700 Subject: [PATCH 0466/3961] updating changelog --- changelog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog b/changelog index 57f6fc2c6d..4aad4980c0 100644 --- a/changelog +++ b/changelog @@ -2,9 +2,12 @@ CHANGELOG 0.9 * adding update method +* adding support for ttls +* adding support for per-query consistency * adding BigInt column (thanks @Lifto) * adding support for timezone aware time uuid functions (thanks @dokai) * only saving collection fields on insert if they've been modified +* adding model method that returns a list of modified columns 0.8.5 * adding support for timeouts From b0feecfed31c81d45a4f8b5b62669e304a2ba2cd Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 12:03:24 -0700 Subject: [PATCH 0467/3961] bumping version number --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 7ada0d303f..b63ba696b7 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.8.5 +0.9 From aa87e3a3a6686abfe2b0ffb11f366bc1c067c62f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 12:44:32 -0700 Subject: [PATCH 0468/3961] ignore docs build folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1e4d8eb344..1b5006e6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ html/ /commitlog /data +docs/_build From 6056f2fac1d4ec695949f19de6fdc44231533775 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 13:14:55 -0700 Subject: [PATCH 0469/3961] docs fix --- docs/topics/queryset.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 6caf910612..2311801219 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -362,7 +362,7 @@ QuerySet method reference Sets the ttl to run the query query with. Note that running a select query with a ttl value will raise an exception - -- method:: update(**values) + .. method:: update(**values) Performs an update on the row selected by the queryset. Include values to update in the update like so: From 03e869c10ad1c7398cc00f11fa3c80a965068793 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 13:20:01 -0700 Subject: [PATCH 0470/3961] ttl docs --- docs/topics/queryset.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 2311801219..a73ed1d05f 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -321,6 +321,15 @@ QuerySet method reference Returns a queryset matching all rows + .. method:: batch(batch_object) + + Sets the batch object to run the query on. Note that running a select query with a batch object will raise an exception + + .. method:: consistency(consistency_setting) + + Sets the consistency level for the operation. Options may be imported from the top level :attr:`cqlengine` package. + + .. method:: count() Returns the number of matching rows in your QuerySet @@ -354,12 +363,12 @@ QuerySet method reference Enables the (usually) unwise practive of querying on a clustering key without also defining a partition key - .. method:: batch(batch_object) - - Sets the batch object to run the query on. Note that running a select query with a batch object will raise an exception .. method:: ttl(ttl_in_seconds) + :param ttl_in_seconds: time in seconds in which the saved values should expire + :type ttl_in_seconds: int + Sets the ttl to run the query query with. Note that running a select query with a ttl value will raise an exception .. method:: update(**values) From 1e6462366184120b05e0b07ffaef6307230c6a67 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 13:48:03 -0700 Subject: [PATCH 0471/3961] implementing delete statement and adding method to get statement context --- cqlengine/statements.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 70db919773..7278b8c434 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -70,6 +70,12 @@ def add_where_clause(self, clause): self.context_counter += clause.get_context_size() self.where_clauses.append(clause) + def get_context(self): + ctx = {} + for clause in self.where_clauses or []: + clause.update_context(ctx) + return ctx + def __str__(self): return str(unicode(self)) @@ -83,7 +89,7 @@ class SelectStatement(BaseCQLStatement): def __init__(self, table, - fields, + fields=None, consistency=None, where=None, order_by=None, @@ -121,18 +127,7 @@ def __unicode__(self): return ' '.join(qs) -class DMLStatement(BaseCQLStatement): - """ mutation statements with ttls """ - - def __init__(self, table, consistency=None, where=None, ttl=None): - super(DMLStatement, self).__init__( - table, - consistency=consistency, - where=where) - self.ttl = ttl - - -class AssignmentStatement(DMLStatement): +class AssignmentStatement(BaseCQLStatement): """ value assignment statements """ def __init__(self, @@ -145,8 +140,8 @@ def __init__(self, table, consistency=consistency, where=where, - ttl=ttl ) + self.ttl = ttl # add assignments self.assignments = [] @@ -202,16 +197,24 @@ def __init__(self, table, assignments, consistency=None, where=None, ttl=None): ) -class DeleteStatement(DMLStatement): +class DeleteStatement(BaseCQLStatement): """ a cql delete statement """ - def __init__(self, table, fields, consistency=None, where=None, ttl=None): + def __init__(self, table, fields=None, consistency=None, where=None): super(DeleteStatement, self).__init__( table, consistency=consistency, where=where, - ttl=ttl ) self.fields = [fields] if isinstance(fields, basestring) else (fields or []) + def __unicode__(self): + qs = ['DELETE'] + qs += [', '.join(['"{}"'.format(f) for f in self.fields]) if self.fields else '*'] + qs += ['FROM', self.table] + + if self.where_clauses: + qs += [self._where] + + return ' '.join(qs) From 7771ceabd72c862ac10f3df74836005f26bf3f38 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 13:48:31 -0700 Subject: [PATCH 0472/3961] adding test around delete statement --- .../tests/statements/test_delete_statement.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py index e69de29bb2..da24b410f7 100644 --- a/cqlengine/tests/statements/test_delete_statement.py +++ b/cqlengine/tests/statements/test_delete_statement.py @@ -0,0 +1,38 @@ +from unittest import TestCase +from cqlengine.statements import DeleteStatement, WhereClause +from cqlengine.operators import * + + +class DeleteStatementTests(TestCase): + + def test_single_field_is_listified(self): + """ tests that passing a string field into the constructor puts it into a list """ + ds = DeleteStatement('table', 'field') + self.assertEqual(ds.fields, ['field']) + + def test_field_rendering(self): + """ tests that fields are properly added to the select statement """ + ds = DeleteStatement('table', ['f1', 'f2']) + self.assertTrue(unicode(ds).startswith('DELETE "f1", "f2"'), unicode(ds)) + self.assertTrue(str(ds).startswith('DELETE "f1", "f2"'), str(ds)) + + def test_none_fields_rendering(self): + """ tests that a '*' is added if no fields are passed in """ + ds = DeleteStatement('table', None) + self.assertTrue(unicode(ds).startswith('DELETE *'), unicode(ds)) + self.assertTrue(str(ds).startswith('DELETE *'), str(ds)) + + def test_table_rendering(self): + ds = DeleteStatement('table', None) + self.assertTrue(unicode(ds).startswith('DELETE * FROM table'), unicode(ds)) + self.assertTrue(str(ds).startswith('DELETE * FROM table'), str(ds)) + + def test_where_clause_rendering(self): + ds = DeleteStatement('table', None) + ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) + self.assertEqual(unicode(ds), 'DELETE * FROM table WHERE "a" = 0', unicode(ds)) + + def test_context(self): + ds = DeleteStatement('table', None) + ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) + self.assertEqual(ds.get_context(), {0: 'b'}) From ff1184080ceacfab6b9b18a975df8a3ee0d8dca5 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 13:48:38 -0700 Subject: [PATCH 0473/3961] adding context test to select statement --- cqlengine/tests/statements/test_select_statement.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 13ed3d5005..25563c795d 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -32,6 +32,11 @@ def test_where_clause_rendering(self): ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = b', unicode(ss)) + def test_context(self): + ss = SelectStatement('table', None) + ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) + self.assertEqual(ss.get_context(), {0: 'b'}) + def test_additional_rendering(self): ss = SelectStatement( 'table', From 2b7dd6f22011a45f8d5dec3a6fd6e6f516a608ee Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 13:54:20 -0700 Subject: [PATCH 0474/3961] adding methods to the base assignment class --- cqlengine/statements.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 7278b8c434..387b61d714 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -64,6 +64,11 @@ def __init__(self, table, consistency=None, where=None): self.add_where_clause(clause) def add_where_clause(self, clause): + """ + adds a where clause to this statement + :param clause: the clause to add + :type clause: WhereClause + """ if not isinstance(clause, WhereClause): raise StatementException("only instances of WhereClause can be added to statements") clause.set_context_id(self.context_counter) @@ -71,6 +76,10 @@ def add_where_clause(self, clause): self.where_clauses.append(clause) def get_context(self): + """ + returns the context dict for this statement + :rtype: dict + """ ctx = {} for clause in self.where_clauses or []: clause.update_context(ctx) @@ -132,7 +141,7 @@ class AssignmentStatement(BaseCQLStatement): def __init__(self, table, - assignments, + assignments=None, consistency=None, where=None, ttl=None): @@ -149,12 +158,23 @@ def __init__(self, self.add_assignment_clause(assignment) def add_assignment_clause(self, clause): + """ + adds an assignment clause to this statement + :param clause: the clause to add + :type clause: AssignmentClause + """ if not isinstance(clause, AssignmentClause): raise StatementException("only instances of AssignmentClause can be added to statements") clause.set_context_id(self.context_counter) self.context_counter += clause.get_context_size() self.assignments.append(clause) + def get_context(self): + ctx = super(AssignmentStatement, self).get_context() + for clause in self.assignments: + clause.update_context(ctx) + return ctx + class InsertStatement(AssignmentStatement): """ an cql insert select statement """ From cc1533f099fab5d1ddebfa8cb40cd4951b56bf41 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 14:16:44 -0700 Subject: [PATCH 0475/3961] implementing update statement and insert ttl --- cqlengine/statements.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 387b61d714..36ef3bb4c9 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -179,14 +179,6 @@ def get_context(self): class InsertStatement(AssignmentStatement): """ an cql insert select statement """ - def __init__(self, table, assignments, consistency=None): - super(InsertStatement, self).__init__( - table, - assignments, - consistency=consistency, - where=None - ) - def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") @@ -201,20 +193,27 @@ def __unicode__(self): qs += ['VALUES'] qs += ["({})".format(', '.join([':{}'.format(v) for v in values]))] + if self.ttl: + qs += ["USING TTL {}".format(self.ttl)] + return ' '.join(qs) class UpdateStatement(AssignmentStatement): """ an cql update select statement """ - def __init__(self, table, assignments, consistency=None, where=None, ttl=None): - super(UpdateStatement, self).__init__( - table, - assignments, - consistency=consistency, - where=where, - ttl=ttl - ) + def __unicode__(self): + qs = ['UPDATE', self.table] + qs += 'SET' + qs += ', '.join([unicode(c) for c in self.assignments]) + + if self.where_clauses: + qs += [self._where] + + if self.ttl: + qs += ["USING TTL {}".format(self.ttl)] + + return ' '.join(qs) class DeleteStatement(BaseCQLStatement): From 70e446631b903a835994f2715bc8c583e991acd1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 14:16:55 -0700 Subject: [PATCH 0476/3961] adding test around insert ttl --- cqlengine/tests/statements/test_insert_statement.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py index 093e799923..51280a314f 100644 --- a/cqlengine/tests/statements/test_insert_statement.py +++ b/cqlengine/tests/statements/test_insert_statement.py @@ -21,4 +21,7 @@ def test_statement(self): ) def test_additional_rendering(self): - self.fail("Implement ttl and consistency") + ist = InsertStatement('table', ttl=60) + ist.add_assignment_clause(AssignmentClause('a', 'b')) + ist.add_assignment_clause(AssignmentClause('c', 'd')) + self.assertIn('USING TTL 60', unicode(ist)) From 89c20ef6c5e1d7717144c953583f1f2359321641 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 14:25:30 -0700 Subject: [PATCH 0477/3961] adding tests around update and related debugging --- cqlengine/statements.py | 15 ++++++++--- .../tests/statements/test_update_statement.py | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 36ef3bb4c9..110fc94dc2 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -11,6 +11,9 @@ def __init__(self, field, value): self.value = value self.context_id = None + def __unicode__(self): + raise NotImplementedError + def __str__(self): return str(unicode(self)) @@ -40,12 +43,15 @@ def __init__(self, field, operator, value): self.operator = operator def __unicode__(self): - return u'"{}" {} {}'.format(self.field, self.operator, self.context_id) + return u'"{}" {} :{}'.format(self.field, self.operator, self.context_id) class AssignmentClause(BaseClause): """ a single variable st statement """ + def __unicode__(self): + return u'"{}" = :{}'.format(self.field, self.context_id) + def insert_tuple(self): return self.field, self.context_id @@ -85,6 +91,9 @@ def get_context(self): clause.update_context(ctx) return ctx + def __unicode__(self): + raise NotImplementedError + def __str__(self): return str(unicode(self)) @@ -204,8 +213,8 @@ class UpdateStatement(AssignmentStatement): def __unicode__(self): qs = ['UPDATE', self.table] - qs += 'SET' - qs += ', '.join([unicode(c) for c in self.assignments]) + qs += ['SET'] + qs += [', '.join([unicode(c) for c in self.assignments])] if self.where_clauses: qs += [self._where] diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py index e69de29bb2..1dabbfe3c7 100644 --- a/cqlengine/tests/statements/test_update_statement.py +++ b/cqlengine/tests/statements/test_update_statement.py @@ -0,0 +1,26 @@ +from unittest import TestCase +from cqlengine.statements import UpdateStatement, WhereClause, AssignmentClause +from cqlengine.operators import * + + +class UpdateStatementTests(TestCase): + + def test_table_rendering(self): + """ tests that fields are properly added to the select statement """ + us = UpdateStatement('table') + self.assertTrue(unicode(us).startswith('UPDATE table SET'), unicode(us)) + self.assertTrue(str(us).startswith('UPDATE table SET'), str(us)) + + def test_rendering(self): + us = UpdateStatement('table') + us.add_assignment_clause(AssignmentClause('a', 'b')) + us.add_assignment_clause(AssignmentClause('c', 'd')) + us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) + self.assertEqual(unicode(us), 'UPDATE table SET "a" = :0, "c" = :1 WHERE "a" = :2', unicode(us)) + + def test_context(self): + us = UpdateStatement('table') + us.add_assignment_clause(AssignmentClause('a', 'b')) + us.add_assignment_clause(AssignmentClause('c', 'd')) + us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) + self.assertEqual(us.get_context(), {0: 'b', 1: 'd', 2: 'x'}) From a7c250561ff8acd10c96d523fa472972e3105da8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 14:37:15 -0700 Subject: [PATCH 0478/3961] adding test around update statement ttls --- cqlengine/models.py | 1 + cqlengine/tests/statements/test_update_statement.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index 21f78c2638..b6da73c8ba 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -69,6 +69,7 @@ def __call__(self, *args, **kwargs): """ raise NotImplementedError + class TTLDescriptor(object): """ returns a query set descriptor diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py index 1dabbfe3c7..6c633852d1 100644 --- a/cqlengine/tests/statements/test_update_statement.py +++ b/cqlengine/tests/statements/test_update_statement.py @@ -24,3 +24,10 @@ def test_context(self): us.add_assignment_clause(AssignmentClause('c', 'd')) us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) self.assertEqual(us.get_context(), {0: 'b', 1: 'd', 2: 'x'}) + + def test_additional_rendering(self): + us = UpdateStatement('table', ttl=60) + us.add_assignment_clause(AssignmentClause('a', 'b')) + us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) + self.assertIn('USING TTL 60', unicode(us)) + From 1ea0f057fac7b398959dcabf9d551c0d1765c51a Mon Sep 17 00:00:00 2001 From: Dvir Volk Date: Sat, 26 Oct 2013 00:50:18 +0300 Subject: [PATCH 0479/3961] Added __repr__ to model instances for prettly logging --- cqlengine/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index 21f78c2638..be48ca20de 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -247,6 +247,17 @@ def __init__(self, **values): self._is_persisted = False self._batch = None + + def __repr__(self): + """ + Pretty printing of models by their primary key + """ + return '{} <{}>'.format(self.__class__.__name__, + ', '.join(('{}={}'.format(k, getattr(self, k)) for k,v in self._primary_keys.iteritems())) + ) + + + @classmethod def _discover_polymorphic_submodels(cls): if not cls._is_polymorphic_base: From 09d440052963b70fd4c4232551d3dbbeed773bd3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 15:00:05 -0700 Subject: [PATCH 0480/3961] removing dml statement tests --- cqlengine/tests/statements/test_dml_statement.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cqlengine/tests/statements/test_dml_statement.py diff --git a/cqlengine/tests/statements/test_dml_statement.py b/cqlengine/tests/statements/test_dml_statement.py deleted file mode 100644 index e69de29bb2..0000000000 From 1bc88c547c23ede7a5ac3808ee4840944fa77f62 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 15:03:27 -0700 Subject: [PATCH 0481/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index b63ba696b7..f374f6662e 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.9 +0.9.1 From 7b24b93a53e3f67e0df0a426b668d0c51bdd246b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 15:05:48 -0700 Subject: [PATCH 0482/3961] adding count and fixing select tests --- cqlengine/statements.py | 7 ++++++- .../tests/statements/test_select_statement.py | 17 ++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 110fc94dc2..7a172bdc16 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -108,6 +108,7 @@ class SelectStatement(BaseCQLStatement): def __init__(self, table, fields=None, + count=False, consistency=None, where=None, order_by=None, @@ -121,13 +122,17 @@ def __init__(self, ) self.fields = [fields] if isinstance(fields, basestring) else (fields or []) + self.count = count self.order_by = [order_by] if isinstance(order_by, basestring) else order_by self.limit = limit self.allow_filtering = allow_filtering def __unicode__(self): qs = ['SELECT'] - qs += [', '.join(['"{}"'.format(f) for f in self.fields]) if self.fields else '*'] + if self.count: + qs += ['COUNT(*)'] + else: + qs += [', '.join(['"{}"'.format(f) for f in self.fields]) if self.fields else '*'] qs += ['FROM', self.table] if self.where_clauses: diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 25563c795d..4150ef3b24 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -18,22 +18,27 @@ def test_field_rendering(self): def test_none_fields_rendering(self): """ tests that a '*' is added if no fields are passed in """ - ss = SelectStatement('table', None) + ss = SelectStatement('table') self.assertTrue(unicode(ss).startswith('SELECT *'), unicode(ss)) self.assertTrue(str(ss).startswith('SELECT *'), str(ss)) def test_table_rendering(self): - ss = SelectStatement('table', None) + ss = SelectStatement('table') self.assertTrue(unicode(ss).startswith('SELECT * FROM table'), unicode(ss)) self.assertTrue(str(ss).startswith('SELECT * FROM table'), str(ss)) def test_where_clause_rendering(self): - ss = SelectStatement('table', None) + ss = SelectStatement('table') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = b', unicode(ss)) + self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = :0', unicode(ss)) + + def test_count(self): + ss = SelectStatement('table', count=True) + ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) + self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = :0', unicode(ss)) def test_context(self): - ss = SelectStatement('table', None) + ss = SelectStatement('table') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(ss.get_context(), {0: 'b'}) @@ -50,5 +55,3 @@ def test_additional_rendering(self): self.assertIn('ORDER BY x, y', qstr) self.assertIn('ALLOW FILTERING', qstr) - self.fail("Implement ttl and consistency") - From 80eba6678937a617ad62b3adf936edbf964a7998 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 15:06:35 -0700 Subject: [PATCH 0483/3961] adding context placeholders to some where tests --- cqlengine/tests/statements/test_delete_statement.py | 2 +- cqlengine/tests/statements/test_where_clause.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py index da24b410f7..58d05c867e 100644 --- a/cqlengine/tests/statements/test_delete_statement.py +++ b/cqlengine/tests/statements/test_delete_statement.py @@ -30,7 +30,7 @@ def test_table_rendering(self): def test_where_clause_rendering(self): ds = DeleteStatement('table', None) ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ds), 'DELETE * FROM table WHERE "a" = 0', unicode(ds)) + self.assertEqual(unicode(ds), 'DELETE * FROM table WHERE "a" = :0', unicode(ds)) def test_context(self): ds = DeleteStatement('table', None) diff --git a/cqlengine/tests/statements/test_where_clause.py b/cqlengine/tests/statements/test_where_clause.py index 353e506fd7..66defeadeb 100644 --- a/cqlengine/tests/statements/test_where_clause.py +++ b/cqlengine/tests/statements/test_where_clause.py @@ -13,5 +13,6 @@ def test_operator_check(self): def test_where_clause_rendering(self): """ tests that where clauses are rendered properly """ wc = WhereClause('a', EqualsOperator(), 'c') - self.assertEqual('"a" = c', unicode(wc)) - self.assertEqual('"a" = c', str(wc)) + wc.set_context_id(5) + self.assertEqual('"a" = :5', unicode(wc)) + self.assertEqual('"a" = :5', str(wc)) From a3df87c558bfc6164544e0e747c91c953ea64265 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 15:59:14 -0700 Subject: [PATCH 0484/3961] replacing select query with statement object --- cqlengine/query.py | 64 +++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a542c303d2..2e0ac06b6d 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -12,6 +12,8 @@ from cqlengine.exceptions import CQLEngineException, ValidationError from cqlengine.functions import QueryValue, Token +from cqlengine import statements, operators + #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -326,41 +328,22 @@ def __len__(self): #----query generation / execution---- - def _where_clause(self): - """ Returns a where clause based on the given filter args """ - return ' AND '.join([f.cql for f in self._where]) - - def _where_values(self): - """ Returns the value dict to be passed to the cql query """ - values = {} - for where in self._where: - values.update(where.get_dict()) - return values - - def _get_select_statement(self): - """ returns the select portion of this queryset's cql statement """ - raise NotImplementedError + def _select_fields(self): + """ returns the fields to select """ + return [] def _select_query(self): """ Returns a select clause based on the given filter args """ - qs = [self._get_select_statement()] - qs += ['FROM {}'.format(self.column_family_name)] - - if self._where: - qs += ['WHERE {}'.format(self._where_clause())] - - if self._order: - qs += ['ORDER BY {}'.format(', '.join(self._order))] - - if self._limit: - qs += ['LIMIT {}'.format(self._limit)] - - if self._allow_filtering: - qs += ['ALLOW FILTERING'] - - return ' '.join(qs) + return statements.SelectStatement( + self.column_family_name, + fields=self._select_fields(), + where=self._where, + order_by=self._order, + limit=self._limit, + allow_filtering=self._allow_filtering + ) #----Reads------ @@ -368,7 +351,8 @@ def _execute_query(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: - columns, self._result_cache = execute(self._select_query(), self._where_values(), self._consistency) + query = self._select_query() + columns, self._result_cache = execute(unicode(query).encode('utf-8'), query.get_context(), self._consistency) self._construct_result = self._get_result_constructor(columns) def _fill_result_cache_to_idx(self, idx): @@ -477,7 +461,7 @@ def filter(self, *args, **kwargs): #add arguments to the where clause filters clone = copy.deepcopy(self) for operator in args: - if not isinstance(operator, QueryOperator): + if not isinstance(operator, statements.WhereClause): raise QueryException('{} is not a valid query operator'.format(operator)) clone._where.append(operator) @@ -493,10 +477,10 @@ def filter(self, *args, **kwargs): raise QueryException("Can't resolve column name: '{}'".format(col_name)) #get query operator, or use equals if not supplied - operator_class = QueryOperator.get_operator(col_op or 'EQ') - operator = operator_class(column, val) + operator_class = operators.BaseWhereOperator.get_operator(col_op or 'EQ') + operator = operator_class() - clone._where.append(operator) + clone._where.append(statements.WhereClause(col_name, operator, val)) return clone @@ -717,6 +701,16 @@ def _get_select_statement(self): else: return 'SELECT *' + def _select_fields(self): + if self._defer_fields or self._only_fields: + fields = self.model._columns.keys() + if self._defer_fields: + fields = [f for f in fields if f not in self._defer_fields] + elif self._only_fields: + fields = self._only_fields + return [self.model._columns[f].db_field_name for f in fields] + return super(ModelQuerySet, self)._select_fields() + def _get_result_constructor(self, names): """ Returns a function that will be used to instantiate query results """ if not self._values_list: From 181991233389aed18e867d46c616fbc53a4c86ee Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 16:01:12 -0700 Subject: [PATCH 0485/3961] fixing operator query objects --- cqlengine/query.py | 12 ++++++------ cqlengine/tests/query/test_queryset.py | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 2e0ac06b6d..bb2f0e82a7 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -181,22 +181,22 @@ def in_(self, item): used in where you'd typically want to use python's `in` operator """ - return InOperator(self._get_column(), item) + return statements.WhereClause(self._get_column(), operators.InOperator(), item) def __eq__(self, other): - return EqualsOperator(self._get_column(), other) + return statements.WhereClause(self._get_column(), operators.EqualsOperator(), other) def __gt__(self, other): - return GreaterThanOperator(self._get_column(), other) + return statements.WhereClause(self._get_column(), operators.GreaterThanOperator(), other) def __ge__(self, other): - return GreaterThanOrEqualOperator(self._get_column(), other) + return statements.WhereClause(self._get_column(), operators.GreaterThanOrEqualOperator(), other) def __lt__(self, other): - return LessThanOperator(self._get_column(), other) + return statements.WhereClause(self._get_column(), operators.LessThanOperator(), other) def __le__(self, other): - return LessThanOrEqualOperator(self._get_column(), other) + return statements.WhereClause(self._get_column(), operators.LessThanOrEqualOperator(), other) class BatchType(object): diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 4fe258178a..2ae18a07f7 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -14,6 +14,9 @@ from datetime import timedelta from datetime import tzinfo +from cqlengine import statements +from cqlengine import operators + class TzOffset(tzinfo): """Minimal implementation of a timezone offset to help testing with timezone @@ -61,14 +64,17 @@ def test_query_filter_parsing(self): assert len(query1._where) == 1 op = query1._where[0] - assert isinstance(op, query.EqualsOperator) + + assert isinstance(op, statements.WhereClause) + assert isinstance(op.operator, operators.EqualsOperator) assert op.value == 5 query2 = query1.filter(expected_result__gte=1) assert len(query2._where) == 2 op = query2._where[1] - assert isinstance(op, query.GreaterThanOrEqualOperator) + self.assertIsInstance(op, statements.WhereClause) + self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator) assert op.value == 1 def test_query_expression_parsing(self): @@ -77,14 +83,16 @@ def test_query_expression_parsing(self): assert len(query1._where) == 1 op = query1._where[0] - assert isinstance(op, query.EqualsOperator) + assert isinstance(op, statements.WhereClause) + assert isinstance(op.operator, operators.EqualsOperator) assert op.value == 5 query2 = query1.filter(TestModel.expected_result >= 1) assert len(query2._where) == 2 op = query2._where[1] - assert isinstance(op, query.GreaterThanOrEqualOperator) + self.assertIsInstance(op, statements.WhereClause) + self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator) assert op.value == 1 def test_using_invalid_column_names_in_filter_kwargs_raises_error(self): From 74ed4f4d001e6a1e31bb49e8e479f826c4adc45e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 16:02:00 -0700 Subject: [PATCH 0486/3961] removing obsolete tests --- cqlengine/tests/query/test_queryset.py | 33 -------------------------- 1 file changed, 33 deletions(-) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 2ae18a07f7..8017c1578a 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -116,39 +116,6 @@ def test_using_non_query_operators_in_query_args_raises_error(self): with self.assertRaises(query.QueryException): TestModel.objects(5) - def test_filter_method_where_clause_generation(self): - """ - Tests the where clause creation - """ - query1 = TestModel.objects(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.filter(expected_result__gte=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_query_expression_where_clause_generation(self): - """ - Tests the where clause creation - """ - 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.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) - - 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 From 181002486366eb00277a723daa9446f416d1025d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 17:16:43 -0700 Subject: [PATCH 0487/3961] getting basic select queries working properly --- cqlengine/models.py | 4 ++++ cqlengine/named.py | 4 ++++ cqlengine/query.py | 18 ++++++++++++------ cqlengine/statements.py | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index b6da73c8ba..19a05f73a7 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -129,7 +129,11 @@ class ColumnQueryEvaluator(AbstractQueryableColumn): def __init__(self, column): self.column = column + def __unicode__(self): + return self.column.db_field_name + def _get_column(self): + """ :rtype: ColumnQueryEvaluator """ return self.column diff --git a/cqlengine/named.py b/cqlengine/named.py index c1d1263a82..2b75443144 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -33,7 +33,11 @@ class NamedColumn(AbstractQueryableColumn): def __init__(self, name): self.name = name + def __unicode__(self): + return self.name + def _get_column(self): + """ :rtype: NamedColumn """ return self @property diff --git a/cqlengine/query.py b/cqlengine/query.py index bb2f0e82a7..80201a5ac1 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -175,28 +175,34 @@ class AbstractQueryableColumn(object): def _get_column(self): raise NotImplementedError + def __unicode__(self): + raise NotImplementedError + + def __str__(self): + return str(unicode(self)) + def in_(self, item): """ Returns an in operator used in where you'd typically want to use python's `in` operator """ - return statements.WhereClause(self._get_column(), operators.InOperator(), item) + return statements.WhereClause(unicode(self), operators.InOperator(), item) def __eq__(self, other): - return statements.WhereClause(self._get_column(), operators.EqualsOperator(), other) + return statements.WhereClause(unicode(self), operators.EqualsOperator(), other) def __gt__(self, other): - return statements.WhereClause(self._get_column(), operators.GreaterThanOperator(), other) + return statements.WhereClause(unicode(self), operators.GreaterThanOperator(), other) def __ge__(self, other): - return statements.WhereClause(self._get_column(), operators.GreaterThanOrEqualOperator(), other) + return statements.WhereClause(unicode(self), operators.GreaterThanOrEqualOperator(), other) def __lt__(self, other): - return statements.WhereClause(self._get_column(), operators.LessThanOperator(), other) + return statements.WhereClause(unicode(self), operators.LessThanOperator(), other) def __le__(self, other): - return statements.WhereClause(self._get_column(), operators.LessThanOrEqualOperator(), other) + return statements.WhereClause(unicode(self), operators.LessThanOrEqualOperator(), other) class BatchType(object): diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 7a172bdc16..9aa6e7febf 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -28,7 +28,7 @@ def set_context_id(self, i): def update_context(self, ctx): """ updates the query context with this clauses values """ assert isinstance(ctx, dict) - ctx[self.context_id] = self.value + ctx[str(self.context_id)] = self.value class WhereClause(BaseClause): From 49e58cf9673642c1dc24ddd9c602cebf1a7c4489 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 17:16:53 -0700 Subject: [PATCH 0488/3961] ensure last item is removed properly --- cqlengine/tests/columns/test_container_columns.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 0d3dc524b2..9fbef0d805 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -51,6 +51,17 @@ def test_empty_set_initial(self): m.int_set.add(5) m.save() + def test_deleting_last_item_should_succeed(self): + m = TestSetModel.create() + m.int_set.add(5) + m.save() + m.int_set.remove(5) + m.save() + + m = TestSetModel.get(partition=m.partition) + self.assertNotIn(5, m.int_set) + + def test_empty_set_retrieval(self): m = TestSetModel.create() m2 = TestSetModel.get(partition=m.partition) From 647ebaf5f078508f6e7682420b23b9d24c009a22 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 17:20:50 -0700 Subject: [PATCH 0489/3961] modifying statement, clause, and operator stringifying to encode the object's unicode --- cqlengine/operators.py | 2 +- cqlengine/statements.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/operators.py b/cqlengine/operators.py index cbbfe4ad66..4fef0a6b5d 100644 --- a/cqlengine/operators.py +++ b/cqlengine/operators.py @@ -15,7 +15,7 @@ def __unicode__(self): return self.cql_symbol def __str__(self): - return str(unicode(self)) + return unicode(self).encode('utf-8') @classmethod def get_operator(cls, symbol): diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 9aa6e7febf..45124c76b4 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -15,7 +15,7 @@ def __unicode__(self): raise NotImplementedError def __str__(self): - return str(unicode(self)) + return unicode(self).encode('utf-8') def get_context_size(self): """ returns the number of entries this clause will add to the query context """ @@ -95,7 +95,7 @@ def __unicode__(self): raise NotImplementedError def __str__(self): - return str(unicode(self)) + return unicode(self).encode('utf-8') @property def _where(self): From ec2337858c011799d796bb7405af548828d3a0a4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 25 Oct 2013 17:23:51 -0700 Subject: [PATCH 0490/3961] excluding order and limit from count queries --- cqlengine/statements.py | 4 ++-- cqlengine/tests/statements/test_select_statement.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 45124c76b4..55dfa27757 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -138,10 +138,10 @@ def __unicode__(self): if self.where_clauses: qs += [self._where] - if self.order_by: + if self.order_by and not self.count: qs += ['ORDER BY {}'.format(', '.join(unicode(o) for o in self.order_by))] - if self.limit: + if self.limit and not self.count: qs += ['LIMIT {}'.format(self.limit)] if self.allow_filtering: diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 4150ef3b24..495258c2fd 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -33,9 +33,11 @@ def test_where_clause_rendering(self): self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = :0', unicode(ss)) def test_count(self): - ss = SelectStatement('table', count=True) + ss = SelectStatement('table', count=True, limit=10, order_by='d') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = :0', unicode(ss)) + self.assertNotIn('LIMIT', unicode(ss)) + self.assertNotIn('ORDER', unicode(ss)) def test_context(self): ss = SelectStatement('table') From 802cd08e09866bc78a725036398ccef84c50e55c Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 25 Oct 2013 17:28:12 -0700 Subject: [PATCH 0491/3961] making sure deletion works on the last item --- cqlengine/tests/columns/test_container_columns.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 9fbef0d805..b1a7268062 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -343,6 +343,18 @@ def test_empty_retrieve(self): tmp2 = TestMapModel.get(partition=tmp.partition) tmp2.int_map['blah'] = 1 + def test_remove_last_entry_works(self): + tmp = TestMapModel.create() + tmp.text_map["blah"] = datetime.now() + tmp.save() + del tmp.text_map["blah"] + tmp.save() + + tmp = TestMapModel.get(partition=tmp.partition) + self.assertNotIn("blah", tmp.int_map) + + + def test_io_success(self): """ Tests that a basic usage works as expected """ k1 = uuid4() From 3556c99724a59a899f0f33e686c0e2e552359c35 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 16:22:31 -0700 Subject: [PATCH 0492/3961] refactoring count query --- cqlengine/query.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 80201a5ac1..c011059867 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -539,18 +539,12 @@ def count(self): """ Returns the number of rows matched by this query """ if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") + #TODO: check for previous query execution and return row count if it exists if self._result_cache is None: - qs = ['SELECT COUNT(*)'] - qs += ['FROM {}'.format(self.column_family_name)] - if self._where: - qs += ['WHERE {}'.format(self._where_clause())] - if self._allow_filtering: - qs += ['ALLOW FILTERING'] - - qs = ' '.join(qs) - - _, result = execute(qs, self._where_values()) + query = self._select_query() + query.count = True + _, result = execute(str(query), query.get_context()) return result[0][0] else: return len(self._result_cache) From cbbef483b20d9613216d2c08326e18cfcdebbd39 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 16:22:47 -0700 Subject: [PATCH 0493/3961] adding quoting wrapper --- cqlengine/statements.py | 43 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 55dfa27757..a7bc9ca882 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -1,9 +1,42 @@ -from cqlengine.operators import BaseWhereOperator, BaseAssignmentOperator +from cqlengine.operators import BaseWhereOperator, InOperator class StatementException(Exception): pass +class ValueQuoter(object): + + def __init__(self, value): + self.value = value + + def __unicode__(self): + from cql.query import cql_quote + if isinstance(self.value, bool): + return 'true' if self.value else 'false' + elif isinstance(self.value, (list, tuple)): + return '[' + ', '.join([cql_quote(v) for v in self.value]) + ']' + elif isinstance(self.value, dict): + return '{' + ', '.join([cql_quote(k) + ':' + cql_quote(v) for k,v in self.value.items()]) + '}' + elif isinstance(self.value, set): + return '{' + ', '.join([cql_quote(v) for v in self.value]) + '}' + return cql_quote(self.value) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.value == other.value + return False + + def __str__(self): + return unicode(self).encode('utf-8') + + +class InQuoter(ValueQuoter): + + def __unicode__(self): + from cql.query import cql_quote + return '(' + ', '.join([cql_quote(v) for v in self.value]) + ')' + + class BaseClause(object): def __init__(self, field, value): @@ -28,7 +61,7 @@ def set_context_id(self, i): def update_context(self, ctx): """ updates the query context with this clauses values """ assert isinstance(ctx, dict) - ctx[str(self.context_id)] = self.value + ctx[str(self.context_id)] = ValueQuoter(self.value) class WhereClause(BaseClause): @@ -45,6 +78,12 @@ def __init__(self, field, operator, value): def __unicode__(self): return u'"{}" {} :{}'.format(self.field, self.operator, self.context_id) + def update_context(self, ctx): + if isinstance(self.operator, InOperator): + ctx[str(self.context_id)] = InQuoter(self.value) + else: + super(WhereClause, self).update_context(ctx) + class AssignmentClause(BaseClause): """ a single variable st statement """ From 88804adaf9155d5cc3e4c327cf66d87670c16b00 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 16:32:59 -0700 Subject: [PATCH 0494/3961] adding where clause equality method --- cqlengine/query.py | 1 - cqlengine/statements.py | 13 +++++++++++++ cqlengine/tests/statements/test_quoter.py | 0 cqlengine/tests/statements/test_where_clause.py | 6 ++++++ 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 cqlengine/tests/statements/test_quoter.py diff --git a/cqlengine/query.py b/cqlengine/query.py index c011059867..464220a4a7 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -540,7 +540,6 @@ def count(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") - #TODO: check for previous query execution and return row count if it exists if self._result_cache is None: query = self._select_query() query.count = True diff --git a/cqlengine/statements.py b/cqlengine/statements.py index a7bc9ca882..281701a971 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -50,6 +50,14 @@ def __unicode__(self): def __str__(self): return unicode(self).encode('utf-8') + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.field == other.field and self.value == other.value + return False + + def __ne__(self, other): + return not self.__eq__(other) + def get_context_size(self): """ returns the number of entries this clause will add to the query context """ return 1 @@ -78,6 +86,11 @@ def __init__(self, field, operator, value): def __unicode__(self): return u'"{}" {} :{}'.format(self.field, self.operator, self.context_id) + def __eq__(self, other): + if super(WhereClause, self).__eq__(other): + return self.operator.__class__ == other.operator.__class__ + return False + def update_context(self, ctx): if isinstance(self.operator, InOperator): ctx[str(self.context_id)] = InQuoter(self.value) diff --git a/cqlengine/tests/statements/test_quoter.py b/cqlengine/tests/statements/test_quoter.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cqlengine/tests/statements/test_where_clause.py b/cqlengine/tests/statements/test_where_clause.py index 66defeadeb..938a2b4919 100644 --- a/cqlengine/tests/statements/test_where_clause.py +++ b/cqlengine/tests/statements/test_where_clause.py @@ -16,3 +16,9 @@ def test_where_clause_rendering(self): wc.set_context_id(5) self.assertEqual('"a" = :5', unicode(wc)) self.assertEqual('"a" = :5', str(wc)) + + def test_equality_method(self): + """ tests that 2 identical where clauses evaluate as == """ + wc1 = WhereClause('a', EqualsOperator(), 'c') + wc2 = WhereClause('a', EqualsOperator(), 'c') + assert wc1 == wc2 From 14008640fca758dabd07e95370a8166cb9121e95 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 16:40:39 -0700 Subject: [PATCH 0495/3961] commenting out old operators --- cqlengine/query.py | 312 +++++++++++++++++++++++---------------------- 1 file changed, 157 insertions(+), 155 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 464220a4a7..cd5d7cec52 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -12,159 +12,161 @@ from cqlengine.exceptions import CQLEngineException, ValidationError from cqlengine.functions import QueryValue, Token -from cqlengine import statements, operators - #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index +from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator +from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator +from cqlengine.statements import WhereClause, SelectStatement + class QueryException(CQLEngineException): pass class DoesNotExist(QueryException): pass class MultipleObjectsReturned(QueryException): pass -class QueryOperatorException(QueryException): pass - - -class QueryOperator(object): - # The symbol that identifies this operator in filter kwargs - # ie: colname__ - symbol = None - - # The comparator symbol this operator uses in cql - cql_symbol = None - - QUERY_VALUE_WRAPPER = QueryValue - - def __init__(self, column, value): - self.column = column - self.value = value - - if isinstance(value, QueryValue): - self.query_value = value - else: - self.query_value = self.QUERY_VALUE_WRAPPER(value) - - #perform validation on this operator - self.validate_operator() - self.validate_value() - - @property - def cql(self): - """ - Returns this operator's portion of the WHERE clause - """ - return '{} {} {}'.format(self.column.cql, self.cql_symbol, self.query_value.cql) - - def validate_operator(self): - """ - Checks that this operator can be used on the column provided - """ - if self.symbol is None: - raise QueryOperatorException( - "{} is not a valid operator, use one with 'symbol' defined".format( - self.__class__.__name__ - ) - ) - if self.cql_symbol is None: - raise QueryOperatorException( - "{} is not a valid operator, use one with 'cql_symbol' defined".format( - self.__class__.__name__ - ) - ) - - def validate_value(self): - """ - Checks that the compare value works with this operator - - Doesn't do anything by default - """ - pass - - def get_dict(self): - """ - Returns this operators contribution to the cql.query arg dictionanry - - ie: if this column's name is colname, and the identifier is colval, - this should return the dict: {'colval':} - SELECT * FROM column_family WHERE colname=:colval - """ - return self.query_value.get_dict(self.column) - - @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 - _recurse(QueryOperator) - try: - return QueryOperator.opmap[symbol.upper()] - except KeyError: - raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) - - # equality operator, used by tests - - def __eq__(self, op): - return self.__class__ is op.__class__ and \ - self.column.db_field_name == op.column.db_field_name and \ - self.value == op.value - - def __ne__(self, op): - return not (self == op) - - def __hash__(self): - return hash(self.column.db_field_name) ^ hash(self.value) - - -class EqualsOperator(QueryOperator): - symbol = 'EQ' - cql_symbol = '=' - - -class IterableQueryValue(QueryValue): - def __init__(self, value): - try: - super(IterableQueryValue, self).__init__(value, [uuid4().hex for i in value]) - except TypeError: - raise QueryException("in operator arguments must be iterable, {} found".format(value)) - - def get_dict(self, column): - return dict((i, column.to_database(v)) for (i, v) in zip(self.identifier, self.value)) - - def get_cql(self): - return '({})'.format(', '.join(':{}'.format(i) for i in self.identifier)) - - -class InOperator(EqualsOperator): - symbol = 'IN' - cql_symbol = 'IN' - - QUERY_VALUE_WRAPPER = IterableQueryValue - - -class GreaterThanOperator(QueryOperator): - symbol = "GT" - cql_symbol = '>' - - -class GreaterThanOrEqualOperator(QueryOperator): - symbol = "GTE" - cql_symbol = '>=' - - -class LessThanOperator(QueryOperator): - symbol = "LT" - cql_symbol = '<' - - -class LessThanOrEqualOperator(QueryOperator): - symbol = "LTE" - cql_symbol = '<=' - +# class QueryOperatorException(QueryException): pass +# +# +# class QueryOperator(object): +# # The symbol that identifies this operator in filter kwargs +# # ie: colname__ +# symbol = None +# +# # The comparator symbol this operator uses in cql +# cql_symbol = None +# +# QUERY_VALUE_WRAPPER = QueryValue +# +# def __init__(self, column, value): +# self.column = column +# self.value = value +# +# if isinstance(value, QueryValue): +# self.query_value = value +# else: +# self.query_value = self.QUERY_VALUE_WRAPPER(value) +# +# #perform validation on this operator +# self.validate_operator() +# self.validate_value() +# +# @property +# def cql(self): +# """ +# Returns this operator's portion of the WHERE clause +# """ +# return '{} {} {}'.format(self.column.cql, self.cql_symbol, self.query_value.cql) +# +# def validate_operator(self): +# """ +# Checks that this operator can be used on the column provided +# """ +# if self.symbol is None: +# raise QueryOperatorException( +# "{} is not a valid operator, use one with 'symbol' defined".format( +# self.__class__.__name__ +# ) +# ) +# if self.cql_symbol is None: +# raise QueryOperatorException( +# "{} is not a valid operator, use one with 'cql_symbol' defined".format( +# self.__class__.__name__ +# ) +# ) +# +# def validate_value(self): +# """ +# Checks that the compare value works with this operator +# +# Doesn't do anything by default +# """ +# pass +# +# def get_dict(self): +# """ +# Returns this operators contribution to the cql.query arg dictionanry +# +# ie: if this column's name is colname, and the identifier is colval, +# this should return the dict: {'colval':} +# SELECT * FROM column_family WHERE colname=:colval +# """ +# return self.query_value.get_dict(self.column) +# +# @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 +# _recurse(QueryOperator) +# try: +# return QueryOperator.opmap[symbol.upper()] +# except KeyError: +# raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) +# +# # equality operator, used by tests +# +# def __eq__(self, op): +# return self.__class__ is op.__class__ and \ +# self.column.db_field_name == op.column.db_field_name and \ +# self.value == op.value +# +# def __ne__(self, op): +# return not (self == op) +# +# def __hash__(self): +# return hash(self.column.db_field_name) ^ hash(self.value) +# +# +# class EqualsOperator(QueryOperator): +# symbol = 'EQ' +# cql_symbol = '=' +# +# +# class IterableQueryValue(QueryValue): +# def __init__(self, value): +# try: +# super(IterableQueryValue, self).__init__(value, [uuid4().hex for i in value]) +# except TypeError: +# raise QueryException("in operator arguments must be iterable, {} found".format(value)) +# +# def get_dict(self, column): +# return dict((i, column.to_database(v)) for (i, v) in zip(self.identifier, self.value)) +# +# def get_cql(self): +# return '({})'.format(', '.join(':{}'.format(i) for i in self.identifier)) +# +# +# class InOperator(EqualsOperator): +# symbol = 'IN' +# cql_symbol = 'IN' +# +# QUERY_VALUE_WRAPPER = IterableQueryValue +# +# +# class GreaterThanOperator(QueryOperator): +# symbol = "GT" +# cql_symbol = '>' +# +# +# class GreaterThanOrEqualOperator(QueryOperator): +# symbol = "GTE" +# cql_symbol = '>=' +# +# +# class LessThanOperator(QueryOperator): +# symbol = "LT" +# cql_symbol = '<' +# +# +# class LessThanOrEqualOperator(QueryOperator): +# symbol = "LTE" +# cql_symbol = '<=' +# class AbstractQueryableColumn(object): """ @@ -187,22 +189,22 @@ def in_(self, item): used in where you'd typically want to use python's `in` operator """ - return statements.WhereClause(unicode(self), operators.InOperator(), item) + return WhereClause(unicode(self), InOperator(), item) def __eq__(self, other): - return statements.WhereClause(unicode(self), operators.EqualsOperator(), other) + return WhereClause(unicode(self), EqualsOperator(), other) def __gt__(self, other): - return statements.WhereClause(unicode(self), operators.GreaterThanOperator(), other) + return WhereClause(unicode(self), GreaterThanOperator(), other) def __ge__(self, other): - return statements.WhereClause(unicode(self), operators.GreaterThanOrEqualOperator(), other) + return WhereClause(unicode(self), GreaterThanOrEqualOperator(), other) def __lt__(self, other): - return statements.WhereClause(unicode(self), operators.LessThanOperator(), other) + return WhereClause(unicode(self), LessThanOperator(), other) def __le__(self, other): - return statements.WhereClause(unicode(self), operators.LessThanOrEqualOperator(), other) + return WhereClause(unicode(self), LessThanOrEqualOperator(), other) class BatchType(object): @@ -342,7 +344,7 @@ def _select_query(self): """ Returns a select clause based on the given filter args """ - return statements.SelectStatement( + return SelectStatement( self.column_family_name, fields=self._select_fields(), where=self._where, @@ -467,7 +469,7 @@ def filter(self, *args, **kwargs): #add arguments to the where clause filters clone = copy.deepcopy(self) for operator in args: - if not isinstance(operator, statements.WhereClause): + if not isinstance(operator, WhereClause): raise QueryException('{} is not a valid query operator'.format(operator)) clone._where.append(operator) @@ -483,10 +485,10 @@ def filter(self, *args, **kwargs): raise QueryException("Can't resolve column name: '{}'".format(col_name)) #get query operator, or use equals if not supplied - operator_class = operators.BaseWhereOperator.get_operator(col_op or 'EQ') + operator_class = BaseWhereOperator.get_operator(col_op or 'EQ') operator = operator_class() - clone._where.append(statements.WhereClause(col_name, operator, val)) + clone._where.append(WhereClause(col_name, operator, val)) return clone From 0807b299dd6b00f2f5bcbc4413fb738f0d660337 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 17:14:27 -0700 Subject: [PATCH 0496/3961] adding equality and hash methods --- cqlengine/query.py | 29 +++++++++++++------------- cqlengine/statements.py | 6 ++++++ cqlengine/tests/query/test_queryset.py | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index cd5d7cec52..dee495c53f 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -340,10 +340,14 @@ def _select_fields(self): """ returns the fields to select """ return [] + def _validate_select(self): + """ put select query validation here """ + def _select_query(self): """ Returns a select clause based on the given filter args """ + self._validate_select() return SelectStatement( self.column_family_name, fields=self._select_fields(), @@ -627,7 +631,9 @@ def delete(self, columns=[]): execute(qs, self._where_values()) def __eq__(self, q): - return set(self._where) == set(q._where) + if len(self._where) == len(q._where): + return all([w in q._where for w in self._where]) + return False def __ne__(self, q): return not (self != q) @@ -667,30 +673,25 @@ class ModelQuerySet(AbstractQuerySet): """ """ - def _validate_where_syntax(self): + def _validate_select(self): """ Checks that a filterset will not create invalid cql """ - #check that there's either a = or IN relationship with a primary key or indexed field - equal_ops = [w for w in self._where if isinstance(w, EqualsOperator)] - token_ops = [w for w in self._where if isinstance(w.value, Token)] - if not any([w.column.primary_key or w.column.index for w in equal_ops]) and not token_ops: + equal_ops = [self.model._columns.get(w.field) for w in self._where if isinstance(w.operator, EqualsOperator)] + token_ops = [self.model._columns.get(w.field) for w in self._where if isinstance(w.operator, Token)] + if not any([w.primary_key or w.index for w in equal_ops]) and not token_ops: raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') if not self._allow_filtering: #if the query is not on an indexed field - if not any([w.column.index for w in equal_ops]): - if not any([w.column.partition_key for w in equal_ops]) and not token_ops: + if not any([w.index for w in equal_ops]): + if not any([w.partition_key for w in equal_ops]) and not token_ops: raise QueryException('Filtering on a clustering key without a partition key is not allowed unless allow_filtering() is called on the querset') - if any(not w.column.partition_key for w in token_ops): + if any(not w.partition_key for w in token_ops): raise QueryException('The token() function is only supported on the partition key') - def _where_clause(self): - """ Returns a where clause based on the given filter args """ - self._validate_where_syntax() - return super(ModelQuerySet, self)._where_clause() - def _get_select_statement(self): """ Returns the fields to be returned by the select query """ + self._validate_select() if self._defer_fields or self._only_fields: fields = self.model._columns.keys() if self._defer_fields: diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 281701a971..80ec6a5aef 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -50,6 +50,9 @@ def __unicode__(self): def __str__(self): return unicode(self).encode('utf-8') + def __hash__(self): + return hash(self.field) ^ hash(self.value) + def __eq__(self, other): if isinstance(other, self.__class__): return self.field == other.field and self.value == other.value @@ -86,6 +89,9 @@ def __init__(self, field, operator, value): def __unicode__(self): return u'"{}" {} :{}'.format(self.field, self.operator, self.context_id) + def __hash__(self): + return super(WhereClause, self).__hash__() ^ hash(self.operator) + def __eq__(self, other): if super(WhereClause, self).__eq__(other): return self.operator.__class__ == other.operator.__class__ diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 8017c1578a..97e6033551 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -491,12 +491,12 @@ def test_conn_is_returned_after_filling_cache(self): assert q._cur is None - class TimeUUIDQueryModel(Model): partition = columns.UUID(primary_key=True) time = columns.TimeUUID(primary_key=True) data = columns.Text(required=False) + class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase): @classmethod From 898fc438ea058de721192c115628c0eed7d95a98 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 17:18:10 -0700 Subject: [PATCH 0497/3961] removing unused methods --- cqlengine/query.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index dee495c53f..46efaad460 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -340,14 +340,15 @@ def _select_fields(self): """ returns the fields to select """ return [] - def _validate_select(self): + def _validate_select_where(self): """ put select query validation here """ def _select_query(self): """ Returns a select clause based on the given filter args """ - self._validate_select() + if self._where: + self._validate_select_where() return SelectStatement( self.column_family_name, fields=self._select_fields(), @@ -656,10 +657,6 @@ class SimpleQuerySet(AbstractQuerySet): """ - def _get_select_statement(self): - """ Returns the fields to be returned by the select query """ - return 'SELECT *' - def _get_result_constructor(self, names): """ Returns a function that will be used to instantiate query results @@ -673,8 +670,8 @@ class ModelQuerySet(AbstractQuerySet): """ """ - def _validate_select(self): - """ Checks that a filterset will not create invalid cql """ + def _validate_select_where(self): + """ Checks that a filterset will not create invalid select statement """ #check that there's either a = or IN relationship with a primary key or indexed field equal_ops = [self.model._columns.get(w.field) for w in self._where if isinstance(w.operator, EqualsOperator)] token_ops = [self.model._columns.get(w.field) for w in self._where if isinstance(w.operator, Token)] @@ -689,20 +686,6 @@ def _validate_select(self): if any(not w.partition_key for w in token_ops): raise QueryException('The token() function is only supported on the partition key') - def _get_select_statement(self): - """ Returns the fields to be returned by the select query """ - self._validate_select() - if self._defer_fields or self._only_fields: - fields = self.model._columns.keys() - if self._defer_fields: - fields = [f for f in fields if f not in self._defer_fields] - elif self._only_fields: - fields = self._only_fields - db_fields = [self.model._columns[f].db_field_name for f in fields] - return 'SELECT {}'.format(', '.join(['"{}"'.format(f) for f in db_fields])) - else: - return 'SELECT *' - def _select_fields(self): if self._defer_fields or self._only_fields: fields = self.model._columns.keys() From 736ec6dc7f33d510173c39c4be024fe7fab615ae Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 19:55:32 -0700 Subject: [PATCH 0498/3961] fixing delete syntax --- cqlengine/statements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 80ec6a5aef..9b5c62790c 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -301,7 +301,8 @@ def __init__(self, table, fields=None, consistency=None, where=None): def __unicode__(self): qs = ['DELETE'] - qs += [', '.join(['"{}"'.format(f) for f in self.fields]) if self.fields else '*'] + if self.fields: + qs += [', '.join(['"{}"'.format(f) for f in self.fields])] qs += ['FROM', self.table] if self.where_clauses: From e0164167c6dcd8425395575ac5594c45ca91fcf2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 27 Oct 2013 19:55:49 -0700 Subject: [PATCH 0499/3961] replacing delete call with statement --- cqlengine/query.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 46efaad460..9db0aebb30 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement class QueryException(CQLEngineException): pass @@ -613,23 +613,24 @@ def defer(self, fields): def create(self, **kwargs): return self.model(**kwargs).batch(self._batch).ttl(self._ttl).consistency(self._consistency).save() - #----delete--- - def delete(self, columns=[]): + def delete(self): """ Deletes the contents of a query """ #validate where clause partition_key = self.model._primary_keys.values()[0] - if not any([c.column.db_field_name == partition_key.db_field_name for c in self._where]): + if not any([c.field == partition_key.column_name for c in self._where]): raise QueryException("The partition key must be defined on delete queries") - qs = ['DELETE FROM {}'.format(self.column_family_name)] - qs += ['WHERE {}'.format(self._where_clause())] - qs = ' '.join(qs) + + dq = DeleteStatement( + self.column_family_name, + where=self._where + ) if self._batch: - self._batch.add_query(qs, self._where_values()) + self._batch.add_query(str(dq), dq.get_context()) else: - execute(qs, self._where_values()) + execute(str(dq), dq.get_context()) def __eq__(self, q): if len(self._where) == len(q._where): From 187c5b4678ce4f3c080bcf3fb296a2fb495c1977 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 28 Oct 2013 08:19:24 -0700 Subject: [PATCH 0500/3961] adding set update clause and supporting unit tests --- cqlengine/query.py | 12 +- cqlengine/statements.py | 63 ++++++++++ .../tests/columns/test_container_columns.py | 2 +- .../statements/test_assignment_clause.py | 13 -- .../statements/test_assignment_clauses.py | 115 ++++++++++++++++++ 5 files changed, 188 insertions(+), 17 deletions(-) delete mode 100644 cqlengine/tests/statements/test_assignment_clause.py create mode 100644 cqlengine/tests/statements/test_assignment_clauses.py diff --git a/cqlengine/query.py b/cqlengine/query.py index 9db0aebb30..c5939f592d 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement class QueryException(CQLEngineException): pass @@ -795,7 +795,10 @@ def update(self, **values): ttl_stmt ) ctx.update(self._where_values()) - execute(qs, ctx, self._consistency) + if self._batch: + self._batch.add_query(qs, ctx) + else: + execute(qs, ctx, self._consistency) if nulled_columns: qs = "DELETE {} FROM {} WHERE {}".format( @@ -803,7 +806,10 @@ def update(self, **values): self.column_family_name, self._where_clause() ) - execute(qs, self._where_values(), self._consistency) + if self._batch: + self._batch.add_query(qs, self._where_values()) + else: + execute(qs, self._where_values(), self._consistency) class DMLQuery(object): diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 9b5c62790c..4812b56510 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -114,6 +114,69 @@ def insert_tuple(self): return self.field, self.context_id +class SetUpdateClause(AssignmentClause): + """ updates a set collection """ + + def __init__(self, field, value, previous=None): + super(SetUpdateClause, self).__init__(field, value) + self.previous = previous + self._assignments = None + self._additions = None + self._removals = None + self._analyzed = False + + def __unicode__(self): + qs = [] + ctx_id = self.context_id + if self._assignments: + qs += ['"{}" = :{}'.format(self.field, ctx_id)] + ctx_id += 1 + if self._additions: + qs += ['"{0}" = "{0}" + :{1}'.format(self.field, ctx_id)] + ctx_id += 1 + if self._removals: + qs += ['"{0}" = "{0}" - :{1}'.format(self.field, ctx_id)] + + return ', '.join(qs) + + def _analyze(self): + """ works out the updates to be performed """ + if self.value is None or self.value == self.previous: + pass + elif self.previous is None or not any({v in self.previous for v in self.value}): + self._assignments = self.value + else: + # partial update time + self._additions = (self.value - self.previous) or None + self._removals = (self.previous - self.value) or None + self._analyzed = True + + def get_context_size(self): + if not self._analyzed: self._analyze() + return int(bool(self._assignments)) + int(bool(self._additions)) + int(bool(self._removals)) + + def update_context(self, ctx): + if not self._analyzed: self._analyze() + ctx_id = self.context_id + if self._assignments: + ctx[str(ctx_id)] = self._assignments + ctx_id += 1 + if self._additions: + ctx[str(ctx_id)] = self._additions + ctx_id += 1 + if self._removals: + ctx[str(ctx_id)] = self._removals + + + +class ListUpdateClause(AssignmentClause): + """ updates a list collection """ + + +class MapUpdateClause(AssignmentClause): + """ updates a map collection """ + + class BaseCQLStatement(object): """ The base cql statement class """ diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 0d3dc524b2..3d83c3be8e 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -105,7 +105,7 @@ def test_partial_update_creation(self): assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 def test_update_from_none(self): - """ Tests that updating an 'None' list creates a straight insert statement """ + """ Tests that updating a 'None' list creates a straight insert statement """ ctx = {} col = columns.Set(columns.Integer, db_field="TEST") statements = col.get_update_statement({1, 2, 3, 4}, None, ctx) diff --git a/cqlengine/tests/statements/test_assignment_clause.py b/cqlengine/tests/statements/test_assignment_clause.py deleted file mode 100644 index b778d4b4a8..0000000000 --- a/cqlengine/tests/statements/test_assignment_clause.py +++ /dev/null @@ -1,13 +0,0 @@ -from unittest import TestCase -from cqlengine.statements import AssignmentClause - - -class AssignmentClauseTests(TestCase): - - def test_rendering(self): - pass - - def test_insert_tuple(self): - ac = AssignmentClause('a', 'b') - ac.set_context_id(10) - self.assertEqual(ac.insert_tuple(), ('a', 10)) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py new file mode 100644 index 0000000000..8f807b0431 --- /dev/null +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -0,0 +1,115 @@ +from unittest import TestCase +from cqlengine.statements import AssignmentClause, SetUpdateClause + + +class AssignmentClauseTests(TestCase): + + def test_rendering(self): + pass + + def test_insert_tuple(self): + ac = AssignmentClause('a', 'b') + ac.set_context_id(10) + self.assertEqual(ac.insert_tuple(), ('a', 10)) + + +class SetUpdateClauseTests(TestCase): + + def test_update_from_none(self): + c = SetUpdateClause('s', {1, 2}, None) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._assignments, {1, 2}) + self.assertIsNone(c._additions) + self.assertIsNone(c._removals) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': {1, 2}}) + + def test_null_update(self): + """ tests setting a set to None creates an empty update statement """ + c = SetUpdateClause('s', None, {1, 2}) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertIsNone(c._additions) + self.assertIsNone(c._removals) + + self.assertEqual(c.get_context_size(), 0) + self.assertEqual(str(c), '') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {}) + + def test_no_update(self): + """ tests an unchanged value creates an empty update statement """ + c = SetUpdateClause('s', {1, 2}, {1, 2}) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertIsNone(c._additions) + self.assertIsNone(c._removals) + + self.assertEqual(c.get_context_size(), 0) + self.assertEqual(str(c), '') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {}) + + def test_additions(self): + c = SetUpdateClause('s', {1, 2, 3}, {1, 2}) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertEqual(c._additions, {3}) + self.assertIsNone(c._removals) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = "s" + :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': {3}}) + + def test_removals(self): + c = SetUpdateClause('s', {1, 2}, {1, 2, 3}) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertIsNone(c._additions) + self.assertEqual(c._removals, {3}) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = "s" - :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': {3}}) + + def test_additions_and_removals(self): + c = SetUpdateClause('s', {2, 3}, {1, 2}) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertEqual(c._additions, {3}) + self.assertEqual(c._removals, {1}) + + self.assertEqual(c.get_context_size(), 2) + self.assertEqual(str(c), '"s" = "s" + :0, "s" = "s" - :1') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': {3}, '1': {1}}) + From 022d31021bff735e8b93f1fa82a63ac49f027bd4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 28 Oct 2013 14:28:53 -0700 Subject: [PATCH 0501/3961] validation around deleting containers --- cqlengine/tests/columns/test_container_columns.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index b1a7268062..5af677fb0c 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -411,9 +411,15 @@ def test_updates_from_none(self): m.int_map = expected m.save() + m2 = TestMapModel.get(partition=m.partition) assert m2.int_map == expected + m2.int_map = None + m2.save() + m3 = TestMapModel.get(partition=m.partition) + assert m3.int_map != expected + def test_updates_to_none(self): """ Tests that setting the field to None works as expected """ m = TestMapModel.create(int_map={1: uuid4()}) From 20afb040701cf03ae41d11306a54df31afdde433 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 28 Oct 2013 18:29:55 -0700 Subject: [PATCH 0502/3961] adding list update clause --- cqlengine/statements.py | 108 +++++++++++++++- .../statements/test_assignment_clauses.py | 121 +++++++++++++++++- 2 files changed, 221 insertions(+), 8 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 4812b56510..43a4c1017a 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -114,16 +114,31 @@ def insert_tuple(self): return self.field, self.context_id -class SetUpdateClause(AssignmentClause): - """ updates a set collection """ +class ContainerUpdateClause(AssignmentClause): def __init__(self, field, value, previous=None): - super(SetUpdateClause, self).__init__(field, value) + super(ContainerUpdateClause, self).__init__(field, value) self.previous = previous self._assignments = None + self._analyzed = False + + def _analyze(self): + raise NotImplementedError + + def get_context_size(self): + raise NotImplementedError + + def update_context(self, ctx): + raise NotImplementedError + + +class SetUpdateClause(ContainerUpdateClause): + """ updates a set collection """ + + def __init__(self, field, value, previous=None): + super(SetUpdateClause, self).__init__(field, value, previous) self._additions = None self._removals = None - self._analyzed = False def __unicode__(self): qs = [] @@ -168,12 +183,91 @@ def update_context(self, ctx): ctx[str(ctx_id)] = self._removals - -class ListUpdateClause(AssignmentClause): +class ListUpdateClause(ContainerUpdateClause): """ updates a list collection """ + def __init__(self, field, value, previous=None): + super(ListUpdateClause, self).__init__(field, value, previous) + self._append = None + self._prepend = None + + def __unicode__(self): + qs = [] + ctx_id = self.context_id + if self._assignments: + qs += ['"{}" = :{}'.format(self.field, ctx_id)] + ctx_id += 1 + + if self._prepend: + qs += ['"{0}" = :{1} + "{0}"'.format(self.field, ctx_id)] + ctx_id += 1 + + if self._append: + qs += ['"{0}" = "{0}" + :{1}'.format(self.field, ctx_id)] + + return ', '.join(qs) + + def get_context_size(self): + if not self._analyzed: self._analyze() + return int(bool(self._assignments)) + int(bool(self._append)) + int(bool(self._prepend)) + + def update_context(self, ctx): + if not self._analyzed: self._analyze() + ctx_id = self.context_id + if self._assignments: + ctx[str(ctx_id)] = self._assignments + ctx_id += 1 + if self._prepend: + # CQL seems to prepend element at a time, starting + # with the element at idx 0, we can either reverse + # it here, or have it inserted in reverse + ctx[str(ctx_id)] = list(reversed(self._prepend)) + ctx_id += 1 + if self._append: + ctx[str(ctx_id)] = self._append + + def _analyze(self): + """ works out the updates to be performed """ + if self.value is None or self.value == self.previous: + pass + + elif self.previous is None: + self._assignments = self.value + + elif len(self.value) < len(self.previous): + # if elements have been removed, + # rewrite the whole list + self._assignments = self.value + + elif len(self.previous) == 0: + # if we're updating from an empty + # list, do a complete insert + self._assignments = self.value + else: + + # the max start idx we want to compare + search_space = len(self.value) - max(0, len(self.previous)-1) + + # the size of the sub lists we want to look at + search_size = len(self.previous) + + for i in range(search_space): + #slice boundary + j = i + search_size + sub = self.value[i:j] + idx_cmp = lambda idx: self.previous[idx] == sub[idx] + if idx_cmp(0) and idx_cmp(-1) and self.previous == sub: + self._prepend = self.value[:i] or None + self._append = self.value[j:] or None + break + + # if both append and prepend are still None after looking + # at both lists, an insert statement will be created + if self._prepend is self._append is None: + self._assignments = self.value + -class MapUpdateClause(AssignmentClause): +class MapUpdateClause(ContainerUpdateClause): """ updates a map collection """ diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 8f807b0431..bb38fb97f8 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -1,5 +1,5 @@ from unittest import TestCase -from cqlengine.statements import AssignmentClause, SetUpdateClause +from cqlengine.statements import AssignmentClause, SetUpdateClause, ListUpdateClause class AssignmentClauseTests(TestCase): @@ -113,3 +113,122 @@ def test_additions_and_removals(self): c.update_context(ctx) self.assertEqual(ctx, {'0': {3}, '1': {1}}) + +class ListUpdateClauseTests(TestCase): + + def test_update_from_none(self): + c = ListUpdateClause('s', [1, 2, 3]) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._assignments, [1, 2, 3]) + self.assertIsNone(c._append) + self.assertIsNone(c._prepend) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': [1, 2, 3]}) + + def test_update_from_empty(self): + c = ListUpdateClause('s', [1, 2, 3], previous=[]) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._assignments, [1, 2, 3]) + self.assertIsNone(c._append) + self.assertIsNone(c._prepend) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': [1, 2, 3]}) + + def test_update_from_different_list(self): + c = ListUpdateClause('s', [1, 2, 3], previous=[3, 2, 1]) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._assignments, [1, 2, 3]) + self.assertIsNone(c._append) + self.assertIsNone(c._prepend) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': [1, 2, 3]}) + + def test_append(self): + c = ListUpdateClause('s', [1, 2, 3, 4], previous=[1, 2]) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertEqual(c._append, [3, 4]) + self.assertIsNone(c._prepend) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = "s" + :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': [3, 4]}) + + def test_prepend(self): + c = ListUpdateClause('s', [1, 2, 3, 4], previous=[3, 4]) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertIsNone(c._append) + self.assertEqual(c._prepend, [1, 2]) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = :0 + "s"') + + ctx = {} + c.update_context(ctx) + # test context list reversal + self.assertEqual(ctx, {'0': [2, 1]}) + + def test_append_and_prepend(self): + c = ListUpdateClause('s', [1, 2, 3, 4, 5, 6], previous=[3, 4]) + c._analyze() + c.set_context_id(0) + + self.assertIsNone(c._assignments) + self.assertEqual(c._append, [5, 6]) + self.assertEqual(c._prepend, [1, 2]) + + self.assertEqual(c.get_context_size(), 2) + self.assertEqual(str(c), '"s" = :0 + "s", "s" = "s" + :1') + + ctx = {} + c.update_context(ctx) + # test context list reversal + self.assertEqual(ctx, {'0': [2, 1], '1': [5, 6]}) + + def test_shrinking_list_update(self): + """ tests that updating to a smaller list results in an insert statement """ + c = ListUpdateClause('s', [1, 2, 3], previous=[1, 2, 3, 4]) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._assignments, [1, 2, 3]) + self.assertIsNone(c._append) + self.assertIsNone(c._prepend) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"s" = :0') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': [1, 2, 3]}) + + From 06632cf53215b7f7d076052f1356e62ae85dbbe4 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 28 Oct 2013 18:56:32 -0700 Subject: [PATCH 0503/3961] adding map update clause, delete clause, and field delete clause --- cqlengine/statements.py | 44 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 43a4c1017a..b3b1c6d5b3 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -266,10 +266,44 @@ def _analyze(self): if self._prepend is self._append is None: self._assignments = self.value + self._analyzed = True + class MapUpdateClause(ContainerUpdateClause): """ updates a map collection """ + def __init__(self, field, value, previous=None): + super(MapUpdateClause, self).__init__(field, value, previous) + + +class BaseDeleteClause(BaseClause): + pass + + +class FieldDeleteClause(BaseDeleteClause): + """ deletes a field from a row """ + + def __init__(self, field): + super(FieldDeleteClause, self).__init__(field, None) + + def __unicode__(self): + return self.field + + def update_context(self, ctx): + pass + + def get_context_size(self): + return 0 + + +class MapDeleteClause(BaseDeleteClause): + """ removes keys from a map """ + + def __init__(self, field, value, previous=None): + super(MapDeleteClause, self).__init__(field, value) + self.previous = previous + self._analysed = False + class BaseCQLStatement(object): """ The base cql statement class """ @@ -454,7 +488,15 @@ def __init__(self, table, fields=None, consistency=None, where=None): consistency=consistency, where=where, ) - self.fields = [fields] if isinstance(fields, basestring) else (fields or []) + for field in fields or []: + self.add_field(field) + + def add_field(self, field): + if isinstance(field, basestring): + field = FieldDeleteClause(field) + if not isinstance(field, BaseClause): + raise StatementException("only instances of AssignmentClause can be added to statements") + self.fields.append(field) def __unicode__(self): qs = ['DELETE'] From cae7a1945a184e6a78add19e9d180b6fbee590f0 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 28 Oct 2013 19:01:44 -0700 Subject: [PATCH 0504/3961] adding not implemented errors and stubbed tests for map and delete field clauses --- cqlengine/statements.py | 37 ++++++++++++++++++- .../statements/test_assignment_clauses.py | 10 +++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index b3b1c6d5b3..a32bd6cbb9 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -192,6 +192,7 @@ def __init__(self, field, value, previous=None): self._prepend = None def __unicode__(self): + if not self._analyzed: self._analyze() qs = [] ctx_id = self.context_id if self._assignments: @@ -275,6 +276,21 @@ class MapUpdateClause(ContainerUpdateClause): def __init__(self, field, value, previous=None): super(MapUpdateClause, self).__init__(field, value, previous) + def get_context_size(self): + if not self._analyzed: + self._analyze() + raise NotImplementedError('implement this') + + def update_context(self, ctx): + if not self._analyzed: + self._analyze() + raise NotImplementedError('implement this') + + def __unicode__(self): + if not self._analyzed: + self._analyze() + raise NotImplementedError('implement this') + class BaseDeleteClause(BaseClause): pass @@ -302,7 +318,26 @@ class MapDeleteClause(BaseDeleteClause): def __init__(self, field, value, previous=None): super(MapDeleteClause, self).__init__(field, value) self.previous = previous - self._analysed = False + self._analyzed = False + self._removals = None + + def _analyze(self): + self._analyzed = True + + def update_context(self, ctx): + if not self._analyzed: + self._analyze() + raise NotImplementedError('implement this') + + def get_context_size(self): + if not self._analyzed: + self._analyze() + raise NotImplementedError('implement this') + + def __unicode__(self): + if not self._analyzed: + self._analyze() + raise NotImplementedError('implement this') class BaseCQLStatement(object): diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index bb38fb97f8..3da24decde 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -232,3 +232,13 @@ def test_shrinking_list_update(self): self.assertEqual(ctx, {'0': [1, 2, 3]}) +class MapUpdateTests(TestCase): + pass + + +class MapDeleteTests(TestCase): + pass + + +class FieldDeleteTests(TestCase): + pass From 871ba29f3bb8e2a9781ec73cd9d3f980127e593d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 30 Oct 2013 20:07:10 -0700 Subject: [PATCH 0505/3961] implementing map update clause and adding stubbed out tests --- cqlengine/statements.py | 31 +++++++++++++------ .../statements/test_assignment_clauses.py | 14 +++++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index a32bd6cbb9..05ea9def94 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -275,21 +275,34 @@ class MapUpdateClause(ContainerUpdateClause): def __init__(self, field, value, previous=None): super(MapUpdateClause, self).__init__(field, value, previous) + self._updates = None + + def _analyze(self): + self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None + self._analyzed = True def get_context_size(self): - if not self._analyzed: - self._analyze() - raise NotImplementedError('implement this') + if not self._analyzed: self._analyze() + return len(self._updates or []) * 2 def update_context(self, ctx): - if not self._analyzed: - self._analyze() - raise NotImplementedError('implement this') + if not self._analyzed: self._analyze() + ctx_id = self.context_id + for key in self._updates or []: + ctx[str(ctx_id)] = key + ctx[str(ctx_id + 1)] = self.value.get(key) + ctx_id += 2 def __unicode__(self): - if not self._analyzed: - self._analyze() - raise NotImplementedError('implement this') + if not self._analyzed: self._analyze() + qs = [] + + ctx_id = self.context_id + for _ in self._updates or []: + qs += ['"{}"[:{}] = :{}'.format(self.field, ctx_id, ctx_id + 1)] + ctx_id += 2 + + return ', '.join(qs) class BaseDeleteClause(BaseClause): diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 3da24decde..bbe044a5bf 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -233,11 +233,21 @@ def test_shrinking_list_update(self): class MapUpdateTests(TestCase): - pass + + def test_update(self): + pass + + def test_update_from_null(self): + pass + + def test_nulled_columns_arent_included(self): + pass class MapDeleteTests(TestCase): - pass + + def test_update(self): + pass class FieldDeleteTests(TestCase): From cdf45bf1abe889c85199e0c120741689e18d3d7e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 30 Oct 2013 20:08:22 -0700 Subject: [PATCH 0506/3961] removing extra line --- docs/topics/models.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 9d214d3a7f..c55a81d687 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -147,7 +147,6 @@ Model Methods -- method:: update(**values) - Performs an update on the model instance. You can pass in values to set on the model for updating, or you can call without values to execute an update against any modified fields. If no fields on the model have been modified since loading, no query will be From 979166f161f46c4c027b1651e92db33dde66b02c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 30 Oct 2013 20:12:12 -0700 Subject: [PATCH 0507/3961] fixing update method syntax --- docs/topics/models.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index c55a81d687..1fae6070d4 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -145,14 +145,14 @@ Model Methods Sets the ttl values to run instance updates and inserts queries with. - -- method:: update(**values) + .. method:: update(**values) Performs an update on the model instance. You can pass in values to set on the model for updating, or you can call without values to execute an update against any modified fields. If no fields on the model have been modified since loading, no query will be performed. Model validation is performed normally. - -- method:: get_changed_columns() + .. method:: get_changed_columns() Returns a list of column names that have changed since the model was instantiated or saved From ff70a5595b2d17d0bf2d5cd7344e37825d2353b8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 31 Oct 2013 21:23:05 -0700 Subject: [PATCH 0508/3961] implementing map delete and field delete --- cqlengine/statements.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 05ea9def94..f5dc82064b 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -276,6 +276,7 @@ class MapUpdateClause(ContainerUpdateClause): def __init__(self, field, value, previous=None): super(MapUpdateClause, self).__init__(field, value, previous) self._updates = None + self.previous = self.previous or {} def _analyze(self): self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None @@ -316,7 +317,7 @@ def __init__(self, field): super(FieldDeleteClause, self).__init__(field, None) def __unicode__(self): - return self.field + return '"{}"'.format(self.field) def update_context(self, ctx): pass @@ -330,27 +331,27 @@ class MapDeleteClause(BaseDeleteClause): def __init__(self, field, value, previous=None): super(MapDeleteClause, self).__init__(field, value) - self.previous = previous + self.value = self.value or {} + self.previous = previous or {} self._analyzed = False self._removals = None def _analyze(self): + self._removals = sorted([k for k in self.previous if k not in self.value]) self._analyzed = True def update_context(self, ctx): - if not self._analyzed: - self._analyze() - raise NotImplementedError('implement this') + if not self._analyzed: self._analyze() + for idx, key in enumerate(self._removals): + ctx[str(self.context_id + idx)] = key def get_context_size(self): - if not self._analyzed: - self._analyze() - raise NotImplementedError('implement this') + if not self._analyzed: self._analyze() + return len(self._removals) def __unicode__(self): - if not self._analyzed: - self._analyze() - raise NotImplementedError('implement this') + if not self._analyzed: self._analyze() + return ', '.join(['"{}"[:{}]'.format(self.field, self.context_id + i) for i in range(len(self._removals))]) class BaseCQLStatement(object): From 6b75dc357b242ff6f0fcfeebd1213540b6d4f794 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 31 Oct 2013 21:23:17 -0700 Subject: [PATCH 0509/3961] adding tests around map update and delete, and field delete --- .../statements/test_assignment_clauses.py | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index bbe044a5bf..03d2a5ab0f 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -1,5 +1,5 @@ from unittest import TestCase -from cqlengine.statements import AssignmentClause, SetUpdateClause, ListUpdateClause +from cqlengine.statements import AssignmentClause, SetUpdateClause, ListUpdateClause, MapUpdateClause, MapDeleteClause, FieldDeleteClause class AssignmentClauseTests(TestCase): @@ -235,20 +235,57 @@ def test_shrinking_list_update(self): class MapUpdateTests(TestCase): def test_update(self): - pass + c = MapUpdateClause('s', {3: 0, 5: 6}, {5: 0, 3: 4}) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._updates, [3, 5]) + self.assertEqual(c.get_context_size(), 4) + self.assertEqual(str(c), '"s"[:0] = :1, "s"[:2] = :3') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': 3, "1": 0, '2': 5, '3': 6}) def test_update_from_null(self): - pass + c = MapUpdateClause('s', {3: 0, 5: 6}) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._updates, [3, 5]) + self.assertEqual(c.get_context_size(), 4) + self.assertEqual(str(c), '"s"[:0] = :1, "s"[:2] = :3') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': 3, "1": 0, '2': 5, '3': 6}) def test_nulled_columns_arent_included(self): - pass + c = MapUpdateClause('s', {3: 0}, {1: 2, 3: 4}) + c._analyze() + c.set_context_id(0) + + self.assertNotIn(1, c._updates) class MapDeleteTests(TestCase): def test_update(self): - pass + c = MapDeleteClause('s', {3: 0}, {1: 2, 3: 4, 5: 6}) + c._analyze() + c.set_context_id(0) + + self.assertEqual(c._removals, [1, 5]) + self.assertEqual(c.get_context_size(), 2) + self.assertEqual(str(c), '"s"[:0], "s"[:1]') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'0': 1, '1': 5}) class FieldDeleteTests(TestCase): - pass + + def test_str(self): + f = FieldDeleteClause("blake") + assert str(f) == '"blake"' From 6d873e604d99079899f9b017e8226b2bb167a1b8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sat, 2 Nov 2013 10:02:38 -0700 Subject: [PATCH 0510/3961] fixing delete statement --- cqlengine/statements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index f5dc82064b..1db9af0773 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -537,6 +537,7 @@ def __init__(self, table, fields=None, consistency=None, where=None): consistency=consistency, where=where, ) + self.fields = [] for field in fields or []: self.add_field(field) From 48870d59b1578a3811cf035af1eec7f6a105c67f Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 07:48:46 -0800 Subject: [PATCH 0511/3961] updating named query tests --- cqlengine/tests/query/test_named.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cqlengine/tests/query/test_named.py b/cqlengine/tests/query/test_named.py index cba46dcb59..8f9781da96 100644 --- a/cqlengine/tests/query/test_named.py +++ b/cqlengine/tests/query/test_named.py @@ -1,4 +1,4 @@ -from cqlengine import query +from cqlengine import operators from cqlengine.named import NamedKeyspace from cqlengine.query import ResultObject from cqlengine.tests.query.test_queryset import BaseQuerySetUsage @@ -21,14 +21,14 @@ def test_query_filter_parsing(self): assert len(query1._where) == 1 op = query1._where[0] - assert isinstance(op, query.EqualsOperator) + assert isinstance(op, operators.EqualsOperator) assert op.value == 5 query2 = query1.filter(expected_result__gte=1) assert len(query2._where) == 2 op = query2._where[1] - assert isinstance(op, query.GreaterThanOrEqualOperator) + assert isinstance(op, operators.GreaterThanOrEqualOperator) assert op.value == 1 def test_query_expression_parsing(self): @@ -37,14 +37,14 @@ def test_query_expression_parsing(self): assert len(query1._where) == 1 op = query1._where[0] - assert isinstance(op, query.EqualsOperator) + assert isinstance(op.operator, operators.EqualsOperator) assert op.value == 5 query2 = query1.filter(self.table.column('expected_result') >= 1) assert len(query2._where) == 2 op = query2._where[1] - assert isinstance(op, query.GreaterThanOrEqualOperator) + assert isinstance(op.operator, operators.GreaterThanOrEqualOperator) assert op.value == 1 def test_filter_method_where_clause_generation(self): From 5b2351488f84e9065069d3f17215966c7986d2ac Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 07:49:11 -0800 Subject: [PATCH 0512/3961] using to_database for filter values --- cqlengine/query.py | 2 +- cqlengine/tests/query/test_datetime_queries.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index c5939f592d..7de3898da6 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -493,7 +493,7 @@ def filter(self, *args, **kwargs): operator_class = BaseWhereOperator.get_operator(col_op or 'EQ') operator = operator_class() - clone._where.append(WhereClause(col_name, operator, val)) + clone._where.append(WhereClause(col_name, operator, column.to_database(val))) return clone diff --git a/cqlengine/tests/query/test_datetime_queries.py b/cqlengine/tests/query/test_datetime_queries.py index e374bd7d18..39cc0e332f 100644 --- a/cqlengine/tests/query/test_datetime_queries.py +++ b/cqlengine/tests/query/test_datetime_queries.py @@ -43,7 +43,7 @@ def test_range_query(self): end = start + timedelta(days=3) results = DateTimeQueryTestModel.filter(user=0, day__gte=start, day__lt=end) - assert len(results) == 3 + assert len(results) == 3 def test_datetime_precision(self): """ Tests that millisecond resolution is preserved when saving datetime objects """ From fb21199f51c42b81a017740dfaabb1d0f92e97cf Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 08:00:25 -0800 Subject: [PATCH 0513/3961] fixing statement tests --- cqlengine/statements.py | 8 ++++++-- cqlengine/tests/statements/test_base_clause.py | 2 +- cqlengine/tests/statements/test_delete_statement.py | 5 +++-- cqlengine/tests/statements/test_select_statement.py | 2 +- cqlengine/tests/statements/test_update_statement.py | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 1db9af0773..697bd6ceb5 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -72,7 +72,7 @@ def set_context_id(self, i): def update_context(self, ctx): """ updates the query context with this clauses values """ assert isinstance(ctx, dict) - ctx[str(self.context_id)] = ValueQuoter(self.value) + ctx[str(self.context_id)] = self.value class WhereClause(BaseClause): @@ -538,6 +538,8 @@ def __init__(self, table, fields=None, consistency=None, where=None): where=where, ) self.fields = [] + if isinstance(fields, basestring): + fields = [fields] for field in fields or []: self.add_field(field) @@ -551,7 +553,9 @@ def add_field(self, field): def __unicode__(self): qs = ['DELETE'] if self.fields: - qs += [', '.join(['"{}"'.format(f) for f in self.fields])] + qs += [', '.join(['{}'.format(f) for f in self.fields])] + else: + qs += ['*'] qs += ['FROM', self.table] if self.where_clauses: diff --git a/cqlengine/tests/statements/test_base_clause.py b/cqlengine/tests/statements/test_base_clause.py index 04d7d52845..c5bbeb402d 100644 --- a/cqlengine/tests/statements/test_base_clause.py +++ b/cqlengine/tests/statements/test_base_clause.py @@ -11,6 +11,6 @@ def test_context_updating(self): ctx = {} ss.set_context_id(10) ss.update_context(ctx) - assert ctx == {10: 'b'} + assert ctx == {'10': 'b'} diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py index 58d05c867e..be4087779a 100644 --- a/cqlengine/tests/statements/test_delete_statement.py +++ b/cqlengine/tests/statements/test_delete_statement.py @@ -8,7 +8,8 @@ class DeleteStatementTests(TestCase): def test_single_field_is_listified(self): """ tests that passing a string field into the constructor puts it into a list """ ds = DeleteStatement('table', 'field') - self.assertEqual(ds.fields, ['field']) + self.assertEqual(len(ds.fields), 1) + self.assertEqual(ds.fields[0].field, 'field') def test_field_rendering(self): """ tests that fields are properly added to the select statement """ @@ -35,4 +36,4 @@ def test_where_clause_rendering(self): def test_context(self): ds = DeleteStatement('table', None) ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(ds.get_context(), {0: 'b'}) + self.assertEqual(ds.get_context(), {'0': 'b'}) diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 495258c2fd..d59c8eb77c 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -42,7 +42,7 @@ def test_count(self): def test_context(self): ss = SelectStatement('table') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(ss.get_context(), {0: 'b'}) + self.assertEqual(ss.get_context(), {'0': 'b'}) def test_additional_rendering(self): ss = SelectStatement( diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py index 6c633852d1..0945d2341e 100644 --- a/cqlengine/tests/statements/test_update_statement.py +++ b/cqlengine/tests/statements/test_update_statement.py @@ -23,7 +23,7 @@ def test_context(self): us.add_assignment_clause(AssignmentClause('a', 'b')) us.add_assignment_clause(AssignmentClause('c', 'd')) us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) - self.assertEqual(us.get_context(), {0: 'b', 1: 'd', 2: 'x'}) + self.assertEqual(us.get_context(), {'0': 'b', '1': 'd', '2': 'x'}) def test_additional_rendering(self): us = UpdateStatement('table', ttl=60) From a0e99934020ae840bb07859dd227118e4eb156af Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 08:14:30 -0800 Subject: [PATCH 0514/3961] fixing delete statement --- cqlengine/statements.py | 2 -- cqlengine/tests/statements/test_delete_statement.py | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 697bd6ceb5..f974a18951 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -554,8 +554,6 @@ def __unicode__(self): qs = ['DELETE'] if self.fields: qs += [', '.join(['{}'.format(f) for f in self.fields])] - else: - qs += ['*'] qs += ['FROM', self.table] if self.where_clauses: diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py index be4087779a..b1055ea4fb 100644 --- a/cqlengine/tests/statements/test_delete_statement.py +++ b/cqlengine/tests/statements/test_delete_statement.py @@ -20,18 +20,18 @@ def test_field_rendering(self): def test_none_fields_rendering(self): """ tests that a '*' is added if no fields are passed in """ ds = DeleteStatement('table', None) - self.assertTrue(unicode(ds).startswith('DELETE *'), unicode(ds)) - self.assertTrue(str(ds).startswith('DELETE *'), str(ds)) + self.assertTrue(unicode(ds).startswith('DELETE FROM'), unicode(ds)) + self.assertTrue(str(ds).startswith('DELETE FROM'), str(ds)) def test_table_rendering(self): ds = DeleteStatement('table', None) - self.assertTrue(unicode(ds).startswith('DELETE * FROM table'), unicode(ds)) - self.assertTrue(str(ds).startswith('DELETE * FROM table'), str(ds)) + self.assertTrue(unicode(ds).startswith('DELETE FROM table'), unicode(ds)) + self.assertTrue(str(ds).startswith('DELETE FROM table'), str(ds)) def test_where_clause_rendering(self): ds = DeleteStatement('table', None) ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ds), 'DELETE * FROM table WHERE "a" = :0', unicode(ds)) + self.assertEqual(unicode(ds), 'DELETE FROM table WHERE "a" = :0', unicode(ds)) def test_context(self): ds = DeleteStatement('table', None) From 2daf6390e6a8ec7b5d99d20861f8db3e98b8216b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 08:19:30 -0800 Subject: [PATCH 0515/3961] fixing IN statement to_database --- cqlengine/query.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 7de3898da6..3cdfd57e77 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -493,7 +493,14 @@ def filter(self, *args, **kwargs): operator_class = BaseWhereOperator.get_operator(col_op or 'EQ') operator = operator_class() - clone._where.append(WhereClause(col_name, operator, column.to_database(val))) + if isinstance(operator, InOperator): + if not isinstance(val, (list, tuple)): + raise QueryException('IN queries must use a list/tuple value') + query_val = [column.to_database(v) for v in val] + else: + query_val = column.to_database(val) + + clone._where.append(WhereClause(col_name, operator, query_val)) return clone From 7d98bfb67c82d5101fb4e5eba683ea4fdeb3fda6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 08:42:51 -0800 Subject: [PATCH 0516/3961] refactoring where test --- cqlengine/tests/query/test_named.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/cqlengine/tests/query/test_named.py b/cqlengine/tests/query/test_named.py index 8f9781da96..e63261f79c 100644 --- a/cqlengine/tests/query/test_named.py +++ b/cqlengine/tests/query/test_named.py @@ -1,5 +1,6 @@ from cqlengine import operators from cqlengine.named import NamedKeyspace +from cqlengine.operators import EqualsOperator, GreaterThanOrEqualOperator from cqlengine.query import ResultObject from cqlengine.tests.query.test_queryset import BaseQuerySetUsage from cqlengine.tests.base import BaseCassEngTestCase @@ -52,14 +53,24 @@ def test_filter_method_where_clause_generation(self): Tests the where clause creation """ query1 = self.table.objects(test_id=5) - ids = [o.query_value.identifier for o in query1._where] - where = query1._where_clause() - assert where == '"test_id" = :{}'.format(*ids) + self.assertEqual(len(query1._where), 1) + where = query1._where[0] + self.assertEqual(where.field, 'test_id') + self.assertEqual(where.value, 5) query2 = query1.filter(expected_result__gte=1) - ids = [o.query_value.identifier for o in query2._where] - where = query2._where_clause() - assert where == '"test_id" = :{} AND "expected_result" >= :{}'.format(*ids) + where = query2._where + self.assertEqual(len(where), 2) + + where = query2._where[0] + self.assertEqual(where.field, 'test_id') + self.assertIsInstance(where.operator, EqualsOperator) + self.assertEqual(where.value, 5) + + where = query2._where[1] + self.assertEqual(where.field, 'expected_result') + self.assertIsInstance(where.operator, GreaterThanOrEqualOperator) + self.assertEqual(where.value, 1) def test_query_expression_where_clause_generation(self): """ From 1dd5b50bd7ce432a9dd39a8bae99fc154c98a077 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 08:43:05 -0800 Subject: [PATCH 0517/3961] refactoring update query --- cqlengine/query.py | 42 ++++++++++----------------- cqlengine/tests/query/test_updates.py | 3 ++ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 3cdfd57e77..545d5810ae 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause class QueryException(CQLEngineException): pass @@ -767,9 +767,8 @@ def update(self, **values): if not values: return - set_statements = [] - ctx = {} nulled_columns = set() + us = UpdateStatement(self.column_family_name, where=self._where, ttl=self._ttl) for name, val in values.items(): col = self.model._columns.get(name) # check for nonexistant columns @@ -784,39 +783,28 @@ def update(self, **values): nulled_columns.add(name) continue # add the update statements - if isinstance(col, (BaseContainerColumn, Counter)): - val_mgr = self.instance._values[name] - set_statements += col.get_update_statement(val, val_mgr.previous_value, ctx) - + if isinstance(col, Counter): + # TODO: implement counter updates + raise NotImplementedError else: - field_id = uuid4().hex - set_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] - ctx[field_id] = val - - if set_statements: - ttl_stmt = "USING TTL {}".format(self._ttl) if self._ttl else "" - qs = "UPDATE {} SET {} WHERE {} {}".format( - self.column_family_name, - ', '.join(set_statements), - self._where_clause(), - ttl_stmt - ) - ctx.update(self._where_values()) + us.add_assignment_clause(AssignmentClause(name, col.to_database(val))) + + if us.assignments: + qs = str(us) + ctx = us.get_context() if self._batch: self._batch.add_query(qs, ctx) else: execute(qs, ctx, self._consistency) if nulled_columns: - qs = "DELETE {} FROM {} WHERE {}".format( - ', '.join(nulled_columns), - self.column_family_name, - self._where_clause() - ) + ds = DeleteStatement(self.column_family_name, fields=nulled_columns, where=self._where) + qs = str(ds) + ctx = ds.get_context() if self._batch: - self._batch.add_query(qs, self._where_values()) + self._batch.add_query(qs, ctx) else: - execute(qs, self._where_values(), self._consistency) + execute(qs, ctx, self._consistency) class DMLQuery(object): diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 0c9c88cf21..b54b294c60 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -112,3 +112,6 @@ def test_mixed_value_and_null_update(self): assert row.cluster == i assert row.count == (6 if i == 3 else i) assert row.text == (None if i == 3 else str(i)) + + def test_counter_updates(self): + pass From 3a5a09fe5ad8d5b6d47ca7b24abd2eaeaaa001d9 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 08:46:12 -0800 Subject: [PATCH 0518/3961] fixing more where construction tests --- cqlengine/tests/query/test_named.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/cqlengine/tests/query/test_named.py b/cqlengine/tests/query/test_named.py index e63261f79c..38df15420a 100644 --- a/cqlengine/tests/query/test_named.py +++ b/cqlengine/tests/query/test_named.py @@ -22,14 +22,14 @@ def test_query_filter_parsing(self): assert len(query1._where) == 1 op = query1._where[0] - assert isinstance(op, operators.EqualsOperator) + assert isinstance(op.operator, operators.EqualsOperator) assert op.value == 5 query2 = query1.filter(expected_result__gte=1) assert len(query2._where) == 2 op = query2._where[1] - assert isinstance(op, operators.GreaterThanOrEqualOperator) + assert isinstance(op.operator, operators.GreaterThanOrEqualOperator) assert op.value == 1 def test_query_expression_parsing(self): @@ -59,8 +59,7 @@ def test_filter_method_where_clause_generation(self): self.assertEqual(where.value, 5) query2 = query1.filter(expected_result__gte=1) - where = query2._where - self.assertEqual(len(where), 2) + self.assertEqual(len(query2._where), 2) where = query2._where[0] self.assertEqual(where.field, 'test_id') @@ -77,14 +76,23 @@ def test_query_expression_where_clause_generation(self): Tests the where clause creation """ query1 = self.table.objects(self.table.column('test_id') == 5) - ids = [o.query_value.identifier for o in query1._where] - where = query1._where_clause() - assert where == '"test_id" = :{}'.format(*ids) + self.assertEqual(len(query1._where), 1) + where = query1._where[0] + self.assertEqual(where.field, 'test_id') + self.assertEqual(where.value, 5) query2 = query1.filter(self.table.column('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) + self.assertEqual(len(query2._where), 2) + + where = query2._where[0] + self.assertEqual(where.field, 'test_id') + self.assertIsInstance(where.operator, EqualsOperator) + self.assertEqual(where.value, 5) + + where = query2._where[1] + self.assertEqual(where.field, 'expected_result') + self.assertIsInstance(where.operator, GreaterThanOrEqualOperator) + self.assertEqual(where.value, 1) class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): @@ -99,7 +107,6 @@ def setUpClass(cls): cls.keyspace = NamedKeyspace(ks) cls.table = cls.keyspace.table(tn) - def test_count(self): """ Tests that adding filtering statements affects the count query as expected """ assert self.table.objects.count() == 12 From f426e0138e1eb25304c01e051ec5d01295e9c4ee Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 09:53:51 -0800 Subject: [PATCH 0519/3961] hacking in the query functions, also getting all tests to pass --- cqlengine/columns.py | 9 ++- cqlengine/functions.py | 63 ++++++++++---------- cqlengine/named.py | 4 ++ cqlengine/query.py | 8 ++- cqlengine/statements.py | 25 +++++++- cqlengine/tests/query/test_queryoperators.py | 34 +++++++---- 6 files changed, 93 insertions(+), 50 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 79b90e25b9..05ace2bb7f 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -865,8 +865,15 @@ def __init__(self, model): self.partition_columns = model._partition_keys.values() super(_PartitionKeysToken, self).__init__(partition_key=True) + @property + def db_field_name(self): + return 'token({})'.format(', '.join(['"{}"'.format(c.db_field_name) for c in self.partition_columns])) + def to_database(self, value): - raise NotImplementedError + from cqlengine.functions import Token + assert isinstance(value, Token) + value.set_columns(self.partition_columns) + return value def get_cql(self): return "token({})".format(", ".join(c.cql for c in self.partition_columns)) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 136618453c..ceb6a78be9 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -9,24 +9,24 @@ class QueryValue(object): be passed into .filter() keyword args """ - _cql_string = ':{}' + format_string = ':{}' - def __init__(self, value, identifier=None): + def __init__(self, value): self.value = value - self.identifier = uuid1().hex if identifier is None else identifier + self.context_id = None + + def __unicode__(self): + return self.format_string.format(self.context_id) - def get_cql(self): - return self._cql_string.format(self.identifier) + def set_context_id(self, ctx_id): + self.context_id = ctx_id - def get_value(self): - return self.value + def get_context_size(self): + return 1 - def get_dict(self, column): - return {self.identifier: column.to_database(self.get_value())} + def update_context(self, ctx): + ctx[str(self.context_id)] = self.value - @property - def cql(self): - return self.get_cql() class BaseQueryFunction(QueryValue): """ @@ -42,7 +42,7 @@ class MinTimeUUID(BaseQueryFunction): http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun """ - _cql_string = 'MinTimeUUID(:{})' + format_string = 'MinTimeUUID(:{})' def __init__(self, value): """ @@ -53,14 +53,11 @@ def __init__(self, value): raise ValidationError('datetime instance is required') super(MinTimeUUID, self).__init__(value) - def get_value(self): + def update_context(self, ctx): epoch = datetime(1970, 1, 1, tzinfo=self.value.tzinfo) offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 + ctx[str(self.context_id)] = long(((self.value - epoch).total_seconds() - offset) * 1000) - return long(((self.value - epoch).total_seconds() - offset) * 1000) - - def get_dict(self, column): - return {self.identifier: self.get_value()} class MaxTimeUUID(BaseQueryFunction): """ @@ -69,7 +66,7 @@ class MaxTimeUUID(BaseQueryFunction): http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun """ - _cql_string = 'MaxTimeUUID(:{})' + format_string = 'MaxTimeUUID(:{})' def __init__(self, value): """ @@ -80,14 +77,11 @@ def __init__(self, value): raise ValidationError('datetime instance is required') super(MaxTimeUUID, self).__init__(value) - def get_value(self): + def update_context(self, ctx): epoch = datetime(1970, 1, 1, tzinfo=self.value.tzinfo) offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 + ctx[str(self.context_id)] = long(((self.value - epoch).total_seconds() - offset) * 1000) - return long(((self.value - epoch).total_seconds() - offset) * 1000) - - def get_dict(self, column): - return {self.identifier: self.get_value()} class Token(BaseQueryFunction): """ @@ -99,15 +93,20 @@ class Token(BaseQueryFunction): def __init__(self, *values): if len(values) == 1 and isinstance(values[0], (list, tuple)): values = values[0] - super(Token, self).__init__(values, [uuid1().hex for i in values]) + super(Token, self).__init__(values) + self._columns = None + + def set_columns(self, columns): + self._columns = columns - def get_dict(self, column): - items = zip(self.identifier, self.value, column.partition_columns) - return dict( - (id, col.to_database(val)) for id, val, col in items - ) + def get_context_size(self): + return len(self.value) - def get_cql(self): - token_args = ', '.join(':{}'.format(id) for id in self.identifier) + def __unicode__(self): + token_args = ', '.join(':{}'.format(self.context_id + i) for i in range(self.get_context_size())) return "token({})".format(token_args) + def update_context(self, ctx): + for i, (col, val) in enumerate(zip(self._columns, self.value)): + ctx[str(self.context_id + i)] = col.to_database(val) + diff --git a/cqlengine/named.py b/cqlengine/named.py index 2b75443144..c6ba3ac995 100644 --- a/cqlengine/named.py +++ b/cqlengine/named.py @@ -40,6 +40,10 @@ def _get_column(self): """ :rtype: NamedColumn """ return self + @property + def db_field_name(self): + return self.name + @property def cql(self): return self.get_cql() diff --git a/cqlengine/query.py b/cqlengine/query.py index 545d5810ae..0418883fef 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -10,7 +10,7 @@ from cqlengine.connection import connection_manager, execute, RowResult from cqlengine.exceptions import CQLEngineException, ValidationError -from cqlengine.functions import QueryValue, Token +from cqlengine.functions import QueryValue, Token, BaseQueryFunction #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -480,12 +480,14 @@ def filter(self, *args, **kwargs): for arg, val in kwargs.items(): col_name, col_op = self._parse_filter_arg(arg) + quote_field = True #resolve column and operator try: column = self.model._get_column(col_name) except KeyError: if col_name == 'pk__token': column = columns._PartitionKeysToken(self.model) + quote_field = False else: raise QueryException("Can't resolve column name: '{}'".format(col_name)) @@ -497,10 +499,12 @@ def filter(self, *args, **kwargs): if not isinstance(val, (list, tuple)): raise QueryException('IN queries must use a list/tuple value') query_val = [column.to_database(v) for v in val] + elif isinstance(val, BaseQueryFunction): + query_val = val else: query_val = column.to_database(val) - clone._where.append(WhereClause(col_name, operator, query_val)) + clone._where.append(WhereClause(column.db_field_name, operator, query_val, quote_field=quote_field)) return clone diff --git a/cqlengine/statements.py b/cqlengine/statements.py index f974a18951..834e647924 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -1,3 +1,4 @@ +from cqlengine.functions import QueryValue from cqlengine.operators import BaseWhereOperator, InOperator @@ -78,16 +79,27 @@ def update_context(self, ctx): class WhereClause(BaseClause): """ a single where statement used in queries """ - def __init__(self, field, operator, value): + def __init__(self, field, operator, value, quote_field=True): + """ + + :param field: + :param operator: + :param value: + :param quote_field: hack to get the token function rendering properly + :return: + """ if not isinstance(operator, BaseWhereOperator): raise StatementException( "operator must be of type {}, got {}".format(BaseWhereOperator, type(operator)) ) super(WhereClause, self).__init__(field, value) self.operator = operator + self.query_value = self.value if isinstance(self.value, QueryValue) else QueryValue(self.value) + self.quote_field = quote_field def __unicode__(self): - return u'"{}" {} :{}'.format(self.field, self.operator, self.context_id) + field = ('"{}"' if self.quote_field else '{}').format(self.field) + return u'{} {} {}'.format(field, self.operator, unicode(self.query_value)) def __hash__(self): return super(WhereClause, self).__hash__() ^ hash(self.operator) @@ -97,11 +109,18 @@ def __eq__(self, other): return self.operator.__class__ == other.operator.__class__ return False + def get_context_size(self): + return self.query_value.get_context_size() + + def set_context_id(self, i): + super(WhereClause, self).set_context_id(i) + self.query_value.set_context_id(i) + def update_context(self, ctx): if isinstance(self.operator, InOperator): ctx[str(self.context_id)] = InQuoter(self.value) else: - super(WhereClause, self).update_context(ctx) + self.query_value.update_context(ctx) class AssignmentClause(BaseClause): diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index 8f5243fbab..11dec4dded 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -1,10 +1,12 @@ from datetime import datetime -import time +from cqlengine.columns import DateTime from cqlengine.tests.base import BaseCassEngTestCase from cqlengine import columns, Model from cqlengine import functions from cqlengine import query +from cqlengine.statements import WhereClause +from cqlengine.operators import EqualsOperator class TestQuerySetOperation(BaseCassEngTestCase): @@ -13,22 +15,26 @@ def test_maxtimeuuid_function(self): Tests that queries with helper functions are generated properly """ now = datetime.now() - col = columns.DateTime() - col.set_column_name('time') - qry = query.EqualsOperator(col, functions.MaxTimeUUID(now)) + where = WhereClause('time', EqualsOperator(), functions.MaxTimeUUID(now)) + where.set_context_id(5) - assert qry.cql == '"time" = MaxTimeUUID(:{})'.format(qry.value.identifier) + self.assertEqual(str(where), '"time" = MaxTimeUUID(:5)') + ctx = {} + where.update_context(ctx) + self.assertEqual(ctx, {'5': DateTime().to_database(now)}) def test_mintimeuuid_function(self): """ Tests that queries with helper functions are generated properly """ now = datetime.now() - col = columns.DateTime() - col.set_column_name('time') - qry = query.EqualsOperator(col, functions.MinTimeUUID(now)) + where = WhereClause('time', EqualsOperator(), functions.MinTimeUUID(now)) + where.set_context_id(5) - assert qry.cql == '"time" = MinTimeUUID(:{})'.format(qry.value.identifier) + self.assertEqual(str(where), '"time" = MinTimeUUID(:5)') + ctx = {} + where.update_context(ctx) + self.assertEqual(ctx, {'5': DateTime().to_database(now)}) def test_token_function(self): @@ -39,12 +45,16 @@ class TestModel(Model): func = functions.Token('a', 'b') q = TestModel.objects.filter(pk__token__gt=func) - self.assertEquals(q._where[0].cql, 'token("p1", "p2") > token(:{}, :{})'.format(*func.identifier)) + where = q._where[0] + where.set_context_id(1) + self.assertEquals(str(where), 'token("p1", "p2") > token(:{}, :{})'.format(1, 2)) - # Token(tuple()) is also possible for convinience + # Token(tuple()) is also possible for convenience # it (allows for Token(obj.pk) syntax) func = functions.Token(('a', 'b')) q = TestModel.objects.filter(pk__token__gt=func) - self.assertEquals(q._where[0].cql, 'token("p1", "p2") > token(:{}, :{})'.format(*func.identifier)) + where = q._where[0] + where.set_context_id(1) + self.assertEquals(str(where), 'token("p1", "p2") > token(:{}, :{})'.format(1, 2)) From b9fcb20ef37ad37518d436db26204d28bba37966 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 12:00:54 -0800 Subject: [PATCH 0520/3961] removing ttl statement method --- cqlengine/query.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 0418883fef..90805820d9 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -738,11 +738,6 @@ def _get_ordering_condition(self, colname): return column.db_field_name, order_type - def _get_ttl_statement(self): - if not self._ttl: - return "" - return "USING TTL {}".format(self._ttl) - def values_list(self, *fields, **kwargs): """ Instructs the query set to return tuples, not model instance """ flat = kwargs.pop('flat', False) From dcdc555422d88e538a078b6f4e7a74993a102bac Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 12:19:00 -0800 Subject: [PATCH 0521/3961] moving delete query to delete statement --- cqlengine/query.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 90805820d9..c5e36618a3 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1003,21 +1003,20 @@ def delete(self): """ Deletes one instance """ if self.instance is None: raise CQLEngineException("DML Query intance attribute is None") - field_values = {} - qs = ['DELETE FROM {}'.format(self.column_family_name)] - qs += ['WHERE'] - where_statements = [] - for name, col in self.model._primary_keys.items(): - field_id = uuid4().hex - field_values[field_id] = col.to_database(getattr(self.instance, name)) - where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] - - qs += [' AND '.join(where_statements)] - qs = ' '.join(qs) + ds = DeleteStatement(self.column_family_name) + for name, col in self.model._primary_keys.items(): + ds.add_where_clause(WhereClause( + col.db_field_name, + EqualsOperator(), + col.to_database(getattr(self.instance, name)) + )) + + qs = str(ds) + ctx = ds.get_context() if self._batch: - self._batch.add_query(qs, field_values) + self._batch.add_query(qs, ctx) else: - execute(qs, field_values, self._consistency) + execute(qs, ctx, self._consistency) From 25e787a82cb18882993ae2812f1f7b40051eb7bc Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 13:34:35 -0800 Subject: [PATCH 0522/3961] moving save insert to insert statement --- cqlengine/query.py | 38 ++++++++++++++++++++------------------ cqlengine/statements.py | 7 +++++++ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index c5e36618a3..35e58459b4 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement class QueryException(CQLEngineException): pass @@ -938,6 +938,7 @@ def update(self): self._delete_null_columns() + # TODO: delete def _get_query_values(self): """ returns all the data needed to do queries @@ -970,31 +971,32 @@ def save(self): raise CQLEngineException("DML Query intance attribute is None") assert type(self.instance) == self.model - values, field_names, field_ids, field_values, query_values = self._get_query_values() - - qs = [] + nulled_fields = set() if self.instance._has_counter or self.instance._can_update(): return self.update() else: - qs += ["INSERT INTO {}".format(self.column_family_name)] - qs += ["({})".format(', '.join(['"{}"'.format(f) for f in field_names]))] - qs += ['VALUES'] - qs += ["({})".format(', '.join([':'+field_ids[f] for f in field_names]))] - - if self._ttl: - qs += ["USING TTL {}".format(self._ttl)] - - qs += [] - qs = ' '.join(qs) - + insert = InsertStatement(self.column_family_name, ttl=self._ttl) + for name, col in self.instance._columns.items(): + val = getattr(self.instance, name, None) + if col._val_is_null(val): + if self.instance._values[name].changed: + nulled_fields.add(col.db_field_name) + continue + insert.add_assignment_clause(AssignmentClause( + col.db_field_name, + col.to_database(getattr(self.instance, name, None)) + )) # skip query execution if it's empty # caused by pointless update queries - if qs: + if not insert.is_empty: + qs = str(insert) + ctx = insert.get_context() + if self._batch: - self._batch.add_query(qs, query_values) + self._batch.add_query(qs, ctx) else: - execute(qs, query_values, self._consistency) + execute(qs, ctx, self._consistency) # delete any nulled columns self._delete_null_columns() diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 834e647924..ce8de5836b 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -414,6 +414,9 @@ def __unicode__(self): def __str__(self): return unicode(self).encode('utf-8') + def __repr__(self): + return self.__unicode__() + @property def _where(self): return 'WHERE {}'.format(' AND '.join([unicode(c) for c in self.where_clauses])) @@ -500,6 +503,10 @@ def add_assignment_clause(self, clause): self.context_counter += clause.get_context_size() self.assignments.append(clause) + @property + def is_empty(self): + return len(self.assignments) == 0 + def get_context(self): ctx = super(AssignmentStatement, self).get_context() for clause in self.assignments: From 042d0aee0e21d1c80ec9a61c83dce11de874b501 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 15:24:37 -0800 Subject: [PATCH 0523/3961] adding BaseCQLStatemtent support to query execution --- cqlengine/connection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 6fec0d3d64..6e08a3580f 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -17,6 +17,7 @@ from contextlib import contextmanager from thrift.transport.TTransport import TTransportException +from cqlengine.statements import BaseCQLStatement LOG = logging.getLogger('cqlengine.cql') @@ -230,6 +231,9 @@ def execute(self, query, params, consistency_level=None): def execute(query, params=None, consistency_level=None): + if isinstance(query, BaseCQLStatement): + params = query.get_context() + query = str(query) params = params or {} if consistency_level is None: consistency_level = connection_pool._consistency From 8767fb18cacc1d33930c8281986db5964acb67d8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 15:24:56 -0800 Subject: [PATCH 0524/3961] encapsulating raw or batch query execution into a querset method --- cqlengine/query.py | 58 ++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 35e58459b4..55588546dc 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement, BaseCQLStatement class QueryException(CQLEngineException): pass @@ -229,7 +229,10 @@ def __init__(self, batch_type=None, timestamp=None, consistency=None): self._consistency = consistency def add_query(self, query, params): - self.queries.append((query, params)) + if not isinstance(query, BaseCQLStatement): + raise CQLEngineException('only BaseCQLStatements can be added to a batch query') + # TODO: modify query's context id starting point + self.queries.append((str(query), query.get_context())) def consistency(self, consistency): self._consistency = consistency @@ -305,6 +308,12 @@ def __init__(self, model): def column_family_name(self): return self.model.column_family_name() + def _execute(self, q, params=None): + if self._batch: + return self._batch.add_query(q, params=params) + else: + return execute(q, params=params, consistency_level=self._consistency) + def __unicode__(self): return self._select_query() @@ -364,8 +373,7 @@ def _execute_query(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: - query = self._select_query() - columns, self._result_cache = execute(unicode(query).encode('utf-8'), query.get_context(), self._consistency) + columns, self._result_cache = self._execute(self._select_query()) self._construct_result = self._get_result_constructor(columns) def _fill_result_cache_to_idx(self, idx): @@ -561,7 +569,7 @@ def count(self): if self._result_cache is None: query = self._select_query() query.count = True - _, result = execute(str(query), query.get_context()) + _, result = self._execute(query) return result[0][0] else: return len(self._result_cache) @@ -637,11 +645,7 @@ def delete(self): self.column_family_name, where=self._where ) - - if self._batch: - self._batch.add_query(str(dq), dq.get_context()) - else: - execute(str(dq), dq.get_context()) + self._execute(dq) def __eq__(self, q): if len(self._where) == len(q._where): @@ -825,6 +829,12 @@ def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None) self._ttl = ttl self._consistency = consistency + def _execute(self, q, params=None): + if self._batch: + return self._batch.add_query(q, params=params) + else: + return execute(q, params=params, consistency_level=self._consistency) + def batch(self, batch_obj): if batch_obj is not None and not isinstance(batch_obj, BatchQuery): raise CQLEngineException('batch_obj must be a BatchQuery instance or None') @@ -864,10 +874,7 @@ def _delete_null_columns(self): qs = ' '.join(qs) - if self._batch: - self._batch.add_query(qs, query_values) - else: - execute(qs, query_values) + self._execute(qs, query_values) def update(self): """ @@ -931,10 +938,7 @@ def update(self): # skip query execution if it's empty # caused by pointless update queries if qs: - if self._batch: - self._batch.add_query(qs, query_values) - else: - execute(qs, query_values, consistency_level=self._consistency) + self._execute(qs, query_values) self._delete_null_columns() @@ -990,13 +994,7 @@ def save(self): # skip query execution if it's empty # caused by pointless update queries if not insert.is_empty: - qs = str(insert) - ctx = insert.get_context() - - if self._batch: - self._batch.add_query(qs, ctx) - else: - execute(qs, ctx, self._consistency) + self._execute(insert) # delete any nulled columns self._delete_null_columns() @@ -1004,7 +1002,7 @@ def save(self): def delete(self): """ Deletes one instance """ if self.instance is None: - raise CQLEngineException("DML Query intance attribute is None") + raise CQLEngineException("DML Query instance attribute is None") ds = DeleteStatement(self.column_family_name) for name, col in self.model._primary_keys.items(): @@ -1013,12 +1011,6 @@ def delete(self): EqualsOperator(), col.to_database(getattr(self.instance, name)) )) - - qs = str(ds) - ctx = ds.get_context() - if self._batch: - self._batch.add_query(qs, ctx) - else: - execute(qs, ctx, self._consistency) + self._execute(ds) From 095ef4d89a2bf273f1675756607ecfd43bb3f619 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 15:48:19 -0800 Subject: [PATCH 0525/3961] moving delete null columns to delete statement --- cqlengine/query.py | 61 ++++++++++++++--------------------------- cqlengine/statements.py | 8 ++++++ 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 55588546dc..c7df62cb65 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement, BaseCQLStatement +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement, BaseCQLStatement, MapUpdateClause, MapDeleteClause class QueryException(CQLEngineException): pass @@ -310,9 +310,9 @@ def column_family_name(self): def _execute(self, q, params=None): if self._batch: - return self._batch.add_query(q, params=params) + return self._batch.add_query(q) else: - return execute(q, params=params, consistency_level=self._consistency) + return execute(q, consistency_level=self._consistency) def __unicode__(self): return self._select_query() @@ -793,21 +793,11 @@ def update(self, **values): us.add_assignment_clause(AssignmentClause(name, col.to_database(val))) if us.assignments: - qs = str(us) - ctx = us.get_context() - if self._batch: - self._batch.add_query(qs, ctx) - else: - execute(qs, ctx, self._consistency) + self._execute(us) if nulled_columns: ds = DeleteStatement(self.column_family_name, fields=nulled_columns, where=self._where) - qs = str(ds) - ctx = ds.get_context() - if self._batch: - self._batch.add_query(qs, ctx) - else: - execute(qs, ctx, self._consistency) + self._execute(ds) class DMLQuery(object): @@ -845,36 +835,27 @@ def _delete_null_columns(self): """ executes a delete query to remove columns that have changed to null """ - values, field_names, field_ids, field_values, query_values = self._get_query_values() - - # delete nulled columns and removed map keys - qs = ['DELETE'] - query_values = {} - - del_statements = [] - for k,v in self.instance._values.items(): + ds = DeleteStatement(self.column_family_name) + deleted_fields = False + for _, v in self.instance._values.items(): col = v.column if v.deleted: - del_statements += ['"{}"'.format(col.db_field_name)] + ds.add_field(col.db_field_name) + deleted_fields = True elif isinstance(col, Map): - del_statements += col.get_delete_statement(v.value, v.previous_value, query_values) + uc = MapDeleteClause(col.db_field_name, v.value, v.previous_value) + if uc.get_context_size() > 0: + ds.add_field(uc) + deleted_fields = True - if del_statements: - qs += [', '.join(del_statements)] - - qs += ['FROM {}'.format(self.column_family_name)] - - qs += ['WHERE'] - where_statements = [] + if deleted_fields: for name, col in self.model._primary_keys.items(): - field_id = uuid4().hex - query_values[field_id] = field_values[name] - where_statements += ['"{}" = :{}'.format(col.db_field_name, field_id)] - qs += [' AND '.join(where_statements)] - - qs = ' '.join(qs) - - self._execute(qs, query_values) + ds.add_where_clause(WhereClause( + col.db_field_name, + EqualsOperator(), + col.to_database(getattr(self.instance, name)) + )) + self._execute(ds) def update(self): """ diff --git a/cqlengine/statements.py b/cqlengine/statements.py index ce8de5836b..4156c9ac6e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -569,11 +569,19 @@ def __init__(self, table, fields=None, consistency=None, where=None): for field in fields or []: self.add_field(field) + def get_context(self): + ctx = super(DeleteStatement, self).get_context() + for field in self.fields: + field.update_context(ctx) + return ctx + def add_field(self, field): if isinstance(field, basestring): field = FieldDeleteClause(field) if not isinstance(field, BaseClause): raise StatementException("only instances of AssignmentClause can be added to statements") + field.set_context_id(self.context_counter) + self.context_counter += field.get_context_size() self.fields.append(field) def __unicode__(self): From 845a52a08196f472663d5bd8432056daaf1eb5ed Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:03:50 -0800 Subject: [PATCH 0526/3961] moving update query to update statement --- cqlengine/query.py | 95 ++++++++++++++--------------------------- cqlengine/statements.py | 35 +++++++++------ 2 files changed, 53 insertions(+), 77 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index c7df62cb65..f5c15e3a23 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -5,7 +5,7 @@ from time import time from uuid import uuid1 from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns -from cqlengine.columns import Counter +from cqlengine.columns import Counter, List, Set from cqlengine.connection import connection_manager, execute, RowResult @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement, BaseCQLStatement, MapUpdateClause, MapDeleteClause +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement, BaseCQLStatement, MapUpdateClause, MapDeleteClause, ListUpdateClause, SetUpdateClause class QueryException(CQLEngineException): pass @@ -868,83 +868,52 @@ def update(self): raise CQLEngineException("DML Query intance attribute is None") assert type(self.instance) == self.model - values, field_names, field_ids, field_values, query_values = self._get_query_values() - - qs = [] - qs += ["UPDATE {}".format(self.column_family_name)] - qs += ["SET"] - - set_statements = [] + statement = UpdateStatement(self.column_family_name, ttl=self._ttl) #get defined fields and their column names for name, col in self.model._columns.items(): if not col.is_primary_key: - val = values.get(name) + val = getattr(self.instance, name, None) + val_mgr = self.instance._values[name] # don't update something that is null if val is None: continue # don't update something if it hasn't changed - if not self.instance._values[name].changed and not isinstance(col, Counter): + if not val_mgr.changed and not isinstance(col, Counter): continue - # add the update statements - if isinstance(col, (BaseContainerColumn, Counter)): - #remove value from query values, the column will handle it - query_values.pop(field_ids.get(name), None) - - val_mgr = self.instance._values[name] - set_statements += col.get_update_statement(val, val_mgr.previous_value, query_values) - + if isinstance(col, List): + clause = ListUpdateClause(col.db_field_name, val, val_mgr.previous_value, column=col) + if clause.get_context_size() > 0: + statement.add_assignment_clause(clause) + elif isinstance(col, Map): + clause = MapUpdateClause(col.db_field_name, val, val_mgr.previous_value, column=col) + if clause.get_context_size() > 0: + statement.add_assignment_clause(clause) + elif isinstance(col, Set): + clause = SetUpdateClause(col.db_field_name, val, val_mgr.previous_value, column=col) + if clause.get_context_size() > 0: + statement.add_assignment_clause(clause) + elif isinstance(col, Counter): + raise NotImplementedError else: - set_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] - qs += [', '.join(set_statements)] - - qs += ['WHERE'] + statement.add_assignment_clause(AssignmentClause( + col.db_field_name, + col.to_database(val) + )) - where_statements = [] - for name, col in self.model._primary_keys.items(): - where_statements += ['"{}" = :{}'.format(col.db_field_name, field_ids[col.db_field_name])] - - qs += [' AND '.join(where_statements)] - - if self._ttl: - qs += ["USING TTL {}".format(self._ttl)] - - # clear the qs if there are no set statements and this is not a counter model - if not set_statements and not self.instance._has_counter: - qs = [] - - qs = ' '.join(qs) - # skip query execution if it's empty - # caused by pointless update queries - if qs: - self._execute(qs, query_values) + if statement.get_context_size() > 0 or self.instance._has_counter: + for name, col in self.model._primary_keys.items(): + statement.add_where_clause(WhereClause( + col.db_field_name, + EqualsOperator(), + col.to_database(getattr(self.instance, name)) + )) + self._execute(statement) self._delete_null_columns() - # TODO: delete - def _get_query_values(self): - """ - returns all the data needed to do queries - """ - #organize data - value_pairs = [] - values = self.instance._as_dict() - - #get defined fields and their column names - for name, col in self.model._columns.items(): - val = values.get(name) - if col._val_is_null(val): continue - value_pairs += [(col.db_field_name, val)] - - #construct query string - field_names = zip(*value_pairs)[0] - field_ids = {n:uuid4().hex for n in field_names} - field_values = dict(value_pairs) - query_values = {field_ids[n]:field_values[n] for n in field_names} - return values, field_names, field_ids, field_values, query_values - def save(self): """ Creates / updates a row. diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 4156c9ac6e..ba13ea0b11 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -135,11 +135,15 @@ def insert_tuple(self): class ContainerUpdateClause(AssignmentClause): - def __init__(self, field, value, previous=None): + def __init__(self, field, value, previous=None, column=None): super(ContainerUpdateClause, self).__init__(field, value) self.previous = previous self._assignments = None self._analyzed = False + self._column = column + + def _to_database(self, val): + return self._column.to_database(val) if self._column else val def _analyze(self): raise NotImplementedError @@ -154,8 +158,8 @@ def update_context(self, ctx): class SetUpdateClause(ContainerUpdateClause): """ updates a set collection """ - def __init__(self, field, value, previous=None): - super(SetUpdateClause, self).__init__(field, value, previous) + def __init__(self, field, value, previous=None, column=None): + super(SetUpdateClause, self).__init__(field, value, previous, column=column) self._additions = None self._removals = None @@ -193,20 +197,20 @@ def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id if self._assignments: - ctx[str(ctx_id)] = self._assignments + ctx[str(ctx_id)] = self._to_database(self._assignments) ctx_id += 1 if self._additions: - ctx[str(ctx_id)] = self._additions + ctx[str(ctx_id)] = self._to_database(self._additions) ctx_id += 1 if self._removals: - ctx[str(ctx_id)] = self._removals + ctx[str(ctx_id)] = self._to_database(self._removals) class ListUpdateClause(ContainerUpdateClause): """ updates a list collection """ - def __init__(self, field, value, previous=None): - super(ListUpdateClause, self).__init__(field, value, previous) + def __init__(self, field, value, previous=None, column=None): + super(ListUpdateClause, self).__init__(field, value, previous, column=column) self._append = None self._prepend = None @@ -235,16 +239,16 @@ def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id if self._assignments: - ctx[str(ctx_id)] = self._assignments + ctx[str(ctx_id)] = self._to_database(self._assignments) ctx_id += 1 if self._prepend: # CQL seems to prepend element at a time, starting # with the element at idx 0, we can either reverse # it here, or have it inserted in reverse - ctx[str(ctx_id)] = list(reversed(self._prepend)) + ctx[str(ctx_id)] = self._to_database(list(reversed(self._prepend))) ctx_id += 1 if self._append: - ctx[str(ctx_id)] = self._append + ctx[str(ctx_id)] = self._to_database(self._append) def _analyze(self): """ works out the updates to be performed """ @@ -292,8 +296,8 @@ def _analyze(self): class MapUpdateClause(ContainerUpdateClause): """ updates a map collection """ - def __init__(self, field, value, previous=None): - super(MapUpdateClause, self).__init__(field, value, previous) + def __init__(self, field, value, previous=None, column=None): + super(MapUpdateClause, self).__init__(field, value, previous, column=column) self._updates = None self.previous = self.previous or {} @@ -310,7 +314,7 @@ def update_context(self, ctx): ctx_id = self.context_id for key in self._updates or []: ctx[str(ctx_id)] = key - ctx[str(ctx_id + 1)] = self.value.get(key) + ctx[str(ctx_id + 1)] = self._to_database(self.value.get(key)) ctx_id += 2 def __unicode__(self): @@ -408,6 +412,9 @@ def get_context(self): clause.update_context(ctx) return ctx + def get_context_size(self): + return len(self.get_context()) + def __unicode__(self): raise NotImplementedError From e3e7c18376cac94b43fb8278cebeabe751b612a1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:12:54 -0800 Subject: [PATCH 0527/3961] adding counter column update clause and supporting unit tests --- cqlengine/statements.py | 19 +++++++++++ .../statements/test_assignment_clauses.py | 33 ++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index ba13ea0b11..c1de6414e2 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -329,6 +329,25 @@ def __unicode__(self): return ', '.join(qs) +class CounterUpdateClause(ContainerUpdateClause): + + def __init__(self, field, value, previous=None, column=None): + super(CounterUpdateClause, self).__init__(field, value, previous, column) + self.previous = self.previous or 0 + + def get_context_size(self): + return 1 if self.value != self.previous else 0 + + def update_context(self, ctx): + if self.value != self.previous: + ctx[str(self.context_id)] = self._to_database(abs(self.value - self.previous)) + + def __unicode__(self): + delta = self.value - self.previous + sign = '-' if delta < 0 else '+' + return '"{0}" = "{0}" {1} :{2}'.format(self.field, sign, self.context_id) + + class BaseDeleteClause(BaseClause): pass diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 03d2a5ab0f..1b2e430e9a 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -1,5 +1,5 @@ from unittest import TestCase -from cqlengine.statements import AssignmentClause, SetUpdateClause, ListUpdateClause, MapUpdateClause, MapDeleteClause, FieldDeleteClause +from cqlengine.statements import AssignmentClause, SetUpdateClause, ListUpdateClause, MapUpdateClause, MapDeleteClause, FieldDeleteClause, CounterUpdateClause class AssignmentClauseTests(TestCase): @@ -268,6 +268,37 @@ def test_nulled_columns_arent_included(self): self.assertNotIn(1, c._updates) +class CounterUpdateTests(TestCase): + + def test_positive_update(self): + c = CounterUpdateClause('a', 5, 3) + c.set_context_id(5) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"a" = "a" + :5') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'5': 2}) + + def test_negative_update(self): + c = CounterUpdateClause('a', 4, 7) + c.set_context_id(3) + + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"a" = "a" - :3') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'3': 3}) + + def noop_update(self): + c = CounterUpdateClause('a', 5, 5) + c.set_context_id(5) + + self.assertEqual(c.get_context_size(), 0) + + class MapDeleteTests(TestCase): def test_update(self): From e66eecf28b1e1cd91f16f3c74fff1da5c8036179 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:16:27 -0800 Subject: [PATCH 0528/3961] adding counter column support to update and condensing the logic --- cqlengine/query.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index f5c15e3a23..b924cae6c2 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -16,7 +16,7 @@ #http://www.datastax.com/docs/1.1/references/cql/index from cqlengine.operators import InOperator, EqualsOperator, GreaterThanOperator, GreaterThanOrEqualOperator from cqlengine.operators import LessThanOperator, LessThanOrEqualOperator, BaseWhereOperator -from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement, BaseCQLStatement, MapUpdateClause, MapDeleteClause, ListUpdateClause, SetUpdateClause +from cqlengine.statements import WhereClause, SelectStatement, DeleteStatement, UpdateStatement, AssignmentClause, InsertStatement, BaseCQLStatement, MapUpdateClause, MapDeleteClause, ListUpdateClause, SetUpdateClause, CounterUpdateClause class QueryException(CQLEngineException): pass @@ -883,20 +883,18 @@ def update(self): if not val_mgr.changed and not isinstance(col, Counter): continue - if isinstance(col, List): - clause = ListUpdateClause(col.db_field_name, val, val_mgr.previous_value, column=col) - if clause.get_context_size() > 0: - statement.add_assignment_clause(clause) - elif isinstance(col, Map): - clause = MapUpdateClause(col.db_field_name, val, val_mgr.previous_value, column=col) - if clause.get_context_size() > 0: - statement.add_assignment_clause(clause) - elif isinstance(col, Set): - clause = SetUpdateClause(col.db_field_name, val, val_mgr.previous_value, column=col) + if isinstance(col, (BaseContainerColumn, Counter)): + # get appropriate clause + if isinstance(col, List): klass = ListUpdateClause + elif isinstance(col, Map): klass = MapUpdateClause + elif isinstance(col, Set): klass = SetUpdateClause + elif isinstance(col, Counter): klass = CounterUpdateClause + else: raise RuntimeError + + # do the stuff + clause = klass(col.db_field_name, val, val_mgr.previous_value, column=col) if clause.get_context_size() > 0: statement.add_assignment_clause(clause) - elif isinstance(col, Counter): - raise NotImplementedError else: statement.add_assignment_clause(AssignmentClause( col.db_field_name, From 7a4e2be45ef59d98407d22b9686c3e871f5ddca3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:23:39 -0800 Subject: [PATCH 0529/3961] fixing map context updates --- cqlengine/statements.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index c1de6414e2..cfa3eb5fbf 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -313,8 +313,9 @@ def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id for key in self._updates or []: - ctx[str(ctx_id)] = key - ctx[str(ctx_id + 1)] = self._to_database(self.value.get(key)) + val = self.value.get(key) + ctx[str(ctx_id)] = self._column.key_col.to_database(key) if self._column else key + ctx[str(ctx_id + 1)] = self._column.value_col.to_database(val) if self._column else val ctx_id += 2 def __unicode__(self): From b41a1d6d63cc63bfbb537959cfa8476f98d91097 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:31:26 -0800 Subject: [PATCH 0530/3961] getting noop counter column updates working properly --- cqlengine/statements.py | 5 ++--- cqlengine/tests/statements/test_assignment_clauses.py | 7 ++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index cfa3eb5fbf..650d237b7c 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -337,11 +337,10 @@ def __init__(self, field, value, previous=None, column=None): self.previous = self.previous or 0 def get_context_size(self): - return 1 if self.value != self.previous else 0 + return 1 def update_context(self, ctx): - if self.value != self.previous: - ctx[str(self.context_id)] = self._to_database(abs(self.value - self.previous)) + ctx[str(self.context_id)] = self._to_database(abs(self.value - self.previous)) def __unicode__(self): delta = self.value - self.previous diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 1b2e430e9a..f7fbda38fb 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -296,7 +296,12 @@ def noop_update(self): c = CounterUpdateClause('a', 5, 5) c.set_context_id(5) - self.assertEqual(c.get_context_size(), 0) + self.assertEqual(c.get_context_size(), 1) + self.assertEqual(str(c), '"a" = "a" + :5') + + ctx = {} + c.update_context(ctx) + self.assertEqual(ctx, {'5': 0}) class MapDeleteTests(TestCase): From e7107c3c0c656139e0cc01b3d3fd7ed38e9ec07b Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:47:56 -0800 Subject: [PATCH 0531/3961] adding support for changing the root context id of a statement --- cqlengine/statements.py | 22 ++++++++++++++++++- .../tests/statements/test_delete_statement.py | 11 +++++++++- .../tests/statements/test_insert_statement.py | 13 +++++++++++ .../tests/statements/test_select_statement.py | 11 ++++++++++ .../tests/statements/test_update_statement.py | 9 ++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 650d237b7c..dd367a0f88 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -403,7 +403,8 @@ def __init__(self, table, consistency=None, where=None): super(BaseCQLStatement, self).__init__() self.table = table self.consistency = consistency - self.context_counter = 0 + self.context_id = 0 + self.context_counter = self.context_id self.where_clauses = [] for clause in where or []: @@ -434,6 +435,13 @@ def get_context(self): def get_context_size(self): return len(self.get_context()) + def update_context_id(self, i): + self.context_id = i + self.context_counter = self.context_id + for clause in self.where_clauses: + clause.set_context_id(self.context_counter) + self.context_counter += clause.get_context_size() + def __unicode__(self): raise NotImplementedError @@ -517,6 +525,12 @@ def __init__(self, for assignment in assignments or []: self.add_assignment_clause(assignment) + def update_context_id(self, i): + super(AssignmentStatement, self).update_context_id(i) + for assignment in self.assignments: + assignment.set_context_id(self.context_counter) + self.context_counter += assignment.get_context_size() + def add_assignment_clause(self, clause): """ adds an assignment clause to this statement @@ -595,6 +609,12 @@ def __init__(self, table, fields=None, consistency=None, where=None): for field in fields or []: self.add_field(field) + def update_context_id(self, i): + super(DeleteStatement, self).update_context_id(i) + for field in self.fields: + field.set_context_id(self.context_counter) + self.context_counter += field.get_context_size() + def get_context(self): ctx = super(DeleteStatement, self).get_context() for field in self.fields: diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py index b1055ea4fb..9ac9554383 100644 --- a/cqlengine/tests/statements/test_delete_statement.py +++ b/cqlengine/tests/statements/test_delete_statement.py @@ -1,5 +1,5 @@ from unittest import TestCase -from cqlengine.statements import DeleteStatement, WhereClause +from cqlengine.statements import DeleteStatement, WhereClause, MapDeleteClause from cqlengine.operators import * @@ -33,6 +33,15 @@ def test_where_clause_rendering(self): ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(unicode(ds), 'DELETE FROM table WHERE "a" = :0', unicode(ds)) + def test_context_update(self): + ds = DeleteStatement('table', None) + ds.add_field(MapDeleteClause('d', {1: 2}, {1:2, 3: 4})) + ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) + + ds.update_context_id(7) + self.assertEqual(unicode(ds), 'DELETE "d"[:8] FROM table WHERE "a" = :7') + self.assertEqual(ds.get_context(), {'7': 'b', '8': 3}) + def test_context(self): ds = DeleteStatement('table', None) ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py index 51280a314f..5e22d7d99e 100644 --- a/cqlengine/tests/statements/test_insert_statement.py +++ b/cqlengine/tests/statements/test_insert_statement.py @@ -20,6 +20,19 @@ def test_statement(self): 'INSERT INTO table ("a", "c") VALUES (:0, :1)' ) + def test_context_update(self): + ist = InsertStatement('table', None) + ist.add_assignment_clause(AssignmentClause('a', 'b')) + ist.add_assignment_clause(AssignmentClause('c', 'd')) + + ist.update_context_id(4) + self.assertEqual( + unicode(ist), + 'INSERT INTO table ("a", "c") VALUES (:4, :5)' + ) + ctx = ist.get_context() + self.assertEqual(ctx, {'4': 'b', '5': 'd'}) + def test_additional_rendering(self): ist = InsertStatement('table', ttl=60) ist.add_assignment_clause(AssignmentClause('a', 'b')) diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index d59c8eb77c..f358c6ae5a 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -44,6 +44,17 @@ def test_context(self): ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(ss.get_context(), {'0': 'b'}) + def test_context_id_update(self): + """ tests that the right things happen the the context id """ + ss = SelectStatement('table') + ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) + self.assertEqual(ss.get_context(), {'0': 'b'}) + self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = :0') + + ss.update_context_id(5) + self.assertEqual(ss.get_context(), {'5': 'b'}) + self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = :5') + def test_additional_rendering(self): ss = SelectStatement( 'table', diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py index 0945d2341e..86dbc8be44 100644 --- a/cqlengine/tests/statements/test_update_statement.py +++ b/cqlengine/tests/statements/test_update_statement.py @@ -25,6 +25,15 @@ def test_context(self): us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) self.assertEqual(us.get_context(), {'0': 'b', '1': 'd', '2': 'x'}) + def test_context_update(self): + us = UpdateStatement('table') + us.add_assignment_clause(AssignmentClause('a', 'b')) + us.add_assignment_clause(AssignmentClause('c', 'd')) + us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) + us.update_context_id(3) + self.assertEqual(unicode(us), 'UPDATE table SET "a" = :4, "c" = :5 WHERE "a" = :3') + self.assertEqual(us.get_context(), {'4': 'b', '5': 'd', '3': 'x'}) + def test_additional_rendering(self): us = UpdateStatement('table', ttl=60) us.add_assignment_clause(AssignmentClause('a', 'b')) From fb502a023fd0f348e67ecadbaff29fd675fa3b5e Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:51:26 -0800 Subject: [PATCH 0532/3961] removing parameters from batch and execute methods --- cqlengine/query.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index b924cae6c2..04379bb0b9 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -228,7 +228,7 @@ def __init__(self, batch_type=None, timestamp=None, consistency=None): self.timestamp = timestamp self._consistency = consistency - def add_query(self, query, params): + def add_query(self, query): if not isinstance(query, BaseCQLStatement): raise CQLEngineException('only BaseCQLStatements can be added to a batch query') # TODO: modify query's context id starting point @@ -308,7 +308,7 @@ def __init__(self, model): def column_family_name(self): return self.model.column_family_name() - def _execute(self, q, params=None): + def _execute(self, q): if self._batch: return self._batch.add_query(q) else: @@ -819,11 +819,11 @@ def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None) self._ttl = ttl self._consistency = consistency - def _execute(self, q, params=None): + def _execute(self, q): if self._batch: - return self._batch.add_query(q, params=params) + return self._batch.add_query(q) else: - return execute(q, params=params, consistency_level=self._consistency) + return execute(q, consistency_level=self._consistency) def batch(self, batch_obj): if batch_obj is not None and not isinstance(batch_obj, BatchQuery): From 5a976717b5446ea797cd77920f396ab039f3447d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:54:39 -0800 Subject: [PATCH 0533/3961] reworking batch to work with the new statement objects --- cqlengine/query.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 04379bb0b9..273f338877 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -231,8 +231,7 @@ def __init__(self, batch_type=None, timestamp=None, consistency=None): def add_query(self, query): if not isinstance(query, BaseCQLStatement): raise CQLEngineException('only BaseCQLStatements can be added to a batch query') - # TODO: modify query's context id starting point - self.queries.append((str(query), query.get_context())) + self.queries.append(query) def consistency(self, consistency): self._consistency = consistency @@ -250,9 +249,13 @@ def execute(self): query_list = [opener] parameters = {} - for query, params in self.queries: - query_list.append(' ' + query) - parameters.update(params) + ctx_counter = 0 + for query in self.queries: + query.update_context_id(ctx_counter) + ctx = query.get_context() + ctx_counter += len(ctx) + query_list.append(' ' + str(query)) + parameters.update(ctx) query_list.append('APPLY BATCH;') From fe6359e4d0bf95d7b75f11bf832c1f061083b5df Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Sun, 3 Nov 2013 16:57:03 -0800 Subject: [PATCH 0534/3961] removing old code --- cqlengine/query.py | 158 ++------------------------------------------- 1 file changed, 5 insertions(+), 153 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 273f338877..04afff145e 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,16 +1,12 @@ import copy from datetime import datetime -from uuid import uuid4 -from hashlib import md5 -from time import time -from uuid import uuid1 -from cqlengine import BaseContainerColumn, BaseValueManager, Map, columns +from cqlengine import BaseContainerColumn, Map, columns from cqlengine.columns import Counter, List, Set -from cqlengine.connection import connection_manager, execute, RowResult +from cqlengine.connection import execute, RowResult from cqlengine.exceptions import CQLEngineException, ValidationError -from cqlengine.functions import QueryValue, Token, BaseQueryFunction +from cqlengine.functions import Token, BaseQueryFunction #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -24,150 +20,6 @@ class DoesNotExist(QueryException): pass class MultipleObjectsReturned(QueryException): pass -# class QueryOperatorException(QueryException): pass -# -# -# class QueryOperator(object): -# # The symbol that identifies this operator in filter kwargs -# # ie: colname__ -# symbol = None -# -# # The comparator symbol this operator uses in cql -# cql_symbol = None -# -# QUERY_VALUE_WRAPPER = QueryValue -# -# def __init__(self, column, value): -# self.column = column -# self.value = value -# -# if isinstance(value, QueryValue): -# self.query_value = value -# else: -# self.query_value = self.QUERY_VALUE_WRAPPER(value) -# -# #perform validation on this operator -# self.validate_operator() -# self.validate_value() -# -# @property -# def cql(self): -# """ -# Returns this operator's portion of the WHERE clause -# """ -# return '{} {} {}'.format(self.column.cql, self.cql_symbol, self.query_value.cql) -# -# def validate_operator(self): -# """ -# Checks that this operator can be used on the column provided -# """ -# if self.symbol is None: -# raise QueryOperatorException( -# "{} is not a valid operator, use one with 'symbol' defined".format( -# self.__class__.__name__ -# ) -# ) -# if self.cql_symbol is None: -# raise QueryOperatorException( -# "{} is not a valid operator, use one with 'cql_symbol' defined".format( -# self.__class__.__name__ -# ) -# ) -# -# def validate_value(self): -# """ -# Checks that the compare value works with this operator -# -# Doesn't do anything by default -# """ -# pass -# -# def get_dict(self): -# """ -# Returns this operators contribution to the cql.query arg dictionanry -# -# ie: if this column's name is colname, and the identifier is colval, -# this should return the dict: {'colval':} -# SELECT * FROM column_family WHERE colname=:colval -# """ -# return self.query_value.get_dict(self.column) -# -# @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 -# _recurse(QueryOperator) -# try: -# return QueryOperator.opmap[symbol.upper()] -# except KeyError: -# raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol)) -# -# # equality operator, used by tests -# -# def __eq__(self, op): -# return self.__class__ is op.__class__ and \ -# self.column.db_field_name == op.column.db_field_name and \ -# self.value == op.value -# -# def __ne__(self, op): -# return not (self == op) -# -# def __hash__(self): -# return hash(self.column.db_field_name) ^ hash(self.value) -# -# -# class EqualsOperator(QueryOperator): -# symbol = 'EQ' -# cql_symbol = '=' -# -# -# class IterableQueryValue(QueryValue): -# def __init__(self, value): -# try: -# super(IterableQueryValue, self).__init__(value, [uuid4().hex for i in value]) -# except TypeError: -# raise QueryException("in operator arguments must be iterable, {} found".format(value)) -# -# def get_dict(self, column): -# return dict((i, column.to_database(v)) for (i, v) in zip(self.identifier, self.value)) -# -# def get_cql(self): -# return '({})'.format(', '.join(':{}'.format(i) for i in self.identifier)) -# -# -# class InOperator(EqualsOperator): -# symbol = 'IN' -# cql_symbol = 'IN' -# -# QUERY_VALUE_WRAPPER = IterableQueryValue -# -# -# class GreaterThanOperator(QueryOperator): -# symbol = "GT" -# cql_symbol = '>' -# -# -# class GreaterThanOrEqualOperator(QueryOperator): -# symbol = "GTE" -# cql_symbol = '>=' -# -# -# class LessThanOperator(QueryOperator): -# symbol = "LT" -# cql_symbol = '<' -# -# -# class LessThanOrEqualOperator(QueryOperator): -# symbol = "LTE" -# cql_symbol = '<=' -# - class AbstractQueryableColumn(object): """ exposes cql query operators through pythons @@ -318,7 +170,7 @@ def _execute(self, q): return execute(q, consistency_level=self._consistency) def __unicode__(self): - return self._select_query() + return unicode(self._select_query()) def __str__(self): return str(self.__unicode__()) @@ -328,7 +180,7 @@ def __call__(self, *args, **kwargs): def __deepcopy__(self, memo): clone = self.__class__(self.model) - for k,v in self.__dict__.items(): + for k, v in self.__dict__.items(): if k in ['_con', '_cur', '_result_cache', '_result_idx']: # don't clone these clone.__dict__[k] = None elif k == '_batch': From cfc77117939fd8701ac4b454b2e080c703a214c3 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 4 Nov 2013 09:58:43 -0800 Subject: [PATCH 0535/3961] fixing bug in set update that would prevent the last item in a set from being removed --- cqlengine/statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index dd367a0f88..b3b95656a2 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -181,7 +181,7 @@ def _analyze(self): """ works out the updates to be performed """ if self.value is None or self.value == self.previous: pass - elif self.previous is None or not any({v in self.previous for v in self.value}): + elif self.previous is None: self._assignments = self.value else: # partial update time From 8210bacac77c33cd6cfc7038e3a8a74f14789fea Mon Sep 17 00:00:00 2001 From: Dvir Volk Date: Tue, 12 Nov 2013 13:03:02 +0200 Subject: [PATCH 0536/3961] Changed List column's default to be list and not set --- cqlengine/columns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 09f6a9fa3a..ef47f284ef 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -647,7 +647,7 @@ def __str__(self): def __nonzero__(self): return bool(self.value) - def __init__(self, value_type, default=set, **kwargs): + def __init__(self, value_type, default=list, **kwargs): return super(List, self).__init__(value_type=value_type, default=default, **kwargs) def validate(self, value): From a080aff3a73351d37126b14eef606061b445aa37 Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Fri, 7 Jun 2013 19:18:07 +0300 Subject: [PATCH 0537/3961] Fixed a bug when NetworkTopologyStrategy is used. Although the Cassandra documentation implies that the `replication_factor` parameter would be ignored in this case its presence will cause an error when creating a keyspace using NetworkTopologyStrategy. --- cqlengine/management.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cqlengine/management.py b/cqlengine/management.py index e05c7fc265..b75412c70c 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -37,6 +37,12 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, } replication_map.update(replication_values) + if strategy_class.lower() != 'simplestrategy': + # Although the Cassandra documentation states for `replication_factor` + # that it is "Required if class is SimpleStrategy; otherwise, + # not used." we get an error if it is present. + replication_map.pop('replication_factor', None) + query = """ CREATE KEYSPACE {} WITH REPLICATION = {} From 4b3edc03d07b3258a38ed070a6fc982b93d288ed Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Tue, 26 Nov 2013 16:48:14 +0200 Subject: [PATCH 0538/3961] Test demonstrating regression in querying with dates --- cqlengine/tests/model/test_model_io.py | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 91d4591246..82abef74ba 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,5 +1,6 @@ from uuid import uuid4 import random +from datetime import date from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import create_table @@ -238,3 +239,33 @@ def test_reserved_cql_words_can_be_used_as_column_names(self): assert model1.insert == model2[0].insert +class TestQueryModel(Model): + test_id = columns.UUID(primary_key=True, default=uuid4) + date = columns.Date(primary_key=True) + description = columns.Text() + + +class TestQuerying(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(TestQuerying, cls).setUpClass() + delete_table(TestQueryModel) + create_table(TestQueryModel) + + @classmethod + def tearDownClass(cls): + super(TestQuerying, cls).tearDownClass() + delete_table(TestQueryModel) + + def test_query_with_date(self): + uid = uuid4() + day = date(2013, 11, 26) + TestQueryModel.create(test_id=uid, date=day, description=u'foo') + + inst = TestQueryModel.filter( + TestQueryModel.test_id == uid, + TestQueryModel.date == day).limit(1).first() + + assert inst.test_id == uid + assert inst.date == day From 225f09212b88fe07e4ce9564fcff682eb84b051c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 26 Nov 2013 07:30:34 -0800 Subject: [PATCH 0539/3961] removing double replication strategy fix --- cqlengine/management.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index b10fd4fc59..81771c48b9 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -42,12 +42,6 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, # not used." we get an error if it is present. replication_map.pop('replication_factor', None) - if strategy_class.lower() != 'simplestrategy': - # Although the Cassandra documentation states for `replication_factor` - # that it is "Required if class is SimpleStrategy; otherwise, - # not used." we get an error if it is present. - replication_map.pop('replication_factor', None) - query = """ CREATE KEYSPACE {} WITH REPLICATION = {} From cfc076749fd874b974ef6fac1d0508ab2ef41660 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 26 Nov 2013 08:12:07 -0800 Subject: [PATCH 0540/3961] fixing polymorphic test --- cqlengine/tests/model/test_polymorphism.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 00078bcb50..4528833517 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -45,8 +45,8 @@ class M1(Base): assert Base._is_polymorphic_base assert not M1._is_polymorphic_base - assert Base._polymorphic_column == Base.type1 - assert M1._polymorphic_column == M1.type1 + assert Base._polymorphic_column is Base._columns['type1'] + assert M1._polymorphic_column is M1._columns['type1'] assert Base._polymorphic_column_name == 'type1' assert M1._polymorphic_column_name == 'type1' From 556a0d776b5f45767fbab63cd60e99aeebc7582d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 26 Nov 2013 08:12:25 -0800 Subject: [PATCH 0541/3961] fixing to_database behavior for functions --- cqlengine/functions.py | 18 ++++++++++++------ cqlengine/query.py | 20 +++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index ceb6a78be9..5796ba4ae4 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -53,10 +53,13 @@ def __init__(self, value): raise ValidationError('datetime instance is required') super(MinTimeUUID, self).__init__(value) - def update_context(self, ctx): - epoch = datetime(1970, 1, 1, tzinfo=self.value.tzinfo) + def to_database(self, val): + epoch = datetime(1970, 1, 1, tzinfo=val.tzinfo) offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 - ctx[str(self.context_id)] = long(((self.value - epoch).total_seconds() - offset) * 1000) + return long(((val - epoch).total_seconds() - offset) * 1000) + + def update_context(self, ctx): + ctx[str(self.context_id)] = self.to_database(self.value) class MaxTimeUUID(BaseQueryFunction): @@ -77,10 +80,13 @@ def __init__(self, value): raise ValidationError('datetime instance is required') super(MaxTimeUUID, self).__init__(value) - def update_context(self, ctx): - epoch = datetime(1970, 1, 1, tzinfo=self.value.tzinfo) + def to_database(self, val): + epoch = datetime(1970, 1, 1, tzinfo=val.tzinfo) offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 - ctx[str(self.context_id)] = long(((self.value - epoch).total_seconds() - offset) * 1000) + return long(((val - epoch).total_seconds() - offset) * 1000) + + def update_context(self, ctx): + ctx[str(self.context_id)] = self.to_database(self.value) class Token(BaseQueryFunction): diff --git a/cqlengine/query.py b/cqlengine/query.py index 04afff145e..297c2b18b4 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -6,7 +6,7 @@ from cqlengine.connection import execute, RowResult from cqlengine.exceptions import CQLEngineException, ValidationError -from cqlengine.functions import Token, BaseQueryFunction +from cqlengine.functions import Token, BaseQueryFunction, QueryValue #CQL 3 reference: #http://www.datastax.com/docs/1.1/references/cql/index @@ -35,28 +35,34 @@ def __unicode__(self): def __str__(self): return str(unicode(self)) + def _to_database(self, val): + if isinstance(val, QueryValue): + return val + else: + return self._get_column().to_database(val) + def in_(self, item): """ Returns an in operator - used in where you'd typically want to use python's `in` operator + used where you'd typically want to use python's `in` operator """ return WhereClause(unicode(self), InOperator(), item) def __eq__(self, other): - return WhereClause(unicode(self), EqualsOperator(), other) + return WhereClause(unicode(self), EqualsOperator(), self._to_database(other)) def __gt__(self, other): - return WhereClause(unicode(self), GreaterThanOperator(), other) + return WhereClause(unicode(self), GreaterThanOperator(), self._to_database(other)) def __ge__(self, other): - return WhereClause(unicode(self), GreaterThanOrEqualOperator(), other) + return WhereClause(unicode(self), GreaterThanOrEqualOperator(), self._to_database(other)) def __lt__(self, other): - return WhereClause(unicode(self), LessThanOperator(), other) + return WhereClause(unicode(self), LessThanOperator(), self._to_database(other)) def __le__(self, other): - return WhereClause(unicode(self), LessThanOrEqualOperator(), other) + return WhereClause(unicode(self), LessThanOrEqualOperator(), self._to_database(other)) class BatchType(object): From 33023837d2d2f17e254c80c447d77f06869334e5 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 26 Nov 2013 08:15:24 -0800 Subject: [PATCH 0542/3961] bumping version and updating changelog --- changelog | 4 ++++ cqlengine/VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 4aad4980c0..28ce74dff7 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.9.2 +* fixing create keyspace with network topology strategy +* fixing regression with query expressions + 0.9 * adding update method * adding support for ttls diff --git a/cqlengine/VERSION b/cqlengine/VERSION index f374f6662e..2003b639c4 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.9.1 +0.9.2 From d315b0f1f743ed7e7ff4e86a2bdff2a415a4c523 Mon Sep 17 00:00:00 2001 From: Russell Haering Date: Sun, 1 Dec 2013 18:33:49 -0800 Subject: [PATCH 0543/3961] validate Token and pk__token filter parameters --- cqlengine/query.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cqlengine/query.py b/cqlengine/query.py index 297c2b18b4..ebb97c51c5 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -355,11 +355,22 @@ def filter(self, *args, **kwargs): column = self.model._get_column(col_name) except KeyError: if col_name == 'pk__token': + if not isinstance(val, Token): + raise QueryException("Virtual column 'pk__token' may only be compared to Token() values") column = columns._PartitionKeysToken(self.model) quote_field = False else: raise QueryException("Can't resolve column name: '{}'".format(col_name)) + if isinstance(val, Token): + if col_name != 'pk__token': + raise QueryException("Token() values may only be compared to the 'pk__token' virtual column") + partition_columns = column.partition_columns + if len(partition_columns) != len(val.value): + raise QueryException( + 'Token() received {} arguments but model has {} partition keys'.format( + len(partition_columns), len(val.value))) + #get query operator, or use equals if not supplied operator_class = BaseWhereOperator.get_operator(col_op or 'EQ') operator = operator_class() From 2a1cb90c9a5077b9617f92e4b67fdd3ab9fc5373 Mon Sep 17 00:00:00 2001 From: Russell Haering Date: Sun, 1 Dec 2013 19:05:09 -0800 Subject: [PATCH 0544/3961] update validation of Token use at query-execution time --- cqlengine/query.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index ebb97c51c5..7608bbf834 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -562,17 +562,15 @@ def _validate_select_where(self): """ Checks that a filterset will not create invalid select statement """ #check that there's either a = or IN relationship with a primary key or indexed field equal_ops = [self.model._columns.get(w.field) for w in self._where if isinstance(w.operator, EqualsOperator)] - token_ops = [self.model._columns.get(w.field) for w in self._where if isinstance(w.operator, Token)] - if not any([w.primary_key or w.index for w in equal_ops]) and not token_ops: + token_comparison = any([w for w in self._where if isinstance(w.value, Token)]) + if not any([w.primary_key or w.index for w in equal_ops]) and not token_comparison: raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') if not self._allow_filtering: #if the query is not on an indexed field if not any([w.index for w in equal_ops]): - if not any([w.partition_key for w in equal_ops]) and not token_ops: + if not any([w.partition_key for w in equal_ops]) and not token_comparison: raise QueryException('Filtering on a clustering key without a partition key is not allowed unless allow_filtering() is called on the querset') - if any(not w.partition_key for w in token_ops): - raise QueryException('The token() function is only supported on the partition key') def _select_fields(self): if self._defer_fields or self._only_fields: From 5a5e7ef5cdc1fb09da29bbe597ccf04deb0fc898 Mon Sep 17 00:00:00 2001 From: Russell Haering Date: Sun, 1 Dec 2013 19:40:19 -0800 Subject: [PATCH 0545/3961] pass partition columns to Token for use serializing values --- cqlengine/query.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/query.py b/cqlengine/query.py index 7608bbf834..1b9ea24489 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -370,6 +370,7 @@ def filter(self, *args, **kwargs): raise QueryException( 'Token() received {} arguments but model has {} partition keys'.format( len(partition_columns), len(val.value))) + val.set_columns(partition_columns) #get query operator, or use equals if not supplied operator_class = BaseWhereOperator.get_operator(col_op or 'EQ') From a597fa5b2f0038f1cda54f8018afab1db7ad6b63 Mon Sep 17 00:00:00 2001 From: Russell Haering Date: Mon, 2 Dec 2013 10:23:55 -0800 Subject: [PATCH 0546/3961] verify SELECT statement generation with token() works now --- cqlengine/tests/query/test_queryoperators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index 11dec4dded..b0f3eb571f 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -49,6 +49,9 @@ class TestModel(Model): where.set_context_id(1) self.assertEquals(str(where), 'token("p1", "p2") > token(:{}, :{})'.format(1, 2)) + # Verify that a SELECT query can be successfully generated + str(q._select_query()) + # Token(tuple()) is also possible for convenience # it (allows for Token(obj.pk) syntax) func = functions.Token(('a', 'b')) @@ -57,4 +60,5 @@ class TestModel(Model): where = q._where[0] where.set_context_id(1) self.assertEquals(str(where), 'token("p1", "p2") > token(:{}, :{})'.format(1, 2)) + str(q._select_query()) From f54c3f3613333d20fa03b9b61b96399e9485a992 Mon Sep 17 00:00:00 2001 From: Russell Haering Date: Mon, 2 Dec 2013 10:25:34 -0800 Subject: [PATCH 0547/3961] add tests to verify Token and pk__token validation --- cqlengine/tests/query/test_queryoperators.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index b0f3eb571f..5988396a62 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -62,3 +62,13 @@ class TestModel(Model): self.assertEquals(str(where), 'token("p1", "p2") > token(:{}, :{})'.format(1, 2)) str(q._select_query()) + # The 'pk__token' virtual column may only be compared to a Token + self.assertRaises(query.QueryException, TestModel.objects.filter, pk__token__gt=10) + + # A Token may only be compared to the `pk__token' virtual column + func = functions.Token('a', 'b') + self.assertRaises(query.QueryException, TestModel.objects.filter, p1__gt=func) + + # The # of arguments to Token must match the # of partition keys + func = functions.Token('a') + self.assertRaises(query.QueryException, TestModel.objects.filter, pk__token__gt=func) From 5fa4ba4ead7dc2e2b7440a23fad1e1bc67c6557d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 5 Dec 2013 07:41:41 -0800 Subject: [PATCH 0548/3961] adding test around token values --- cqlengine/tests/query/test_queryoperators.py | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index 11dec4dded..69fb0a6d4b 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -7,6 +7,7 @@ from cqlengine import query from cqlengine.statements import WhereClause from cqlengine.operators import EqualsOperator +from cqlengine.management import sync_table, drop_table class TestQuerySetOperation(BaseCassEngTestCase): @@ -36,7 +37,41 @@ def test_mintimeuuid_function(self): where.update_context(ctx) self.assertEqual(ctx, {'5': DateTime().to_database(now)}) + +class TokenTestModel(Model): + key = columns.Integer(primary_key=True) + val = columns.Integer() + + +class TestTokenFunction(BaseCassEngTestCase): + + def setUp(self): + super(TestTokenFunction, self).setUp() + sync_table(TokenTestModel) + + def tearDown(self): + super(TestTokenFunction, self).tearDown() + drop_table(TokenTestModel) + def test_token_function(self): + """ Tests that token functions work properly """ + assert TokenTestModel.objects().count() == 0 + for i in range(10): + TokenTestModel.create(key=i, val=i) + assert TokenTestModel.objects().count() == 10 + seen_keys = set() + last_token = None + for instance in TokenTestModel.objects().limit(5): + last_token = instance.key + seen_keys.add(last_token) + assert len(seen_keys) == 5 + for instance in TokenTestModel.objects(pk__token__gt=functions.Token(last_token)): + seen_keys.add(instance.key) + + assert len(seen_keys) == 10 + assert all([i in seen_keys for i in range(10)]) + + def test_compound_pk_token_function(self): class TestModel(Model): p1 = columns.Text(partition_key=True) From 2951ba35de1fc7213121a9b45b7ff679d543c5d3 Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Thu, 28 Nov 2013 14:26:46 +0200 Subject: [PATCH 0549/3961] Use CL.ONE explicitly when interacting with the system keyspace The system keyspace uses "LocalStrategy" for replication so it needs to be accessed with CL.ONE. If the default consistency level is set to something else all management commands which rely on introspecting the system keyspace will fail. --- cqlengine/management.py | 16 ++++++++++------ .../tests/connections/test_connection_pool.py | 3 ++- cqlengine/tests/management/test_management.py | 3 ++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 81771c48b9..b7cdf4d127 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -1,6 +1,7 @@ import json import warnings from cqlengine import SizeTieredCompactionStrategy, LeveledCompactionStrategy +from cqlengine import ONE from cqlengine.named import NamedTable from cqlengine.connection import connection_manager, execute @@ -28,7 +29,7 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, :param **replication_values: 1.2 only, additional values to ad to the replication data map """ with connection_manager() as con: - _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) + _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}, ONE) if name not in [r[0] for r in keyspaces]: #try the 1.2 method replication_map = { @@ -55,7 +56,7 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, def delete_keyspace(name): with connection_manager() as con: - _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}) + _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}, ONE) if name in [r[0] for r in keyspaces]: execute("DROP KEYSPACE {}".format(name)) @@ -80,7 +81,8 @@ def sync_table(model, create_missing_keyspace=True): with connection_manager() as con: tables = con.execute( "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", - {'ks_name': ks_name} + {'ks_name': ks_name}, + ONE ) tables = [x[0] for x in tables.results] @@ -115,7 +117,8 @@ def sync_table(model, create_missing_keyspace=True): with connection_manager() as con: _, idx_names = con.execute( "SELECT index_name from system.\"IndexInfo\" WHERE table_name=:table_name", - {'table_name': raw_cf_name} + {'table_name': raw_cf_name}, + ONE ) idx_names = [i[0] for i in idx_names] @@ -230,7 +233,7 @@ def get_fields(model): logger.debug("get_fields %s %s", ks_name, col_family) - tmp = con.execute(query, {'ks_name':ks_name, 'col_family':col_family}) + tmp = con.execute(query, {'ks_name': ks_name, 'col_family': col_family}, ONE) return [Field(x[0], x[1]) for x in tmp.results] # convert to Field named tuples @@ -282,7 +285,8 @@ def drop_table(model): with connection_manager() as con: _, tables = con.execute( "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", - {'ks_name': ks_name} + {'ks_name': ks_name}, + ONE ) raw_cf_name = model.column_family_name(include_keyspace=False) if raw_cf_name not in [t[0] for t in tables]: diff --git a/cqlengine/tests/connections/test_connection_pool.py b/cqlengine/tests/connections/test_connection_pool.py index 29b3ea50a9..b34ea47182 100644 --- a/cqlengine/tests/connections/test_connection_pool.py +++ b/cqlengine/tests/connections/test_connection_pool.py @@ -2,6 +2,7 @@ from cql import OperationalError from mock import MagicMock, patch, Mock +from cqlengine import ONE from cqlengine.connection import ConnectionPool, Host @@ -18,4 +19,4 @@ def cursor(self): with patch.object(p, 'get', return_value=MockConnection()): with self.assertRaises(OperationalError): - p.execute("select * from system.peers", {}) + p.execute("select * from system.peers", {}, ONE) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 88405b1e40..25f4d268b6 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,5 +1,6 @@ from mock import MagicMock, patch +from cqlengine import ONE from cqlengine.exceptions import CQLEngineException from cqlengine.management import create_table, delete_table, get_fields from cqlengine.tests.base import BaseCassEngTestCase @@ -22,7 +23,7 @@ def test_totally_dead_pool(self): with patch('cqlengine.connection.cql.connect') as mock: mock.side_effect=CQLEngineException with self.assertRaises(CQLEngineException): - self.pool.execute("select * from system.peers", {}) + self.pool.execute("select * from system.peers", {}, ONE) def test_dead_node(self): """ From 380eeeb8cabce3cd817ffa775d58c596b0e53c81 Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Tue, 10 Dec 2013 11:57:24 +0200 Subject: [PATCH 0550/3961] Make get_table_settings use CL.ONE explicitly --- cqlengine/management.py | 5 +++-- cqlengine/query.py | 10 +++++----- setup.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index b7cdf4d127..6d41489e6f 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -239,8 +239,9 @@ def get_fields(model): def get_table_settings(model): - return schema_columnfamilies.get(keyspace_name=model._get_keyspace(), - columnfamily_name=model.column_family_name(include_keyspace=False)) + return schema_columnfamilies.objects.consistency(ONE).get( + keyspace_name=model._get_keyspace(), + columnfamily_name=model.column_family_name(include_keyspace=False)) def update_compaction(model): diff --git a/cqlengine/query.py b/cqlengine/query.py index 1b9ea24489..ed59e07700 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -318,6 +318,11 @@ def first(self): def all(self): return copy.deepcopy(self) + def consistency(self, consistency): + clone = copy.deepcopy(self) + clone._consistency = consistency + return clone + def _parse_filter_arg(self, arg): """ Parses a filter arg in the format: @@ -626,11 +631,6 @@ def values_list(self, *fields, **kwargs): clone._flat_values_list = flat return clone - def consistency(self, consistency): - clone = copy.deepcopy(self) - clone._consistency = consistency - return clone - def ttl(self, ttl): clone = copy.deepcopy(self) clone._ttl = ttl diff --git a/setup.py b/setup.py index 2965a6d73a..98700ad684 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='cqlengine', - version=version, + version='0.9.2-2951ba35de1fc7213121a9b45b7ff679d543c5d3', description='Cassandra CQL 3 Object Mapper for Python', long_description=long_desc, classifiers = [ From 10c516138e6e3c4560889a1d87a4400954c63c2a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 11 Dec 2013 10:41:46 -0800 Subject: [PATCH 0551/3961] execute_on_exception in batches --- cqlengine/query.py | 8 ++-- cqlengine/tests/query/test_batch_query.py | 49 +++++++++++++++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 1b9ea24489..33d0ae15e7 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -78,13 +78,15 @@ class BatchQuery(object): """ _consistency = None - def __init__(self, batch_type=None, timestamp=None, consistency=None): + + def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on_exception=False): self.queries = [] self.batch_type = batch_type if timestamp is not None and not isinstance(timestamp, datetime): raise CQLEngineException('timestamp object must be an instance of datetime') self.timestamp = timestamp self._consistency = consistency + self._execute_on_exception = execute_on_exception def add_query(self, query): if not isinstance(query, BaseCQLStatement): @@ -125,8 +127,8 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - #don't execute if there was an exception - if exc_type is not None: return + #don't execute if there was an exception by default + if exc_type is not None and not self._execute_on_exception: return self.execute() diff --git a/cqlengine/tests/query/test_batch_query.py b/cqlengine/tests/query/test_batch_query.py index 37e8d28d56..94080729de 100644 --- a/cqlengine/tests/query/test_batch_query.py +++ b/cqlengine/tests/query/test_batch_query.py @@ -3,7 +3,7 @@ from uuid import uuid4 import random from cqlengine import Model, columns -from cqlengine.management import delete_table, create_table +from cqlengine.management import drop_table, sync_table from cqlengine.query import BatchQuery, DMLQuery from cqlengine.tests.base import BaseCassEngTestCase @@ -13,18 +13,23 @@ class TestMultiKeyModel(Model): count = columns.Integer(required=False) text = columns.Text(required=False) +class BatchQueryLogModel(Model): + # simple k/v table + k = columns.Integer(primary_key=True) + v = columns.Integer() + class BatchQueryTests(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(BatchQueryTests, cls).setUpClass() - delete_table(TestMultiKeyModel) - create_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) + sync_table(TestMultiKeyModel) @classmethod def tearDownClass(cls): super(BatchQueryTests, cls).tearDownClass() - delete_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) def setUp(self): super(BatchQueryTests, self).setUp() @@ -123,3 +128,39 @@ def test_dml_none_success_case(self): q.batch(None) assert q._batch is None + + def test_batch_execute_on_exception_succeeds(self): + # makes sure if execute_on_exception == True we still apply the batch + drop_table(BatchQueryLogModel) + sync_table(BatchQueryLogModel) + + obj = BatchQueryLogModel.objects(k=1) + self.assertEqual(0, len(obj)) + + try: + with BatchQuery(execute_on_exception=True) as b: + BatchQueryLogModel.batch(b).create(k=1, v=1) + raise Exception("Blah") + except: + pass + + obj = BatchQueryLogModel.objects(k=1) + self.assertEqual(1, len(obj)) + + def test_batch_execute_on_exception_skips_if_not_specified(self): + # makes sure if execute_on_exception == True we still apply the batch + drop_table(BatchQueryLogModel) + sync_table(BatchQueryLogModel) + + obj = BatchQueryLogModel.objects(k=2) + self.assertEqual(0, len(obj)) + + try: + with BatchQuery(execute_on_exception=True) as b: + BatchQueryLogModel.batch(b).create(k=2, v=2) + raise Exception("Blah") + except: + pass + + obj = BatchQueryLogModel.objects(k=2) + self.assertEqual(1, len(obj)) From 3d351e475d28f927b0c25f6806da38512d7de488 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 11 Dec 2013 14:59:39 -0800 Subject: [PATCH 0552/3961] fixed weird bug in tests which allowed them to pass but for the wrong reasons --- cqlengine/tests/query/test_batch_query.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/query/test_batch_query.py b/cqlengine/tests/query/test_batch_query.py index 94080729de..f4a138509e 100644 --- a/cqlengine/tests/query/test_batch_query.py +++ b/cqlengine/tests/query/test_batch_query.py @@ -140,7 +140,7 @@ def test_batch_execute_on_exception_succeeds(self): try: with BatchQuery(execute_on_exception=True) as b: BatchQueryLogModel.batch(b).create(k=1, v=1) - raise Exception("Blah") + raise Exception("Blah") except: pass @@ -156,11 +156,11 @@ def test_batch_execute_on_exception_skips_if_not_specified(self): self.assertEqual(0, len(obj)) try: - with BatchQuery(execute_on_exception=True) as b: + with BatchQuery() as b: BatchQueryLogModel.batch(b).create(k=2, v=2) - raise Exception("Blah") + raise Exception("Blah") except: pass obj = BatchQueryLogModel.objects(k=2) - self.assertEqual(1, len(obj)) + self.assertEqual(0, len(obj)) From 70e55fd0e66016889461edf6ea26a3dbc733648c Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 11 Dec 2013 15:13:46 -0800 Subject: [PATCH 0553/3961] added inline explanation of why the 2 new batch exception tests should pass --- cqlengine/tests/query/test_batch_query.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cqlengine/tests/query/test_batch_query.py b/cqlengine/tests/query/test_batch_query.py index f4a138509e..1d7e1804db 100644 --- a/cqlengine/tests/query/test_batch_query.py +++ b/cqlengine/tests/query/test_batch_query.py @@ -145,6 +145,7 @@ def test_batch_execute_on_exception_succeeds(self): pass obj = BatchQueryLogModel.objects(k=1) + # should be 1 because the batch should execute self.assertEqual(1, len(obj)) def test_batch_execute_on_exception_skips_if_not_specified(self): @@ -163,4 +164,6 @@ def test_batch_execute_on_exception_skips_if_not_specified(self): pass obj = BatchQueryLogModel.objects(k=2) + + # should be 0 because the batch should not execute self.assertEqual(0, len(obj)) From 448d861c7bcb42a1c2e5026c38417c954d266b28 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 11 Dec 2013 15:25:06 -0800 Subject: [PATCH 0554/3961] updated batch docs --- docs/topics/queryset.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index a73ed1d05f..4cf0d20488 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -312,6 +312,20 @@ Batch Queries ExampleModel.objects(id=some_id2).batch(b).delete() b.execute() + + Typically you will not want the block to execute if an exception occurs inside the `with` block. However, in the case that this is desirable, it's achievable by using the following syntax: + + .. code-block:: python + + with BatchQuery(execute_on_exception=True) as b: + LogEntry.batch(b).create(k=1, v=1) + mystery_function() # exception thrown in here + LogEntry.batch(b).create(k=1, v=2) # this code is never reached due to the exception, but anything leading up to here will execute in the batch. + + If an exception is thrown somewhere in the block, any statements that have been added to the batch will still be executed. This is useful for some logging situations. + + + QuerySet method reference ========================= From a45d867b4f7010d190afda555ce45ec283a10a7c Mon Sep 17 00:00:00 2001 From: Konstantin Podshumok Date: Thu, 12 Dec 2013 11:06:29 +0300 Subject: [PATCH 0555/3961] log exception on _create_connection failure --- cqlengine/connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 6e08a3580f..cc352a6549 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -197,9 +197,9 @@ def _create_connection(self): ) new_conn.set_cql_version('3.0.0') return new_conn - except Exception as e: - logging.debug("Could not establish connection to {}:{}".format(host.name, host.port)) - pass + except Exception as exc: + logging.debug("Could not establish connection to" + " {}:{} ({!r})".format(host.name, host.port, exc)) raise CQLConnectionError("Could not connect to any server in cluster") From 57741b1f7df2cdb58cb2b181c06736ecf562bf75 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Thu, 12 Dec 2013 12:11:45 +0200 Subject: [PATCH 0556/3961] fix ttl in update statement --- cqlengine/statements.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index b3b95656a2..8ba5741756 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -582,15 +582,16 @@ class UpdateStatement(AssignmentStatement): def __unicode__(self): qs = ['UPDATE', self.table] + + if self.ttl: + qs += ["USING TTL {}".format(self.ttl)] + qs += ['SET'] qs += [', '.join([unicode(c) for c in self.assignments])] if self.where_clauses: qs += [self._where] - if self.ttl: - qs += ["USING TTL {}".format(self.ttl)] - return ' '.join(qs) From 29912bb944cec7ac48e44cd8597f354b2276d8b2 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 10:53:04 -0800 Subject: [PATCH 0557/3961] test verifying the TTL syntax is valid --- cqlengine/tests/test_ttl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 716caface8..0d465ac09a 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -63,6 +63,12 @@ def test_update_includes_ttl(self): query = m.call_args[0][0] self.assertIn("USING TTL", query) + def test_update_syntax_valid(self): + # sanity test that ensures the TTL syntax is accepted by cassandra + model = TestTTLModel.create(text="goodbye blake") + model.ttl(60).update(text="goodbye forever") + + From 2b8a811bd77c6a79e8c4cf6529fc67731bd0b26b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 10:53:04 -0800 Subject: [PATCH 0558/3961] test verifying the TTL syntax is valid --- cqlengine/tests/test_ttl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 716caface8..0d465ac09a 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -63,6 +63,12 @@ def test_update_includes_ttl(self): query = m.call_args[0][0] self.assertIn("USING TTL", query) + def test_update_syntax_valid(self): + # sanity test that ensures the TTL syntax is accepted by cassandra + model = TestTTLModel.create(text="goodbye blake") + model.ttl(60).update(text="goodbye forever") + + From 25bd8af1d35cb3dc9c6d1c030592eef5714b7cf1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 13:46:52 -0800 Subject: [PATCH 0559/3961] fixed issue with setting a list to an empty list --- cqlengine/statements.py | 7 ++-- .../tests/columns/test_container_columns.py | 39 ++++++++++++++----- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 8ba5741756..45f3fabead 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -218,7 +218,7 @@ def __unicode__(self): if not self._analyzed: self._analyze() qs = [] ctx_id = self.context_id - if self._assignments: + if self._assignments is not None: qs += ['"{}" = :{}'.format(self.field, ctx_id)] ctx_id += 1 @@ -233,12 +233,12 @@ def __unicode__(self): def get_context_size(self): if not self._analyzed: self._analyze() - return int(bool(self._assignments)) + int(bool(self._append)) + int(bool(self._prepend)) + return int(self._assignments is not None) + int(bool(self._append)) + int(bool(self._prepend)) def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id - if self._assignments: + if self._assignments is not None: ctx[str(ctx_id)] = self._to_database(self._assignments) ctx_id += 1 if self._prepend: @@ -263,6 +263,7 @@ def _analyze(self): # rewrite the whole list self._assignments = self.value + elif len(self.previous) == 0: # if we're updating from an empty # list, do a complete insert diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 1cb19f81f3..2a3707d03b 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -4,7 +4,7 @@ from cqlengine import Model, ValidationError from cqlengine import columns -from cqlengine.management import create_table, delete_table +from cqlengine.management import sync_table, drop_table from cqlengine.tests.base import BaseCassEngTestCase @@ -34,13 +34,13 @@ class TestSetColumn(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestSetColumn, cls).setUpClass() - delete_table(TestSetModel) - create_table(TestSetModel) + drop_table(TestSetModel) + sync_table(TestSetModel) @classmethod def tearDownClass(cls): super(TestSetColumn, cls).tearDownClass() - delete_table(TestSetModel) + drop_table(TestSetModel) def test_empty_set_initial(self): """ @@ -187,13 +187,13 @@ class TestListColumn(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestListColumn, cls).setUpClass() - delete_table(TestListModel) - create_table(TestListModel) + drop_table(TestListModel) + sync_table(TestListModel) @classmethod def tearDownClass(cls): super(TestListColumn, cls).tearDownClass() - delete_table(TestListModel) + drop_table(TestListModel) def test_initial(self): tmp = TestListModel.create() @@ -315,6 +315,24 @@ def test_default_empty_container_saving(self): m = TestListModel.get(partition=pkey) self.assertEqual(m.int_list, [1,2,3,4]) + def test_remove_entry_works(self): + pkey = uuid4() + tmp = TestListModel.create(partition=pkey, int_list=[1,2]) + tmp.int_list.pop() + tmp.update() + tmp = TestListModel.get(partition=pkey) + self.assertEqual(tmp.int_list, [1]) + + def test_update_from_non_empty_to_empty(self): + pkey = uuid4() + tmp = TestListModel.create(partition=pkey, int_list=[1,2]) + tmp.int_list = [] + tmp.update() + + tmp = TestListModel.get(partition=pkey) + self.assertEqual(tmp.int_list, []) + + class TestMapModel(Model): partition = columns.UUID(primary_key=True, default=uuid4) @@ -326,13 +344,13 @@ class TestMapColumn(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestMapColumn, cls).setUpClass() - delete_table(TestMapModel) - create_table(TestMapModel) + drop_table(TestMapModel) + sync_table(TestMapModel) @classmethod def tearDownClass(cls): super(TestMapColumn, cls).tearDownClass() - delete_table(TestMapModel) + drop_table(TestMapModel) def test_empty_default(self): tmp = TestMapModel.create() @@ -343,6 +361,7 @@ def test_empty_retrieve(self): tmp2 = TestMapModel.get(partition=tmp.partition) tmp2.int_map['blah'] = 1 + def test_remove_last_entry_works(self): tmp = TestMapModel.create() tmp.text_map["blah"] = datetime.now() From 5c425e819333a89cfd1f31620b691d60017fedda Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 15:19:53 -0800 Subject: [PATCH 0560/3961] disallow saving None inside set and list --- cqlengine/columns.py | 5 +++++ cqlengine/tests/columns/test_container_columns.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index ef47f284ef..11a9f60dcf 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -574,6 +574,9 @@ def validate(self, value): else: raise ValidationError('{} cannot be coerced to a set object'.format(val)) + if None in val: + raise ValidationError("None not allowed in a set") + return {self.value_col.validate(v) for v in val} def to_python(self, value): @@ -655,6 +658,8 @@ def validate(self, value): if val is None: return if not isinstance(val, (set, list, tuple)): raise ValidationError('{} is not a list object'.format(val)) + if None in val: + raise ValidationError("None is not allowed in a list") return [self.value_col.validate(v) for v in val] def to_python(self, value): diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 2a3707d03b..da26582322 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -42,6 +42,10 @@ def tearDownClass(cls): super(TestSetColumn, cls).tearDownClass() drop_table(TestSetModel) + def test_add_none_fails(self): + with self.assertRaises(ValidationError): + m = TestSetModel.create(int_set=set([None])) + def test_empty_set_initial(self): """ tests that sets are set() by default, should never be none @@ -332,6 +336,13 @@ def test_update_from_non_empty_to_empty(self): tmp = TestListModel.get(partition=pkey) self.assertEqual(tmp.int_list, []) + def test_insert_none(self): + pkey = uuid4() + with self.assertRaises(ValidationError): + TestListModel.create(partition=pkey, int_list=[None]) + + + class TestMapModel(Model): From daa4772e45cd844107ef41f151495cd0aa4d8ea4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 15:23:00 -0800 Subject: [PATCH 0561/3961] Fixed #90, putting None inside containers now throws a validation error --- cqlengine/tests/columns/test_container_columns.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index da26582322..d54a339f06 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -367,6 +367,15 @@ def test_empty_default(self): tmp = TestMapModel.create() tmp.int_map['blah'] = 1 + def test_add_none_as_map_key(self): + with self.assertRaises(ValidationError): + TestMapModel.create(int_map={None:1}) + + def test_add_none_as_map_value(self): + with self.assertRaises(ValidationError): + TestMapModel.create(int_map={None:1}) + + def test_empty_retrieve(self): tmp = TestMapModel.create() tmp2 = TestMapModel.get(partition=tmp.partition) From 1791544946a3465a5f742ae5841d1b1f73954003 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 16:00:39 -0800 Subject: [PATCH 0562/3961] ensure we aren't trying to include polymorphic keys when we blah.objects.delete --- cqlengine/tests/model/test_polymorphism.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 4528833517..f950bbcdf8 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -1,7 +1,9 @@ import uuid +import mock from cqlengine import columns from cqlengine import models +from cqlengine.connection import ConnectionPool from cqlengine.tests.base import BaseCassEngTestCase from cqlengine import management @@ -118,6 +120,21 @@ def test_query_deserialization(self): assert isinstance(p1r, Poly1) assert isinstance(p2r, Poly2) + def test_delete_on_polymorphic_subclass_does_not_include_polymorphic_key(self): + p1 = Poly1.create() + + with mock.patch.object(ConnectionPool, 'execute') as m: + Poly1.objects(partition=p1.partition).delete() + + # make sure our polymorphic key isn't in the CQL + # not sure how we would even get here if it was in there + # since the CQL would fail. + + self.assertNotIn("row_type", m.call_args[0][0]) + + + + class UnindexedPolyBase(models.Model): partition = columns.UUID(primary_key=True, default=uuid.uuid4) From adcfbb9b13f435841b215ac8e87a59fd11c74217 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 16:56:30 -0800 Subject: [PATCH 0563/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 2003b639c4..78bc1abd14 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.9.2 +0.10.0 From 4e8244022bcf0d3e3012ba8ea9a514a19a66f457 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 12 Dec 2013 17:02:11 -0800 Subject: [PATCH 0564/3961] fixed version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 98700ad684..2965a6d73a 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='cqlengine', - version='0.9.2-2951ba35de1fc7213121a9b45b7ff679d543c5d3', + version=version, description='Cassandra CQL 3 Object Mapper for Python', long_description=long_desc, classifiers = [ From 5165bb078c3fbd51dbe112e25a7c5c7974800397 Mon Sep 17 00:00:00 2001 From: Kai Lautaportti Date: Sat, 14 Dec 2013 20:23:21 +0200 Subject: [PATCH 0565/3961] Fix logic determining if compaction options need syncing The compaction_strategy_options as received from the `system.schema_columnfamilies` table are all string valued and comparison to typed values (ints, floats) as defined in models results in sync_table() issuning ALTER TABLE statements redundantly even if there were no changes to compaction options. This patch casts the values defined in models to strings to ensure they compare equal as needed. Additionally, this patch normalizes handling of the `min_threshold` and `max_threshold` options for SizeTieredCompactionStrategy which exist outside the `compaction_strategy_options` mapping and have different names in the `system.schema_columnfamilies` table compared to the CQL options. A typo in the `BaseModel.__compaction_tombstone_threshold__` option is also fixed. --- cqlengine/management.py | 29 +++++++++++--- cqlengine/models.py | 2 +- .../management/test_compaction_settings.py | 40 ++++++++++++++++++- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 6d41489e6f..04c23992da 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -207,9 +207,12 @@ def setter(key, limited_to_strategy = None): raise CQLEngineException("{} is limited to {}".format(key, limited_to_strategy)) if tmp: - result[key] = tmp + # Explicitly cast the values to strings to be able to compare the + # values against introspected values from Cassandra. + result[key] = str(tmp) setter('tombstone_compaction_interval') + setter('tombstone_threshold') setter('bucket_high', SizeTieredCompactionStrategy) setter('bucket_low', SizeTieredCompactionStrategy) @@ -217,7 +220,7 @@ def setter(key, limited_to_strategy = None): setter('min_threshold', SizeTieredCompactionStrategy) setter('min_sstable_size', SizeTieredCompactionStrategy) - setter("sstable_size_in_mb", LeveledCompactionStrategy) + setter('sstable_size_in_mb', LeveledCompactionStrategy) return result @@ -245,6 +248,14 @@ def get_table_settings(model): def update_compaction(model): + """Updates the compaction options for the given model if necessary. + + :param model: The model to update. + + :return: `True`, if the compaction options were modified in Cassandra, + `False` otherwise. + :rtype: bool + """ logger.debug("Checking %s for compaction differences", model) row = get_table_settings(model) # check compaction_strategy_class @@ -253,13 +264,17 @@ def update_compaction(model): do_update = not row['compaction_strategy_class'].endswith(model.__compaction__) - existing_options = row['compaction_strategy_options'] - existing_options = json.loads(existing_options) + existing_options = json.loads(row['compaction_strategy_options']) + # The min/max thresholds are stored differently in the system data dictionary + existing_options.update({ + 'min_threshold': str(row['min_compaction_threshold']), + 'max_threshold': str(row['max_compaction_threshold']), + }) desired_options = get_compaction_options(model) desired_options.pop('class', None) - for k,v in desired_options.items(): + for k, v in desired_options.items(): val = existing_options.pop(k, None) if val != v: do_update = True @@ -273,12 +288,16 @@ def update_compaction(model): query = "ALTER TABLE {} with compaction = {}".format(cf_name, options) logger.debug(query) execute(query) + return True + + return False def delete_table(model): warnings.warn("delete_table has been deprecated in favor of drop_table()", DeprecationWarning) return drop_table(model) + def drop_table(model): # don't try to delete non existant tables diff --git a/cqlengine/models.py b/cqlengine/models.py index 04854c020a..252984a6bb 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -214,7 +214,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass # compaction options __compaction__ = None __compaction_tombstone_compaction_interval__ = None - __compaction_tombstone_threshold = None + __compaction_tombstone_threshold__ = None # compaction - size tiered options __compaction_bucket_high__ = None diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index 014abcb2db..e2147e6ccd 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -39,7 +39,7 @@ def test_size_tiered(self): def test_min_threshold(self): self.model.__compaction_min_threshold__ = 2 result = get_compaction_options(self.model) - assert result['min_threshold'] == 2 + assert result['min_threshold'] == '2' class LeveledCompactionTest(BaseCompactionTest): @@ -69,7 +69,7 @@ def test_sstable_size_in_mb(self): with patch.object(self.model, '__compaction_sstable_size_in_mb__', 32): result = get_compaction_options(self.model) - assert result['sstable_size_in_mb'] == 32 + assert result['sstable_size_in_mb'] == '32' class LeveledcompactionTestTable(Model): @@ -90,6 +90,42 @@ def test_alter_is_called_table(self): sync_table(LeveledcompactionTestTable) assert mock.called == 1 + def test_compaction_not_altered_without_changes_leveled(self): + from cqlengine.management import update_compaction + + class LeveledCompactionChangesDetectionTest(Model): + __compaction__ = LeveledCompactionStrategy + __compaction_sstable_size_in_mb__ = 160 + __compaction_tombstone_threshold__ = 0.125 + __compaction_tombstone_compaction_interval__ = 3600 + + pk = columns.Integer(primary_key=True) + + drop_table(LeveledCompactionChangesDetectionTest) + sync_table(LeveledCompactionChangesDetectionTest) + + assert not update_compaction(LeveledCompactionChangesDetectionTest) + + def test_compaction_not_altered_without_changes_sizetiered(self): + from cqlengine.management import update_compaction + + class SizeTieredCompactionChangesDetectionTest(Model): + __compaction__ = SizeTieredCompactionStrategy + __compaction_bucket_high__ = 20 + __compaction_bucket_low__ = 10 + __compaction_max_threshold__ = 200 + __compaction_min_threshold__ = 100 + __compaction_min_sstable_size__ = 1000 + __compaction_tombstone_threshold__ = 0.125 + __compaction_tombstone_compaction_interval__ = 3600 + + pk = columns.Integer(primary_key=True) + + drop_table(SizeTieredCompactionChangesDetectionTest) + sync_table(SizeTieredCompactionChangesDetectionTest) + + assert not update_compaction(SizeTieredCompactionChangesDetectionTest) + def test_alter_actually_alters(self): tmp = copy.deepcopy(LeveledcompactionTestTable) drop_table(tmp) From 2810fead112e32f9082aeafee92c10dec441b296 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Mon, 16 Dec 2013 15:04:57 +0200 Subject: [PATCH 0566/3961] bugfix: connection.setup - cast port to int --- cqlengine/connection.py | 9 +++++++-- .../connections/test_connection_setup.py | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 cqlengine/tests/connections/test_connection_setup.py diff --git a/cqlengine/connection.py b/cqlengine/connection.py index cc352a6549..01b94023fa 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -85,12 +85,17 @@ def setup( host = host.strip() host = host.split(':') if len(host) == 1: - _hosts.append(Host(host[0], 9160)) + port = 9160 elif len(host) == 2: - _hosts.append(Host(*host)) + try: + port = int(host[1]) + except ValueError: + raise CQLConnectionError("Can't parse {}".format(''.join(host))) else: raise CQLConnectionError("Can't parse {}".format(''.join(host))) + _hosts.append(Host(host[0], port)) + if not _hosts: raise CQLConnectionError("At least one host required") diff --git a/cqlengine/tests/connections/test_connection_setup.py b/cqlengine/tests/connections/test_connection_setup.py new file mode 100644 index 0000000000..5f1f7b774c --- /dev/null +++ b/cqlengine/tests/connections/test_connection_setup.py @@ -0,0 +1,20 @@ +from unittest import TestCase +from mock import MagicMock, patch, Mock + +from cqlengine.connection import setup, CQLConnectionError, Host + + +class OperationalErrorLoggingTest(TestCase): + @patch('cqlengine.connection.ConnectionPool', return_value=None, autospec=True) + def test_setup_hosts(self, PatchedConnectionPool): + with self.assertRaises(CQLConnectionError): + setup(hosts=['localhost:abcd']) + self.assertEqual(len(PatchedConnectionPool.mock_calls), 0) + + with self.assertRaises(CQLConnectionError): + setup(hosts=['localhost:9160:abcd']) + self.assertEqual(len(PatchedConnectionPool.mock_calls), 0) + + setup(hosts=['localhost:9161', 'remotehost']) + self.assertEqual(len(PatchedConnectionPool.mock_calls), 1) + self.assertEqual(PatchedConnectionPool.call_args[0][0], [Host('localhost', 9161), Host('remotehost', 9160)]) From d2786f8f791c35fc4a0e3cb9e6cad61a7d7fa137 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 12:19:44 -0800 Subject: [PATCH 0567/3961] throwing timestamp options everywhere --- cqlengine/models.py | 30 +++++++++++++++++++++++++++--- cqlengine/query.py | 3 ++- cqlengine/statements.py | 11 +++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 04854c020a..28a5a0fad7 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -93,6 +93,28 @@ def ttl_setter(ts): def __call__(self, *args, **kwargs): raise NotImplementedError +class TimestampDescriptor(object): + """ + returns a query set descriptor with a timestamp specified + """ + def __get__(self, instance, model): + if instance: + # instance method + def timestamp_setter(ts): + instance._timestamp = ts + return instance + return timestamp_setter + + qs = model.__queryset__(model) + + def timestamp_setter(ts): + qs._timestamp = ts + return qs + + return timestamp_setter + + def __call__(self, *args, **kwargs): + raise NotImplementedError class ConsistencyDescriptor(object): """ @@ -200,6 +222,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass objects = QuerySetDescriptor() ttl = TTLDescriptor() consistency = ConsistencyDescriptor() + timestamp = TimestampDescriptor() #table names will be generated automatically from it's model and package name #however, you can also define them manually here @@ -231,8 +254,9 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass __queryset__ = ModelQuerySet __dmlquery__ = DMLQuery - __ttl__ = None + #__ttl__ = None # this doesn't seem to be used __consistency__ = None # can be set per query + __timestamp__ = None # optional timestamp to include with the operation __read_repair_chance__ = 0.1 @@ -257,11 +281,11 @@ def __repr__(self): """ Pretty printing of models by their primary key """ - return '{} <{}>'.format(self.__class__.__name__, + return '{} <{}>'.format(self.__class__.__name__, ', '.join(('{}={}'.format(k, getattr(self, k)) for k,v in self._primary_keys.iteritems())) ) - + @classmethod def _discover_polymorphic_submodels(cls): diff --git a/cqlengine/query.py b/cqlengine/query.py index 78d273c03e..24a3374455 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -684,13 +684,14 @@ class DMLQuery(object): _ttl = None _consistency = None - def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None): + def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance self._batch = batch self._ttl = ttl self._consistency = consistency + self._timestamp = timestamp def _execute(self, q): if self._batch: diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 45f3fabead..2afeae0156 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -400,12 +400,13 @@ def __unicode__(self): class BaseCQLStatement(object): """ The base cql statement class """ - def __init__(self, table, consistency=None, where=None): + def __init__(self, table, consistency=None, timestamp=None, where=None): super(BaseCQLStatement, self).__init__() self.table = table self.consistency = consistency self.context_id = 0 self.context_counter = self.context_id + self.timestamp = timestamp self.where_clauses = [] for clause in where or []: @@ -513,7 +514,8 @@ def __init__(self, assignments=None, consistency=None, where=None, - ttl=None): + ttl=None, + timestamp=None): super(AssignmentStatement, self).__init__( table, consistency=consistency, @@ -575,6 +577,9 @@ def __unicode__(self): if self.ttl: qs += ["USING TTL {}".format(self.ttl)] + if self.timestamp: + qs += ["USING TIMESTAMP {}".format(self.timestamp)] + return ' '.join(qs) @@ -586,6 +591,8 @@ def __unicode__(self): if self.ttl: qs += ["USING TTL {}".format(self.ttl)] + if self.timestamp: + qs += ["USING TIMESTAMP {}".format(self.timestamp)] qs += ['SET'] qs += [', '.join([unicode(c) for c in self.assignments])] From 81131ac2af2ab767cbe818f4f41eb7cda34a6423 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 16:00:44 -0800 Subject: [PATCH 0568/3961] added sure library, basic test setup --- cqlengine/statements.py | 22 +++++++++++++++++++++ cqlengine/tests/test_timestamp.py | 32 +++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 55 insertions(+) create mode 100644 cqlengine/tests/test_timestamp.py diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 2afeae0156..815ccfe34f 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from cqlengine.functions import QueryValue from cqlengine.operators import BaseWhereOperator, InOperator @@ -444,6 +445,20 @@ def update_context_id(self, i): clause.set_context_id(self.context_counter) self.context_counter += clause.get_context_size() + @property + def timestamp_normalized(self): + if isinstance(self.timestamp, int): + return self.timestamp + + if isinstance(self.timestamp, datetime): + # do stuff + return self.timestamp + + if isinstance(self.timestamp, timedelta): + # do more stuff + return self.timestamp + + def __unicode__(self): raise NotImplementedError @@ -645,6 +660,13 @@ def __unicode__(self): qs += [', '.join(['{}'.format(f) for f in self.fields])] qs += ['FROM', self.table] + delete_option = [] + if self.timestamp: + delete_option += ["TIMESTAMP {}".format(self.timestamp)] + + if delete_option: + qs += ["USING {}".format(" AND ".join(delete_option))] + if self.where_clauses: qs += [self._where] diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py new file mode 100644 index 0000000000..f1e7c91ac3 --- /dev/null +++ b/cqlengine/tests/test_timestamp.py @@ -0,0 +1,32 @@ +""" +Tests surrounding the blah.timestamp( timedelta(seconds=30) ) format. +""" +from datetime import timedelta + +import unittest +from uuid import uuid4 +import sure +from cqlengine import Model, columns +from cqlengine.management import sync_table +from cqlengine.tests.base import BaseCassEngTestCase + + +class TestTimestampModel(Model): + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + count = columns.Integer() + + +class CreateWithTimestampTest(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(CreateWithTimestampTest, cls).setUpClass() + sync_table(TestTimestampModel) + + def test_batch(self): + pass + + def test_non_batch(self): + tmp = TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) + tmp.should.be.ok + diff --git a/requirements.txt b/requirements.txt index 04a6bc4465..1fd68cdd9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ ipython==0.13.1 ipdb==0.7 Sphinx==1.1.3 mock==1.0.1 +sure From 9640b1c02e99e3ee045a9416e3aba8866d1452a6 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 16:15:33 -0800 Subject: [PATCH 0569/3961] better class structure for the billions of tests i'm about to write --- cqlengine/models.py | 10 ++++++---- cqlengine/query.py | 5 ++++- cqlengine/tests/test_timestamp.py | 22 ++++++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 28a5a0fad7..7b254c9900 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -222,16 +222,18 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass objects = QuerySetDescriptor() ttl = TTLDescriptor() consistency = ConsistencyDescriptor() + + # custom timestamps, see USING TIMESTAMP X timestamp = TimestampDescriptor() - #table names will be generated automatically from it's model and package name - #however, you can also define them manually here + # table names will be generated automatically from it's model + # however, you can also define them manually here __table_name__ = None - #the keyspace for this model + # the keyspace for this model __keyspace__ = None - #polymorphism options + # polymorphism options __polymorphic_key__ = None # compaction options diff --git a/cqlengine/query.py b/cqlengine/query.py index 24a3374455..fea1ec196c 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -166,6 +166,7 @@ def __init__(self, model): self._batch = None self._ttl = None self._consistency = None + self._timestamp = None @property def column_family_name(self): @@ -510,7 +511,9 @@ def defer(self, fields): return self._only_or_defer('defer', fields) def create(self, **kwargs): - return self.model(**kwargs).batch(self._batch).ttl(self._ttl).consistency(self._consistency).save() + return self.model(**kwargs).batch(self._batch).ttl(self._ttl).\ + consistency(self._consistency).\ + timestamp(self._timestamp).save() def delete(self): """ diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index f1e7c91ac3..845b97b9aa 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -5,8 +5,10 @@ import unittest from uuid import uuid4 +import mock import sure from cqlengine import Model, columns +from cqlengine.connection import ConnectionPool from cqlengine.management import sync_table from cqlengine.tests.base import BaseCassEngTestCase @@ -16,17 +18,29 @@ class TestTimestampModel(Model): count = columns.Integer() -class CreateWithTimestampTest(BaseCassEngTestCase): - +class BaseTimestampTest(BaseCassEngTestCase): @classmethod def setUpClass(cls): - super(CreateWithTimestampTest, cls).setUpClass() + super(BaseTimestampTest, cls).setUpClass() sync_table(TestTimestampModel) + +class CreateWithTimestampTest(BaseTimestampTest): + def test_batch(self): pass - def test_non_batch(self): + def test_non_batch_syntax_integration(self): tmp = TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) tmp.should.be.ok + def test_non_batch_syntax_unit(self): + + with mock.patch.object(ConnectionPool, "execute") as m: + TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) + + query = m.call_args[0][0] + + "USING TIMESTAMP".should.be.within(query) + + From a4c439af10439bea496fc52cf1dadad9de0e713d Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 16:18:29 -0800 Subject: [PATCH 0570/3961] ensure we're only operating on a copy of the instance --- cqlengine/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 7b254c9900..869a02012b 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -1,6 +1,6 @@ from collections import OrderedDict import re - +import copy from cqlengine import columns from cqlengine.exceptions import ModelException, CQLEngineException, ValidationError from cqlengine.query import ModelQuerySet, DMLQuery, AbstractQueryableColumn @@ -76,6 +76,7 @@ class TTLDescriptor(object): """ def __get__(self, instance, model): if instance: + instance = copy.deepcopy(instance) # instance method def ttl_setter(ts): instance._ttl = ts @@ -100,6 +101,7 @@ class TimestampDescriptor(object): def __get__(self, instance, model): if instance: # instance method + instance = copy.deepcopy(instance) def timestamp_setter(ts): instance._timestamp = ts return instance @@ -122,6 +124,7 @@ class ConsistencyDescriptor(object): """ def __get__(self, instance, model): if instance: + instance = copy.deepcopy(instance) def consistency_setter(consistency): instance.__consistency__ = consistency return instance From 74e55366d5ac1bc7ee27316da35fdffba4edaeed Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 16:29:41 -0800 Subject: [PATCH 0571/3961] tracking down issues with missing timestamp values --- cqlengine/models.py | 4 +++- cqlengine/query.py | 3 ++- cqlengine/tests/test_timestamp.py | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 869a02012b..a78678e5e2 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -261,7 +261,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #__ttl__ = None # this doesn't seem to be used __consistency__ = None # can be set per query - __timestamp__ = None # optional timestamp to include with the operation + __timestamp__ = None # optional timestamp to include with the operation (USING TIMESTAMP) __read_repair_chance__ = 0.1 @@ -464,6 +464,7 @@ def save(self): self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl, + timestamp=self._timestamp, consistency=self.__consistency__).save() #reset the value managers @@ -498,6 +499,7 @@ def update(self, **values): self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl, + timestamp=self.__timestamp__, consistency=self.__consistency__).update() #reset the value managers diff --git a/cqlengine/query.py b/cqlengine/query.py index fea1ec196c..9de5a745f8 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -686,6 +686,7 @@ class DMLQuery(object): """ _ttl = None _consistency = None + _timestamp = None def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None): self.model = model @@ -804,7 +805,7 @@ def save(self): if self.instance._has_counter or self.instance._can_update(): return self.update() else: - insert = InsertStatement(self.column_family_name, ttl=self._ttl) + insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp) for name, col in self.instance._columns.items(): val = getattr(self.instance, name, None) if col._val_is_null(val): diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 845b97b9aa..c58952c183 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -30,6 +30,11 @@ class CreateWithTimestampTest(BaseTimestampTest): def test_batch(self): pass + def test_timestamp_is_set_on_model_queryset(self): + delta = timedelta(seconds=30) + tmp = TestTimestampModel.timestamp(delta) + tmp._timestamp.should.equal(delta) + def test_non_batch_syntax_integration(self): tmp = TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) tmp.should.be.ok From 67ccf4ce69eb53ef1d350d17141c0ee6ba21de8f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 16:30:42 -0800 Subject: [PATCH 0572/3961] ensuring we store the timestamp on the insert statement --- cqlengine/statements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 815ccfe34f..da1bcb38ae 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -621,11 +621,12 @@ def __unicode__(self): class DeleteStatement(BaseCQLStatement): """ a cql delete statement """ - def __init__(self, table, fields=None, consistency=None, where=None): + def __init__(self, table, fields=None, consistency=None, where=None, timestamp=None): super(DeleteStatement, self).__init__( table, consistency=consistency, where=where, + timestamp=timestamp ) self.fields = [] if isinstance(fields, basestring): From 3ecad5814a8d14b7886f5f29b8d93d6a434f0bbe Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 16:32:50 -0800 Subject: [PATCH 0573/3961] assignment statement saves the timestamp --- cqlengine/statements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index da1bcb38ae..d8d9121e8e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -537,6 +537,7 @@ def __init__(self, where=where, ) self.ttl = ttl + self.timestamp = timestamp # add assignments self.assignments = [] From 826b5b78660e472e91cfe848b51e677c1d0e4ccb Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 16:46:13 -0800 Subject: [PATCH 0574/3961] standard insert seems to work with custom timestamp --- cqlengine/statements.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index d8d9121e8e..1ffc479058 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -447,16 +447,23 @@ def update_context_id(self, i): @property def timestamp_normalized(self): - if isinstance(self.timestamp, int): - return self.timestamp - - if isinstance(self.timestamp, datetime): - # do stuff + """ + we're expecting self.timestamp to be either a long, a datetime, or a timedelta + :return: + """ + if isinstance(self.timestamp, (int, long)): return self.timestamp if isinstance(self.timestamp, timedelta): - # do more stuff - return self.timestamp + tmp = datetime.now() + self.timestamp + else: + tmp = self.timestamp + + epoch = datetime(1970, 1, 1, tzinfo=tmp.tzinfo) + + # do more stuff + offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 + return long(((tmp - epoch).total_seconds() - offset) * 1000) def __unicode__(self): @@ -594,7 +601,7 @@ def __unicode__(self): qs += ["USING TTL {}".format(self.ttl)] if self.timestamp: - qs += ["USING TIMESTAMP {}".format(self.timestamp)] + qs += ["USING TIMESTAMP {}".format(self.timestamp_normalized)] return ' '.join(qs) From 3f52742374930c710362dc5d5ee5226569aa15e9 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 17:18:32 -0800 Subject: [PATCH 0575/3961] standardizing on _timestamp across the board --- cqlengine/models.py | 7 +++++-- cqlengine/statements.py | 3 +++ cqlengine/tests/test_timestamp.py | 18 ++++++++++++++++++ cqlengine/tests/test_ttl.py | 4 ++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index a78678e5e2..294f1a57e4 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -261,13 +261,16 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #__ttl__ = None # this doesn't seem to be used __consistency__ = None # can be set per query - __timestamp__ = None # optional timestamp to include with the operation (USING TIMESTAMP) __read_repair_chance__ = 0.1 + + _timestamp = None # optional timestamp to include with the operation (USING TIMESTAMP) + def __init__(self, **values): self._values = {} self._ttl = None + self._timestamp = None for name, column in self._columns.items(): value = values.get(name, None) @@ -499,7 +502,7 @@ def update(self, **values): self.__dmlquery__(self.__class__, self, batch=self._batch, ttl=self._ttl, - timestamp=self.__timestamp__, + timestamp=self._timestamp, consistency=self.__consistency__).update() #reset the value managers diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 1ffc479058..dd5042d33f 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -451,6 +451,9 @@ def timestamp_normalized(self): we're expecting self.timestamp to be either a long, a datetime, or a timedelta :return: """ + if not self.timestamp: + return None + if isinstance(self.timestamp, (int, long)): return self.timestamp diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index c58952c183..ea0c42a94c 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -30,6 +30,12 @@ class CreateWithTimestampTest(BaseTimestampTest): def test_batch(self): pass + def test_timestamp_not_included_on_normal_create(self): + with mock.patch.object(ConnectionPool, "execute") as m: + TestTimestampModel.create(count=2) + + "USING TIMESTAMP".shouldnt.be.within(m.call_args[0][0]) + def test_timestamp_is_set_on_model_queryset(self): delta = timedelta(seconds=30) tmp = TestTimestampModel.timestamp(delta) @@ -49,3 +55,15 @@ def test_non_batch_syntax_unit(self): "USING TIMESTAMP".should.be.within(query) +class UpdateWithTimestampTest(BaseTimestampTest): + def setUp(self): + self.instance = TestTimestampModel.create(count=1) + + def test_instance_update_includes_timestamp_in_query(self): + + with mock.patch.object(ConnectionPool, "execute") as m: + self.instance.timestamp(timedelta(seconds=30)).update(count=2) + + query = m.call_args[0][0] + + "USING TIMESTAMP".should.be.within(query) diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 0d465ac09a..c08a1a19e0 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -80,13 +80,13 @@ def test_instance_is_returned(self): """ o = TestTTLModel.create(text="whatever") o.text = "new stuff" - o.ttl(60) + o = o.ttl(60) self.assertEqual(60, o._ttl) def test_ttl_is_include_with_query_on_update(self): o = TestTTLModel.create(text="whatever") o.text = "new stuff" - o.ttl(60) + o = o.ttl(60) with mock.patch.object(ConnectionPool, 'execute') as m: o.save() From 44baa39e54c761b60daa3c3b7312d4b1dd86e4fd Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 17:38:46 -0800 Subject: [PATCH 0576/3961] fixed deprecated calls, it seems that timestamp is working but broke batch --- cqlengine/query.py | 4 ++-- cqlengine/statements.py | 10 ++++++++-- cqlengine/tests/test_batch_query.py | 10 ++++++---- cqlengine/tests/test_timestamp.py | 4 +--- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 9de5a745f8..d237e4a721 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -647,7 +647,7 @@ def update(self, **values): return nulled_columns = set() - us = UpdateStatement(self.column_family_name, where=self._where, ttl=self._ttl) + us = UpdateStatement(self.column_family_name, where=self._where, ttl=self._ttl, timestamp=self._timestamp) for name, val in values.items(): col = self.model._columns.get(name) # check for nonexistant columns @@ -746,7 +746,7 @@ def update(self): raise CQLEngineException("DML Query intance attribute is None") assert type(self.instance) == self.model - statement = UpdateStatement(self.column_family_name, ttl=self._ttl) + statement = UpdateStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp) #get defined fields and their column names for name, col in self.model._columns.items(): if not col.is_primary_key: diff --git a/cqlengine/statements.py b/cqlengine/statements.py index dd5042d33f..e004b33bd1 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -615,10 +615,16 @@ class UpdateStatement(AssignmentStatement): def __unicode__(self): qs = ['UPDATE', self.table] + using_options = [] + if self.ttl: - qs += ["USING TTL {}".format(self.ttl)] + using_options += ["TTL {}".format(self.ttl)] + if self.timestamp: - qs += ["USING TIMESTAMP {}".format(self.timestamp)] + using_options += ["TIMESTAMP {}".format(self.timestamp_normalized)] + + if using_options: + qs += ["USING {}".format(" AND ".join(using_options))] qs += ['SET'] qs += [', '.join([unicode(c) for c in self.assignments])] diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index 7ee98b7084..e55c37a20f 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -1,8 +1,10 @@ from unittest import skip from uuid import uuid4 import random +import sure + from cqlengine import Model, columns -from cqlengine.management import delete_table, create_table +from cqlengine.management import drop_table, sync_table from cqlengine.query import BatchQuery from cqlengine.tests.base import BaseCassEngTestCase @@ -17,13 +19,13 @@ class BatchQueryTests(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(BatchQueryTests, cls).setUpClass() - delete_table(TestMultiKeyModel) - create_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) + sync_table(TestMultiKeyModel) @classmethod def tearDownClass(cls): super(BatchQueryTests, cls).tearDownClass() - delete_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) def setUp(self): super(BatchQueryTests, self).setUp() diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index ea0c42a94c..58409b1253 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -64,6 +64,4 @@ def test_instance_update_includes_timestamp_in_query(self): with mock.patch.object(ConnectionPool, "execute") as m: self.instance.timestamp(timedelta(seconds=30)).update(count=2) - query = m.call_args[0][0] - - "USING TIMESTAMP".should.be.within(query) + "USING TIMESTAMP".should.be.within(m.call_args[0][0]) From 2d62e523f1ea638730d4153f0406e53d0f97f55a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 17:51:51 -0800 Subject: [PATCH 0577/3961] working to fix batches --- cqlengine/tests/test_batch_query.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index e55c37a20f..7698ee68bd 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -1,6 +1,9 @@ from unittest import skip from uuid import uuid4 import random +from cqlengine.connection import ConnectionPool + +import mock import sure from cqlengine import Model, columns @@ -38,13 +41,26 @@ def test_insert_success_case(self): b = BatchQuery() inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') + with self.assertRaises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=2) - b.execute() + with mock.patch.object(ConnectionPool, 'execute') as m: + b.execute() + + m.call_count.should.be(1) TestMultiKeyModel.get(partition=self.pkey, cluster=2) + def test_batch_is_executed(self): + b = BatchQuery() + inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') + + with self.assertRaises(TestMultiKeyModel.DoesNotExist): + TestMultiKeyModel.get(partition=self.pkey, cluster=2) + + b.execute() + def test_update_success_case(self): inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4') From 5407f41e951a4a34c483170bf6935fc9b38bdc1a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Dec 2013 17:58:28 -0800 Subject: [PATCH 0578/3961] fixed brokenness... --- cqlengine/models.py | 6 +++--- cqlengine/tests/test_batch_query.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 294f1a57e4..df528746f9 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -76,7 +76,7 @@ class TTLDescriptor(object): """ def __get__(self, instance, model): if instance: - instance = copy.deepcopy(instance) + #instance = copy.deepcopy(instance) # instance method def ttl_setter(ts): instance._ttl = ts @@ -101,7 +101,7 @@ class TimestampDescriptor(object): def __get__(self, instance, model): if instance: # instance method - instance = copy.deepcopy(instance) + #instance = copy.deepcopy(instance) def timestamp_setter(ts): instance._timestamp = ts return instance @@ -124,7 +124,7 @@ class ConsistencyDescriptor(object): """ def __get__(self, instance, model): if instance: - instance = copy.deepcopy(instance) + #instance = copy.deepcopy(instance) def consistency_setter(consistency): instance.__consistency__ = consistency return instance diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index 7698ee68bd..4beb33d9da 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -45,10 +45,8 @@ def test_insert_success_case(self): with self.assertRaises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=2) - with mock.patch.object(ConnectionPool, 'execute') as m: - b.execute() + b.execute() - m.call_count.should.be(1) TestMultiKeyModel.get(partition=self.pkey, cluster=2) From 55cbca4127c396944093219276dc99226be69d1b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Dec 2013 12:42:29 -0800 Subject: [PATCH 0579/3961] delete is pushing timestamp in when it's specified --- cqlengine/models.py | 8 +++++++- cqlengine/query.py | 5 +++-- cqlengine/statements.py | 5 +++-- cqlengine/tests/test_timestamp.py | 23 +++++++++++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index df528746f9..838028f46c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -514,7 +514,7 @@ def update(self, **values): def delete(self): """ Deletes this instance """ - self.__dmlquery__(self.__class__, self, batch=self._batch).delete() + self.__dmlquery__(self.__class__, self, batch=self._batch, timestamp=self._timestamp).delete() def get_changed_columns(self): """ returns a list of the columns that have been updated since instantiation or save """ @@ -528,9 +528,15 @@ def _inst_batch(self, batch): self._batch = batch return self + # def __deepcopy__(self): + # tmp = type(self)() + # tmp.__dict__.update(self.__dict__) + # return tmp + batch = hybrid_classmethod(_class_batch, _inst_batch) + class ModelMetaClass(type): def __new__(cls, name, bases, attrs): diff --git a/cqlengine/query.py b/cqlengine/query.py index d237e4a721..0cad904477 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -526,7 +526,8 @@ def delete(self): dq = DeleteStatement( self.column_family_name, - where=self._where + where=self._where, + timestamp=self._timestamp ) self._execute(dq) @@ -830,7 +831,7 @@ def delete(self): if self.instance is None: raise CQLEngineException("DML Query instance attribute is None") - ds = DeleteStatement(self.column_family_name) + ds = DeleteStatement(self.column_family_name, timestamp=self._timestamp) for name, col in self.model._primary_keys.items(): ds.add_where_clause(WhereClause( col.db_field_name, diff --git a/cqlengine/statements.py b/cqlengine/statements.py index e004b33bd1..766f590317 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -679,11 +679,12 @@ def __unicode__(self): qs += ['FROM', self.table] delete_option = [] + if self.timestamp: - delete_option += ["TIMESTAMP {}".format(self.timestamp)] + delete_option += ["TIMESTAMP {}".format(self.timestamp_normalized)] if delete_option: - qs += ["USING {}".format(" AND ".join(delete_option))] + qs += [" USING {} ".format(" AND ".join(delete_option))] if self.where_clauses: qs += [self._where] diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 58409b1253..43583f6031 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -65,3 +65,26 @@ def test_instance_update_includes_timestamp_in_query(self): self.instance.timestamp(timedelta(seconds=30)).update(count=2) "USING TIMESTAMP".should.be.within(m.call_args[0][0]) + +class DeleteWithTimestampTest(BaseTimestampTest): + def test_non_batch(self): + """ + we don't expect the model to come back at the end because the deletion timestamp should be in the future + """ + uid = uuid4() + tmp = TestTimestampModel.create(id=uid, count=1) + + TestTimestampModel.get(id=uid).should.be.ok + + tmp.timestamp(timedelta(seconds=30)).delete() + + tmp = TestTimestampModel.create(id=uid, count=1) + + with self.assertRaises(TestTimestampModel.DoesNotExist): + TestTimestampModel.get(id=uid) + + + + + + From c393d433c1c766b0f2bb2905acd40a7272616d9f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Dec 2013 15:50:42 -0800 Subject: [PATCH 0580/3961] fixed timezone issue. python must be on same TZ as cassandra when using timedelta and .timestamp --- cqlengine/statements.py | 8 ++------ cqlengine/tests/test_timestamp.py | 5 ++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 766f590317..0e85c9292e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -448,7 +448,7 @@ def update_context_id(self, i): @property def timestamp_normalized(self): """ - we're expecting self.timestamp to be either a long, a datetime, or a timedelta + we're expecting self.timestamp to be either a long, int, a datetime, or a timedelta :return: """ if not self.timestamp: @@ -462,11 +462,7 @@ def timestamp_normalized(self): else: tmp = self.timestamp - epoch = datetime(1970, 1, 1, tzinfo=tmp.tzinfo) - - # do more stuff - offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0 - return long(((tmp - epoch).total_seconds() - offset) * 1000) + return long(((tmp - datetime.fromtimestamp(0)).total_seconds()) * 1000000) def __unicode__(self): diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 43583f6031..4c9901c78d 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -76,7 +76,10 @@ def test_non_batch(self): TestTimestampModel.get(id=uid).should.be.ok - tmp.timestamp(timedelta(seconds=30)).delete() + tmp.timestamp(timedelta(seconds=5)).delete() + + with self.assertRaises(TestTimestampModel.DoesNotExist): + TestTimestampModel.get(id=uid) tmp = TestTimestampModel.create(id=uid, count=1) From a327b462dd9308c01e7ca524780330d90165d6d7 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Dec 2013 16:25:50 -0800 Subject: [PATCH 0581/3961] updated changelog --- changelog | 8 ++++++++ cqlengine/tests/test_timestamp.py | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog b/changelog index 28ce74dff7..806b3c442d 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,13 @@ CHANGELOG +0.11.0 + +* support for USING TIMESTAMP + +0.10.0 + +* support for execute_on_exception within batches + 0.9.2 * fixing create keyspace with network topology strategy * fixing regression with query expressions diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 4c9901c78d..647673b830 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -7,7 +7,7 @@ from uuid import uuid4 import mock import sure -from cqlengine import Model, columns +from cqlengine import Model, columns, BatchQuery from cqlengine.connection import ConnectionPool from cqlengine.management import sync_table from cqlengine.tests.base import BaseCassEngTestCase @@ -25,10 +25,12 @@ def setUpClass(cls): sync_table(TestTimestampModel) + + class CreateWithTimestampTest(BaseTimestampTest): def test_batch(self): - pass + assert False def test_timestamp_not_included_on_normal_create(self): with mock.patch.object(ConnectionPool, "execute") as m: From 817e386f35c0ea69b9f4e6d99de17a42d7a3a1c5 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Dec 2013 16:26:06 -0800 Subject: [PATCH 0582/3961] changelog --- changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog b/changelog index 806b3c442d..cca5fbd101 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,6 @@ CHANGELOG -0.11.0 +0.11.0 (in progress) * support for USING TIMESTAMP From 6cb5a6caa3985aa3ff2ae437ad788c5b7213d5f4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Dec 2013 17:26:08 -0800 Subject: [PATCH 0583/3961] fixed batch queries --- changelog | 2 ++ cqlengine/query.py | 16 ++++++++++++---- cqlengine/tests/test_timestamp.py | 6 ++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index cca5fbd101..0764800fb2 100644 --- a/changelog +++ b/changelog @@ -3,6 +3,8 @@ CHANGELOG 0.11.0 (in progress) * support for USING TIMESTAMP + - allows for long, timedelta, and datetime +* fixed use of USING TIMESTAMP in batches 0.10.0 diff --git a/cqlengine/query.py b/cqlengine/query.py index 0cad904477..a21720fcf9 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,5 +1,5 @@ import copy -from datetime import datetime +from datetime import datetime, timedelta from cqlengine import BaseContainerColumn, Map, columns from cqlengine.columns import Counter, List, Set @@ -82,7 +82,7 @@ class BatchQuery(object): def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on_exception=False): self.queries = [] self.batch_type = batch_type - if timestamp is not None and not isinstance(timestamp, datetime): + if timestamp is not None and not isinstance(timestamp, (datetime, timedelta)): raise CQLEngineException('timestamp object must be an instance of datetime') self.timestamp = timestamp self._consistency = consistency @@ -103,8 +103,16 @@ def execute(self): opener = 'BEGIN ' + (self.batch_type + ' ' if self.batch_type else '') + ' BATCH' if self.timestamp: - epoch = datetime(1970, 1, 1) - ts = long((self.timestamp - epoch).total_seconds() * 1000) + + if isinstance(self.timestamp, (int, long)): + ts = self.timestamp + elif isinstance(self.timestamp, timedelta): + ts = long((datetime.now() + self.timestamp - datetime.fromtimestamp(0)).total_seconds() * 1000000) + elif isinstance(self.timestamp, datetime): + ts = long((self.timestamp - datetime.fromtimestamp(0)).total_seconds() * 1000000) + else: + raise ValueError("Batch expects a long, a timedelta, or a datetime") + opener += ' USING TIMESTAMP {}'.format(ts) query_list = [opener] diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 647673b830..386d6db1ff 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -24,6 +24,12 @@ def setUpClass(cls): super(BaseTimestampTest, cls).setUpClass() sync_table(TestTimestampModel) +class BatchTest(BaseTimestampTest): + def test_batch_is_included(self): + with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery(timestamp=timedelta(seconds=30)) as b: + TestTimestampModel.batch(b).create(count=1) + + "USING TIMESTAMP".should.be.within(m.call_args[0][0]) From 97b439bf9507c3f072b1b5a00f41d24102ae33fb Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 20 Dec 2013 14:37:45 -0800 Subject: [PATCH 0584/3961] timestamp working in batch mode --- cqlengine/tests/test_timestamp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 386d6db1ff..33c2a9c2a9 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -36,7 +36,11 @@ def test_batch_is_included(self): class CreateWithTimestampTest(BaseTimestampTest): def test_batch(self): - assert False + with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery() as b: + TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1) + + "USING TIMESTAMP".should.be.within(m.call_args[0][0]) + def test_timestamp_not_included_on_normal_create(self): with mock.patch.object(ConnectionPool, "execute") as m: From ee4f9af63400faa0753d67b23ec77f9f96316d5e Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 20 Dec 2013 15:38:46 -0800 Subject: [PATCH 0585/3961] use a regex instead of simple text match for better error catching --- cqlengine/tests/test_timestamp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 33c2a9c2a9..0bb6a8ca90 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -39,7 +39,7 @@ def test_batch(self): with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery() as b: TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1) - "USING TIMESTAMP".should.be.within(m.call_args[0][0]) + m.call_args[0][0].should.match(r"INSERT.*USING TIMESTAMP") def test_timestamp_not_included_on_normal_create(self): From ea203929a4cdcdf6ec47f7d8204ab29da4e3585f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 20 Dec 2013 15:41:52 -0800 Subject: [PATCH 0586/3961] more testing around our query --- cqlengine/tests/test_timestamp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 0bb6a8ca90..32bebcd252 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -39,7 +39,11 @@ def test_batch(self): with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery() as b: TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1) - m.call_args[0][0].should.match(r"INSERT.*USING TIMESTAMP") + query = m.call_args[0][0] + + query.should.match(r"INSERT.*USING TIMESTAMP") + query.should_not.match(r"TIMESTAMP.*INSERT") + def test_timestamp_not_included_on_normal_create(self): From be20a9a9f03d7b6e80ead12a0bd423d26162ff12 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 20 Dec 2013 15:57:19 -0800 Subject: [PATCH 0587/3961] checked batch updates for timestamp --- cqlengine/tests/test_timestamp.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 32bebcd252..6aec5702fd 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -40,7 +40,7 @@ def test_batch(self): TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1) query = m.call_args[0][0] - + query.should.match(r"INSERT.*USING TIMESTAMP") query.should_not.match(r"TIMESTAMP.*INSERT") @@ -76,12 +76,20 @@ def setUp(self): self.instance = TestTimestampModel.create(count=1) def test_instance_update_includes_timestamp_in_query(self): + # not a batch with mock.patch.object(ConnectionPool, "execute") as m: self.instance.timestamp(timedelta(seconds=30)).update(count=2) "USING TIMESTAMP".should.be.within(m.call_args[0][0]) + def test_instance_update_in_batch(self): + with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery() as b: + self.instance.batch(b).timestamp(timedelta(seconds=30)).update(count=2) + + query = m.call_args[0][0] + "USING TIMESTAMP".should.be.within(query) + class DeleteWithTimestampTest(BaseTimestampTest): def test_non_batch(self): """ From ba22b4c9b62618f83e952b0bf60615b8f8dba426 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 20 Dec 2013 16:26:25 -0800 Subject: [PATCH 0588/3961] blind deletes function properly --- cqlengine/query.py | 5 +++++ cqlengine/tests/test_timestamp.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/cqlengine/query.py b/cqlengine/query.py index a21720fcf9..f991f5e9cb 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -650,6 +650,11 @@ def ttl(self, ttl): clone._ttl = ttl return clone + def timestamp(self, timestamp): + clone = copy.deepcopy(self) + clone._timestamp = timestamp + return clone + def update(self, **values): """ Updates the rows in this queryset """ if not values: diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 6aec5702fd..48ff93d5be 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -111,6 +111,24 @@ def test_non_batch(self): TestTimestampModel.get(id=uid) + def test_blind_delete(self): + """ + we don't expect the model to come back at the end because the deletion timestamp should be in the future + """ + uid = uuid4() + tmp = TestTimestampModel.create(id=uid, count=1) + + TestTimestampModel.get(id=uid).should.be.ok + + TestTimestampModel.objects(id=uid).timestamp(timedelta(seconds=5)).delete() + + with self.assertRaises(TestTimestampModel.DoesNotExist): + TestTimestampModel.get(id=uid) + + tmp = TestTimestampModel.create(id=uid, count=1) + + with self.assertRaises(TestTimestampModel.DoesNotExist): + TestTimestampModel.get(id=uid) From c0eb900ede9e6859e888fe137534834b63740e04 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 20 Dec 2013 16:38:27 -0800 Subject: [PATCH 0589/3961] updated docs with timestamp information, test that timestamps in the past work properly --- cqlengine/tests/test_timestamp.py | 35 ++++++++++++++++++++++++++++++- docs/topics/models.rst | 4 ++++ docs/topics/queryset.rst | 3 +++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 48ff93d5be..33ec3624ef 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -1,7 +1,7 @@ """ Tests surrounding the blah.timestamp( timedelta(seconds=30) ) format. """ -from datetime import timedelta +from datetime import timedelta, datetime import unittest from uuid import uuid4 @@ -130,6 +130,39 @@ def test_blind_delete(self): with self.assertRaises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) + def test_blind_delete_with_datetime(self): + """ + we don't expect the model to come back at the end because the deletion timestamp should be in the future + """ + uid = uuid4() + tmp = TestTimestampModel.create(id=uid, count=1) + + TestTimestampModel.get(id=uid).should.be.ok + + plus_five_seconds = datetime.now() + timedelta(seconds=5) + + TestTimestampModel.objects(id=uid).timestamp(plus_five_seconds).delete() + + with self.assertRaises(TestTimestampModel.DoesNotExist): + TestTimestampModel.get(id=uid) + + tmp = TestTimestampModel.create(id=uid, count=1) + + with self.assertRaises(TestTimestampModel.DoesNotExist): + TestTimestampModel.get(id=uid) + + def test_delete_in_the_past(self): + uid = uuid4() + tmp = TestTimestampModel.create(id=uid, count=1) + + TestTimestampModel.get(id=uid).should.be.ok + + # delete the in past, should not affect the object created above + TestTimestampModel.objects(id=uid).timestamp(timedelta(seconds=-60)).delete() + + TestTimestampModel.get(id=uid) + + diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 1fae6070d4..b34d7f5332 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -141,6 +141,10 @@ Model Methods Sets the batch object to run instance updates and inserts queries with. + .. method:: timestamp(timedelta_or_datetime) + + Sets the timestamp for the query + .. method:: ttl(ttl_in_sec) Sets the ttl values to run instance updates and inserts queries with. diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 4cf0d20488..569fb58103 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -377,6 +377,9 @@ QuerySet method reference Enables the (usually) unwise practive of querying on a clustering key without also defining a partition key + .. method:: timestamp(timestamp_or_long_or_datetime) + + Allows for custom timestamps to be saved with the record. .. method:: ttl(ttl_in_seconds) From b541d3a420983c4103f82508808189b99bda92ca Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Mon, 30 Dec 2013 14:58:58 -0800 Subject: [PATCH 0590/3961] making connection test imports play nice with nose --- cqlengine/tests/connections/test_connection_setup.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cqlengine/tests/connections/test_connection_setup.py b/cqlengine/tests/connections/test_connection_setup.py index 5f1f7b774c..e07820f93c 100644 --- a/cqlengine/tests/connections/test_connection_setup.py +++ b/cqlengine/tests/connections/test_connection_setup.py @@ -1,20 +1,22 @@ from unittest import TestCase -from mock import MagicMock, patch, Mock +from mock import patch -from cqlengine.connection import setup, CQLConnectionError, Host +from cqlengine.connection import setup as setup_connection +from cqlengine.connection import CQLConnectionError, Host class OperationalErrorLoggingTest(TestCase): + @patch('cqlengine.connection.ConnectionPool', return_value=None, autospec=True) def test_setup_hosts(self, PatchedConnectionPool): with self.assertRaises(CQLConnectionError): - setup(hosts=['localhost:abcd']) + setup_connection(hosts=['localhost:abcd']) self.assertEqual(len(PatchedConnectionPool.mock_calls), 0) with self.assertRaises(CQLConnectionError): - setup(hosts=['localhost:9160:abcd']) + setup_connection(hosts=['localhost:9160:abcd']) self.assertEqual(len(PatchedConnectionPool.mock_calls), 0) - setup(hosts=['localhost:9161', 'remotehost']) + setup_connection(hosts=['localhost:9161', 'remotehost']) self.assertEqual(len(PatchedConnectionPool.mock_calls), 1) self.assertEqual(PatchedConnectionPool.call_args[0][0], [Host('localhost', 9161), Host('remotehost', 9160)]) From c28ec3fab47b41c0580f5a5b7844d2afe5b68d39 Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Thu, 9 Jan 2014 14:44:53 -0800 Subject: [PATCH 0591/3961] accept uuid strings with or without dashes before converting to python UUID type --- cqlengine/columns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 11a9f60dcf..4ba74273b1 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -372,7 +372,7 @@ class UUID(Column): """ db_type = 'uuid' - re_uuid = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') + re_uuid = re.compile(r'[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}') def validate(self, value): val = super(UUID, self).validate(value) From fb73405e58d14716f2d37b51fe3ea1d41f7cda68 Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Fri, 10 Jan 2014 08:53:32 -0800 Subject: [PATCH 0592/3961] added test for uuids without dashes --- cqlengine/tests/columns/test_validation.py | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 61020e27a5..4232b4003c 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -162,6 +162,33 @@ def test_decimal_io(self): dt2 = self.DecimalTest.objects(test_id=0).first() assert dt2.dec_val == D('5') +class TestUUID(BaseCassEngTestCase): + class UUIDTest(Model): + test_id = Integer(primary_key=True) + a_uuid = UUID(default=uuid4()) + + @classmethod + def setUpClass(cls): + super(TestUUID, cls).setUpClass() + create_table(cls.UUIDTest) + + @classmethod + def tearDownClass(cls): + super(TestUUID, cls).tearDownClass() + delete_table(cls.UUIDTest) + + def test_uuid_str_with_dashes(self): + a_uuid = uuid4() + t0 = self.UUIDTest.create(test_id=0, a_uuid=str(a_uuid)) + t1 = self.UUIDTest.get(test_id=0) + assert a_uuid == t1.a_uuid + + def test_uuid_str_no_dashes(self): + a_uuid = uuid4() + t0 = self.UUIDTest.create(test_id=1, a_uuid=a_uuid.hex) + t1 = self.UUIDTest.get(test_id=1) + assert a_uuid == t1.a_uuid + class TestTimeUUID(BaseCassEngTestCase): class TimeUUIDTest(Model): test_id = Integer(primary_key=True) From cf369e848e6d7e687a22823b66740633f58a3de4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 14 Jan 2014 12:18:39 -0800 Subject: [PATCH 0593/3961] reset ttl and timestamp after save / update --- cqlengine/models.py | 10 ++++++---- cqlengine/tests/test_timestamp.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 838028f46c..4e170d2dd0 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -475,6 +475,9 @@ def save(self): v.reset_previous_value() self._is_persisted = True + self._ttl = None + self._timestamp = None + return self def update(self, **values): @@ -510,6 +513,9 @@ def update(self, **values): v.reset_previous_value() self._is_persisted = True + self._ttl = None + self._timestamp = None + return self def delete(self): @@ -528,10 +534,6 @@ def _inst_batch(self, batch): self._batch = batch return self - # def __deepcopy__(self): - # tmp = type(self)() - # tmp.__dict__.update(self.__dict__) - # return tmp batch = hybrid_classmethod(_class_batch, _inst_batch) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 33ec3624ef..a5d9b870cc 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -110,6 +110,21 @@ def test_non_batch(self): with self.assertRaises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) + # calling .timestamp sets the TS on the model + tmp.timestamp(timedelta(seconds=5)) + tmp._timestamp.should.be.ok + + # calling save clears the set timestamp + tmp.save() + tmp._timestamp.shouldnt.be.ok + + tmp.timestamp(timedelta(seconds=5)) + tmp.update() + tmp._timestamp.shouldnt.be.ok + + + + def test_blind_delete(self): """ From 1a0325443dda9ae94e9ee681d420375faafd84bc Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 14 Jan 2014 12:22:02 -0800 Subject: [PATCH 0594/3961] changelog update --- changelog | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 0764800fb2..c9e9b0ff51 100644 --- a/changelog +++ b/changelog @@ -2,9 +2,10 @@ CHANGELOG 0.11.0 (in progress) -* support for USING TIMESTAMP +* support for USING TIMESTAMP via a .timestamp(timedelta(seconds=30)) syntax - allows for long, timedelta, and datetime * fixed use of USING TIMESTAMP in batches +* clear TTL and timestamp off models after persisting to DB 0.10.0 From 516b16ee5610a32f9926e2d99bebb464f0104e6f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 15 Jan 2014 12:37:57 -0800 Subject: [PATCH 0595/3961] simplified return of the timestamp on the class --- cqlengine/models.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 4e170d2dd0..af5da3ffe1 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -1,6 +1,5 @@ from collections import OrderedDict import re -import copy from cqlengine import columns from cqlengine.exceptions import ModelException, CQLEngineException, ValidationError from cqlengine.query import ModelQuerySet, DMLQuery, AbstractQueryableColumn @@ -101,19 +100,13 @@ class TimestampDescriptor(object): def __get__(self, instance, model): if instance: # instance method - #instance = copy.deepcopy(instance) def timestamp_setter(ts): instance._timestamp = ts return instance return timestamp_setter - qs = model.__queryset__(model) - - def timestamp_setter(ts): - qs._timestamp = ts - return qs + return model.objects.timestamp - return timestamp_setter def __call__(self, *args, **kwargs): raise NotImplementedError From 19777eb68e75bab674ccc4e75d6aef2837ef821a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 15 Jan 2014 12:42:47 -0800 Subject: [PATCH 0596/3961] release process --- RELEASE.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 RELEASE.txt diff --git a/RELEASE.txt b/RELEASE.txt new file mode 100644 index 0000000000..c4def8ddce --- /dev/null +++ b/RELEASE.txt @@ -0,0 +1,7 @@ +Check changelog +Ensure docs are updated +Tests pass +Update VERSION +Push tag to github +Push release to pypi + From 701099e69db492c081309f3c27243efdf1409136 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 15 Jan 2014 13:47:32 -0800 Subject: [PATCH 0597/3961] updated changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index c9e9b0ff51..71d3dd469f 100644 --- a/changelog +++ b/changelog @@ -6,6 +6,7 @@ CHANGELOG - allows for long, timedelta, and datetime * fixed use of USING TIMESTAMP in batches * clear TTL and timestamp off models after persisting to DB +* allows UUID without - (Thanks to Michael Haddad, github.com/mahall) 0.10.0 From 5a0f945294a1b7120a745727951ed25f52c78e2f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 15 Jan 2014 15:53:07 -0800 Subject: [PATCH 0598/3961] updated changelog with note for dokai's fix to table settings --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index 71d3dd469f..a908f72092 100644 --- a/changelog +++ b/changelog @@ -7,6 +7,7 @@ CHANGELOG * fixed use of USING TIMESTAMP in batches * clear TTL and timestamp off models after persisting to DB * allows UUID without - (Thanks to Michael Haddad, github.com/mahall) +* fixes regarding syncing schema settings (thanks Kai Lautaportti github.com/dokai) 0.10.0 From 83d26c2f8d4381fb81ea4c3541f02fe21bc4b734 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 15 Jan 2014 15:58:26 -0800 Subject: [PATCH 0599/3961] test around delete consistency #148 --- cqlengine/models.py | 2 +- cqlengine/tests/test_consistency.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 57a9f2df24..4f70c3eed3 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -513,7 +513,7 @@ def update(self, **values): def delete(self): """ Deletes this instance """ - self.__dmlquery__(self.__class__, self, batch=self._batch, timestamp=self._timestamp).delete() + self.__dmlquery__(self.__class__, self, batch=self._batch, timestamp=self._timestamp, consistency=self.__consistency__).delete() def get_changed_columns(self): """ returns a list of the columns that have been updated since instantiation or save """ diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 94a236aaf8..2f54ae243c 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -76,3 +76,19 @@ def test_blind_update(self): args = m.call_args self.assertEqual(ALL, args[0][2]) + + + def test_delete(self): + # ensures we always carry consistency through on delete statements + t = TestConsistencyModel.create(text="bacon and eggs") + t.text = "ham and cheese sandwich" + uid = t.id + + with mock.patch.object(ConnectionPool, 'execute') as m: + t.consistency(ALL).delete() + + with mock.patch.object(ConnectionPool, 'execute') as m: + TestConsistencyModel.objects(id=uid).consistency(ALL).delete() + + args = m.call_args + self.assertEqual(ALL, args[0][2]) From e4e4bb7d322c5ad946a7c5e9b89b1630b14903c0 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 15 Jan 2014 17:30:49 -0800 Subject: [PATCH 0600/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 78bc1abd14..d9df1bbc0c 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.10.0 +0.11.0 From ac2dd3aa5bed561e0ca708466d2a991207c83b54 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Wed, 22 Jan 2014 11:53:34 +0100 Subject: [PATCH 0601/3961] Use validation method to normalize Boolean values. --- cqlengine/columns.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 4ba74273b1..f899f92af6 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -442,11 +442,15 @@ class Quoter(ValueQuoter): def __str__(self): return 'true' if self.value else 'false' - def to_python(self, value): + def validate(self, value): + """ Always returns a Python boolean. """ return bool(value) + def to_python(self, value): + return self.validate(value) + def to_database(self, value): - return self.Quoter(bool(value)) + return self.Quoter(self.validate(value)) class Float(Column): From 3d2268c6e8f94d7aa19699efb205643661ddb547 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Wed, 22 Jan 2014 11:54:27 +0100 Subject: [PATCH 0602/3961] Unquote boolean values when normalizing. --- cqlengine/columns.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index f899f92af6..2452a91c9c 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -444,6 +444,8 @@ def __str__(self): def validate(self, value): """ Always returns a Python boolean. """ + if isinstance(value, self.Quoter): + value = value.value return bool(value) def to_python(self, value): From b768b6e6b86f7e790f136c5fa83a9b29352a9b0f Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Wed, 22 Jan 2014 11:56:57 +0100 Subject: [PATCH 0603/3961] Update changelog. --- changelog | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index a908f72092..1284fa6015 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,10 @@ CHANGELOG -0.11.0 (in progress) +0.11.1 (in progress) + +* Normalize and unquote boolean values. + +0.11.0 * support for USING TIMESTAMP via a .timestamp(timedelta(seconds=30)) syntax - allows for long, timedelta, and datetime From af8662d87efd37b06f0da0b30b06eeeb03711f9c Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Wed, 22 Jan 2014 11:57:51 +0100 Subject: [PATCH 0604/3961] Add myself to the list of contributors. --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index a220cb26d6..70cdaeaf80 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,5 +6,5 @@ Jon Haddad CONTRIBUTORS Eric Scrivner - test environment, connection pooling - +Kevin Deldycke From 70d866018d3fc56e960d0f7746a5a6d33f8aadd8 Mon Sep 17 00:00:00 2001 From: drewlll2ll Date: Wed, 22 Jan 2014 11:08:16 -0500 Subject: [PATCH 0605/3961] Fixed incompatibilities w/ cassandra 2.0 --- cqlengine/columns.py | 14 ++++++++++++-- cqlengine/statements.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 11a9f60dcf..2ee93ecd0a 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -5,6 +5,7 @@ import re from uuid import uuid1, uuid4 from cql.query import cql_quote +from cql.cqltypes import DateType from cqlengine.exceptions import ValidationError @@ -209,7 +210,11 @@ class Bytes(Column): def to_database(self, value): val = super(Bytes, self).to_database(value) if val is None: return - return val.encode('hex') + return '0x' + val.encode('hex') + + def to_python(self, value): + #return value[2:].decode('hex') + return value class Ascii(Column): @@ -326,7 +331,10 @@ def to_python(self, value): return value elif isinstance(value, date): return datetime(*(value.timetuple()[:6])) - return datetime.utcfromtimestamp(value) + try: + return datetime.utcfromtimestamp(value) + except TypeError: + return datetime.utcfromtimestamp(DateType.deserialize(value)) def to_database(self, value): value = super(DateTime, self).to_database(value) @@ -352,6 +360,8 @@ def to_python(self, value): return value.date() elif isinstance(value, date): return value + else: + value = DateType.deserialize(value) return datetime.utcfromtimestamp(value).date() diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 45f3fabead..014939ba86 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -5,6 +5,25 @@ class StatementException(Exception): pass +# Monkey patch cql_quote to allow raw hex values +import cql.query + + +def cql_quote_replacement(term): + if isinstance(term, basestring) and term.startswith('0x'): + if isinstance(term, unicode): + return term.encode('utf8') + else: + return term + elif isinstance(term, unicode): + return "'%s'" % cql.query.__escape_quotes(term.encode('utf8')) + elif isinstance(term, (str, bool)): + return "'%s'" % cql.query.__escape_quotes(str(term)) + else: + return str(term) +cql.query.cql_quote = cql_quote_replacement + + class ValueQuoter(object): def __init__(self, value): From c533ae61996447fdc035c9a9561a3a26e546a3ed Mon Sep 17 00:00:00 2001 From: drewlll2ll Date: Wed, 22 Jan 2014 11:32:59 -0500 Subject: [PATCH 0606/3961] Fixed Date column --- cqlengine/columns.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 2ee93ecd0a..7365d2b21a 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -353,17 +353,16 @@ def to_database(self, value): class Date(Column): db_type = 'timestamp' - def to_python(self, value): if value is None: return if isinstance(value, datetime): return value.date() elif isinstance(value, date): return value - else: - value = DateType.deserialize(value) - - return datetime.utcfromtimestamp(value).date() + try: + return datetime.utcfromtimestamp(value).date() + except TypeError: + return datetime.utcfromtimestamp(DateType.deserialize(value)).date() def to_database(self, value): value = super(Date, self).to_database(value) From a2a532aa5aac0c6156d14eed64afedfec082d1aa Mon Sep 17 00:00:00 2001 From: drewlll2ll Date: Wed, 22 Jan 2014 12:11:03 -0500 Subject: [PATCH 0607/3961] Updated get_fields to be 2.0 compatible --- cqlengine/management.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 6d41489e6f..0eb3bdf390 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -228,13 +228,19 @@ def get_fields(model): col_family = model.column_family_name(include_keyspace=False) with connection_manager() as con: - query = "SELECT column_name, validator FROM system.schema_columns \ + query = "SELECT * FROM system.schema_columns \ WHERE keyspace_name = :ks_name AND columnfamily_name = :col_family" logger.debug("get_fields %s %s", ks_name, col_family) tmp = con.execute(query, {'ks_name': ks_name, 'col_family': col_family}, ONE) - return [Field(x[0], x[1]) for x in tmp.results] + + column_indices = [tmp.columns.index('column_name'), tmp.columns.index('validator')] + try: + type_index = tmp.columns.index('type') + return [Field(x[column_indices[0]], x[column_indices[1]]) for x in tmp.results if x[type_index] == 'regular'] + except ValueError: + return [Field(x[column_indices[0]], x[column_indices[1]]) for x in tmp.results] # convert to Field named tuples From 74bc38e7e73f3a4a03203052c1fc0b5dc2f3f8d3 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 23 Jan 2014 10:46:49 +0100 Subject: [PATCH 0608/3961] Unit test boolean value quoting. --- cqlengine/tests/columns/test_value_io.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 54d8f5d5c5..c4958f40dd 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -144,6 +144,19 @@ class TestBooleanIO(BaseColumnIOTest): pkey_val = True data_val = False + def comparator_converter(self, val): + return val.value if isinstance(val, columns.Boolean.Quoter) else val + +class TestBooleanQuoter(BaseColumnIOTest): + + column = columns.Boolean + + pkey_val = True + data_val = columns.Boolean.Quoter(False) + + def comparator_converter(self, val): + return val.value if isinstance(val, columns.Boolean.Quoter) else val + class TestFloatIO(BaseColumnIOTest): column = columns.Float From cda56c117ba77977b9d2f14ba7ca85229445565d Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Mon, 27 Jan 2014 18:54:43 +0200 Subject: [PATCH 0609/3961] fix (rare) race condition in ConnectionPool.get() --- cqlengine/connection.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 01b94023fa..6872bb8e97 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -3,7 +3,11 @@ #http://cassandra.apache.org/doc/cql/CQL.html from collections import namedtuple -import Queue +try: + import Queue as queue +except ImportError: + # python 3 + import queue import random import cql @@ -118,7 +122,7 @@ def __init__( self._consistency = consistency self._timeout = timeout - self._queue = Queue.Queue(maxsize=_max_connections) + self._queue = queue.Queue(maxsize=_max_connections) def clear(self): """ @@ -138,11 +142,14 @@ def get(self): a new one. """ try: - if self._queue.empty(): - return self._create_connection() + # get with blocking=False (default) returns an item if one + # is immediately available, else raises the Empty exception return self._queue.get() - except CQLConnectionError as cqle: - raise cqle + except queue.Empty: + try: + return self._create_connection() + except CQLConnectionError as cqle: + raise cqle def put(self, conn): """ From 1b0cbe27d59e1869b8de410de5b52ca06620db15 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Sun, 2 Feb 2014 13:46:25 +0200 Subject: [PATCH 0610/3961] connection queue - issue non-blocking get to pool queue --- cqlengine/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 6872bb8e97..2eb2d30ef9 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -142,9 +142,9 @@ def get(self): a new one. """ try: - # get with blocking=False (default) returns an item if one + # get with block=False returns an item if one # is immediately available, else raises the Empty exception - return self._queue.get() + return self._queue.get(block=False) except queue.Empty: try: return self._create_connection() From 79fc15ecd825af9a3884c254407065d32b17c296 Mon Sep 17 00:00:00 2001 From: Travis Glines Date: Thu, 6 Feb 2014 16:04:02 -0800 Subject: [PATCH 0611/3961] Added test to verify limit immutability and deep copy in queryset --- cqlengine/tests/query/test_queryset.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 97e6033551..fbe2affdcc 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -127,6 +127,20 @@ def test_queryset_is_immutable(self): assert len(query2._where) == 2 assert len(query1._where) == 1 + def test_queryset_limit_immutability(self): + """ + Tests that calling a queryset function that changes it's state returns a new queryset with same limit + """ + query1 = TestModel.objects(test_id=5).limit(1) + assert query1._limit == 1 + + query2 = query1.filter(expected_result__gte=1) + assert query2._limit == 1 + + query3 = query1.filter(expected_result__gte=1).limit(2) + assert query1._limit == 1 + assert query3._limit == 2 + def test_the_all_method_duplicates_queryset(self): """ Tests that calling all on a queryset with previously defined filters duplicates queryset From 3cf722a963652dc4f6ac45456521b9e133f95a49 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Fri, 7 Feb 2014 16:27:22 +0100 Subject: [PATCH 0612/3961] Allow acces to instance columns as if it is a dict. --- changelog | 1 + cqlengine/models.py | 31 ++++++++++++++++++++++++++ cqlengine/tests/model/test_model_io.py | 21 +++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/changelog b/changelog index a908f72092..8f14d773cf 100644 --- a/changelog +++ b/changelog @@ -8,6 +8,7 @@ CHANGELOG * clear TTL and timestamp off models after persisting to DB * allows UUID without - (Thanks to Michael Haddad, github.com/mahall) * fixes regarding syncing schema settings (thanks Kai Lautaportti github.com/dokai) +* allow acces to instance columns as if it is a dict 0.10.0 diff --git a/cqlengine/models.py b/cqlengine/models.py index 4f70c3eed3..27f0316bb2 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -421,6 +421,37 @@ def validate(self): val = col.validate(getattr(self, name)) setattr(self, name, val) + ### Let an instance be used like a dict of its columns keys/values + + def __iter__(self): + """ Iterate over column ids. """ + for column_id in self._columns.keys(): + yield column_id + + def __getitem__(self, key): + """ Returns column's value. """ + if not isinstance(key, basestring): + raise TypeError + if key not in self._columns.keys(): + raise KeyError + return getattr(self, key) + + def __len__(self): + """ Returns the number of columns defined on that model. """ + return len(self._columns.keys()) + + def keys(self): + """ Returns list of column's IDs. """ + return [k for k in self] + + def values(self): + """ Returns list of column's values. """ + return [self[k] for k in self] + + def items(self): + """ Returns a dictionnary of columns's IDs/values. """ + return [(k, self[k]) for k in self] + def _as_dict(self): """ Returns a map of column names to cleaned values """ values = self._dynamic_columns or {} diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 82abef74ba..512aa2324f 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -1,6 +1,7 @@ from uuid import uuid4 import random from datetime import date +from operator import itemgetter from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import create_table @@ -43,6 +44,26 @@ def test_model_save_and_load(self): for cname in tm._columns.keys(): self.assertEquals(getattr(tm, cname), getattr(tm2, cname)) + def test_model_read_as_dict(self): + """ + Tests that columns of an instance can be read as a dict. + """ + tm = TestModel.create(count=8, text='123456789', a_bool=True) + column_dict = { + 'id': tm.id, + 'count': tm.count, + 'text': tm.text, + 'a_bool': tm.a_bool, + } + self.assertEquals(sorted(tm.keys()), sorted(column_dict.keys())) + self.assertEquals(sorted(tm.values()), sorted(column_dict.values())) + self.assertEquals( + sorted(tm.items(), key=itemgetter(0)), + sorted(column_dict.items(), key=itemgetter(0))) + self.assertEquals(len(tm), len(column_dict)) + for column_id in column_dict.keys(): + self.assertEqual(tm[column_id], column_dict[column_id]) + def test_model_updating_works_properly(self): """ Tests that subsequent saves after initial model creation work From ef5745bc5f85f613a663ed1d2102320e1f0acd1b Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Fri, 7 Feb 2014 16:31:39 +0100 Subject: [PATCH 0613/3961] Fix comment. --- cqlengine/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 27f0316bb2..5655c9df54 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -449,7 +449,7 @@ def values(self): return [self[k] for k in self] def items(self): - """ Returns a dictionnary of columns's IDs/values. """ + """ Returns a list of columns's IDs/values. """ return [(k, self[k]) for k in self] def _as_dict(self): From 95d99bf35982a75df961522367a0122effec4f24 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Sun, 9 Feb 2014 14:39:26 -0500 Subject: [PATCH 0614/3961] Implements the ability to blind update add to a set. --- cqlengine/query.py | 20 +++++++++++++++----- cqlengine/statements.py | 9 ++++++--- cqlengine/tests/query/test_updates.py | 12 +++++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index f991f5e9cb..f226a70ed4 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -663,24 +663,34 @@ def update(self, **values): nulled_columns = set() us = UpdateStatement(self.column_family_name, where=self._where, ttl=self._ttl, timestamp=self._timestamp) for name, val in values.items(): - col = self.model._columns.get(name) + col_name, col_op = self._parse_filter_arg(name) + col = self.model._columns.get(col_name) # check for nonexistant columns if col is None: - raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.model.__name__, name)) + raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.model.__name__, col_name)) # check for primary key update attempts if col.is_primary_key: - raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(name, self.__module__, self.model.__name__)) + raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(col_name, self.__module__, self.model.__name__)) val = col.validate(val) if val is None: - nulled_columns.add(name) + nulled_columns.add(col_name) continue + # add the update statements if isinstance(col, Counter): # TODO: implement counter updates raise NotImplementedError + elif isinstance(col, BaseContainerColumn): + if isinstance(col, List): klass = ListUpdateClause + elif isinstance(col, Map): klass = MapUpdateClause + elif isinstance(col, Set): klass = SetUpdateClause + else: raise RuntimeError + us.add_assignment_clause(klass( + col_name, col.to_database(val), operation=col_op)) else: - us.add_assignment_clause(AssignmentClause(name, col.to_database(val))) + us.add_assignment_clause(AssignmentClause( + col_name, col.to_database(val))) if us.assignments: self._execute(us) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 0e85c9292e..ba4d7061c1 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -136,10 +136,11 @@ def insert_tuple(self): class ContainerUpdateClause(AssignmentClause): - def __init__(self, field, value, previous=None, column=None): + def __init__(self, field, value, operation=None, previous=None, column=None): super(ContainerUpdateClause, self).__init__(field, value) self.previous = previous self._assignments = None + self._operation = operation self._analyzed = False self._column = column @@ -159,8 +160,8 @@ def update_context(self, ctx): class SetUpdateClause(ContainerUpdateClause): """ updates a set collection """ - def __init__(self, field, value, previous=None, column=None): - super(SetUpdateClause, self).__init__(field, value, previous, column=column) + def __init__(self, field, value, operation=None, previous=None, column=None): + super(SetUpdateClause, self).__init__(field, value, operation, previous, column=column) self._additions = None self._removals = None @@ -182,6 +183,8 @@ def _analyze(self): """ works out the updates to be performed """ if self.value is None or self.value == self.previous: pass + elif self._operation == "add": + self._additions = self.value elif self.previous is None: self._assignments = self.value else: diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index b54b294c60..13f2be039c 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -13,7 +13,7 @@ class TestQueryUpdateModel(Model): cluster = columns.Integer(primary_key=True) count = columns.Integer(required=False) text = columns.Text(required=False, index=True) - + text_set = columns.Set(columns.Text, required=False) class QueryUpdateTests(BaseCassEngTestCase): @@ -115,3 +115,13 @@ def test_mixed_value_and_null_update(self): def test_counter_updates(self): pass + + def test_set_add_updates(self): + partition = uuid4() + cluster = 1 + TestQueryUpdateModel.objects.create( + partition=partition, cluster=cluster, text_set={"foo"}) + TestQueryUpdateModel.objects( + partition=partition, cluster=cluster).update(text_set__add={'bar'}) + obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + self.assertEqual(obj.text_set, {"foo", "bar"}) From b777f8cfc6977b59cccf6bc8cc7e78a8a5b0e357 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Sun, 9 Feb 2014 14:42:39 -0500 Subject: [PATCH 0615/3961] Adds a test for a blind update append when the record doesn't exist yet --- cqlengine/tests/query/test_updates.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 13f2be039c..36b229dfc7 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -125,3 +125,13 @@ def test_set_add_updates(self): partition=partition, cluster=cluster).update(text_set__add={'bar'}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_set, {"foo", "bar"}) + + def test_set_add_updates_new_record(self): + """ If the key doesn't exist yet, an update creates the record + """ + partition = uuid4() + cluster = 1 + TestQueryUpdateModel.objects( + partition=partition, cluster=cluster).update(text_set__add={'bar'}) + obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + self.assertEqual(obj.text_set, {"bar"}) From 47b1d2e3cf3619dc86342bc68c73080bbc226ea5 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Sun, 9 Feb 2014 14:45:27 -0500 Subject: [PATCH 0616/3961] Adds blind set removal, and a test that it works --- cqlengine/statements.py | 2 ++ cqlengine/tests/query/test_updates.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index ba4d7061c1..111574c167 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -185,6 +185,8 @@ def _analyze(self): pass elif self._operation == "add": self._additions = self.value + elif self._operation == "remove": + self._removals = self.value elif self.previous is None: self._assignments = self.value else: diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 36b229dfc7..fc1fe0e171 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -135,3 +135,14 @@ def test_set_add_updates_new_record(self): partition=partition, cluster=cluster).update(text_set__add={'bar'}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_set, {"bar"}) + + def test_set_remove_updates(self): + partition = uuid4() + cluster = 1 + TestQueryUpdateModel.objects.create( + partition=partition, cluster=cluster, text_set={"foo", "baz"}) + TestQueryUpdateModel.objects( + partition=partition, cluster=cluster).update( + text_set__remove={'foo'}) + obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + self.assertEqual(obj.text_set, {"baz"}) From cc52ad6c6652b2f52a11fcdc075e0f3df27f927f Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Sun, 9 Feb 2014 14:47:42 -0500 Subject: [PATCH 0617/3961] Adds a test that removing something that's not in a set just fails quietly --- cqlengine/tests/query/test_updates.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index fc1fe0e171..65f8f57211 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -146,3 +146,16 @@ def test_set_remove_updates(self): text_set__remove={'foo'}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_set, {"baz"}) + + def test_set_remove_new_record(self): + """ Removing something not in the set should silently do nothing + """ + partition = uuid4() + cluster = 1 + TestQueryUpdateModel.objects.create( + partition=partition, cluster=cluster, text_set={"foo"}) + TestQueryUpdateModel.objects( + partition=partition, cluster=cluster).update( + text_set__remove={'afsd'}) + obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + self.assertEqual(obj.text_set, {"foo"}) From 57f92c0777d1fe3d48141b5b947f897b1e2ddc76 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Sun, 9 Feb 2014 15:42:28 -0500 Subject: [PATCH 0618/3961] Adds ability to blind append and prepend to a list --- cqlengine/query.py | 3 +-- cqlengine/statements.py | 13 ++++++++++--- cqlengine/tests/query/test_updates.py | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index f226a70ed4..5f32a87cb4 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -681,9 +681,8 @@ def update(self, **values): if isinstance(col, Counter): # TODO: implement counter updates raise NotImplementedError - elif isinstance(col, BaseContainerColumn): + elif isinstance(col, (List, Set)): if isinstance(col, List): klass = ListUpdateClause - elif isinstance(col, Map): klass = MapUpdateClause elif isinstance(col, Set): klass = SetUpdateClause else: raise RuntimeError us.add_assignment_clause(klass( diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 111574c167..d53e0cd0fa 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -215,8 +215,8 @@ def update_context(self, ctx): class ListUpdateClause(ContainerUpdateClause): """ updates a list collection """ - def __init__(self, field, value, previous=None, column=None): - super(ListUpdateClause, self).__init__(field, value, previous, column=column) + def __init__(self, field, value, operation=None, previous=None, column=None): + super(ListUpdateClause, self).__init__(field, value, operation, previous, column=column) self._append = None self._prepend = None @@ -261,6 +261,14 @@ def _analyze(self): if self.value is None or self.value == self.previous: pass + elif self._operation == "append": + self._append = self.value + + elif self._operation == "prepend": + # self.value is a Quoter but we reverse self._prepend later as if + # it's a list, so we have to set it to the underlying list + self._prepend = self.value.value + elif self.previous is None: self._assignments = self.value @@ -269,7 +277,6 @@ def _analyze(self): # rewrite the whole list self._assignments = self.value - elif len(self.previous) == 0: # if we're updating from an empty # list, do a complete insert diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 65f8f57211..1f62e8ce5d 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -14,6 +14,7 @@ class TestQueryUpdateModel(Model): count = columns.Integer(required=False) text = columns.Text(required=False, index=True) text_set = columns.Set(columns.Text, required=False) + text_list = columns.List(columns.Text, required=False) class QueryUpdateTests(BaseCassEngTestCase): @@ -159,3 +160,25 @@ def test_set_remove_new_record(self): text_set__remove={'afsd'}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_set, {"foo"}) + + def test_list_append_updates(self): + partition = uuid4() + cluster = 1 + TestQueryUpdateModel.objects.create( + partition=partition, cluster=cluster, text_list=["foo"]) + TestQueryUpdateModel.objects( + partition=partition, cluster=cluster).update( + text_list__append=['bar']) + obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + self.assertEqual(obj.text_list, ["foo", "bar"]) + + def test_list_prepend_updates(self): + partition = uuid4() + cluster = 1 + TestQueryUpdateModel.objects.create( + partition=partition, cluster=cluster, text_list=["foo"]) + TestQueryUpdateModel.objects( + partition=partition, cluster=cluster).update( + text_list__prepend=['bar']) + obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + self.assertEqual(obj.text_list, ["bar", "foo"]) From e11f53e370e634dc271a18056cd158204971d561 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Mon, 10 Feb 2014 10:15:52 -0500 Subject: [PATCH 0619/3961] Updates the prepend to list test to make sure order is preserved when multiple items are prepended --- cqlengine/tests/query/test_updates.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 1f62e8ce5d..1541f69968 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -173,12 +173,13 @@ def test_list_append_updates(self): self.assertEqual(obj.text_list, ["foo", "bar"]) def test_list_prepend_updates(self): + """ Prepend two things since order is reversed by default by CQL """ partition = uuid4() cluster = 1 TestQueryUpdateModel.objects.create( partition=partition, cluster=cluster, text_list=["foo"]) TestQueryUpdateModel.objects( partition=partition, cluster=cluster).update( - text_list__prepend=['bar']) + text_list__prepend=['bar', 'baz']) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_list, ["bar", "foo"]) + self.assertEqual(obj.text_list, ["bar", "baz", "foo"]) From 9dc55ec54bef7c218ad2d9036228a2555b2b6aa5 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Mon, 10 Feb 2014 11:28:03 -0500 Subject: [PATCH 0620/3961] Adds a map __merge update mode, to merge in map with the existing values on the server. --- cqlengine/query.py | 3 ++- cqlengine/statements.py | 12 +++++++----- cqlengine/tests/query/test_updates.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 5f32a87cb4..ce7036554b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -681,9 +681,10 @@ def update(self, **values): if isinstance(col, Counter): # TODO: implement counter updates raise NotImplementedError - elif isinstance(col, (List, Set)): + elif isinstance(col, (List, Set, Map)): if isinstance(col, List): klass = ListUpdateClause elif isinstance(col, Set): klass = SetUpdateClause + elif isinstance(col, Map): klass = MapUpdateClause else: raise RuntimeError us.add_assignment_clause(klass( col_name, col.to_database(val), operation=col_op)) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index d53e0cd0fa..5ae1d213d4 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -310,13 +310,16 @@ def _analyze(self): class MapUpdateClause(ContainerUpdateClause): """ updates a map collection """ - def __init__(self, field, value, previous=None, column=None): - super(MapUpdateClause, self).__init__(field, value, previous, column=column) + def __init__(self, field, value, operation=None, previous=None, column=None): + super(MapUpdateClause, self).__init__(field, value, operation, previous, column=column) self._updates = None self.previous = self.previous or {} def _analyze(self): - self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None + if self._operation == "merge": + self._updates = self.value.value + else: + self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None self._analyzed = True def get_context_size(self): @@ -326,8 +329,7 @@ def get_context_size(self): def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id - for key in self._updates or []: - val = self.value.get(key) + for key, val in self._updates.items(): ctx[str(ctx_id)] = self._column.key_col.to_database(key) if self._column else key ctx[str(ctx_id + 1)] = self._column.value_col.to_database(val) if self._column else val ctx_id += 2 diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 1541f69968..723a8e87f2 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -15,6 +15,7 @@ class TestQueryUpdateModel(Model): text = columns.Text(required=False, index=True) text_set = columns.Set(columns.Text, required=False) text_list = columns.List(columns.Text, required=False) + text_map = columns.Map(columns.Text, columns.Text, required=False) class QueryUpdateTests(BaseCassEngTestCase): @@ -183,3 +184,16 @@ def test_list_prepend_updates(self): text_list__prepend=['bar', 'baz']) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_list, ["bar", "baz", "foo"]) + + def test_map_merge_updates(self): + """ Merge a dictionary into existing value """ + partition = uuid4() + cluster = 1 + TestQueryUpdateModel.objects.create( + partition=partition, cluster=cluster, + text_map={"foo": '1', "bar": '2'}) + TestQueryUpdateModel.objects( + partition=partition, cluster=cluster).update( + text_map__merge={"bar": '3', "baz": '4'}) + obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + self.assertEqual(obj.text_map, {"foo": '1', "bar": '3', "baz": '4'}) From c344f1e2742dc49e7d2ea17f4ec5a0632daabef2 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Mon, 10 Feb 2014 11:54:31 -0500 Subject: [PATCH 0621/3961] Tries to add a test that doesn't work because of a bug in the underlying cql library. So doing an update with a dict __merge with a None value will not be possible for the time being. --- cqlengine/tests/query/test_updates.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 723a8e87f2..592eb28595 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -197,3 +197,22 @@ def test_map_merge_updates(self): text_map__merge={"bar": '3', "baz": '4'}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_map, {"foo": '1', "bar": '3', "baz": '4'}) + + def test_map_merge_none_deletes_key(self): + """ The CQL behavior is if you set a key in a map to null it deletes + that key from the map. Test that this works with __merge. + + This test fails because of a bug in the cql python library not + converting None to null (and the cql library is no longer in active + developement). + """ + # partition = uuid4() + # cluster = 1 + # TestQueryUpdateModel.objects.create( + # partition=partition, cluster=cluster, + # text_map={"foo": '1', "bar": '2'}) + # TestQueryUpdateModel.objects( + # partition=partition, cluster=cluster).update( + # text_map__merge={"bar": None}) + # obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) + # self.assertEqual(obj.text_map, {"foo": '1'}) From a2c7a547ee2a8e0401862f190fafeb3ab9c2d4d4 Mon Sep 17 00:00:00 2001 From: Niklas Korz Date: Tue, 11 Feb 2014 18:53:09 +0100 Subject: [PATCH 0622/3961] QueryException Token values len I assume these two have to be swapped, otherwise it doesn't make much sense. --- cqlengine/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index f991f5e9cb..3c629ebf23 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -385,7 +385,7 @@ def filter(self, *args, **kwargs): if len(partition_columns) != len(val.value): raise QueryException( 'Token() received {} arguments but model has {} partition keys'.format( - len(partition_columns), len(val.value))) + len(val.value), len(partition_columns))) val.set_columns(partition_columns) #get query operator, or use equals if not supplied From a32b92165863db3af83240c23fa71811b058558a Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 11 Feb 2014 11:19:03 -0800 Subject: [PATCH 0623/3961] removing uneccesary try/except block --- cqlengine/connection.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 2eb2d30ef9..e03a35b136 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -146,10 +146,7 @@ def get(self): # is immediately available, else raises the Empty exception return self._queue.get(block=False) except queue.Empty: - try: - return self._create_connection() - except CQLConnectionError as cqle: - raise cqle + return self._create_connection() def put(self, conn): """ From 7fa6a6ed9b3dad79e4e7393322b39c1c46b5ff18 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 11 Feb 2014 11:22:42 -0800 Subject: [PATCH 0624/3961] updating changelog --- changelog | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog b/changelog index 1284fa6015..af328c69fd 100644 --- a/changelog +++ b/changelog @@ -2,7 +2,8 @@ CHANGELOG 0.11.1 (in progress) -* Normalize and unquote boolean values. +* Normalize and unquote boolean values. (Thanks Kevin Deldycke github.com/kdeldycke) +* Fix race condition in connection manager (Thanks Roey Berman github.com/bergundy) 0.11.0 @@ -10,7 +11,7 @@ CHANGELOG - allows for long, timedelta, and datetime * fixed use of USING TIMESTAMP in batches * clear TTL and timestamp off models after persisting to DB -* allows UUID without - (Thanks to Michael Haddad, github.com/mahall) +* allows UUID without dashes - (Thanks to Michael Hall, github.com/mahall) * fixes regarding syncing schema settings (thanks Kai Lautaportti github.com/dokai) 0.10.0 From f5d329d72354f9492b8752631b31a8d0a5aa7ee6 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Fri, 21 Feb 2014 17:30:51 -0500 Subject: [PATCH 0625/3961] reverts so we don't need to change the update_context method for map, we just allow the dict column Quoter to have get() and keys() methods --- cqlengine/columns.py | 6 ++++++ cqlengine/statements.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 4ba74273b1..1fbb9d037a 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -763,6 +763,12 @@ def __str__(self): cq = cql_quote return '{' + ', '.join([cq(k) + ':' + cq(v) for k,v in self.value.items()]) + '}' + def get(self, key): + return self.value.get(key) + + def keys(self): + return self.value.keys() + def __init__(self, key_type, value_type, default=dict, **kwargs): """ :param key_type: a column class indicating the types of the key diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 5ae1d213d4..8d64d7ec6e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -317,7 +317,7 @@ def __init__(self, field, value, operation=None, previous=None, column=None): def _analyze(self): if self._operation == "merge": - self._updates = self.value.value + self._updates = self.value.keys() else: self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None self._analyzed = True @@ -329,7 +329,8 @@ def get_context_size(self): def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id - for key, val in self._updates.items(): + for key in self._updates or []: + val = self.value.get(key) ctx[str(ctx_id)] = self._column.key_col.to_database(key) if self._column else key ctx[str(ctx_id + 1)] = self._column.value_col.to_database(val) if self._column else val ctx_id += 2 From 76a8abd11cff544af7e6b4372f151c8d20019c00 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Fri, 21 Feb 2014 17:59:22 -0500 Subject: [PATCH 0626/3961] Fixes issue because params got re-ordered --- cqlengine/query.py | 3 ++- cqlengine/statements.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index ce7036554b..288ccf07fa 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -794,7 +794,8 @@ def update(self): else: raise RuntimeError # do the stuff - clause = klass(col.db_field_name, val, val_mgr.previous_value, column=col) + clause = klass(col.db_field_name, val, + previous=val_mgr.previous_value, column=col) if clause.get_context_size() > 0: statement.add_assignment_clause(clause) else: diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 8d64d7ec6e..2ee5f97652 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -350,7 +350,7 @@ def __unicode__(self): class CounterUpdateClause(ContainerUpdateClause): def __init__(self, field, value, previous=None, column=None): - super(CounterUpdateClause, self).__init__(field, value, previous, column) + super(CounterUpdateClause, self).__init__(field, value, previous=previous, column=column) self.previous = self.previous or 0 def get_context_size(self): From dfd8b444286c2ca0bd5a5c57bcced5abe10e0395 Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Fri, 21 Feb 2014 18:08:53 -0500 Subject: [PATCH 0627/3961] updates tests that had broken because of arguments being rearranged --- .../tests/statements/test_assignment_clauses.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index f7fbda38fb..64b35f892b 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -16,7 +16,7 @@ def test_insert_tuple(self): class SetUpdateClauseTests(TestCase): def test_update_from_none(self): - c = SetUpdateClause('s', {1, 2}, None) + c = SetUpdateClause('s', {1, 2}, previous=None) c._analyze() c.set_context_id(0) @@ -33,7 +33,7 @@ def test_update_from_none(self): def test_null_update(self): """ tests setting a set to None creates an empty update statement """ - c = SetUpdateClause('s', None, {1, 2}) + c = SetUpdateClause('s', None, previous={1, 2}) c._analyze() c.set_context_id(0) @@ -50,7 +50,7 @@ def test_null_update(self): def test_no_update(self): """ tests an unchanged value creates an empty update statement """ - c = SetUpdateClause('s', {1, 2}, {1, 2}) + c = SetUpdateClause('s', {1, 2}, previous={1, 2}) c._analyze() c.set_context_id(0) @@ -66,7 +66,7 @@ def test_no_update(self): self.assertEqual(ctx, {}) def test_additions(self): - c = SetUpdateClause('s', {1, 2, 3}, {1, 2}) + c = SetUpdateClause('s', {1, 2, 3}, previous={1, 2}) c._analyze() c.set_context_id(0) @@ -82,7 +82,7 @@ def test_additions(self): self.assertEqual(ctx, {'0': {3}}) def test_removals(self): - c = SetUpdateClause('s', {1, 2}, {1, 2, 3}) + c = SetUpdateClause('s', {1, 2}, previous={1, 2, 3}) c._analyze() c.set_context_id(0) @@ -98,7 +98,7 @@ def test_removals(self): self.assertEqual(ctx, {'0': {3}}) def test_additions_and_removals(self): - c = SetUpdateClause('s', {2, 3}, {1, 2}) + c = SetUpdateClause('s', {2, 3}, previous={1, 2}) c._analyze() c.set_context_id(0) From fc405a1c0693419f3e6c307367111399782d0922 Mon Sep 17 00:00:00 2001 From: Caleb Rackliffe Date: Tue, 25 Feb 2014 17:50:58 -0800 Subject: [PATCH 0628/3961] Fixed the __gte filtering example --- docs/topics/queryset.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 569fb58103..471ed91260 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -29,7 +29,7 @@ Retrieving all objects .. _retrieving-objects-with-filters: Retrieving objects with filters ----------------------------------------- +------------------------------- Typically, you'll want to query only a subset of the records in your database. That can be accomplished with the QuerySet's ``.filter(\*\*)`` method. @@ -156,7 +156,7 @@ Filtering Operators # or the nicer syntax - Automobile.objects.filter(Automobile.manufacturer == 'Tesla') + q.filter(Automobile.year >= 2010) :attr:`< (__lt) ` @@ -270,7 +270,7 @@ Values Lists Batch Queries -=============== +============= cqlengine now supports batch queries using the BatchQuery class. Batch queries can be started and stopped manually, or within a context manager. To add queries to the batch object, you just need to precede the create/save/delete call with a call to batch, and pass in the batch object. From bb482fbf837b3506c9fd7870c377e535636cfd4b Mon Sep 17 00:00:00 2001 From: Caleb Rackliffe Date: Wed, 26 Feb 2014 10:20:55 -0800 Subject: [PATCH 0629/3961] Fixed a couple little formatting things pointed out by the inspector... --- docs/conf.py | 12 +++++------- docs/topics/connection.rst | 4 ++-- docs/topics/models.rst | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 37422f20a5..1c01444817 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -54,7 +54,6 @@ release = version - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None @@ -186,8 +185,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'cqlengine.tex', u'cqlengine Documentation', - u'Blake Eggleston', 'manual'), + ('index', 'cqlengine.tex', u'cqlengine Documentation', u'Blake Eggleston', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -230,9 +228,9 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'cqlengine', u'cqlengine Documentation', - u'Blake Eggleston', 'cqlengine', 'One line description of project.', - 'Miscellaneous'), + ('index', 'cqlengine', u'cqlengine Documentation', + u'Blake Eggleston', 'cqlengine', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/docs/topics/connection.rst b/docs/topics/connection.rst index 6140584704..53eaeafad6 100644 --- a/docs/topics/connection.rst +++ b/docs/topics/connection.rst @@ -1,6 +1,6 @@ -============== +========== Connection -============== +========== **Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ diff --git a/docs/topics/models.rst b/docs/topics/models.rst index b34d7f5332..d3c002a1a0 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -263,7 +263,7 @@ Extending Model Validation Compaction Options -==================== +================== As of cqlengine 0.7 we've added support for specifying compaction options. cqlengine will only use your compaction options if you have a strategy set. When a table is synced, it will be altered to match the compaction options set on your table. This means that if you are changing settings manually they will be changed back on resync. Do not use the compaction settings of cqlengine if you want to manage your compaction settings manually. From dff75adf4422e055a34d1bddda09e66a114f6396 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Thu, 6 Mar 2014 18:39:39 +0300 Subject: [PATCH 0630/3961] Fixed a typo in the title. --- docs/topics/manage_schemas.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/manage_schemas.rst b/docs/topics/manage_schemas.rst index 413b53c481..7f9c63c1d5 100644 --- a/docs/topics/manage_schemas.rst +++ b/docs/topics/manage_schemas.rst @@ -1,6 +1,6 @@ -=============== -Managing Schmas -=============== +================ +Managing Schemas +================ **Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ From 1ef653da5931b17cbf33fef0d2c9edf221fc2c59 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 11 Mar 2014 14:15:19 +0100 Subject: [PATCH 0631/3961] fix(docs): add `import uuid` for Getting Started For better copy & paste experience --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 15c591cb8b..b99b182834 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ pip install cqlengine ```python #first, define a model +import uuid from cqlengine import columns from cqlengine.models import Model From 1c6660199b3f1b5ab6d054151a39cb3cd8ad73a1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 11 Mar 2014 13:08:53 -0700 Subject: [PATCH 0632/3961] reformatting --- cqlengine/tests/test_timestamp.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index a5d9b870cc..261564319f 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -3,7 +3,6 @@ """ from datetime import timedelta, datetime -import unittest from uuid import uuid4 import mock import sure @@ -19,12 +18,15 @@ class TestTimestampModel(Model): class BaseTimestampTest(BaseCassEngTestCase): + @classmethod def setUpClass(cls): super(BaseTimestampTest, cls).setUpClass() sync_table(TestTimestampModel) + class BatchTest(BaseTimestampTest): + def test_batch_is_included(self): with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery(timestamp=timedelta(seconds=30)) as b: TestTimestampModel.batch(b).create(count=1) @@ -32,7 +34,6 @@ def test_batch_is_included(self): "USING TIMESTAMP".should.be.within(m.call_args[0][0]) - class CreateWithTimestampTest(BaseTimestampTest): def test_batch(self): @@ -44,8 +45,6 @@ def test_batch(self): query.should.match(r"INSERT.*USING TIMESTAMP") query.should_not.match(r"TIMESTAMP.*INSERT") - - def test_timestamp_not_included_on_normal_create(self): with mock.patch.object(ConnectionPool, "execute") as m: TestTimestampModel.create(count=2) @@ -90,7 +89,9 @@ def test_instance_update_in_batch(self): query = m.call_args[0][0] "USING TIMESTAMP".should.be.within(query) + class DeleteWithTimestampTest(BaseTimestampTest): + def test_non_batch(self): """ we don't expect the model to come back at the end because the deletion timestamp should be in the future @@ -122,10 +123,6 @@ def test_non_batch(self): tmp.update() tmp._timestamp.shouldnt.be.ok - - - - def test_blind_delete(self): """ we don't expect the model to come back at the end because the deletion timestamp should be in the future @@ -178,6 +175,3 @@ def test_delete_in_the_past(self): TestTimestampModel.get(id=uid) - - - From cc93d45f3cfc050742030b24023d63a6e2125c56 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 11 Mar 2014 13:27:12 -0700 Subject: [PATCH 0633/3961] more formatting --- cqlengine/statements.py | 1 - cqlengine/tests/query/test_queryset.py | 84 +++++++++++++------------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index f4cd28836a..2a90c5bc43 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -483,7 +483,6 @@ def timestamp_normalized(self): return long(((tmp - datetime.fromtimestamp(0)).total_seconds()) * 1000000) - def __unicode__(self): raise NotImplementedError diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index fbe2affdcc..2144dee5ac 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -22,6 +22,7 @@ class TzOffset(tzinfo): """Minimal implementation of a timezone offset to help testing with timezone aware datetimes. """ + def __init__(self, offset): self._offset = timedelta(hours=offset) @@ -34,6 +35,7 @@ def tzname(self, dt): def dst(self, dt): return timedelta(0) + class TestModel(Model): test_id = columns.Integer(primary_key=True) attempt_id = columns.Integer(primary_key=True) @@ -41,6 +43,7 @@ class TestModel(Model): expected_result = columns.Integer() test_result = columns.Integer() + class IndexedTestModel(Model): test_id = columns.Integer(primary_key=True) attempt_id = columns.Integer(index=True) @@ -48,6 +51,7 @@ class IndexedTestModel(Model): expected_result = columns.Integer() test_result = columns.Integer(index=True) + class TestMultiClusteringModel(Model): one = columns.Integer(primary_key=True) two = columns.Integer(primary_key=True) @@ -55,7 +59,6 @@ class TestMultiClusteringModel(Model): class TestQuerySetOperation(BaseCassEngTestCase): - def test_query_filter_parsing(self): """ Tests the queryset filter method parses it's kwargs properly @@ -164,8 +167,8 @@ def test_defining_only_or_defer_on_nonexistant_fields_fails(self): Tests that setting only or defer fields that don't exist raises an exception """ -class BaseQuerySetUsage(BaseCassEngTestCase): +class BaseQuerySetUsage(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(BaseQuerySetUsage, cls).setUpClass() @@ -201,9 +204,12 @@ def setUpClass(cls): IndexedTestModel.objects.create(test_id=7, attempt_id=3, description='try8', expected_result=20, test_result=20) IndexedTestModel.objects.create(test_id=8, attempt_id=0, description='try9', expected_result=50, test_result=40) - IndexedTestModel.objects.create(test_id=9, attempt_id=1, description='try10', expected_result=60, test_result=40) - IndexedTestModel.objects.create(test_id=10, attempt_id=2, description='try11', expected_result=70, test_result=45) - IndexedTestModel.objects.create(test_id=11, attempt_id=3, description='try12', expected_result=75, test_result=45) + IndexedTestModel.objects.create(test_id=9, attempt_id=1, description='try10', expected_result=60, + test_result=40) + IndexedTestModel.objects.create(test_id=10, attempt_id=2, description='try11', expected_result=70, + test_result=45) + IndexedTestModel.objects.create(test_id=11, attempt_id=3, description='try12', expected_result=75, + test_result=45) @classmethod def tearDownClass(cls): @@ -212,8 +218,8 @@ def tearDownClass(cls): drop_table(IndexedTestModel) drop_table(TestMultiClusteringModel) -class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): +class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): def test_count(self): """ Tests that adding filtering statements affects the count query as expected """ assert TestModel.objects.count() == 12 @@ -232,7 +238,7 @@ 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)]) + 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 @@ -243,7 +249,7 @@ def test_iteration(self): q = TestModel.objects(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)]) + 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 @@ -254,7 +260,7 @@ def test_iteration(self): 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)]) + 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 @@ -266,7 +272,7 @@ def test_multiple_iterations_work_properly(self): # test with both the filtering method and the query method 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)]) + 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 @@ -274,7 +280,7 @@ def test_multiple_iterations_work_properly(self): assert len(compare_set) == 0 #try it again - compare_set = set([(0,5), (1,10), (2,15), (3,20)]) + 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 @@ -287,7 +293,7 @@ def test_multiple_iterators_are_isolated(self): """ 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] + expected_order = [0, 1, 2, 3] iter1 = iter(q) iter2 = iter(q) for attempt_id in expected_order: @@ -354,18 +360,18 @@ def test_allow_filtering_flag(self): """ """ + class TestQuerySetOrdering(BaseQuerySetUsage): def test_order_by_success_case(self): - q = TestModel.objects(test_id=0).order_by('attempt_id') - expected_order = [0,1,2,3] - for model, expect in zip(q,expected_order): + expected_order = [0, 1, 2, 3] + for model, expect in zip(q, expected_order): assert model.attempt_id == expect q = q.order_by('-attempt_id') expected_order.reverse() - for model, expect in zip(q,expected_order): + for model, expect in zip(q, expected_order): assert model.attempt_id == expect def test_ordering_by_non_second_primary_keys_fail(self): @@ -403,7 +409,6 @@ def test_ordering_on_multiple_clustering_columns(self): class TestQuerySetSlicing(BaseQuerySetUsage): - def test_out_of_range_index_raises_error(self): q = TestModel.objects(test_id=0).order_by('attempt_id') with self.assertRaises(IndexError): @@ -411,32 +416,32 @@ def test_out_of_range_index_raises_error(self): def test_array_indexing_works_properly(self): q = TestModel.objects(test_id=0).order_by('attempt_id') - expected_order = [0,1,2,3] + expected_order = [0, 1, 2, 3] for i in range(len(q)): assert q[i].attempt_id == expected_order[i] def test_negative_indexing_works_properly(self): q = TestModel.objects(test_id=0).order_by('attempt_id') - expected_order = [0,1,2,3] + expected_order = [0, 1, 2, 3] assert q[-1].attempt_id == expected_order[-1] assert q[-2].attempt_id == expected_order[-2] def test_slicing_works_properly(self): q = TestModel.objects(test_id=0).order_by('attempt_id') - expected_order = [0,1,2,3] + expected_order = [0, 1, 2, 3] for model, expect in zip(q[1:3], expected_order[1:3]): assert model.attempt_id == expect def test_negative_slicing(self): q = TestModel.objects(test_id=0).order_by('attempt_id') - expected_order = [0,1,2,3] + expected_order = [0, 1, 2, 3] for model, expect in zip(q[-3:], expected_order[-3:]): assert model.attempt_id == expect for model, expect in zip(q[:-1], expected_order[:-1]): assert model.attempt_id == expect -class TestQuerySetValidation(BaseQuerySetUsage): +class TestQuerySetValidation(BaseQuerySetUsage): def test_primary_key_or_index_must_be_specified(self): """ Tests that queries that don't have an equals relation to a primary key or indexed field fail @@ -453,7 +458,6 @@ def test_primary_key_or_index_must_have_equal_relation_filter(self): q = TestModel.objects(test_id__gt=0) list([i for i in q]) - def test_indexed_field_can_be_queried(self): """ Tests that queries on an indexed field will work without any primary key relations specified @@ -461,8 +465,8 @@ def test_indexed_field_can_be_queried(self): q = IndexedTestModel.objects(test_result=25) assert q.count() == 4 -class TestQuerySetDelete(BaseQuerySetUsage): +class TestQuerySetDelete(BaseQuerySetUsage): def test_delete(self): TestModel.objects.create(test_id=3, attempt_id=0, description='try9', expected_result=50, test_result=40) TestModel.objects.create(test_id=3, attempt_id=1, description='try10', expected_result=60, test_result=40) @@ -487,15 +491,15 @@ def test_delete_without_any_where_args(self): with self.assertRaises(query.QueryException): TestModel.objects(attempt_id=0).delete() -class TestQuerySetConnectionHandling(BaseQuerySetUsage): +class TestQuerySetConnectionHandling(BaseQuerySetUsage): def test_conn_is_returned_after_filling_cache(self): """ Tests that the queryset returns it's connection after it's fetched all of it's 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)]) + 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 @@ -506,13 +510,12 @@ def test_conn_is_returned_after_filling_cache(self): class TimeUUIDQueryModel(Model): - partition = columns.UUID(primary_key=True) - time = columns.TimeUUID(primary_key=True) - data = columns.Text(required=False) + partition = columns.UUID(primary_key=True) + time = columns.TimeUUID(primary_key=True) + data = columns.Text(required=False) class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase): - @classmethod def setUpClass(cls): super(TestMinMaxTimeUUIDFunctions, cls).setUpClass() @@ -585,14 +588,14 @@ def test_success_case(self): 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 + assert '1' in datas + assert '2' in datas q = TimeUUIDQueryModel.filter(partition=pk, time__gte=functions.MinTimeUUID(midpoint)) assert len(q) == 2 datas = [d.data for d in q] - assert '3' in datas - assert '4' in datas + assert '3' in datas + assert '4' in datas # test query expression filtering q = TimeUUIDQueryModel.filter( @@ -602,8 +605,8 @@ def test_success_case(self): 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 + assert '1' in datas + assert '2' in datas q = TimeUUIDQueryModel.filter( TimeUUIDQueryModel.partition == pk, @@ -611,15 +614,14 @@ def test_success_case(self): ) assert len(q) == 2 datas = [d.data for d in q] - assert '3' in datas - assert '4' in datas + assert '3' in datas + assert '4' in datas class TestInOperator(BaseQuerySetUsage): - def test_kwarg_success_case(self): """ Tests the in operator works with the kwarg query method """ - q = TestModel.filter(test_id__in=[0,1]) + q = TestModel.filter(test_id__in=[0, 1]) assert q.count() == 8 def test_query_expression_success_case(self): @@ -637,8 +639,8 @@ def test_values_list(self): item = q.values_list('expected_result', flat=True).first() assert item == 10 -class TestObjectsProperty(BaseQuerySetUsage): +class TestObjectsProperty(BaseQuerySetUsage): def test_objects_property_returns_fresh_queryset(self): assert TestModel.objects._result_cache is None len(TestModel.objects) # evaluate queryset From b924eec0be5647783f6d4145cdac53627ce19f8c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 11 Mar 2014 13:43:16 -0700 Subject: [PATCH 0634/3961] adding setitem method to base model --- cqlengine/models.py | 8 ++++++++ cqlengine/tests/model/test_model_io.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/cqlengine/models.py b/cqlengine/models.py index 5655c9df54..f771b94f04 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -436,6 +436,14 @@ def __getitem__(self, key): raise KeyError return getattr(self, key) + def __setitem__(self, key, val): + """ Sets a column's value. """ + if not isinstance(key, basestring): + raise TypeError + if key not in self._columns.keys(): + raise KeyError + return setattr(self, key, val) + def __len__(self): """ Returns the number of columns defined on that model. """ return len(self._columns.keys()) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 512aa2324f..e20e588571 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -64,6 +64,9 @@ def test_model_read_as_dict(self): for column_id in column_dict.keys(): self.assertEqual(tm[column_id], column_dict[column_id]) + tm['count'] = 6 + self.assertEqual(tm.count, 6) + def test_model_updating_works_properly(self): """ Tests that subsequent saves after initial model creation work From a8d86c6cfc0d84d979b747daeed4f8761f0d520d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 11 Mar 2014 13:48:08 -0700 Subject: [PATCH 0635/3961] updating the changelog --- changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog b/changelog index aea10c7b79..aa0bdb5590 100644 --- a/changelog +++ b/changelog @@ -1,9 +1,10 @@ CHANGELOG -0.11.1 (in progress) +0.12.0 (in progress) * Normalize and unquote boolean values. (Thanks Kevin Deldycke github.com/kdeldycke) * Fix race condition in connection manager (Thanks Roey Berman github.com/bergundy) +* allow access to instance columns as if it is a dict (Thanks Kevin Deldycke github.com/kdeldycke) 0.11.0 @@ -13,7 +14,6 @@ CHANGELOG * clear TTL and timestamp off models after persisting to DB * allows UUID without dashes - (Thanks to Michael Hall, github.com/mahall) * fixes regarding syncing schema settings (thanks Kai Lautaportti github.com/dokai) -* allow acces to instance columns as if it is a dict 0.10.0 From 17d24bbab866342104eb8ea4f253940b0be4b121 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Tue, 11 Mar 2014 13:49:37 -0700 Subject: [PATCH 0636/3961] adding documentation around model dict behavior --- docs/topics/models.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index d3c002a1a0..ee044f0e11 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -312,3 +312,24 @@ Compaction Options __compaction_tombstone_compaction_interval__ = 86400 Tables may use `LeveledCompactionStrategy` or `SizeTieredCompactionStrategy`. Both options are available in the top level cqlengine module. To reiterate, you will need to set your `__compaction__` option explicitly in order for cqlengine to handle any of your settings. + +Manipulating model instances as dictionaries +============================================ + + As of cqlengine 0.12, we've added support for treating model instances like dictionaries. See below for examples + + .. code-block:: python + + class Person(Model): + first_name = columns.Text() + last_name = columns.Text() + + kevin = Person.create(first_name="Kevin", last_name="Deldycke") + dict(kevin) # returns {'first_name': 'Kevin', 'last_name': 'Deldycke'} + kevin['first_name'] # returns 'Kevin' + kevin.keys() # returns ['first_name', 'last_name'] + kevin.values() # returns ['Kevin', 'Deldycke'] + kevin.items() # returns [('first_name', 'Kevin'), ('last_name', 'Deldycke')] + + kevin['first_name'] = 'KEVIN5000' # changes the models first name + From 877427ee179aab0846f52c4e9e4cae6781f0000f Mon Sep 17 00:00:00 2001 From: Danny Cosson Date: Wed, 12 Mar 2014 10:27:02 -0400 Subject: [PATCH 0637/3961] renames the map "merge" operation to "update" to be consistent with the python dict method --- cqlengine/statements.py | 2 +- cqlengine/tests/query/test_updates.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 92def0eb13..f2869c14c5 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -335,7 +335,7 @@ def __init__(self, field, value, operation=None, previous=None, column=None): self.previous = self.previous or {} def _analyze(self): - if self._operation == "merge": + if self._operation == "update": self._updates = self.value.keys() else: self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index 592eb28595..d32755af8f 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -185,7 +185,7 @@ def test_list_prepend_updates(self): obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_list, ["bar", "baz", "foo"]) - def test_map_merge_updates(self): + def test_map_update_updates(self): """ Merge a dictionary into existing value """ partition = uuid4() cluster = 1 @@ -194,13 +194,13 @@ def test_map_merge_updates(self): text_map={"foo": '1', "bar": '2'}) TestQueryUpdateModel.objects( partition=partition, cluster=cluster).update( - text_map__merge={"bar": '3', "baz": '4'}) + text_map__update={"bar": '3', "baz": '4'}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) self.assertEqual(obj.text_map, {"foo": '1', "bar": '3', "baz": '4'}) - def test_map_merge_none_deletes_key(self): + def test_map_update_none_deletes_key(self): """ The CQL behavior is if you set a key in a map to null it deletes - that key from the map. Test that this works with __merge. + that key from the map. Test that this works with __update. This test fails because of a bug in the cql python library not converting None to null (and the cql library is no longer in active @@ -213,6 +213,6 @@ def test_map_merge_none_deletes_key(self): # text_map={"foo": '1', "bar": '2'}) # TestQueryUpdateModel.objects( # partition=partition, cluster=cluster).update( - # text_map__merge={"bar": None}) + # text_map__update={"bar": None}) # obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) # self.assertEqual(obj.text_map, {"foo": '1'}) From 83c1f151ef935e4dd0f39b0b986ca115c2e5c7a2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 12 Mar 2014 10:50:55 -0700 Subject: [PATCH 0638/3961] updating dict docs --- docs/topics/models.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index ee044f0e11..7de1a5e40e 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -316,7 +316,7 @@ Compaction Options Manipulating model instances as dictionaries ============================================ - As of cqlengine 0.12, we've added support for treating model instances like dictionaries. See below for examples + As of cqlengine 0.12, we've added support for treating model instances like dictionaries. See below for examples. .. code-block:: python From fc140fcfc53ab3291e62c495949506614da55dd7 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 12 Mar 2014 10:57:24 -0700 Subject: [PATCH 0639/3961] making the container update class selection logic more readable --- cqlengine/query.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 84fe695437..eb82479b42 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -682,12 +682,15 @@ def update(self, **values): # TODO: implement counter updates raise NotImplementedError elif isinstance(col, (List, Set, Map)): - if isinstance(col, List): klass = ListUpdateClause - elif isinstance(col, Set): klass = SetUpdateClause - elif isinstance(col, Map): klass = MapUpdateClause - else: raise RuntimeError - us.add_assignment_clause(klass( - col_name, col.to_database(val), operation=col_op)) + if isinstance(col, List): + klass = ListUpdateClause + elif isinstance(col, Set): + klass = SetUpdateClause + elif isinstance(col, Map): + klass = MapUpdateClause + else: + raise RuntimeError + us.add_assignment_clause(klass(col_name, col.to_database(val), operation=col_op)) else: us.add_assignment_clause(AssignmentClause( col_name, col.to_database(val))) From 71eff2b6a4e4d7dffce4038e286cffed6595f688 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 12 Mar 2014 16:28:39 -0700 Subject: [PATCH 0640/3961] adding documentation around blind container updates --- docs/topics/queryset.rst | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 471ed91260..3323604089 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -400,3 +400,57 @@ QuerySet method reference Per column validation will be performed, but instance level validation will not (`Model.validate` is not called). + The queryset update method also supports blindly adding and removing elements from container columns, without + loading a model instance from Cassandra. + + Using the syntax `.update(column_name={x, y, z})` will overwrite the contents of the container, like updating a + non container column. However, adding `__` to the end of the keyword arg, makes the update call add + or remove items from the collection, without overwriting then entire column. + + + Given the model below, here are the operations that can be performed on the different container columns: + + .. code-block:: python + + class Row(Model): + row_id = columns.Integer(primary_key=True) + set_column = columns.Set(Integer) + list_column = columns.Set(Integer) + map_column = columns.Set(Integer, Integer) + + :class:`~cqlengine.columns.Set` + + - `add`: adds the elements of the given set to the column + - `remove`: removes the elements of the given set to the column + + + .. code-block:: python + + # add elements to a set + Row.objects(row_id=5).update(set_column__add={6}) + + # remove elements to a set + Row.objects(row_id=5).update(set_column__remove={4}) + + :class:`~cqlengine.columns.List` + + - `append`: appends the elements of the given list to the end of the column + - `prepend`: prepends the elements of the given list to the beginning of the column + + .. code-block:: python + + # append items to a list + Row.objects(row_id=5).update(list_column__append=[6, 7]) + + # prepend items to a list + Row.objects(row_id=5).update(list_column__prepend=[1, 2]) + + + :class:`~cqlengine.columns.Map` + + - `update`: adds the given keys/values to the columns, creating new entries if they didn't exist, and overwriting old ones if they did + + .. code-block:: python + + # add items to a map + Row.objects(row_id=5).update(map_column__update={1: 2, 3: 4}) From 3b05616fc404676f7b569ce337ee58f1782a89b6 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 12 Mar 2014 16:30:47 -0700 Subject: [PATCH 0641/3961] updating changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index aa0bdb5590..52415504a3 100644 --- a/changelog +++ b/changelog @@ -5,6 +5,7 @@ CHANGELOG * Normalize and unquote boolean values. (Thanks Kevin Deldycke github.com/kdeldycke) * Fix race condition in connection manager (Thanks Roey Berman github.com/bergundy) * allow access to instance columns as if it is a dict (Thanks Kevin Deldycke github.com/kdeldycke) +* Added support for blind partial updates to queryset (Thanks Danny Cosson github.com/dcosson) 0.11.0 From cf101be618bcbbfbd93ef3af012191f46717f74c Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 12 Mar 2014 16:35:56 -0700 Subject: [PATCH 0642/3961] doing version bump --- changelog | 2 +- cqlengine/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog b/changelog index 52415504a3..de52d15e79 100644 --- a/changelog +++ b/changelog @@ -1,6 +1,6 @@ CHANGELOG -0.12.0 (in progress) +0.12.0 * Normalize and unquote boolean values. (Thanks Kevin Deldycke github.com/kdeldycke) * Fix race condition in connection manager (Thanks Roey Berman github.com/bergundy) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index d9df1bbc0c..ac454c6a1f 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.11.0 +0.12.0 From 270c8ca68357f92999474fbf110fed7b01cdfdf2 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 13 Mar 2014 10:31:00 +0100 Subject: [PATCH 0643/3961] Use proper way to access package resources. --- cqlengine/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 51b3fc2f22..91828b6f29 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,11 +1,14 @@ import os +import pkg_resources from cqlengine.columns import * from cqlengine.functions import * from cqlengine.models import Model from cqlengine.query import BatchQuery -__cqlengine_version_path__ = os.path.realpath(__file__ + '/../VERSION') + +__cqlengine_version_path__ = pkg_resources.resource_filename('cqlengine', + 'VERSION') __version__ = open(__cqlengine_version_path__, 'r').readline().strip() # compaction From 23b8b12fa5ecb0797849e65d2631577ef4d88cd1 Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Thu, 13 Mar 2014 09:52:55 -0700 Subject: [PATCH 0644/3961] compare against other instead of self in __eq__ --- cqlengine/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index f771b94f04..6f6662863e 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -376,7 +376,7 @@ def __eq__(self, other): # check attribute keys keys = set(self._columns.keys()) - other_keys = set(self._columns.keys()) + other_keys = set(other._columns.keys()) if keys != other_keys: return False From 9b05f970e3ed293b63a279d59bf7400fd5dd0152 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Fri, 14 Mar 2014 14:29:42 -0700 Subject: [PATCH 0645/3961] removing note about indices on models with multiple primary keys --- docs/topics/models.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 7de1a5e40e..f6695d749b 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -84,8 +84,6 @@ Column Options :attr:`~cqlengine.columns.BaseColumn.index` If True, an index will be created for this column. Defaults to False. - *Note: Indexes can only be created on models with one primary key* - :attr:`~cqlengine.columns.BaseColumn.db_field` Explicitly sets the name of the column in the database table. If this is left blank, the column name will be the same as the name of the column attribute. Defaults to None. From cc503f1bf0857a0c1834d70cf03133bf8aae8679 Mon Sep 17 00:00:00 2001 From: Nati CT Date: Sun, 16 Mar 2014 16:51:25 +0200 Subject: [PATCH 0646/3961] fixed camelCase naming for container column names --- cqlengine/columns.py | 4 ++-- .../tests/columns/test_container_columns.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 2e203e44d6..7888981a97 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -537,7 +537,7 @@ def get_column_def(self): Returns a column definition for CQL table definition """ db_type = self.db_type.format(self.value_type.db_type) - return '{} {}'.format(self.db_field_name, db_type) + return '{} {}'.format(self.cql, db_type) def get_update_statement(self, val, prev, ctx): """ @@ -813,7 +813,7 @@ def get_column_def(self): self.key_type.db_type, self.value_type.db_type ) - return '{} {}'.format(self.db_field_name, db_type) + return '{} {}'.format(self.cql, db_type) def validate(self, value): val = super(Map, self).validate(value) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index d54a339f06..2f803913f6 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -522,3 +522,23 @@ def test_default_empty_container_saving(self): # assert len([v for v in ctx.values() if [7,8,9] == v.value]) == 1 # assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 # assert len([s for s in statements if '+ "TEST"' in s]) == 1 + +class TestCamelMapModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) + camelMap = columns.Map(columns.Text, columns.Integer, required=False) + +class TestCamelMapColumn(BaseCassEngTestCase): + @classmethod + def setUpClass(cls): + super(TestCamelMapColumn, cls).setUpClass() + drop_table(TestCamelMapModel) + sync_table(TestCamelMapModel) + + @classmethod + def tearDownClass(cls): + super(TestCamelMapColumn, cls).tearDownClass() + drop_table(TestCamelMapModel) + + def test_camelcase_column(self): + TestCamelMapModel.create(partition=None, camelMap={'blah': 1}) + From 109c019836c229ee317e4867def49ec2fbe9dde2 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Mar 2014 16:32:14 -0700 Subject: [PATCH 0647/3961] adding tests around equality --- cqlengine/tests/model/test_model.py | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 cqlengine/tests/model/test_model.py diff --git a/cqlengine/tests/model/test_model.py b/cqlengine/tests/model/test_model.py new file mode 100644 index 0000000000..a0a6929a27 --- /dev/null +++ b/cqlengine/tests/model/test_model.py @@ -0,0 +1,33 @@ +from unittest import TestCase + +from cqlengine.models import Model +from cqlengine import columns + + +class TestModel(TestCase): + """ Tests the non-io functionality of models """ + + def test_instance_equality(self): + """ tests the model equality functionality """ + class EqualityModel(Model): + pk = columns.Integer(primary_key=True) + + m0 = EqualityModel(pk=0) + m1 = EqualityModel(pk=1) + + self.assertEqual(m0, m0) + self.assertNotEqual(m0, m1) + + def test_model_equality(self): + """ tests the model equality functionality """ + class EqualityModel0(Model): + pk = columns.Integer(primary_key=True) + + class EqualityModel1(Model): + kk = columns.Integer(primary_key=True) + + m0 = EqualityModel0(pk=0) + m1 = EqualityModel1(kk=1) + + self.assertEqual(m0, m0) + self.assertNotEqual(m0, m1) From 31d3ae10595ba2e4766f517b8d40dc48c2d53636 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Mar 2014 16:38:24 -0700 Subject: [PATCH 0648/3961] updating changelog --- changelog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog b/changelog index de52d15e79..515195fbab 100644 --- a/changelog +++ b/changelog @@ -6,6 +6,8 @@ CHANGELOG * Fix race condition in connection manager (Thanks Roey Berman github.com/bergundy) * allow access to instance columns as if it is a dict (Thanks Kevin Deldycke github.com/kdeldycke) * Added support for blind partial updates to queryset (Thanks Danny Cosson github.com/dcosson) +* Model instance equality check bugfix (Thanks to Michael Hall, github.com/mahall) +* Fixed bug syncing tables with camel cased names (Thanks to Netanel Cohen-Tzemach, github.com/natict) 0.11.0 From 48b33772d6b942c8323d56a51c0ce937974c6f34 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Mar 2014 16:46:53 -0700 Subject: [PATCH 0649/3961] removing unused import --- cqlengine/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index 91828b6f29..cd9f8f3557 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,4 +1,3 @@ -import os import pkg_resources from cqlengine.columns import * From 2fb3d5abe7897a946e1338ea3d83cbb79b5494f1 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Mar 2014 16:48:04 -0700 Subject: [PATCH 0650/3961] updating changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index 515195fbab..eb99e95382 100644 --- a/changelog +++ b/changelog @@ -8,6 +8,7 @@ CHANGELOG * Added support for blind partial updates to queryset (Thanks Danny Cosson github.com/dcosson) * Model instance equality check bugfix (Thanks to Michael Hall, github.com/mahall) * Fixed bug syncing tables with camel cased names (Thanks to Netanel Cohen-Tzemach, github.com/natict) +* Fixed bug dealing with eggs (Thanks Kevin Deldycke github.com/kdeldycke) 0.11.0 From 65f2ef9216609d7b4fd818c5d5fba1b01b7b24e8 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 20 Mar 2014 17:32:08 -0700 Subject: [PATCH 0651/3961] updating authors list --- AUTHORS | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 70cdaeaf80..2da11977a5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,8 +3,14 @@ PRIMARY AUTHORS Blake Eggleston Jon Haddad -CONTRIBUTORS +CONTRIBUTORS (let us know if we missed you) Eric Scrivner - test environment, connection pooling Kevin Deldycke - +Roey Berman +Danny Cosson +Michael Hall +Netanel Cohen-Tzemach +Mariusz Kryński +Greg Doermann +@pandu-rao \ No newline at end of file From ae43b6f2a9a46e88dcc853542fd5a6323cae7ccb Mon Sep 17 00:00:00 2001 From: Daniel Dotsenko Date: Wed, 9 Apr 2014 13:27:08 -0700 Subject: [PATCH 0652/3961] adding callbacks support to BatchQuery --- cqlengine/query.py | 45 ++++++++++- cqlengine/tests/test_batch_query.py | 113 +++++++++++++++++++++++++--- docs/topics/queryset.rst | 93 +++++++++++++++++++++++ 3 files changed, 239 insertions(+), 12 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index eb82479b42..8586ca572e 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -78,8 +78,35 @@ class BatchQuery(object): """ _consistency = None + @property + def callbacks(self): + """ + Returns the set object that contains all of the callbacks presently attached to this batch. - def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on_exception=False): + :rtype: set + """ + return self._callbacks + + def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on_exception=False, callbacks=None): + """ + :param batch_type: (optional) One of batch type values available through BatchType enum + :type batch_type: str or None + :param timestamp: (optional) A datetime or timedelta object with desired timestamp to be applied + to the batch transaction. + :type timestamp: datetime or timedelta or None + :param consistency: (optional) One of consistency values ("ANY", "ONE", "QUORUM" etc) + :type consistency: str or None + :param execute_on_exception: (Defaults to False) Indicates that when the BatchQuery instance is used + as a context manager the queries accumulated within the context must be executed despite + encountering an error within the context. By default, any exception raised from within + the context scope will cause the batched queries not to be executed. + :type execute_on_exception: bool + :param callbacks: A list of functions to be executed after the batch executes. Note, that if the batch + does not execute, the callbacks are not executed. This, thus, effectively is a list of "on success" + callback handlers. If defined, must be a collection of callables. + At this time only one argument is passed into the callback - the batch instance. + :type callbacks: list or set or tuple + """ self.queries = [] self.batch_type = batch_type if timestamp is not None and not isinstance(timestamp, (datetime, timedelta)): @@ -87,6 +114,7 @@ def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on self.timestamp = timestamp self._consistency = consistency self._execute_on_exception = execute_on_exception + self._callbacks = set(callbacks or []) def add_query(self, query): if not isinstance(query, BaseCQLStatement): @@ -96,9 +124,23 @@ def add_query(self, query): def consistency(self, consistency): self._consistency = consistency + def _execute_callbacks(self): + for callback in self._callbacks: + if callable(callback): + stored_args = [] + else: + stored_args = callback[1:] + callback = callback[0] + callback(self, *stored_args) + + # trying to clear up the ref counts for objects mentioned in the set + del self._callbacks + def execute(self): if len(self.queries) == 0: # Empty batch is a no-op + # except for callbacks + self._execute_callbacks() return opener = 'BEGIN ' + (self.batch_type + ' ' if self.batch_type else '') + ' BATCH' @@ -130,6 +172,7 @@ def execute(self): execute('\n'.join(query_list), parameters, self._consistency) self.queries = [] + self._execute_callbacks() def __enter__(self): return self diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index 4beb33d9da..da7c944f97 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -17,6 +17,7 @@ class TestMultiKeyModel(Model): count = columns.Integer(required=False) text = columns.Text(required=False) + class BatchQueryTests(BaseCassEngTestCase): @classmethod @@ -41,24 +42,13 @@ def test_insert_success_case(self): b = BatchQuery() inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') - with self.assertRaises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=2) b.execute() - TestMultiKeyModel.get(partition=self.pkey, cluster=2) - def test_batch_is_executed(self): - b = BatchQuery() - inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') - - with self.assertRaises(TestMultiKeyModel.DoesNotExist): - TestMultiKeyModel.get(partition=self.pkey, cluster=2) - - b.execute() - def test_update_success_case(self): inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4') @@ -125,3 +115,104 @@ def test_empty_batch(self): with BatchQuery() as b: pass + +class BatchQueryCallbacksTests(BaseCassEngTestCase): + + def test_API_managing_callbacks(self): + + # Callbacks can be added at init and after + + def my_callback(batch, *args, **kwargs): + pass + + # adding on init: + b = BatchQuery(callbacks=[ + my_callback, + my_callback, # will be filtered out automatically + (my_callback, 'more', 'args'), + (my_callback, 'more', 'args'), # will be filtered out too + ]) + assert b.callbacks == { + my_callback, + (my_callback, 'more', 'args') + } + + # adding using set API post-init: + + b.callbacks.add(my_callback) + b.callbacks.add((my_callback, 'more', 'args')) + b.callbacks.add((my_callback, 'yet more', 'args')) + assert b.callbacks == { + my_callback, + (my_callback, 'more', 'args'), + (my_callback, 'yet more', 'args') + } + + b.callbacks.remove(my_callback) + assert b.callbacks == { + (my_callback, 'more', 'args'), + (my_callback, 'yet more', 'args') + } + + b.callbacks.remove((my_callback, 'more', 'args')) + assert b.callbacks == { + (my_callback, 'yet more', 'args') + } + + + # insure that we disallow setting the callback colleciton on instance: + # (thus forcing use of init and set-like api tested above) + with self.assertRaises(AttributeError): + b.callbacks = set() + + def test_callbacks_properly_execute_callables_and_tuples(self): + + call_history = [] + def my_callback(*args, **kwargs): + call_history.append(args) + + # adding on init: + b = BatchQuery(callbacks=[ + my_callback, + (my_callback, 'more', 'args') + ]) + + b.execute() + + assert len(call_history) == 2 + assert {(b,), (b, 'more', 'args')} == set(call_history) + + def test_callbacks_tied_to_execute(self): + """Batch callbacks should NOT fire if batch is not executed in context manager mode""" + + call_history = [] + def my_callback(*args, **kwargs): + call_history.append(args) + + with BatchQuery(callbacks=[my_callback]) as b: + pass + + assert len(call_history) == 1 + + class SomeError(Exception): + pass + + with self.assertRaises(SomeError): + with BatchQuery(callbacks=[my_callback]) as b: + # this error bubbling up through context manager + # should prevent callback runs (along with b.execute()) + raise SomeError + + # still same call history + assert len(call_history) == 1 + + # but if execute run, even with an error bubbling through + # the callbacks also whoudl have fired + with self.assertRaises(SomeError): + with BatchQuery(execute_on_exception=True, callbacks=[my_callback]) as b: + # this error bubbling up through context manager + # should prevent callback runs (along with b.execute()) + raise SomeError + + # still same call history + assert len(call_history) == 2 diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 3323604089..586344469e 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -274,6 +274,9 @@ Batch Queries cqlengine now supports batch queries using the BatchQuery class. Batch queries can be started and stopped manually, or within a context manager. To add queries to the batch object, you just need to precede the create/save/delete call with a call to batch, and pass in the batch object. +Batch Query General Use Pattern +------------------------------- + You can only create, update, and delete rows with a batch query, attempting to read rows out of the database with a batch query will fail. .. code-block:: python @@ -324,6 +327,96 @@ Batch Queries If an exception is thrown somewhere in the block, any statements that have been added to the batch will still be executed. This is useful for some logging situations. +Batch Query Execution Callbacks +------------------------------- + + In order to allow secondary tasks to be chained to the end of batch, BatchQuery instances allow callbacks to be + registered with the batch, to be executed immediately after the batch executes. + + Let's say an iterative action over a group of records needs to be followed up by a "maintenance" task like "Update + count of children on parent model." + If your were to kick off that task following manipulation on each child, while the batch is not executed, the reads + the maintenance task would do would operate on old data since the batch is not yet commited. + (You also risk kicking off too many of the same tasks, while only one is needed at the end of the batch.) + + The callbacks attached to a given batch instance are executed only if the batch executes. If the batch is used as a + context manager and an exception bubbles up, the queued up callbacks will not be run. + + The callback should be a callable object that accepts at least one (positional) argument - instance of the batch object. + + .. code-block:: python + + def example_callback_handler(batch, *args, **kwargs): + pass + + Multiple callbacks can be attached to same BatchQuery instance both, at the point of instantiation and later + directly to the batch object instance. + + The callbacks collection is implemented as a ``set``, which naturally deduplicates the callback handlers: + + .. code-block:: python + + # let's add a few of the same callbacks at init + batch = BatchQuery(callbacks=([ + example_callback_handler, + example_callback_handler + ]) + + # and one more time same callback now using the collection API on the batch instance + batch.callbacks.add(example_callback_handler) + + assert len(batch.callbacks) == 1 + assert batch.callbacks == {example_callback_handler} + + # this, of course, also means that if you remove a callback, + # you negate "all additions" of that callback + batch.callbacks.remove(example_callback_handler) + + assert len(batch.callbacks) == 0 + + In order to benefit from the natural deduplication of the callback handlers it is allowed to pass in tuples of + callback and its positional arguments. + This allows one to pre-package additional call arguments for the callable without the need to wrap the callable and + args into a closure or ``functools.partial``. + + .. code-block:: python + + batch = BatchQuery(callbacks=([ + (example_callback_handler, 'some', 'args'), + (example_callback_handler, 'some', 'args'), + (example_callback_handler, 'some other', 'args'), + ]) + + batch.callbacks.add((example_callback_handler, 'some other', 'args')) + + assert len(batch.callbacks) == 2 + assert batch.callbacks == { + (example_callback_handler, 'some', 'args'), + (example_callback_handler, 'some other', 'args') + } + + Note that the tuple callback object format prevents the possibily of essentially same callback firing multiple times, + which might happen if you package same callback and arguments several times with ``functools.partial``, which returns a new callable with each call. + + Arguments packaged into callbacks communicated as tuples are passed to callback handler **after** the ``batch`` argument. + + .. code-block:: python + + call_args = [] + + def callback_handler(batch, *args, **kwargs): + call_args.append([batch] + list(args)) + + batch = BatchQuery(callbacks=([(callback_handler, 'some', 'args')]) + + batch.execute() + + assert call_args == [[batch, 'some', 'args']] + + + Failure in any of the callbacks does not affect the batch's execution, as the callbacks are started after the execution + of the batch is complete and no effort to "roll it back" is made. + QuerySet method reference From 86ffd6a9741872c3094da22bac63b082e74ffe82 Mon Sep 17 00:00:00 2001 From: Daniel Dotsenko Date: Mon, 14 Apr 2014 11:56:30 -0700 Subject: [PATCH 0653/3961] removing `batch` from the list of args passed to BatchQuery callbacks --- cqlengine/query.py | 3 +-- cqlengine/tests/test_batch_query.py | 2 +- docs/topics/queryset.rst | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 8586ca572e..f2b6da14c1 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -104,7 +104,6 @@ def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on :param callbacks: A list of functions to be executed after the batch executes. Note, that if the batch does not execute, the callbacks are not executed. This, thus, effectively is a list of "on success" callback handlers. If defined, must be a collection of callables. - At this time only one argument is passed into the callback - the batch instance. :type callbacks: list or set or tuple """ self.queries = [] @@ -131,7 +130,7 @@ def _execute_callbacks(self): else: stored_args = callback[1:] callback = callback[0] - callback(self, *stored_args) + callback(*stored_args) # trying to clear up the ref counts for objects mentioned in the set del self._callbacks diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index da7c944f97..f956323a69 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -180,7 +180,7 @@ def my_callback(*args, **kwargs): b.execute() assert len(call_history) == 2 - assert {(b,), (b, 'more', 'args')} == set(call_history) + assert {tuple(), ('more', 'args')} == set(call_history) def test_callbacks_tied_to_execute(self): """Batch callbacks should NOT fire if batch is not executed in context manager mode""" diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index 586344469e..c05665527e 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -342,14 +342,16 @@ Batch Query Execution Callbacks The callbacks attached to a given batch instance are executed only if the batch executes. If the batch is used as a context manager and an exception bubbles up, the queued up callbacks will not be run. - The callback should be a callable object that accepts at least one (positional) argument - instance of the batch object. + The callback arguments signature is not prescribed. However, while it's possible to trap arguments you want to be + passed into the callback in a "callback" tuple you add to the batch, only passing of positional arguments is + supported at this time. This means there is no elegant way to communicate named arguments to the callback. .. code-block:: python - def example_callback_handler(batch, *args, **kwargs): + def example_callback_handler(*args): pass - Multiple callbacks can be attached to same BatchQuery instance both, at the point of instantiation and later + Multiple callbacks can be attached to same BatchQuery instance both, at the point of batch instantiation and later, directly to the batch object instance. The callbacks collection is implemented as a ``set``, which naturally deduplicates the callback handlers: @@ -396,22 +398,21 @@ Batch Query Execution Callbacks } Note that the tuple callback object format prevents the possibily of essentially same callback firing multiple times, - which might happen if you package same callback and arguments several times with ``functools.partial``, which returns a new callable with each call. - - Arguments packaged into callbacks communicated as tuples are passed to callback handler **after** the ``batch`` argument. + which might happen if you package same callback and arguments several times with ``functools.partial``, which returns + a new callable with each call. .. code-block:: python call_args = [] - def callback_handler(batch, *args, **kwargs): - call_args.append([batch] + list(args)) + def callback_handler(*args, **kwargs): + call_args.append(list(args)) batch = BatchQuery(callbacks=([(callback_handler, 'some', 'args')]) batch.execute() - assert call_args == [[batch, 'some', 'args']] + assert call_args[0] == ['some', 'args'] Failure in any of the callbacks does not affect the batch's execution, as the callbacks are started after the execution From 8093ac4bad555bf2f2f06c46b120a0cd7752a945 Mon Sep 17 00:00:00 2001 From: Daniel Dotsenko Date: Mon, 14 Apr 2014 18:10:48 -0700 Subject: [PATCH 0654/3961] switching to list as BatchQuery callbacks collection + add_callback() API --- cqlengine/query.py | 39 +++++++------- cqlengine/tests/test_batch_query.py | 78 ++++++++++----------------- docs/topics/queryset.rst | 82 +++++++---------------------- 3 files changed, 68 insertions(+), 131 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index f2b6da14c1..9e81f5a553 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -78,16 +78,7 @@ class BatchQuery(object): """ _consistency = None - @property - def callbacks(self): - """ - Returns the set object that contains all of the callbacks presently attached to this batch. - - :rtype: set - """ - return self._callbacks - - def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on_exception=False, callbacks=None): + def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on_exception=False): """ :param batch_type: (optional) One of batch type values available through BatchType enum :type batch_type: str or None @@ -113,7 +104,7 @@ def __init__(self, batch_type=None, timestamp=None, consistency=None, execute_on self.timestamp = timestamp self._consistency = consistency self._execute_on_exception = execute_on_exception - self._callbacks = set(callbacks or []) + self._callbacks = [] def add_query(self, query): if not isinstance(query, BaseCQLStatement): @@ -124,17 +115,29 @@ def consistency(self, consistency): self._consistency = consistency def _execute_callbacks(self): - for callback in self._callbacks: - if callable(callback): - stored_args = [] - else: - stored_args = callback[1:] - callback = callback[0] - callback(*stored_args) + for callback, args, kwargs in self._callbacks: + callback(*args, **kwargs) # trying to clear up the ref counts for objects mentioned in the set del self._callbacks + def add_callback(self, fn, *args, **kwargs): + """Add a function and arguments to be passed to it to be executed after the batch executes. + + A batch can support multiple callbacks. + + Note, that if the batch does not execute, the callbacks are not executed. + A callback, thus, is an "on batch success" handler. + + :param fn: Callable object + :type fn: callable + :param *args: Positional arguments to be passed to the callback at the time of execution + :param **kwargs: Named arguments to be passed to the callback at the time of execution + """ + if not callable(fn): + raise ValueError("Value for argument 'fn' is {} and is not a callable object.".format(type(fn))) + self._callbacks.append((fn, args, kwargs)) + def execute(self): if len(self.queries) == 0: # Empty batch is a no-op diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index f956323a69..7066643d39 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -122,48 +122,21 @@ def test_API_managing_callbacks(self): # Callbacks can be added at init and after - def my_callback(batch, *args, **kwargs): + def my_callback(*args, **kwargs): pass # adding on init: - b = BatchQuery(callbacks=[ - my_callback, - my_callback, # will be filtered out automatically - (my_callback, 'more', 'args'), - (my_callback, 'more', 'args'), # will be filtered out too - ]) - assert b.callbacks == { - my_callback, - (my_callback, 'more', 'args') - } - - # adding using set API post-init: - - b.callbacks.add(my_callback) - b.callbacks.add((my_callback, 'more', 'args')) - b.callbacks.add((my_callback, 'yet more', 'args')) - assert b.callbacks == { - my_callback, - (my_callback, 'more', 'args'), - (my_callback, 'yet more', 'args') - } - - b.callbacks.remove(my_callback) - assert b.callbacks == { - (my_callback, 'more', 'args'), - (my_callback, 'yet more', 'args') - } - - b.callbacks.remove((my_callback, 'more', 'args')) - assert b.callbacks == { - (my_callback, 'yet more', 'args') - } - - - # insure that we disallow setting the callback colleciton on instance: - # (thus forcing use of init and set-like api tested above) - with self.assertRaises(AttributeError): - b.callbacks = set() + batch = BatchQuery() + + batch.add_callback(my_callback) + batch.add_callback(my_callback, 2, named_arg='value') + batch.add_callback(my_callback, 1, 3) + + assert batch._callbacks == [ + (my_callback, (), {}), + (my_callback, (2,), {'named_arg':'value'}), + (my_callback, (1, 3), {}) + ] def test_callbacks_properly_execute_callables_and_tuples(self): @@ -172,15 +145,15 @@ def my_callback(*args, **kwargs): call_history.append(args) # adding on init: - b = BatchQuery(callbacks=[ - my_callback, - (my_callback, 'more', 'args') - ]) + batch = BatchQuery() - b.execute() + batch.add_callback(my_callback) + batch.add_callback(my_callback, 'more', 'args') + + batch.execute() assert len(call_history) == 2 - assert {tuple(), ('more', 'args')} == set(call_history) + assert [(), ('more', 'args')] == call_history def test_callbacks_tied_to_execute(self): """Batch callbacks should NOT fire if batch is not executed in context manager mode""" @@ -189,7 +162,8 @@ def test_callbacks_tied_to_execute(self): def my_callback(*args, **kwargs): call_history.append(args) - with BatchQuery(callbacks=[my_callback]) as b: + with BatchQuery() as batch: + batch.add_callback(my_callback) pass assert len(call_history) == 1 @@ -198,18 +172,20 @@ class SomeError(Exception): pass with self.assertRaises(SomeError): - with BatchQuery(callbacks=[my_callback]) as b: + with BatchQuery() as batch: + batch.add_callback(my_callback) # this error bubbling up through context manager # should prevent callback runs (along with b.execute()) raise SomeError - # still same call history + # still same call history. Nothing added assert len(call_history) == 1 - # but if execute run, even with an error bubbling through - # the callbacks also whoudl have fired + # but if execute ran, even with an error bubbling through + # the callbacks also would have fired with self.assertRaises(SomeError): - with BatchQuery(execute_on_exception=True, callbacks=[my_callback]) as b: + with BatchQuery(execute_on_exception=True) as batch: + batch.add_callback(my_callback) # this error bubbling up through context manager # should prevent callback runs (along with b.execute()) raise SomeError diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index c05665527e..d751152f2a 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -339,81 +339,39 @@ Batch Query Execution Callbacks the maintenance task would do would operate on old data since the batch is not yet commited. (You also risk kicking off too many of the same tasks, while only one is needed at the end of the batch.) + Multiple callbacks can be attached to same BatchQuery instance + The callbacks attached to a given batch instance are executed only if the batch executes. If the batch is used as a context manager and an exception bubbles up, the queued up callbacks will not be run. - The callback arguments signature is not prescribed. However, while it's possible to trap arguments you want to be - passed into the callback in a "callback" tuple you add to the batch, only passing of positional arguments is - supported at this time. This means there is no elegant way to communicate named arguments to the callback. - - .. code-block:: python - - def example_callback_handler(*args): - pass - - Multiple callbacks can be attached to same BatchQuery instance both, at the point of batch instantiation and later, - directly to the batch object instance. + The callback arguments signature is not prescribed. Moreover, the callback API allows trapping the arguments you want to be + passed into the callback. If you need to inspect the batch itself within the callback, simply trap the batch instance + as part of the arguments stored with the callback. - The callbacks collection is implemented as a ``set``, which naturally deduplicates the callback handlers: + Internally the callbacks collection is implemented as an ordered collection, which means the execution order follows + the order in which the callbacks are added to the batch. .. code-block:: python - # let's add a few of the same callbacks at init - batch = BatchQuery(callbacks=([ - example_callback_handler, - example_callback_handler - ]) - - # and one more time same callback now using the collection API on the batch instance - batch.callbacks.add(example_callback_handler) - - assert len(batch.callbacks) == 1 - assert batch.callbacks == {example_callback_handler} - - # this, of course, also means that if you remove a callback, - # you negate "all additions" of that callback - batch.callbacks.remove(example_callback_handler) - - assert len(batch.callbacks) == 0 - - In order to benefit from the natural deduplication of the callback handlers it is allowed to pass in tuples of - callback and its positional arguments. - This allows one to pre-package additional call arguments for the callable without the need to wrap the callable and - args into a closure or ``functools.partial``. - - .. code-block:: python - - batch = BatchQuery(callbacks=([ - (example_callback_handler, 'some', 'args'), - (example_callback_handler, 'some', 'args'), - (example_callback_handler, 'some other', 'args'), - ]) - - batch.callbacks.add((example_callback_handler, 'some other', 'args')) - - assert len(batch.callbacks) == 2 - assert batch.callbacks == { - (example_callback_handler, 'some', 'args'), - (example_callback_handler, 'some other', 'args') - } - - Note that the tuple callback object format prevents the possibily of essentially same callback firing multiple times, - which might happen if you package same callback and arguments several times with ``functools.partial``, which returns - a new callable with each call. - - .. code-block:: python + def my_callback(*args, **kwargs): + pass - call_args = [] + batch = BatchQuery() - def callback_handler(*args, **kwargs): - call_args.append(list(args)) + batch.add_callback(my_callback) + batch.add_callback(my_callback, 'positional arg', named_arg='named arg value') - batch = BatchQuery(callbacks=([(callback_handler, 'some', 'args')]) + # if you need reference to the batch within the callback, + # just trap it in the arguments to be passed to the callback: + batch.add_callback(my_callback, cqlengine_batch=batch) + # once the batch executes... batch.execute() - assert call_args[0] == ['some', 'args'] - + # the effect of the above scheduled callbacks will be similar to + my_callback() + my_callback('positional arg', named_arg='named arg value') + my_callback(cqlengine_batch=batch) Failure in any of the callbacks does not affect the batch's execution, as the callbacks are started after the execution of the batch is complete and no effort to "roll it back" is made. From 2d22138c0dfa1f115f32d8d308a1dad3a79b0505 Mon Sep 17 00:00:00 2001 From: Daniel Dotsenko Date: Wed, 9 Apr 2014 10:49:04 -0700 Subject: [PATCH 0655/3961] fix for "ValueError: 'column_name' is not in list" error on sync_table for models with primary keys only https://github.com/cqlengine/cqlengine/issues/175 --- cqlengine/management.py | 25 ++++++++++++++++--- cqlengine/tests/management/test_management.py | 21 +++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 7b40c30f91..ed31eb5759 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -65,6 +65,16 @@ def create_table(model, create_missing_keyspace=True): sync_table(model, create_missing_keyspace) def sync_table(model, create_missing_keyspace=True): + """ + Inspects the model and creates / updates the corresponding table and columns. + + Note that the attributes removed from the model are not deleted on the database. + They become effectively ignored by (will not show up on) the model. + + :param create_missing_keyspace: (Defaults to True) Flags to us that we need to create missing keyspace + mentioned in the model automatically. + :type create_missing_keyspace: bool + """ if model.__abstract__: raise CQLEngineException("cannot create table from abstract model") @@ -238,12 +248,19 @@ def get_fields(model): tmp = con.execute(query, {'ks_name': ks_name, 'col_family': col_family}, ONE) - column_indices = [tmp.columns.index('column_name'), tmp.columns.index('validator')] + # Tables containing only primary keys do not appear to create + # any entries in system.schema_columns, as only non-primary-key attributes + # appear to be inserted into the schema_columns table + if not tmp.results: + return [] + + column_name_positon = tmp.columns.index('column_name') + validator_positon = tmp.columns.index('validator') try: - type_index = tmp.columns.index('type') - return [Field(x[column_indices[0]], x[column_indices[1]]) for x in tmp.results if x[type_index] == 'regular'] + type_position = tmp.columns.index('type') + return [Field(x[column_name_positon], x[validator_positon]) for x in tmp.results if x[type_position] == 'regular'] except ValueError: - return [Field(x[column_indices[0]], x[column_indices[1]]) for x in tmp.results] + return [Field(x[column_name_positon], x[validator_positon]) for x in tmp.results] # convert to Field named tuples diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 25f4d268b6..5179dee3a1 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -2,7 +2,7 @@ from cqlengine import ONE from cqlengine.exceptions import CQLEngineException -from cqlengine.management import create_table, delete_table, get_fields +from cqlengine.management import create_table, delete_table, get_fields, sync_table from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.connection import ConnectionPool, Host from cqlengine import management @@ -77,6 +77,11 @@ class CapitalizedKeyModel(Model): secondKey = columns.Integer(primary_key=True) someData = columns.Text() +class PrimaryKeysOnlyModel(Model): + first_ey = columns.Integer(primary_key=True) + second_key = columns.Integer(primary_key=True) + + class CapitalizedKeyTest(BaseCassEngTestCase): def test_table_definition(self): @@ -142,3 +147,17 @@ def test_add_column(self): self.assertEqual(len(fields), 4) +class SyncTableTests(BaseCassEngTestCase): + + def setUp(self): + delete_table(PrimaryKeysOnlyModel) + + def test_sync_table_works_with_primary_keys_only_tables(self): + sync_table(PrimaryKeysOnlyModel) + + # primary-keys-only tables do not create entries in system.schema_columns + # table. Only non-primary keys are added to that table. + # Our code must deal with that eventuality properly (not crash) + # on subsequent runs of sync_table (which runs get_fields internally) + get_fields(PrimaryKeysOnlyModel) + sync_table(PrimaryKeysOnlyModel) From 97826f038d8906497dfb5e165d24f9657a9063d5 Mon Sep 17 00:00:00 2001 From: Daniel Dotsenko Date: Wed, 16 Apr 2014 23:05:44 -0700 Subject: [PATCH 0656/3961] more blah for primary-key-only table sync_table test --- cqlengine/tests/management/test_management.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 5179dee3a1..0ba27cd5a1 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -8,8 +8,7 @@ from cqlengine import management from cqlengine.tests.query.test_queryset import TestModel from cqlengine.models import Model -from cqlengine import columns - +from cqlengine import columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy class ConnectionPoolFailoverTestCase(BaseCassEngTestCase): """Test cassandra connection pooling.""" @@ -78,6 +77,8 @@ class CapitalizedKeyModel(Model): someData = columns.Text() class PrimaryKeysOnlyModel(Model): + __compaction__ = LeveledCompactionStrategy + first_ey = columns.Integer(primary_key=True) second_key = columns.Integer(primary_key=True) @@ -153,11 +154,31 @@ def setUp(self): delete_table(PrimaryKeysOnlyModel) def test_sync_table_works_with_primary_keys_only_tables(self): + + # This is "create table": + sync_table(PrimaryKeysOnlyModel) + # let's make sure settings persisted correctly: + + assert PrimaryKeysOnlyModel.__compaction__ == LeveledCompactionStrategy + # blows up with DoesNotExist if table does not exist + table_settings = management.get_table_settings(PrimaryKeysOnlyModel) + # let make sure the flag we care about + assert LeveledCompactionStrategy in table_settings['compaction_strategy_class'] + + + # Now we are "updating" the table: + + # setting up something to change + PrimaryKeysOnlyModel.__compaction__ = SizeTieredCompactionStrategy + # primary-keys-only tables do not create entries in system.schema_columns # table. Only non-primary keys are added to that table. # Our code must deal with that eventuality properly (not crash) # on subsequent runs of sync_table (which runs get_fields internally) get_fields(PrimaryKeysOnlyModel) sync_table(PrimaryKeysOnlyModel) + + table_settings = management.get_table_settings(PrimaryKeysOnlyModel) + assert SizeTieredCompactionStrategy in table_settings['compaction_strategy_class'] From 70e58416d0753a081f35e35f6dfacd841afc222d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 17 Apr 2014 15:27:46 -0700 Subject: [PATCH 0657/3961] cleaning up batch callback docs --- docs/topics/queryset.rst | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index d751152f2a..a8eb2af576 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -333,23 +333,11 @@ Batch Query Execution Callbacks In order to allow secondary tasks to be chained to the end of batch, BatchQuery instances allow callbacks to be registered with the batch, to be executed immediately after the batch executes. - Let's say an iterative action over a group of records needs to be followed up by a "maintenance" task like "Update - count of children on parent model." - If your were to kick off that task following manipulation on each child, while the batch is not executed, the reads - the maintenance task would do would operate on old data since the batch is not yet commited. - (You also risk kicking off too many of the same tasks, while only one is needed at the end of the batch.) - - Multiple callbacks can be attached to same BatchQuery instance + Multiple callbacks can be attached to same BatchQuery instance, they are executed in the same order that they + are added to the batch. The callbacks attached to a given batch instance are executed only if the batch executes. If the batch is used as a - context manager and an exception bubbles up, the queued up callbacks will not be run. - - The callback arguments signature is not prescribed. Moreover, the callback API allows trapping the arguments you want to be - passed into the callback. If you need to inspect the batch itself within the callback, simply trap the batch instance - as part of the arguments stored with the callback. - - Internally the callbacks collection is implemented as an ordered collection, which means the execution order follows - the order in which the callbacks are added to the batch. + context manager and an exception is raised, the queued up callbacks will not be run. .. code-block:: python @@ -374,7 +362,7 @@ Batch Query Execution Callbacks my_callback(cqlengine_batch=batch) Failure in any of the callbacks does not affect the batch's execution, as the callbacks are started after the execution - of the batch is complete and no effort to "roll it back" is made. + of the batch is complete. From e9403e562c8651772c1be8960e875ebc4744cb9e Mon Sep 17 00:00:00 2001 From: Nati CT Date: Tue, 22 Apr 2014 15:16:12 +0300 Subject: [PATCH 0658/3961] fixed issue with timestamp and DST --- cqlengine/query.py | 10 ++++++---- cqlengine/statements.py | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 9e81f5a553..cfc56eef46 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -1,4 +1,5 @@ import copy +import time from datetime import datetime, timedelta from cqlengine import BaseContainerColumn, Map, columns from cqlengine.columns import Counter, List, Set @@ -150,10 +151,11 @@ def execute(self): if isinstance(self.timestamp, (int, long)): ts = self.timestamp - elif isinstance(self.timestamp, timedelta): - ts = long((datetime.now() + self.timestamp - datetime.fromtimestamp(0)).total_seconds() * 1000000) - elif isinstance(self.timestamp, datetime): - ts = long((self.timestamp - datetime.fromtimestamp(0)).total_seconds() * 1000000) + elif isinstance(self.timestamp, (datetime, timedelta)): + ts = self.timestamp + if isinstance(self.timestamp, timedelta): + ts += datetime.now() # Apply timedelta + long(time.mktime(ts.timetuple()) * 1e+6 + ts.microsecond) else: raise ValueError("Batch expects a long, a timedelta, or a datetime") diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 2fb9a88a5d..70321b5818 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -1,3 +1,4 @@ +import time from datetime import datetime, timedelta from cqlengine.functions import QueryValue from cqlengine.operators import BaseWhereOperator, InOperator @@ -496,7 +497,7 @@ def timestamp_normalized(self): else: tmp = self.timestamp - return long(((tmp - datetime.fromtimestamp(0)).total_seconds()) * 1000000) + return long(time.mktime(tmp.timetuple()) * 1e+6 + tmp.microsecond) def __unicode__(self): raise NotImplementedError From dec7c24bb4ba5d98fa696dde91d2737e6607825b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 22 Apr 2014 23:12:35 -0700 Subject: [PATCH 0659/3961] timestamp as a long wasn't being assigned --- cqlengine/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index cfc56eef46..383b9c0510 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -155,7 +155,7 @@ def execute(self): ts = self.timestamp if isinstance(self.timestamp, timedelta): ts += datetime.now() # Apply timedelta - long(time.mktime(ts.timetuple()) * 1e+6 + ts.microsecond) + ts = long(time.mktime(ts.timetuple()) * 1e+6 + ts.microsecond) else: raise ValueError("Batch expects a long, a timedelta, or a datetime") From e12272515ff1d16eb1ac115821a216a7d261da30 Mon Sep 17 00:00:00 2001 From: Nati CT Date: Sun, 13 Apr 2014 18:46:53 +0300 Subject: [PATCH 0660/3961] Removed cql monkey patch in favour of Quoter to allow Bytes support --- cqlengine/columns.py | 7 ++++++- cqlengine/statements.py | 19 ------------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 7888981a97..e78e9255eb 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -207,10 +207,15 @@ def _val_is_null(self, val): class Bytes(Column): db_type = 'blob' + class Quoter(ValueQuoter): + def __str__(self): + return '0x' + self.value.encode('hex') + def to_database(self, value): val = super(Bytes, self).to_database(value) if val is None: return - return '0x' + val.encode('hex') + + return self.Quoter(val) def to_python(self, value): #return value[2:].decode('hex') diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 70321b5818..c7b06945d2 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -7,25 +7,6 @@ class StatementException(Exception): pass -# Monkey patch cql_quote to allow raw hex values -import cql.query - - -def cql_quote_replacement(term): - if isinstance(term, basestring) and term.startswith('0x'): - if isinstance(term, unicode): - return term.encode('utf8') - else: - return term - elif isinstance(term, unicode): - return "'%s'" % cql.query.__escape_quotes(term.encode('utf8')) - elif isinstance(term, (str, bool)): - return "'%s'" % cql.query.__escape_quotes(str(term)) - else: - return str(term) -cql.query.cql_quote = cql_quote_replacement - - class ValueQuoter(object): def __init__(self, value): From c5381ee834e291c827867f832722f63f7df512e9 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Wed, 23 Apr 2014 11:04:56 +0200 Subject: [PATCH 0661/3961] Tests that adding query with a limit affects the count as expected. --- cqlengine/tests/query/test_queryset.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 2144dee5ac..97c5c4e8b0 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -234,6 +234,13 @@ def test_query_expression_count(self): q = TestModel.objects(TestModel.test_id == 0) assert q.count() == 4 + def test_query_limit_count(self): + """ Tests that adding query with a limit affects the count as expected """ + assert TestModel.objects.count() == 12 + + q = TestModel.objects(TestModel.test_id == 0).limit(2) + assert q.count() == 2 + def test_iteration(self): """ Tests that iterating over a query set pulls back all of the expected results """ q = TestModel.objects(test_id=0) From ece104cc99aa77a1d739ff9e76082f21bcd9750a Mon Sep 17 00:00:00 2001 From: Nati CT Date: Wed, 23 Apr 2014 12:24:05 +0300 Subject: [PATCH 0662/3961] Added a test to show the problem with Text values and '0x' prefix --- cqlengine/tests/columns/test_value_io.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index c4958f40dd..4fa36d6cc1 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -89,6 +89,13 @@ class TestTextIO(BaseColumnIOTest): pkey_val = 'bacon' data_val = 'monkey' + +class TestNonBinaryTextIO(BaseColumnIOTest): + + column = columns.Text + pkey_val = 'bacon' + data_val = '0xmonkey' + class TestInteger(BaseColumnIOTest): column = columns.Integer From 7d76f0138eac2bd13ea1bcd827d2111213088f84 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Wed, 23 Apr 2014 12:45:39 -0700 Subject: [PATCH 0663/3961] updating changelog and version --- changelog | 7 +++++++ cqlengine/VERSION | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index eb99e95382..697b99f5ff 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,12 @@ CHANGELOG +0.13.0 +* adding support for post batch callbacks (thanks Daniel Dotsenko github.com/dvdotsenko) +* fixing sync table for tables with multiple keys (thanks Daniel Dotsenko github.com/dvdotsenko) +* fixing bug in Bytes column (thanks Netanel Cohen-Tzemach github.com/natict) +* fixing bug with timestamps and DST (thanks Netanel Cohen-Tzemach github.com/natict) +* performance improvements (thanks Jon Haddad) + 0.12.0 * Normalize and unquote boolean values. (Thanks Kevin Deldycke github.com/kdeldycke) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index ac454c6a1f..54d1a4f2a4 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.12.0 +0.13.0 From 87ab5bd49959823a89cc6f2b4b8b15e707a8a19e Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 24 Apr 2014 16:40:54 +0200 Subject: [PATCH 0664/3961] Check number of elements in collections. --- changelog | 4 ++++ cqlengine/columns.py | 12 +++++++++- .../tests/columns/test_container_columns.py | 24 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index 697b99f5ff..c2d7aa2055 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,9 @@ CHANGELOG +0.14.0-dev + +* Check number of elements in collections. + 0.13.0 * adding support for post batch callbacks (thanks Daniel Dotsenko github.com/dvdotsenko) * fixing sync table for tables with multiple keys (thanks Daniel Dotsenko github.com/dvdotsenko) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index e78e9255eb..79cb373467 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -513,7 +513,9 @@ def to_database(self, value): class BaseContainerColumn(Column): """ - Base Container type + Base Container type for collection-like columns. + + https://cassandra.apache.org/doc/cql3/CQL.html#collections """ def __init__(self, value_type, **kwargs): @@ -537,6 +539,14 @@ def __init__(self, value_type, **kwargs): super(BaseContainerColumn, self).__init__(**kwargs) + def validate(self, value): + value = super(BaseContainerColumn, self).validate(value) + # It is dangerous to let collections have more than 65535. + # See: https://issues.apache.org/jira/browse/CASSANDRA-5428 + if value is not None and len(value) > 65535: + raise ValidationError("Collection can't have more than 65535 elements.") + return value + def get_column_def(self): """ Returns a column definition for CQL table definition diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 2f803913f6..63af8ab17d 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -92,6 +92,14 @@ def test_type_validation(self): with self.assertRaises(ValidationError): TestSetModel.create(int_set={'string', True}, text_set={1, 3.0}) + def test_element_count_validation(self): + """ + Tests that big collections are detected and raise an exception. + """ + TestSetModel.create(text_set={str(uuid4()) for i in range(65535)}) + with self.assertRaises(ValidationError): + TestSetModel.create(text_set={str(uuid4()) for i in range(65536)}) + def test_partial_updates(self): """ Tests that partial udpates work as expected """ m1 = TestSetModel.create(int_set={1, 2, 3, 4}) @@ -232,6 +240,14 @@ def test_type_validation(self): with self.assertRaises(ValidationError): TestListModel.create(int_list=['string', True], text_list=[1, 3.0]) + def test_element_count_validation(self): + """ + Tests that big collections are detected and raise an exception. + """ + TestListModel.create(text_list=[str(uuid4()) for i in range(65535)]) + with self.assertRaises(ValidationError): + TestListModel.create(text_list=[str(uuid4()) for i in range(65536)]) + def test_partial_updates(self): """ Tests that partial udpates work as expected """ final = range(10) @@ -423,6 +439,14 @@ def test_type_validation(self): with self.assertRaises(ValidationError): TestMapModel.create(int_map={'key': 2, uuid4(): 'val'}, text_map={2: 5}) + def test_element_count_validation(self): + """ + Tests that big collections are detected and raise an exception. + """ + TestMapModel.create(text_map={str(uuid4()): i for i in range(65535)}) + with self.assertRaises(ValidationError): + TestMapModel.create(text_map={str(uuid4()): i for i in range(65536)}) + def test_partial_updates(self): """ Tests that partial udpates work as expected """ now = datetime.now() From b430157605ca661c44eeaafc55ccc0ace224bc86 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 24 Apr 2014 16:41:13 +0200 Subject: [PATCH 0665/3961] Fix test layout. --- .../tests/columns/test_container_columns.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 63af8ab17d..135ef05796 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -9,12 +9,14 @@ class TestSetModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) int_set = columns.Set(columns.Integer, required=False) text_set = columns.Set(columns.Text, required=False) class JsonTestColumn(columns.Column): + db_type = 'text' def to_python(self, value): @@ -65,7 +67,6 @@ def test_deleting_last_item_should_succeed(self): m = TestSetModel.get(partition=m.partition) self.assertNotIn(5, m.int_set) - def test_empty_set_retrieval(self): m = TestSetModel.create() m2 = TestSetModel.get(partition=m.partition) @@ -190,12 +191,14 @@ def test_default_empty_container_saving(self): class TestListModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) int_list = columns.List(columns.Integer, required=False) text_list = columns.List(columns.Text, required=False) class TestListColumn(BaseCassEngTestCase): + @classmethod def setUpClass(cls): super(TestListColumn, cls).setUpClass() @@ -358,16 +361,15 @@ def test_insert_none(self): TestListModel.create(partition=pkey, int_list=[None]) - - - class TestMapModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) int_map = columns.Map(columns.Integer, columns.UUID, required=False) text_map = columns.Map(columns.Text, columns.DateTime, required=False) class TestMapColumn(BaseCassEngTestCase): + @classmethod def setUpClass(cls): super(TestMapColumn, cls).setUpClass() @@ -391,13 +393,11 @@ def test_add_none_as_map_value(self): with self.assertRaises(ValidationError): TestMapModel.create(int_map={None:1}) - def test_empty_retrieve(self): tmp = TestMapModel.create() tmp2 = TestMapModel.get(partition=tmp.partition) tmp2.int_map['blah'] = 1 - def test_remove_last_entry_works(self): tmp = TestMapModel.create() tmp.text_map["blah"] = datetime.now() @@ -408,8 +408,6 @@ def test_remove_last_entry_works(self): tmp = TestMapModel.get(partition=tmp.partition) self.assertNotIn("blah", tmp.int_map) - - def test_io_success(self): """ Tests that a basic usage works as expected """ k1 = uuid4() @@ -474,7 +472,6 @@ def test_updates_from_none(self): m.int_map = expected m.save() - m2 = TestMapModel.get(partition=m.partition) assert m2.int_map == expected @@ -547,11 +544,15 @@ def test_default_empty_container_saving(self): # assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 # assert len([s for s in statements if '+ "TEST"' in s]) == 1 + class TestCamelMapModel(Model): + partition = columns.UUID(primary_key=True, default=uuid4) camelMap = columns.Map(columns.Text, columns.Integer, required=False) + class TestCamelMapColumn(BaseCassEngTestCase): + @classmethod def setUpClass(cls): super(TestCamelMapColumn, cls).setUpClass() @@ -565,4 +566,3 @@ def tearDownClass(cls): def test_camelcase_column(self): TestCamelMapModel.create(partition=None, camelMap={'blah': 1}) - From 2af6b9b55167e90e0bfa4483a82123db49b1366b Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Wed, 16 Apr 2014 16:50:37 +0200 Subject: [PATCH 0666/3961] Add support for simple table properties. Closes #181 and #191. --- cqlengine/__init__.py | 6 ++++- cqlengine/management.py | 18 ++++++++----- cqlengine/models.py | 14 ++++++++-- docs/topics/models.rst | 58 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index cd9f8f3557..e1df6aff81 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -14,6 +14,11 @@ SizeTieredCompactionStrategy = "SizeTieredCompactionStrategy" LeveledCompactionStrategy = "LeveledCompactionStrategy" +# Caching constants. +ALL = "all" +KEYS_ONLY = "keys_only" +ROWS_ONLY = "rows_only" +NONE = "none" ANY = "ANY" ONE = "ONE" @@ -23,4 +28,3 @@ LOCAL_QUORUM = "LOCAL_QUORUM" EACH_QUORUM = "EACH_QUORUM" ALL = "ALL" - diff --git a/cqlengine/management.py b/cqlengine/management.py index ed31eb5759..6c9ff5939d 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -170,22 +170,28 @@ def add_column(col): qs += ['({})'.format(', '.join(qtypes))] - with_qs = ['read_repair_chance = {}'.format(model.__read_repair_chance__)] + with_qs = [] - _order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] + table_properties = ['bloom_filter_fp_chance', 'caching', 'comment', + 'dclocal_read_repair_chance', 'gc_grace_seconds', 'read_repair_chance', + 'replicate_on_write'] + for prop_name in table_properties: + prop_value = getattr(model, '__{}__'.format(prop_name), None) + if prop_value is not None: + with_qs.append('{} = {}'.format(prop_name, prop_value)) + _order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] if _order: with_qs.append('clustering order by ({})'.format(', '.join(_order))) compaction_options = get_compaction_options(model) - if compaction_options: compaction_options = json.dumps(compaction_options).replace('"', "'") with_qs.append("compaction = {}".format(compaction_options)) - # add read_repair_chance - qs += ['WITH {}'.format(' AND '.join(with_qs))] - + # Add table properties. + if with_qs: + qs += ['WITH {}'.format(' AND '.join(with_qs))] qs = ' '.join(qs) return qs diff --git a/cqlengine/models.py b/cqlengine/models.py index 6f6662863e..6723c5c2cf 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -255,8 +255,18 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass #__ttl__ = None # this doesn't seem to be used __consistency__ = None # can be set per query - __read_repair_chance__ = 0.1 - + # Additional table properties + __bloom_filter_fp_chance__ = None + __caching__ = None + __comment__ = None + __dclocal_read_repair_chance__ = None + __default_time_to_live__ = None + __gc_grace_seconds__ = None + __index_interval__ = None + __memtable_flush_period_in_ms__ = None + __populate_io_cache_on_flush__ = None + __read_repair_chance__ = None + __replicate_on_write__ = None _timestamp = None # optional timestamp to include with the operation (USING TIMESTAMP) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index f6695d749b..14f905b40d 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -260,6 +260,63 @@ Extending Model Validation if validation fails +Table Properties +================ + + Each table can have its own set of configuration options. + These can be specified on a model with the following attributes: + + .. attribute:: Model.__bloom_filter_fp_chance + + .. attribute:: Model.__caching__ + + .. attribute:: Model.__comment__ + + .. attribute:: Model.__dclocal_read_repair_chance__ + + .. attribute:: Model.__default_time_to_live__ + + .. attribute:: Model.__gc_grace_seconds__ + + .. attribute:: Model.__index_interval__ + + .. attribute:: Model.__memtable_flush_period_in_ms__ + + .. attribute:: Model.__populate_io_cache_on_flush__ + + .. attribute:: Model.__read_repair_chance__ + + .. attribute:: Model.__replicate_on_write__ + + Example: + + .. code-block:: python + + from cqlengine import ROWS_ONLY, columns + from cqlengine.models import Model + + class User(Model): + __caching__ = ROWS_ONLY # cache only rows instead of keys only by default + __gc_grace_seconds__ = 86400 # 1 day instead of the default 10 days + + user_id = columns.UUID(primary_key=True) + name = columns.Text() + + Will produce the following CQL statement: + + .. code-block:: sql + + CREATE TABLE cqlengine.user ( + user_id uuid, + name text, + PRIMARY KEY (user_id) + ) WITH caching = 'rows_only' + AND gc_grace_seconds = 86400; + + See the `list of supported table properties for more information + `_. + + Compaction Options ================== @@ -311,6 +368,7 @@ Compaction Options Tables may use `LeveledCompactionStrategy` or `SizeTieredCompactionStrategy`. Both options are available in the top level cqlengine module. To reiterate, you will need to set your `__compaction__` option explicitly in order for cqlengine to handle any of your settings. + Manipulating model instances as dictionaries ============================================ From d407d1b2feee6c2671f12dc2972f00e2421bc996 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 24 Apr 2014 19:03:11 +0200 Subject: [PATCH 0667/3961] Update changelog. --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index c2d7aa2055..34c6d9cc43 100644 --- a/changelog +++ b/changelog @@ -3,6 +3,7 @@ CHANGELOG 0.14.0-dev * Check number of elements in collections. +* Add support for table properties. 0.13.0 * adding support for post batch callbacks (thanks Daniel Dotsenko github.com/dvdotsenko) From 5338f483cf2eac8eb71c0ba3fbd1efabee7cb3f4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 2 May 2014 15:13:31 -0700 Subject: [PATCH 0668/3961] merged optimizations --- cqlengine/columns.py | 6 +++--- cqlengine/connection.py | 6 +++--- cqlengine/models.py | 13 +++++++++---- cqlengine/tests/model/test_class_construction.py | 11 +++++++++++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 79cb373467..96ac7cc6c6 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -1,5 +1,5 @@ #column field types -from copy import deepcopy +from copy import deepcopy, copy from datetime import datetime from datetime import date import re @@ -33,7 +33,7 @@ def changed(self): return self.value != self.previous_value def reset_previous_value(self): - self.previous_value = deepcopy(self.value) + self.previous_value = copy(self.value) def getval(self): return self.value @@ -394,7 +394,7 @@ def validate(self, value): from uuid import UUID as _UUID if isinstance(val, _UUID): return val if isinstance(val, basestring) and self.re_uuid.match(val): - return _UUID(val) + return _UUID(val) raise ValidationError("{} is not a valid uuid".format(value)) def to_python(self, value): diff --git a/cqlengine/connection.py b/cqlengine/connection.py index e03a35b136..0b4fa7eca3 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -157,10 +157,10 @@ def put(self, conn): :type conn: connection """ - if self._queue.full(): + try: + self._queue.put(conn, block=False) + except queue.Full: conn.close() - else: - self._queue.put(conn) def _create_transport(self, host): """ diff --git a/cqlengine/models.py b/cqlengine/models.py index 6f6662863e..a0df093c75 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -179,10 +179,9 @@ def __get__(self, instance, owner): :param instance: the model instance :type instance: Model """ - - if instance: + try: return instance._values[self.column.column_name].getval() - else: + except AttributeError as e: return self.query_evaluator def __set__(self, instance, value): @@ -222,6 +221,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass # custom timestamps, see USING TIMESTAMP X timestamp = TimestampDescriptor() + # _len is lazily created by __len__ + # table names will be generated automatically from it's model # however, you can also define them manually here __table_name__ = None @@ -446,7 +447,11 @@ def __setitem__(self, key, val): def __len__(self): """ Returns the number of columns defined on that model. """ - return len(self._columns.keys()) + try: + return self._len + except: + self._len = len(self._columns.keys()) + return self._len def keys(self): """ Returns list of column's IDs. """ diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 6ae46753ec..517798f2f4 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -330,6 +330,17 @@ class CDQModel(Model): CDQModel().save() +class TestCachedLengthIsNotCarriedToSubclasses(BaseCassEngTestCase): + def test_subclassing(self): + + length = len(ConcreteModelWithCol()) + + class AlreadyLoadedTest(ConcreteModelWithCol): + new_field = columns.Integer() + + self.assertGreater(len(AlreadyLoadedTest()), length) + + From 884745224e896da644f4aaebd5a55360d0e38638 Mon Sep 17 00:00:00 2001 From: tarzan Date: Mon, 5 May 2014 10:46:23 +0700 Subject: [PATCH 0669/3961] Log query (DEBUG level) before execute. --- cqlengine/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index e03a35b136..2f7d940ced 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -221,11 +221,11 @@ def execute(self, query, params, consistency_level=None): con = self.get() if not con: raise CQLEngineException("Error calling execute without calling setup.") + LOG.debug('{} {}'.format(query, repr(params))) cur = con.cursor() cur.execute(query, params, consistency_level=consistency_level) columns = [i[0] for i in cur.description or []] results = [RowResult(r) for r in cur.fetchall()] - LOG.debug('{} {}'.format(query, repr(params))) self.put(con) return QueryResult(columns, results) except CQLConnectionError as ex: From dce28bd05c02da0e403e83d78dcd47dd8a171593 Mon Sep 17 00:00:00 2001 From: Nati CT Date: Thu, 8 May 2014 11:03:01 +0300 Subject: [PATCH 0670/3961] Changed connection error messages to be more informative --- cqlengine/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 081f167fac..1274e3e7fa 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -94,9 +94,9 @@ def setup( try: port = int(host[1]) except ValueError: - raise CQLConnectionError("Can't parse {}".format(''.join(host))) + raise CQLConnectionError("Can't parse port as int {}".format(':'.join(host))) else: - raise CQLConnectionError("Can't parse {}".format(''.join(host))) + raise CQLConnectionError("Can't parse host string {}".format(':'.join(host))) _hosts.append(Host(host[0], port)) From 7359a87be04ed78293c7fe2947ac2857efa214fc Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Thu, 8 May 2014 09:22:34 -0400 Subject: [PATCH 0671/3961] test to show blind update of empty map causes AttributeError --- cqlengine/tests/columns/test_container_columns.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 135ef05796..6637be1ece 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -480,6 +480,21 @@ def test_updates_from_none(self): m3 = TestMapModel.get(partition=m.partition) assert m3.int_map != expected + def test_blind_updates_from_none(self): + """ Tests that updates from None work as expected """ + m = TestMapModel.create(int_map=None) + expected = {1: uuid4()} + m.int_map = expected + m.save() + + m2 = TestMapModel.get(partition=m.partition) + assert m2.int_map == expected + + TestMapModel.objects(partition=m.partition).update(int_map={}) + + m3 = TestMapModel.get(partition=m.partition) + assert m3.int_map != expected + def test_updates_to_none(self): """ Tests that setting the field to None works as expected """ m = TestMapModel.create(int_map={1: uuid4()}) From a4143795d22fb7ab177f94aa67554f63eedb2fe6 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Thu, 8 May 2014 12:55:12 -0400 Subject: [PATCH 0672/3961] test to show blind update of empty set causes CQLEngineException --- cqlengine/tests/columns/test_container_columns.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 6637be1ece..d1531e7c53 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -67,6 +67,16 @@ def test_deleting_last_item_should_succeed(self): m = TestSetModel.get(partition=m.partition) self.assertNotIn(5, m.int_set) + def test_blind_deleting_last_item_should_succeed(self): + m = TestSetModel.create() + m.int_set.add(5) + m.save() + + TestSetModel.objects(partition=m.partition).update(int_set=set()) + + m = TestSetModel.get(partition=m.partition) + self.assertNotIn(5, m.int_set) + def test_empty_set_retrieval(self): m = TestSetModel.create() m2 = TestSetModel.get(partition=m.partition) From 161eda98a0ce6eb978649f096541eda0f02ff6aa Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 22 May 2014 15:33:45 +0200 Subject: [PATCH 0673/3961] Add missing table properties. --- cqlengine/management.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 6c9ff5939d..dc2440ddc6 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -173,8 +173,9 @@ def add_column(col): with_qs = [] table_properties = ['bloom_filter_fp_chance', 'caching', 'comment', - 'dclocal_read_repair_chance', 'gc_grace_seconds', 'read_repair_chance', - 'replicate_on_write'] + 'dclocal_read_repair_chance', 'default_time_to_live', 'gc_grace_seconds', + 'index_interval', 'memtable_flush_period_in_ms', 'populate_io_cache_on_flush', + 'read_repair_chance', 'replicate_on_write'] for prop_name in table_properties: prop_value = getattr(model, '__{}__'.format(prop_name), None) if prop_value is not None: From 7ed8fbdb5f0fc5a77cc7296db0f3d3718b00b1da Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 22 May 2014 15:34:18 +0200 Subject: [PATCH 0674/3961] Uppercase constants. That's how they're normalized by Cassandra. --- cqlengine/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index e1df6aff81..e1a1859745 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -15,10 +15,10 @@ LeveledCompactionStrategy = "LeveledCompactionStrategy" # Caching constants. -ALL = "all" -KEYS_ONLY = "keys_only" -ROWS_ONLY = "rows_only" -NONE = "none" +ALL = "ALL" +KEYS_ONLY = "KEYS_ONLY" +ROWS_ONLY = "ROWS_ONLY" +NONE = "NONE" ANY = "ANY" ONE = "ONE" From 44f6ea7bf7c1f82044cf6a0da1f84dc38c553d00 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 22 May 2014 16:13:32 +0200 Subject: [PATCH 0675/3961] Unit test table properties setting. --- cqlengine/tests/management/test_management.py | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 0ba27cd5a1..f64efa67f2 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,6 +1,9 @@ +import random +import string + from mock import MagicMock, patch -from cqlengine import ONE +from cqlengine import ONE, ALL, KEYS_ONLY, ROWS_ONLY, NONE from cqlengine.exceptions import CQLEngineException from cqlengine.management import create_table, delete_table, get_fields, sync_table from cqlengine.tests.base import BaseCassEngTestCase @@ -10,6 +13,7 @@ from cqlengine.models import Model from cqlengine import columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy + class ConnectionPoolFailoverTestCase(BaseCassEngTestCase): """Test cassandra connection pooling.""" @@ -148,6 +152,36 @@ def test_add_column(self): self.assertEqual(len(fields), 4) +class TablePropertiesTests(BaseCassEngTestCase): + + table_properties = { + 'bloom_filter_fp_chance': random.randint(0, 99999) / 100000.0, + 'caching': random.choice([ALL, KEYS_ONLY, ROWS_ONLY, NONE]), + 'comment': ''.join(random.choice(string.ascii_letters) + for i in range(random.randint(4, 99))), + 'dclocal_read_repair_chance': random.randint(0, 99999) / 100000.0, + 'default_time_to_live': random.randint(0, 99999), + 'gc_grace_seconds': random.randint(0, 99999), + 'index_interval': random.randint(0, 99999), + 'memtable_flush_period_in_ms': random.randint(0, 99999), + 'populate_io_cache_on_flush': random.choice([True, False]), + 'read_repair_chance': random.randint(0, 99999) / 100000.0, + 'replicate_on_write': random.choice([True, False]), + } + + def setUp(self): + delete_table(FirstModel) + + def test_set_table_properties(self): + for property_name, property_value in self.table_properties.items(): + property_id = '__{}__'.format(property_name) + setattr(FirstModel, property_id, property_value) + sync_table(FirstModel) + table_settings = management.get_table_settings(FirstModel) + for property_name, property_value in self.table_properties.items(): + assert table_settings[property_name] == property_value + + class SyncTableTests(BaseCassEngTestCase): def setUp(self): From ea9ca3abff15fb9244adbb3227dc0bfe6ba2acfa Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 22 May 2014 16:14:24 +0200 Subject: [PATCH 0676/3961] Table setting strings needs to be single quoted. --- cqlengine/management.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index dc2440ddc6..db445290ee 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -179,7 +179,10 @@ def add_column(col): for prop_name in table_properties: prop_value = getattr(model, '__{}__'.format(prop_name), None) if prop_value is not None: - with_qs.append('{} = {}'.format(prop_name, prop_value)) + # Strings needs to be single quoted + if isinstance(prop_value, basestring): + prop_value = "'{}'".format(prop_value) + with_qs.append("{} = {}".format(prop_name, prop_value)) _order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()] if _order: From f5eab308167e1a7ae96e0a95eea51ee81011b7c4 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 22 May 2014 16:14:57 +0200 Subject: [PATCH 0677/3961] Handle mismatching schema table property name. --- cqlengine/tests/management/test_management.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index f64efa67f2..f0d7c6e909 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -179,6 +179,11 @@ def test_set_table_properties(self): sync_table(FirstModel) table_settings = management.get_table_settings(FirstModel) for property_name, property_value in self.table_properties.items(): + if property_name == 'dclocal_read_repair_chance': + # For some reason 'dclocal_read_repair_chance' in CQL is called + # just 'local_read_repair_chance' in the schema table. + # Source: https://issues.apache.org/jira/browse/CASSANDRA-6717 + property_name = 'local_read_repair_chance' assert table_settings[property_name] == property_value From 64fe9ac6bc4b036141311ae269f78ac3763cc755 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 23 May 2014 17:00:24 -0500 Subject: [PATCH 0678/3961] Add UserDefinedType support --- cassandra/cqltypes.py | 40 ++++++++++++++++++++++++++++++++++++++++ cassandra/protocol.py | 4 +++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index e79b881518..461d3de7b1 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -28,6 +28,7 @@ # .from_cql_literal() and .as_cql_literal() classmethods (or whatever). import calendar +from collections import namedtuple from decimal import Decimal import re import socket @@ -735,6 +736,45 @@ def serialize_safe(cls, themap): return buf.getvalue() +class UserDefinedType(_ParameterizedType): + typename = "'org.apache.cassandra.db.marshal.UserType'" + + FIELD_LENGTH = 4 + + @classmethod + def apply_parameters(cls, subtypes, names): + newname = subtypes[1].cassname.decode("hex") + field_names = [encoded_name.decode("hex") for encoded_name in names[2:]] + return type(newname, (cls,), {'subtypes': subtypes[2:], + 'cassname': cls.cassname, + 'typename': newname, + 'fieldnames': field_names}) + + @classmethod + def cql_parameterized_type(cls): + return cls.typename + + @classmethod + def deserialize_safe(cls, byts): + p = 0 + Result = namedtuple(cls.typename, cls.fieldnames) + result = [] + for col_type in cls.subtypes: + if p == len(byts): + break + itemlen = int32_unpack(byts[p:p + cls.FIELD_LENGTH]) + p += cls.FIELD_LENGTH + item = byts[p:p + itemlen] + p += itemlen + result.append(col_type.from_binary(item)) + + if len(result) < len(cls.subtypes): + nones = [None] * (len(cls.subtypes) - len(result)) + result = result + nones + + return Result(*result) + + class CompositeType(_ParameterizedType): typename = "'org.apache.cassandra.db.marshal.CompositeType'" num_subtypes = 'UNKNOWN' diff --git a/cassandra/protocol.py b/cassandra/protocol.py index e9cb59f4de..3c7e8c2c03 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -29,7 +29,8 @@ DoubleType, FloatType, Int32Type, InetAddressType, IntegerType, ListType, LongType, MapType, SetType, TimeUUIDType, - UTF8Type, UUIDType, lookup_casstype) + UTF8Type, UUIDType, UserDefinedType, + lookup_casstype) from cassandra.policies import WriteType log = logging.getLogger(__name__) @@ -527,6 +528,7 @@ class ResultMessage(_MessageType): 0x0020: ListType, 0x0021: MapType, 0x0022: SetType, + 0x0030: UserDefinedType, } _FLAGS_GLOBAL_TABLES_SPEC = 0x0001 From 2c2c2c48da2eba15d1cdb762d01ef3b1487c3dca Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 23 May 2014 17:24:24 -0500 Subject: [PATCH 0679/3961] Pass protocol version to ser/deser functions --- cassandra/cluster.py | 2 +- cassandra/connection.py | 2 +- cassandra/cqltypes.py | 126 +++++++++++++++++++++++----------------- cassandra/protocol.py | 26 ++++----- cassandra/query.py | 4 +- 5 files changed, 91 insertions(+), 69 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 3f60687aac..ba7b2bd3db 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1134,7 +1134,7 @@ def _create_response_future(self, query, parameters, trace): if isinstance(query, six.string_types): query = SimpleStatement(query) elif isinstance(query, PreparedStatement): - query = query.bind(parameters) + query = query.bind(parameters, self._protocol_version) cl = query.consistency_level if query.consistency_level is not None else self.default_consistency_level fetch_size = query.fetch_size diff --git a/cassandra/connection.py b/cassandra/connection.py index 0660382cb0..f76e5cd80d 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -316,7 +316,7 @@ def process_msg(self, msg, body_len): else: raise ProtocolError("Got negative body length: %r" % body_len) - response = decode_response(stream_id, flags, opcode, body, self.decompressor) + response = decode_response(given_version, stream_id, flags, opcode, body, self.decompressor) except Exception as exc: log.exception("Error decoding response from Cassandra. " "opcode: %04x; message contents: %r", opcode, msg) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 461d3de7b1..1140337b4e 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -215,7 +215,7 @@ def validate(val): return val @classmethod - def from_binary(cls, byts): + def from_binary(cls, byts, protocol_version): """ Deserialize a bytestring into a value. See the deserialize() method for more information. This method differs in that if None or the empty @@ -225,19 +225,19 @@ def from_binary(cls, byts): return None elif len(byts) == 0 and not cls.empty_binary_ok: return EMPTY if cls.support_empty_values else None - return cls.deserialize(byts) + return cls.deserialize(byts, protocol_version) @classmethod - def to_binary(cls, val): + def to_binary(cls, val, protocol_version): """ Serialize a value into a bytestring. See the serialize() method for more information. This method differs in that if None is passed in, the result is the empty string. """ - return b'' if val is None else cls.serialize(val) + return b'' if val is None else cls.serialize(val, protocol_version) @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): """ Given a bytestring, deserialize into a value according to the protocol for this type. Note that this does not create a new instance of this @@ -247,7 +247,7 @@ def deserialize(byts): return byts @staticmethod - def serialize(val): + def serialize(val, protocol_version): """ Given a value appropriate for this class, serialize it according to the protocol for this type and return the corresponding bytestring. @@ -348,7 +348,7 @@ def validate(val): return bytearray(val) @staticmethod - def serialize(val): + def serialize(val, protocol_version): return six.binary_type(val) @@ -360,13 +360,13 @@ def validate(val): return Decimal(val) @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): scale = int32_unpack(byts[:4]) unscaled = varint_unpack(byts[4:]) return Decimal('%de%d' % (unscaled, -scale)) @staticmethod - def serialize(dec): + def serialize(dec, protocol_version): try: sign, digits, exponent = dec.as_tuple() except AttributeError: @@ -383,11 +383,11 @@ class UUIDType(_CassandraType): typename = 'uuid' @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): return UUID(bytes=byts) @staticmethod - def serialize(uuid): + def serialize(uuid, protocol_version): try: return uuid.bytes except AttributeError: @@ -402,11 +402,11 @@ def validate(val): return bool(val) @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): return bool(int8_unpack(byts)) @staticmethod - def serialize(truth): + def serialize(truth, protocol_version): return int8_pack(truth) @@ -420,11 +420,11 @@ class AsciiType(_CassandraType): empty_binary_ok = True @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): return byts.decode('ascii') @staticmethod - def serialize(var): + def serialize(var, protocol_version): try: return var.encode('ascii') except UnicodeDecodeError: @@ -434,36 +434,61 @@ def serialize(var): class FloatType(_CassandraType): typename = 'float' - deserialize = staticmethod(float_unpack) - serialize = staticmethod(float_pack) + @staticmethod + def deserialize(byts, protocol_version): + return float_unpack(byts) + + @staticmethod + def serialize(byts, protocol_version): + return float_pack(byts) class DoubleType(_CassandraType): typename = 'double' - deserialize = staticmethod(double_unpack) - serialize = staticmethod(double_pack) + @staticmethod + def deserialize(byts, protocol_version): + return double_unpack(byts) + + @staticmethod + def serialize(byts, protocol_version): + return double_pack(byts) class LongType(_CassandraType): typename = 'bigint' - deserialize = staticmethod(int64_unpack) - serialize = staticmethod(int64_pack) + @staticmethod + def deserialize(byts, protocol_version): + return int64_unpack(byts) + + @staticmethod + def serialize(byts, protocol_version): + return int64_pack(byts) class Int32Type(_CassandraType): typename = 'int' - deserialize = staticmethod(int32_unpack) - serialize = staticmethod(int32_pack) + @staticmethod + def deserialize(byts, protocol_version): + return int32_unpack(byts) + + @staticmethod + def serialize(byts, protocol_version): + return int32_pack(byts) class IntegerType(_CassandraType): typename = 'varint' - deserialize = staticmethod(varint_unpack) - serialize = staticmethod(varint_pack) + @staticmethod + def deserialize(byts, protocol_version): + return varint_unpack(byts) + + @staticmethod + def serialize(byts, protocol_version): + return varint_pack(byts) have_ipv6_packing = hasattr(socket, 'inet_ntop') @@ -476,7 +501,7 @@ class InetAddressType(_CassandraType): # inet_ntop and inet_pton aren't available on Windows @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): if len(byts) == 16: if not have_ipv6_packing: raise Exception( @@ -486,7 +511,7 @@ def deserialize(byts): return socket.inet_ntoa(byts) @staticmethod - def serialize(addr): + def serialize(addr, protocol_version): if ':' in addr: fam = socket.AF_INET6 if not have_ipv6_packing: @@ -498,12 +523,9 @@ def serialize(addr): return socket.inet_aton(addr) -class CounterColumnType(_CassandraType): +class CounterColumnType(LongType): typename = 'counter' - deserialize = staticmethod(int64_unpack) - serialize = staticmethod(int64_pack) - cql_time_formats = ( '%Y-%m-%d %H:%M', @@ -545,11 +567,11 @@ def my_timestamp(self): return self.val @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): return datetime.utcfromtimestamp(int64_unpack(byts) / 1000.0) @staticmethod - def serialize(v): + def serialize(v, protocol_version): global _have_warned_about_timestamps try: converted = calendar.timegm(v.utctimetuple()) @@ -588,11 +610,11 @@ def my_timestamp(self): return unix_time_from_uuid1(self.val) @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): return UUID(bytes=byts) @staticmethod - def serialize(timeuuid): + def serialize(timeuuid, protocol_version): try: return timeuuid.bytes except AttributeError: @@ -604,11 +626,11 @@ class UTF8Type(_CassandraType): empty_binary_ok = True @staticmethod - def deserialize(byts): + def deserialize(byts, protocol_version): return byts.decode('utf8') @staticmethod - def serialize(ustr): + def serialize(ustr, protocol_version): try: return ustr.encode('utf-8') except UnicodeDecodeError: @@ -627,18 +649,18 @@ def __init__(self, val): _CassandraType.__init__(self, val) @classmethod - def deserialize(cls, byts): + def deserialize(cls, byts, protocol_version): if not cls.subtypes: raise NotImplementedError("can't deserialize unparameterized %s" % cls.typename) - return cls.deserialize_safe(byts) + return cls.deserialize_safe(byts, protocol_version) @classmethod - def serialize(cls, val): + def serialize(cls, val, protocol_version): if not cls.subtypes: raise NotImplementedError("can't serialize unparameterized %s" % cls.typename) - return cls.serialize_safe(val) + return cls.serialize_safe(val, protocol_version) class _SimpleParameterizedType(_ParameterizedType): @@ -648,7 +670,7 @@ def validate(cls, val): return cls.adapter([subtype.validate(subval) for subval in val]) @classmethod - def deserialize_safe(cls, byts): + def deserialize_safe(cls, byts, protocol_version): subtype, = cls.subtypes numelements = uint16_unpack(byts[:2]) p = 2 @@ -662,7 +684,7 @@ def deserialize_safe(cls, byts): return cls.adapter(result) @classmethod - def serialize_safe(cls, items): + def serialize_safe(cls, items, protocol_version): if isinstance(items, six.string_types): raise TypeError("Received a string for a type that expects a sequence") @@ -670,7 +692,7 @@ def serialize_safe(cls, items): buf = six.BytesIO() buf.write(uint16_pack(len(items))) for item in items: - itembytes = subtype.to_binary(item) + itembytes = subtype.to_binary(item, protocol_version) buf.write(uint16_pack(len(itembytes))) buf.write(itembytes) return buf.getvalue() @@ -698,7 +720,7 @@ def validate(cls, val): return dict((subkeytype.validate(k), subvaltype.validate(v)) for (k, v) in six.iteritems(val)) @classmethod - def deserialize_safe(cls, byts): + def deserialize_safe(cls, byts, protocol_version): subkeytype, subvaltype = cls.subtypes numelements = uint16_unpack(byts[:2]) p = 2 @@ -718,7 +740,7 @@ def deserialize_safe(cls, byts): return themap @classmethod - def serialize_safe(cls, themap): + def serialize_safe(cls, themap, protocol_version): subkeytype, subvaltype = cls.subtypes buf = six.BytesIO() buf.write(uint16_pack(len(themap))) @@ -727,8 +749,8 @@ def serialize_safe(cls, themap): except AttributeError: raise TypeError("Got a non-map object for a map value") for key, val in items: - keybytes = subkeytype.to_binary(key) - valbytes = subvaltype.to_binary(val) + keybytes = subkeytype.to_binary(key, protocol_version) + valbytes = subvaltype.to_binary(val, protocol_version) buf.write(uint16_pack(len(keybytes))) buf.write(keybytes) buf.write(uint16_pack(len(valbytes))) @@ -755,7 +777,7 @@ def cql_parameterized_type(cls): return cls.typename @classmethod - def deserialize_safe(cls, byts): + def deserialize_safe(cls, byts, protocol_version): p = 0 Result = namedtuple(cls.typename, cls.fieldnames) result = [] @@ -800,14 +822,14 @@ class ReversedType(_ParameterizedType): num_subtypes = 1 @classmethod - def deserialize_safe(cls, byts): + def deserialize_safe(cls, byts, protocol_version): subtype, = cls.subtypes return subtype.from_binary(byts) @classmethod - def serialize_safe(cls, val): + def serialize_safe(cls, val, protocol_version): subtype, = cls.subtypes - return subtype.to_binary(val) + return subtype.to_binary(val, protocol_version) def is_counter_type(t): diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 3c7e8c2c03..a146b26110 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -101,7 +101,7 @@ def _get_params(message_obj): ) -def decode_response(stream_id, flags, opcode, body, decompressor=None): +def decode_response(protocol_version, stream_id, flags, opcode, body, decompressor=None): if flags & COMPRESSED_FLAG: if decompressor is None: raise Exception("No de-compressor available for compressed frame!") @@ -119,7 +119,7 @@ def decode_response(stream_id, flags, opcode, body, decompressor=None): log.warning("Unknown protocol flags set: %02x. May cause problems.", flags) msg_class = _message_types_by_opcode[opcode] - msg = msg_class.recv_body(body) + msg = msg_class.recv_body(body, protocol_version) msg.stream_id = stream_id msg.trace_id = trace_id return msg @@ -139,7 +139,7 @@ def __init__(self, code, message, info): self.info = info @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): code = read_int(f) msg = read_string(f) subcls = error_classes.get(code, cls) @@ -339,7 +339,7 @@ class ReadyMessage(_MessageType): name = 'READY' @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): return cls() @@ -351,7 +351,7 @@ def __init__(self, authenticator): self.authenticator = authenticator @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): authname = read_string(f) return cls(authenticator=authname) @@ -383,7 +383,7 @@ def __init__(self, challenge): self.challenge = challenge @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): return cls(read_longstring(f)) @@ -406,7 +406,7 @@ def __init__(self, token): self.token = token @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): return cls(read_longstring(f)) @@ -427,7 +427,7 @@ def __init__(self, cql_versions, options): self.options = options @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): options = read_stringmultimap(f) cql_versions = options.pop('CQL_VERSION') return cls(cql_versions=cql_versions, options=options) @@ -541,13 +541,13 @@ def __init__(self, kind, results, paging_state=None): self.paging_state = paging_state @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): kind = read_int(f) paging_state = None if kind == RESULT_KIND_VOID: results = None elif kind == RESULT_KIND_ROWS: - paging_state, results = cls.recv_results_rows(f) + paging_state, results = cls.recv_results_rows(f, protocol_version) elif kind == RESULT_KIND_SET_KEYSPACE: ksname = read_string(f) results = ksname @@ -558,7 +558,7 @@ def recv_body(cls, f): return cls(kind, results, paging_state) @classmethod - def recv_results_rows(cls, f): + def recv_results_rows(cls, f, protocol_version): paging_state, column_metadata = cls.recv_results_metadata(f) rowcount = read_int(f) rows = [cls.recv_row(f, len(column_metadata)) for _ in range(rowcount)] @@ -566,7 +566,7 @@ def recv_results_rows(cls, f): coltypes = [c[3] for c in column_metadata] return ( paging_state, - (colnames, [tuple(ctype.from_binary(val) for ctype, val in zip(coltypes, row)) + (colnames, [tuple(ctype.from_binary(val, protocol_version) for ctype, val in zip(coltypes, row)) for row in rows])) @classmethod @@ -749,7 +749,7 @@ def __init__(self, event_type, event_args): self.event_args = event_args @classmethod - def recv_body(cls, f): + def recv_body(cls, f, protocol_version): event_type = read_string(f).upper() if event_type in known_event_types: read_method = getattr(cls, 'recv_' + event_type.lower()) diff --git a/cassandra/query.py b/cassandra/query.py index 3f1785396a..d8d4cf9092 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -382,7 +382,7 @@ def __init__(self, prepared_statement, *args, **kwargs): Statement.__init__(self, *args, **kwargs) - def bind(self, values): + def bind(self, values, protocol_version): """ Binds a sequence of values for the prepared statement parameters and returns this instance. Note that `values` *must* be: @@ -442,7 +442,7 @@ def bind(self, values): col_type = col_spec[-1] try: - self.values.append(col_type.serialize(value)) + self.values.append(col_type.serialize(value, protocol_version)) except (TypeError, struct.error): col_name = col_spec[2] expected_type = col_type From e3e6c4040f88f235d948d8a5d1d5d5c270f0f24e Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 23 May 2014 17:29:14 -0500 Subject: [PATCH 0680/3961] Minor fixes and unit test updates --- cassandra/cqltypes.py | 6 +++--- cassandra/query.py | 4 ++-- tests/unit/test_marshalling.py | 4 ++-- tests/unit/test_parameter_binding.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 1140337b4e..30988959f3 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -680,7 +680,7 @@ def deserialize_safe(cls, byts, protocol_version): p += 2 item = byts[p:p + itemlen] p += itemlen - result.append(subtype.from_binary(item)) + result.append(subtype.from_binary(item, protocol_version)) return cls.adapter(result) @classmethod @@ -734,8 +734,8 @@ def deserialize_safe(cls, byts, protocol_version): p += 2 valbytes = byts[p:p + val_len] p += val_len - key = subkeytype.from_binary(keybytes) - val = subvaltype.from_binary(valbytes) + key = subkeytype.from_binary(keybytes, protocol_version) + val = subvaltype.from_binary(valbytes, protocol_version) themap[key] = val return themap diff --git a/cassandra/query.py b/cassandra/query.py index d8d4cf9092..022892a26e 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -337,13 +337,13 @@ def from_message(cls, query_id, column_metadata, cluster_metadata, query, keyspa return PreparedStatement(column_metadata, query_id, routing_key_indexes, query, keyspace) - def bind(self, values): + def bind(self, values, protocol_version): """ Creates and returns a :class:`BoundStatement` instance using `values`. The `values` parameter **must** be a sequence, such as a tuple or list, even if there is only one value to bind. """ - return BoundStatement(self).bind(values) + return BoundStatement(self).bind(values, protocol_version) def __str__(self): consistency = ConsistencyLevel.value_to_name.get(self.consistency_level, 'Not Set') diff --git a/tests/unit/test_marshalling.py b/tests/unit/test_marshalling.py index ea9acef07d..685eaf5632 100644 --- a/tests/unit/test_marshalling.py +++ b/tests/unit/test_marshalling.py @@ -109,7 +109,7 @@ class TestUnmarshal(unittest.TestCase): def test_unmarshalling(self): for serializedval, valtype, nativeval in marshalled_value_pairs: unmarshaller = lookup_casstype(valtype) - whatwegot = unmarshaller.from_binary(serializedval) + whatwegot = unmarshaller.from_binary(serializedval, 1) self.assertEqual(whatwegot, nativeval, msg='Unmarshaller for %s (%s) failed: unmarshal(%r) got %r instead of %r' % (valtype, unmarshaller, serializedval, whatwegot, nativeval)) @@ -120,7 +120,7 @@ def test_unmarshalling(self): def test_marshalling(self): for serializedval, valtype, nativeval in marshalled_value_pairs: marshaller = lookup_casstype(valtype) - whatwegot = marshaller.to_binary(nativeval) + whatwegot = marshaller.to_binary(nativeval, 1) self.assertEqual(whatwegot, serializedval, msg='Marshaller for %s (%s) failed: marshal(%r) got %r instead of %r' % (valtype, marshaller, nativeval, whatwegot, serializedval)) diff --git a/tests/unit/test_parameter_binding.py b/tests/unit/test_parameter_binding.py index b9edbbe3fb..fad61e2ee9 100644 --- a/tests/unit/test_parameter_binding.py +++ b/tests/unit/test_parameter_binding.py @@ -89,7 +89,7 @@ def test_invalid_argument_type(self): values = ['nonint', 1] try: - bound_statement.bind(values) + bound_statement.bind(values, protocol_version=1) except TypeError as e: self.assertIn('foo1', str(e)) self.assertIn('Int32Type', str(e)) @@ -100,7 +100,7 @@ def test_invalid_argument_type(self): values = [1, ['1', '2']] try: - bound_statement.bind(values) + bound_statement.bind(values, protocol_version=1) except TypeError as e: self.assertIn('foo2', str(e)) self.assertIn('Int32Type', str(e)) From e0babd88ce43624330b41ae0db2787497fc05963 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 27 May 2014 12:47:13 -0500 Subject: [PATCH 0681/3961] Debug log when responding to auth challenge Relates to #128 --- cassandra/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cassandra/connection.py b/cassandra/connection.py index 0660382cb0..a06adc2e68 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -478,6 +478,7 @@ def _handle_auth_response(self, auth_response): elif isinstance(auth_response, AuthChallengeMessage): response = self.authenticator.evaluate_challenge(auth_response.challenge) msg = AuthResponseMessage("" if response is None else response) + log.debug("Responding to auth challenge on %s", self) self.send_msg(msg, self._handle_auth_response) elif isinstance(auth_response, ErrorMessage): log.debug("Received ErrorMessage on new connection (%s) from %s: %s", From 1191edf6551023206a02860a949766c84366da74 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 27 May 2014 13:24:26 -0500 Subject: [PATCH 0682/3961] Fix Cluster._is_shutdown check in decorator --- cassandra/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index d0b8c0405d..08f998873b 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -121,7 +121,7 @@ def run_in_executor(f): @wraps(f) def new_f(self, *args, **kwargs): - if self.is_shutdown: + if self._is_shutdown: return try: future = self.executor.submit(f, self, *args, **kwargs) From cc12ed9505abad81da08d16ba07b8aee0d7d8c1a Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 27 May 2014 13:25:18 -0500 Subject: [PATCH 0683/3961] Log DC details about discovered hosts --- cassandra/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 08f998873b..f6c5fdb9a1 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -799,7 +799,7 @@ def add_host(self, address, datacenter=None, rack=None, signal=True): """ new_host = self.metadata.add_host(address, datacenter, rack) if new_host and signal: - log.info("New Cassandra host %s added", address) + log.info("New Cassandra host %r discovered", new_host) self.on_add(new_host) return new_host From b5fbb206d99006d896e8afcb98927a3a943e94f3 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 27 May 2014 13:49:39 -0500 Subject: [PATCH 0684/3961] Avoid unneeded rebuild of token replica map --- CHANGELOG.rst | 3 +++ cassandra/metadata.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1226608fe4..137a6a4acf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,9 @@ Bug Fixes * Avoid submitting tasks to the ThreadPoolExecutor after shutdown. With retries enabled, this could cause Cluster.shutdown() to hang under some circumstances. +* Fix unintended rebuild of token replica map when keyspaces are + discovered (on startup), added, or updated and TokenAwarePolicy is not + in use. 1.1.2 ===== diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 147c2e5a69..af53a4d9e7 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -181,11 +181,11 @@ def table_changed(self, keyspace, table, cf_results, col_results): def _keyspace_added(self, ksname): if self.token_map: - self.token_map.rebuild_keyspace(ksname) + self.token_map.rebuild_keyspace(ksname, build_if_absent=False) def _keyspace_updated(self, ksname): if self.token_map: - self.token_map.rebuild_keyspace(ksname) + self.token_map.rebuild_keyspace(ksname, build_if_absent=False) def _keyspace_removed(self, ksname): if self.token_map: @@ -950,10 +950,14 @@ def __init__(self, token_class, token_to_host_owner, all_tokens, metadata): self.tokens_to_hosts_by_ks = {} self._metadata = metadata + self._rebuild_lock = RLock() - def rebuild_keyspace(self, keyspace): - self.tokens_to_hosts_by_ks[keyspace] = \ - self.replica_map_for_keyspace(self._metadata.keyspaces[keyspace]) + def rebuild_keyspace(self, keyspace, build_if_absent=False): + with self._rebuild_lock: + current = self.tokens_to_hosts_by_ks.get(keyspace, None) + if (build_if_absent and current is None) or (not build_if_absent and current is not None): + replica_map = self.replica_map_for_keyspace(self._metadata.keyspaces[keyspace]) + self.tokens_to_hosts_by_ks[keyspace] = replica_map def replica_map_for_keyspace(self, ks_metadata): strategy = ks_metadata.replication_strategy @@ -972,7 +976,7 @@ def get_replicas(self, keyspace, token): """ tokens_to_hosts = self.tokens_to_hosts_by_ks.get(keyspace, None) if tokens_to_hosts is None: - self.rebuild_keyspace(keyspace) + self.rebuild_keyspace(keyspace, build_if_absent=True) tokens_to_hosts = self.tokens_to_hosts_by_ks.get(keyspace, None) if tokens_to_hosts is None: return [] From 0bdb5475d99e187ecff21d4908b46f6754d9c16d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 27 May 2014 14:05:17 -0500 Subject: [PATCH 0685/3961] Avoid rebuilding token metadata when topology has not changed --- CHANGELOG.rst | 2 ++ cassandra/cluster.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 137a6a4acf..d1d64a3773 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,8 @@ Bug Fixes * Fix unintended rebuild of token replica map when keyspaces are discovered (on startup), added, or updated and TokenAwarePolicy is not in use. +* Avoid rebuilding token metadata when cluster topology has not + actually changed 1.1.2 ===== diff --git a/cassandra/cluster.py b/cassandra/cluster.py index f6c5fdb9a1..aab7117e85 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1629,6 +1629,7 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None): if partitioner and tokens: token_map[host] = tokens + should_rebuild_token_map = False found_hosts = set() for row in peers_result: addr = row.get("rpc_address") @@ -1645,8 +1646,9 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None): if host is None: log.debug("[control connection] Found new host to connect to: %s", addr) host = self._cluster.add_host(addr, datacenter, rack, signal=True) + should_rebuild_token_map = True else: - self._update_location_info(host, datacenter, rack) + should_rebuild_token_map |= self._update_location_info(host, datacenter, rack) tokens = row.get("tokens") if partitioner and tokens: @@ -1657,15 +1659,17 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None): old_host.address not in found_hosts and \ old_host.address not in self._cluster.contact_points: log.debug("[control connection] Found host that has been removed: %r", old_host) + should_rebuild_token_map = True self._cluster.remove_host(old_host) - if partitioner: - log.debug("[control connection] Fetched ring info, rebuilding metadata") + log.debug("[control connection] Finished fetching ring info") + if partitioner and should_rebuild_token_map: + log.debug("[control connection] Rebuilding token map due to topology changes") self._cluster.metadata.rebuild_token_map(partitioner, token_map) def _update_location_info(self, host, datacenter, rack): if host.datacenter == datacenter and host.rack == rack: - return + return False # If the dc/rack information changes, we need to update the load balancing policy. # For that, we remove and re-add the node against the policy. Not the most elegant, and assumes @@ -1673,6 +1677,7 @@ def _update_location_info(self, host, datacenter, rack): self._cluster.load_balancing_policy.on_down(host) host.set_location_info(datacenter, rack) self._cluster.load_balancing_policy.on_up(host) + return True def _handle_topology_change(self, event): change_type = event["change_type"] From ec2f3226986ef134bfcb06a8d6167e675697bdd5 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 27 May 2014 14:15:13 -0500 Subject: [PATCH 0686/3961] Avoid preparing queries against ignored hosts --- CHANGELOG.rst | 2 ++ cassandra/cluster.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d1d64a3773..a300833fbd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,8 @@ Bug Fixes in use. * Avoid rebuilding token metadata when cluster topology has not actually changed +* Avoid preparing queries for hosts that should be ignored (such as + remote hosts when using the DCAwareRoundRobinPolicy) 1.1.2 ===== diff --git a/cassandra/cluster.py b/cassandra/cluster.py index aab7117e85..f5048093bb 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -716,13 +716,22 @@ def on_add(self, host): if self._is_shutdown: return - log.debug("Adding or renewing pools for new host %s and notifying listeners", host) - self._prepare_all_queries(host) - log.debug("Done preparing queries for new host %s", host) + log.debug("Handling new host %r and notifying listeners", host) + + distance = self.load_balancing_policy.distance(host) + if distance != HostDistance.IGNORED: + self._prepare_all_queries(host) + log.debug("Done preparing queries for new host %r", host) self.load_balancing_policy.on_add(host) self.control_connection.on_add(host) + if distance == HostDistance.IGNORED: + log.debug("Not adding connection pool for new host %r because the " + "load balancing policy has marked it as IGNORED", host) + self._finalize_add(host) + return + futures_lock = Lock() futures_results = [] futures = set() From e9668b46dd1ad2bf6d6e09de015b21b2c9371eae Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 27 May 2014 14:18:22 -0500 Subject: [PATCH 0687/3961] Update changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a300833fbd..b20ffffabe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,7 +22,7 @@ Bug Fixes * Avoid rebuilding token metadata when cluster topology has not actually changed * Avoid preparing queries for hosts that should be ignored (such as - remote hosts when using the DCAwareRoundRobinPolicy) + remote hosts when using the DCAwareRoundRobinPolicy) (PYTHON-75) 1.1.2 ===== From 663e71645daf005f7a3fd8c3eaf6a1f884b52f9f Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 11:31:11 -0500 Subject: [PATCH 0688/3961] Protocol-version specific collection decode/encode --- cassandra/cqltypes.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 30988959f3..09bf680d8c 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -672,12 +672,18 @@ def validate(cls, val): @classmethod def deserialize_safe(cls, byts, protocol_version): subtype, = cls.subtypes - numelements = uint16_unpack(byts[:2]) - p = 2 + if protocol_version >= 3: + unpack = int32_unpack + length = 4 + else: + unpack = uint16_unpack + length = 2 + numelements = unpack(byts[:length]) + p = length result = [] for _ in range(numelements): - itemlen = uint16_unpack(byts[p:p + 2]) - p += 2 + itemlen = unpack(byts[p:p + length]) + p += length item = byts[p:p + itemlen] p += itemlen result.append(subtype.from_binary(item, protocol_version)) @@ -722,16 +728,22 @@ def validate(cls, val): @classmethod def deserialize_safe(cls, byts, protocol_version): subkeytype, subvaltype = cls.subtypes - numelements = uint16_unpack(byts[:2]) - p = 2 + if protocol_version >= 3: + unpack = int32_unpack + length = 4 + else: + unpack = uint16_unpack + length = 2 + numelements = unpack(byts[:length]) + p = length themap = OrderedDict() for _ in range(numelements): - key_len = uint16_unpack(byts[p:p + 2]) - p += 2 + key_len = unpack(byts[p:p + length]) + p += length keybytes = byts[p:p + key_len] p += key_len - val_len = uint16_unpack(byts[p:p + 2]) - p += 2 + val_len = unpack(byts[p:p + length]) + p += length valbytes = byts[p:p + val_len] p += val_len key = subkeytype.from_binary(keybytes, protocol_version) @@ -788,7 +800,7 @@ def deserialize_safe(cls, byts, protocol_version): p += cls.FIELD_LENGTH item = byts[p:p + itemlen] p += itemlen - result.append(col_type.from_binary(item)) + result.append(col_type.from_binary(item, protocol_version)) if len(result) < len(cls.subtypes): nones = [None] * (len(cls.subtypes) - len(result)) From 33c523b02cf0405232cc3769db85e1432a0a23ef Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 11:38:33 -0500 Subject: [PATCH 0689/3961] Add link to cassandra.concurrent in Performance docs --- docs/performance.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/performance.rst b/docs/performance.rst index 404a6dbf75..8ba9aed9dd 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -244,6 +244,11 @@ dramatically: Until this is improved, you should limit the number of callback chains you run. +For many use cases, you don't need to implement this pattern yourself. You can +simply use :meth:`cassandra.concurrent.execute_concurrent` and +:meth:`cassandra.concurrent.execute_concurrent_with_args`, which implement +this pattern for you with a synchronous API. + PyPy ---- Almost all of these patterns become CPU-bound pretty quickly with CPython, the From e59fe8503d8fe442064c384ac8e48c3bde8304c9 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 12:10:21 -0500 Subject: [PATCH 0690/3961] Move auth_provider validation to attr setter --- cassandra/cluster.py | 59 ++++++++++++++-------- tests/integration/standard/test_cluster.py | 12 ++++- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index dd8ab463d2..293a3c9423 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -198,18 +198,41 @@ class Cluster(object): Setting this to :const:`False` disables compression. """ - auth_provider = None - """ - When :attr:`~.Cluster.protocol_version` is 2 or higher, this should - be an instance of a subclass of :class:`~cassandra.auth.AuthProvider`, - such ass :class:`~.PlainTextAuthProvider`. + _auth_provider = None + _auth_provider_callable = None - When :attr:`~.Cluster.protocol_version` is 1, this should be - a function that accepts one argument, the IP address of a node, - and returns a dict of credentials for that node. + @property + def auth_provider(self): + """ + When :attr:`~.Cluster.protocol_version` is 2 or higher, this should + be an instance of a subclass of :class:`~cassandra.auth.AuthProvider`, + such ass :class:`~.PlainTextAuthProvider`. - When not using authentication, this should be left as :const:`None`. - """ + When :attr:`~.Cluster.protocol_version` is 1, this should be + a function that accepts one argument, the IP address of a node, + and returns a dict of credentials for that node. + + When not using authentication, this should be left as :const:`None`. + """ + return self._auth_provider + + @auth_provider.setter # noqa + def auth_provider(self, value): + if not value: + self._auth_provider = value + return + + try: + self._auth_provider_callable = value.new_authenticator + except AttributeError: + if self.protocol_version > 1: + raise TypeError("auth_provider must implement the cassandra.auth.AuthProvider " + "interface when protocol_version >= 2") + elif not callable(value): + raise TypeError("auth_provider must be callable when protocol_version == 1") + self._auth_provider_callable = value + + self._auth_provider = value load_balancing_policy = None """ @@ -339,15 +362,8 @@ def __init__(self, self.contact_points = contact_points self.port = port self.compression = compression - - if auth_provider is not None: - if not hasattr(auth_provider, 'new_authenticator'): - if protocol_version > 1: - raise TypeError("auth_provider must implement the cassandra.auth.AuthProvider " - "interface when protocol_version >= 2") - self.auth_provider = auth_provider - else: - self.auth_provider = auth_provider.new_authenticator + self.protocol_version = protocol_version + self.auth_provider = auth_provider if load_balancing_policy is not None: if isinstance(load_balancing_policy, type): @@ -381,7 +397,6 @@ def __init__(self, self.ssl_options = ssl_options self.sockopts = sockopts self.cql_version = cql_version - self.protocol_version = protocol_version self.max_schema_agreement_wait = max_schema_agreement_wait self.control_connection_timeout = control_connection_timeout @@ -492,8 +507,8 @@ def _make_connection_factory(self, host, *args, **kwargs): return partial(self.connection_class.factory, host.address, *args, **kwargs) def _make_connection_kwargs(self, address, kwargs_dict): - if self.auth_provider: - kwargs_dict['authenticator'] = self.auth_provider(address) + if self._auth_provider_callable: + kwargs_dict['authenticator'] = self._auth_provider_callable(address) kwargs_dict['port'] = self.port kwargs_dict['compression'] = self.compression diff --git a/tests/integration/standard/test_cluster.py b/tests/integration/standard/test_cluster.py index e3037a14a4..28ef0009d5 100644 --- a/tests/integration/standard/test_cluster.py +++ b/tests/integration/standard/test_cluster.py @@ -117,8 +117,18 @@ def test_auth_provider_is_callable(self): """ Ensure that auth_providers are always callable """ + self.assertRaises(TypeError, Cluster, auth_provider=1, protocol_version=1) + c = Cluster(protocol_version=1) + self.assertRaises(TypeError, setattr, c, 'auth_provider', 1) - self.assertRaises(ValueError, Cluster, auth_provider=1) + def test_v2_auth_provider(self): + """ + Check for v2 auth_provider compliance + """ + bad_auth_provider = lambda x: {'username': 'foo', 'password': 'bar'} + self.assertRaises(TypeError, Cluster, auth_provider=bad_auth_provider, protocol_version=2) + c = Cluster(protocol_version=2) + self.assertRaises(TypeError, setattr, c, 'auth_provider', bad_auth_provider) def test_conviction_policy_factory_is_callable(self): """ From d354b66f682f1eb45eb2c5994e21791dd8ce4267 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 12:27:27 -0500 Subject: [PATCH 0691/3961] Add Security docs page --- docs/index.rst | 1 + docs/security.rst | 77 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 docs/security.rst diff --git a/docs/index.rst b/docs/index.rst index f6c84d2a92..bb79940952 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Python Cassandra Driver getting_started performance query_paging + security Indices and Tables ================== diff --git a/docs/security.rst b/docs/security.rst new file mode 100644 index 0000000000..41e6b91544 --- /dev/null +++ b/docs/security.rst @@ -0,0 +1,77 @@ +Security +======== +The two main security components you will use with the +Python driver are Authentication and SSL. + +Authentication +-------------- +Versions 2.0 and higher of the driver support a SASL-based +authentication mechanism when :attr:`~.Cluster.protocol_version` +is set to 2 or higher. To use this authentication, set +:attr:`~.Cluster.auth_provider` to an instance of a subclass +of :class:`~cassandra.auth.AuthProvider`. When working +with Cassandra's ``PasswordAuthenticator``, you can use +the :class:`~cassandra.auth.PlainTextAuthProvider` class. + +For example, suppose Cassandra is setup with its default +'cassandra' user with a password of 'cassandra': + +.. code-block:: python + + from cassandra.cluster import Cluster + from cassandra.auth import PlainTextAuthProvider + + auth_provider = PlainTextAuthProvider(username='cassandra', password='cassandra') + cluster = Cluster(auth_provider=auth_provider, protocol_version=2) + + +When working with version 2 or higher of the driver, the protocol +version is set to 2 by default, but we've included it in the example +to be explicit. + +Custom Authenticators +^^^^^^^^^^^^^^^^^^^^^ +If you're using something other than Cassandra's ``PasswordAuthenticator``, +you may need to create your own subclasses of :class:`~.AuthProvider` and +:class:`~.Authenticator`. You can use :class:`~.PlainTextAuthProvider` +and :class:`~.PlainTextAuthenticator` as example implementations. + +Protocol v1 Authentication +^^^^^^^^^^^^^^^^^^^^^^^^^^ +When working with Cassandra 1.2 (or a higher version with +:attr:`~.Cluster.protocol_version` set to ``1``), you will not pass in +an :class:`~.AuthProvider` instance. Instead, you should pass a dict +of credentials with a ``username`` and ``password`` key: + +.. code-block:: python + + from cassandra.cluster import Cluster + + credentials = {'username': 'joe', 'password': '1234'} + cluster = Cluster(auth_provider=credentials, protocol_version=1) + +SSL +--- +To enable SSL you will need to set :attr:`.Cluster.ssl_options` to a +dict of options. These will be passed as kwargs to ``ssl.wrap_socket()`` +when new sockets are created. This should be used when client encryption +is enabled in Cassandra. + +By default, a ``ca_certs`` value should be supplied (the value should be +a string pointing to the location of the CA certs file), and you probably +want to specify ``ssl_version`` as ``ssl.PROTOCOL_TLSv1`` to match +Cassandra's default protocol. + +For example: + +.. code-block:: python + + from cassandra.cluster import Cluster + from ssl import PROTOCOL_TLSv1 + + ssl_opts = {'ca_certs': '/path/to/my/ca.certs', + 'ssl_version': PROTOCOL_TLSv1} + cluster = Cluster(ssl_options=ssl_opts) + +For further reading, Andrew Mussey has published a thorough guide on +`Using SSL with the DataStax Python driver `_. From 479786741f59aabce9271e338f28abb81af81550 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 14:00:56 -0500 Subject: [PATCH 0692/3961] Create Upgrading section of docs --- docs/index.rst | 1 + docs/query_paging.rst | 2 ++ docs/security.rst | 2 ++ docs/upgrading.rst | 76 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 docs/upgrading.rst diff --git a/docs/index.rst b/docs/index.rst index bb79940952..ba051cc142 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,7 @@ Python Cassandra Driver api/index installation getting_started + upgrading performance query_paging security diff --git a/docs/query_paging.rst b/docs/query_paging.rst index df239d52e9..26cbceb786 100644 --- a/docs/query_paging.rst +++ b/docs/query_paging.rst @@ -1,3 +1,5 @@ +.. _query-paging: + Paging Large Queries ==================== Cassandra 2.0+ offers support for automatic query paging. Starting with diff --git a/docs/security.rst b/docs/security.rst index 41e6b91544..04ecda017c 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -1,3 +1,5 @@ +.. _security: + Security ======== The two main security components you will use with the diff --git a/docs/upgrading.rst b/docs/upgrading.rst new file mode 100644 index 0000000000..046dfb76d6 --- /dev/null +++ b/docs/upgrading.rst @@ -0,0 +1,76 @@ +Upgrading +========= + +Upgrading to 2.0 from 1.x +------------------------- +Version 2.0 of the DataStax python driver for Apache Cassandra +includes some notable improvements over version 1.x. This version +of the driver supports Cassandra 1.2, 2.0, and 2.1. However, not +all features may be used with Cassandra 1.2, and some new features +in 2.1 are not yet supported. + +Using the v2 Native Protocol +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +By default, the driver will attempt to use version 2 of Cassandra's +native protocol. When working with Cassandra 1.2, you will need to +explicitly set the :attr:`~.Cluster.protocol_version` to 1: + +.. code-block:: python + + from cassandra.cluster import Cluster + + cluster = Cluster(protocol_version=1) + +Automatic Query Paging +^^^^^^^^^^^^^^^^^^^^^^ +Version 2 of the native protocol adds support for automatic query +paging, which can make dealing with large result sets much simpler. + +See :ref:`query-paging` for full details. + +Protocol-Level Batch Statements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +With version 1 of the native protocol, batching of statements required +using a `BATCH cql query `_. +With version 2 of the native protocol, you can now batch statements at +the protocol level. This allows you to use many different prepared +statements within a single batch. + +See :class:`~.query.BatchStatement` for details and usage examples. + +SASL-based Authentication +^^^^^^^^^^^^^^^^^^^^^^^^^ +Also new in version 2 of the native protocol is SASL-based authentication. +See the section on :ref:`security` for details and examples. + +Lightweight Transactions +^^^^^^^^^^^^^^^^^^^^^^^^ +`Lightweight transactions `_ are another new feature. To use lightweight transactions, add ``IF`` clauses +to your CQL queries and set the :attr:`~.Statement.serial_consistency_level` +on your statements. + +Deprecations +^^^^^^^^^^^^ +The following functions have moved from ``cassandra.decoder`` to ``cassandra.query``. +The original functions have been left in place with a :exc:`DeprecationWarning` for +now: + +* :attr:`cassandra.decoder.tuple_factory` has moved to + :attr:`cassandra.query.tuple_factory` +* :attr:`cassandra.decoder.named_tuple_factory` has moved to + :attr:`cassandra.query.named_tuple_factory +* :attr:`cassandra.decoder.dict_factory` has moved to + :attr:`cassandra.query.dict_factory` +* :attr:`cassandra.decoder.ordered_dict_factory` has moved to + :attr:`cassandra.query.ordered_dict_factory` + +Dependency Changes +^^^^^^^^^^^^^^^^^^ +The following dependencies have officially been made optional: + +* ``scales`` +* ``blist`` + +And one new dependency has been added (to enable Python 3 support): + +* ``six`` From b22ca0a1a35681dc635fe5e53b20742835e6fae1 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 14:10:39 -0500 Subject: [PATCH 0693/3961] Various doc fixes --- cassandra/cluster.py | 6 +++--- cassandra/metrics.py | 3 +++ cassandra/query.py | 8 ++++---- docs/api/cassandra/metrics.rst | 8 ++++++++ docs/api/index.rst | 1 + 5 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 docs/api/cassandra/metrics.rst diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 293a3c9423..15336dfdb0 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -206,7 +206,7 @@ def auth_provider(self): """ When :attr:`~.Cluster.protocol_version` is 2 or higher, this should be an instance of a subclass of :class:`~cassandra.auth.AuthProvider`, - such ass :class:`~.PlainTextAuthProvider`. + such as :class:`~.PlainTextAuthProvider`. When :attr:`~.Cluster.protocol_version` is 1, this should be a function that accepts one argument, the IP address of a node, @@ -264,12 +264,12 @@ def auth_provider(self, value): metrics_enabled = False """ Whether or not metric collection is enabled. If enabled, :attr:`.metrics` - will be an instance of :class:`.metrics.Metrics`. + will be an instance of :class:`~cassandra.metrics.Metrics`. """ metrics = None """ - An instance of :class:`.metrics.Metrics` if :attr:`.metrics_enabled` is + An instance of :class:`cassandra.metrics.Metrics` if :attr:`.metrics_enabled` is :const:`True`, else :const:`None`. """ diff --git a/cassandra/metrics.py b/cassandra/metrics.py index dcaa1783cb..8a07c06c8f 100644 --- a/cassandra/metrics.py +++ b/cassandra/metrics.py @@ -26,6 +26,9 @@ class Metrics(object): + """ + A collection of timers and counters for various performance metrics. + """ request_timer = None """ diff --git a/cassandra/query.py b/cassandra/query.py index 3f1785396a..70fa5f9e61 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -227,12 +227,12 @@ def _del_serial_consistency_level(self): :attr:`~.ConsistencyLevel.QUORUM` read is guaranteed to see that write. But if the regular :attr:`~.consistency_level` of that write is :attr:`~.ConsistencyLevel.ANY`, then only a read with a - :attr`~.consistency_level` of :attr:`~.ConsistencyLevel.SERIAL` is + :attr:`~.consistency_level` of :attr:`~.ConsistencyLevel.SERIAL` is guaranteed to see it (even a read with consistency :attr:`~.ConsistencyLevel.ALL` is not guaranteed to be enough). - The serial consistency can only be one of :attr:`~ConsistencyLevel.SERIAL` - or :attr:`~ConsistencyLevel.LOCAL_SERIAL`. While ``SERIAL`` guarantees full + The serial consistency can only be one of :attr:`~.ConsistencyLevel.SERIAL` + or :attr:`~.ConsistencyLevel.LOCAL_SERIAL`. While ``SERIAL`` guarantees full linearizability (with other ``SERIAL`` updates), ``LOCAL_SERIAL`` only guarantees it in the local data center. @@ -241,7 +241,7 @@ def _del_serial_consistency_level(self): :attr:`consistency_level`. Serial consistency levels may only be used against Cassandra 2.0+ - and the :attr:`~Cluster.protocol_version` must be set to 2 or higher. + and the :attr:`~.Cluster.protocol_version` must be set to 2 or higher. """) @property diff --git a/docs/api/cassandra/metrics.rst b/docs/api/cassandra/metrics.rst new file mode 100644 index 0000000000..755ab42924 --- /dev/null +++ b/docs/api/cassandra/metrics.rst @@ -0,0 +1,8 @@ +``cassandra.metrics`` - Performance Metrics +=========================================== + +.. module:: cassandra.metrics + :members: + +.. autoclass:: cassandra.metrics.Metrics () + :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index d8106d1083..65545028e0 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -9,6 +9,7 @@ API Documentation cassandra/policies cassandra/auth cassandra/metadata + cassandra/metrics cassandra/query cassandra/pool cassandra/decoder From d1ae7c6f1d8dbe1d4acc8cc5f30f6df6de8f4aa0 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 14:20:58 -0500 Subject: [PATCH 0694/3961] Better index page for driver docs --- docs/index.rst | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index ba051cc142..12180f0048 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,55 @@ Python Cassandra Driver ======================= +A Python client driver for `Apache Cassandra `_. +This driver works exclusively with the Cassandra Query Language v3 (CQL3) +and Cassandra's native protocol. Cassandra 1.2+ is supported. + +The driver supports Python 2.6, 2.7, 3.3, and 3.4. + +This driver is open source under the +`Apache v2 License `_. +The source code for this driver can be found on `Github `_. + +Contents +-------- +:doc:`installation` + How to install the driver. + +:doc:`getting_started` + A guide through the first steps of connecting to Cassandra and executing queries. + +:doc:`api/index` + The API documentation. + +:doc:`upgrading` + A guide to upgrading versions of the driver. + +:doc:`performance` + Tips for getting good performance. + +:doc:`query_paging` + Notes on paging large query results. + +:doc:`security` + An overview of the security features of the driver. + +Getting Help +------------ +Please send questions to the `mailing list `_. + +Alternatively, you can use IRC. Connect to the #datastax-drivers channel on irc.freenode.net. +If you don't have an IRC client, you can use `freenode's web-based client `_. + +Reporting Issues +---------------- +Please report any bugs and make any feature requests on the +`JIRA `_ issue tracker. + +If you would like to contribute, please feel free to open a pull request. + .. toctree:: - :maxdepth: 2 + :hidden: api/index installation From 3e28e270d7567d64a6d55298afb1b3986817b1c5 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 14:32:05 -0500 Subject: [PATCH 0695/3961] Add missing 'versionadded' directives, minor cleanup --- cassandra/cluster.py | 12 +++++++++--- cassandra/query.py | 21 ++++++++++++++++++++- docs/api/cassandra/decoder.rst | 8 ++++---- docs/api/cassandra/query.rst | 4 ++-- docs/upgrading.rst | 2 +- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 15336dfdb0..b6b3d5c666 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1002,7 +1002,7 @@ class Session(object): :meth:`.ResponseFuture.add_errback`; even if a query exceeds this default timeout, neither the registered callback or errback will be called. - .. versionadded:: 2.0.0b1 + .. versionadded:: 2.0.0 """ default_consistency_level = ConsistencyLevel.ONE @@ -1032,7 +1032,7 @@ class Session(object): This only takes effect when protocol version 2 or higher is used. See :attr:`.Cluster.protocol_version` for details. - .. versionadded:: 2.0.0b1 + .. versionadded:: 2.0.0 """ _lock = None @@ -2152,6 +2152,8 @@ def has_more_pages(self): Returns :const:`True` if there are more pages left in the query results, :const:`False` otherwise. This should only be checked after the first page has been returned. + + .. versionadded:: 2.0.0 """ return self._paging_state is not None @@ -2162,6 +2164,8 @@ def start_fetching_next_page(self): is raised. Also see :attr:`.has_more_pages`. This should only be called after the first page has been returned. + + .. versionadded:: 2.0.0 """ if not self._paging_state: raise QueryExhausted() @@ -2582,6 +2586,8 @@ class QueryExhausted(Exception): Raised when :meth:`.ResponseFuture.start_fetching_next_page()` is called and there are no more pages. You can check :attr:`.ResponseFuture.has_more_pages` before calling to avoid this. + + .. versionadded:: 2.0.0 """ pass @@ -2605,7 +2611,7 @@ class will be returned. an :class:`Exception` to be raised while fetching the next page, just like you might see on a normal call to ``session.execute()``. - .. versionadded: 2.0.0b1 + .. versionadded: 2.0.0 """ def __init__(self, response_future, initial_response): diff --git a/cassandra/query.py b/cassandra/query.py index 70fa5f9e61..74d565ae07 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -63,6 +63,9 @@ def tuple_factory(colnames, rows): >>> rows = session.execute("SELECT name, age FROM users LIMIT 1") >>> print rows[0] ('Bob', 42) + + .. versionchanged:: 2.0.0 + moved from ``cassandra.decoder`` to ``cassandra.query`` """ return rows @@ -92,6 +95,9 @@ def named_tuple_factory(colnames, rows): >>> age = user[1] >>> print "name: %s, age: %d" % (name, age) name: Bob, age: 42 + + .. versionchanged:: 2.0.0 + moved from ``cassandra.decoder`` to ``cassandra.query`` """ Row = namedtuple('Row', map(_clean_column_name, colnames)) return [Row(*row) for row in rows] @@ -110,6 +116,8 @@ def dict_factory(colnames, rows): >>> print rows[0] {'age': 42, 'name': 'Bob'} + .. versionchanged:: 2.0.0 + moved from ``cassandra.decoder`` to ``cassandra.query`` """ return [dict(zip(colnames, row)) for row in rows] @@ -118,6 +126,9 @@ def ordered_dict_factory(colnames, rows): """ Like :meth:`~cassandra.query.dict_factory`, but returns each row as an OrderedDict, so the order of the columns is preserved. + + .. versionchanged:: 2.0.0 + moved from ``cassandra.decoder`` to ``cassandra.query`` """ return [OrderedDict(zip(colnames, row)) for row in rows] @@ -156,6 +167,8 @@ class Statement(object): This only takes effect when protocol version 2 or higher is used. See :attr:`.Cluster.protocol_version` for details. + + .. versionadded:: 2.0.0 """ _serial_consistency_level = None @@ -242,6 +255,8 @@ def _del_serial_consistency_level(self): Serial consistency levels may only be used against Cassandra 2.0+ and the :attr:`~.Cluster.protocol_version` must be set to 2 or higher. + + .. versionadded:: 2.0.0 """) @property @@ -494,6 +509,8 @@ class BatchType(object): """ A BatchType is used with :class:`.BatchStatement` instances to control the atomicity of the batch operation. + + .. versionadded:: 2.0.0 """ LOGGED = None @@ -531,6 +548,8 @@ class BatchStatement(Statement): """ A protocol-level batch of operations which are applied atomically by default. + + .. versionadded:: 2.0.0 """ batch_type = None @@ -574,7 +593,7 @@ def __init__(self, batch_type=BatchType.LOGGED, retry_policy=None, batch.add(SimpleStatement("DELETE FROM pending_users WHERE name=%s", (name,)) session.execute(batch) - .. versionadded:: 2.0.0b1 + .. versionadded:: 2.0.0 """ self.batch_type = batch_type self._statements_and_parameters = [] diff --git a/docs/api/cassandra/decoder.rst b/docs/api/cassandra/decoder.rst index 83caab6c2f..e213cc6d74 100644 --- a/docs/api/cassandra/decoder.rst +++ b/docs/api/cassandra/decoder.rst @@ -5,16 +5,16 @@ .. function:: tuple_factory - **Deprecated** Use :meth:`cassandra.query.tuple_factory` + **Deprecated in 2.0.0.** Use :meth:`cassandra.query.tuple_factory` .. function:: named_tuple_factory - **Deprecated** Use :meth:`cassandra.query.named_tuple_factory` + **Deprecated in 2.0.0.** Use :meth:`cassandra.query.named_tuple_factory` .. function:: dict_factory - **Deprecated** Use :meth:`cassandra.query.dict_factory` + **Deprecated in 2.0.0.** Use :meth:`cassandra.query.dict_factory` .. function:: ordered_dict_factory - **Deprecated** Use :meth:`cassandra.query.ordered_dict_factory` + **Deprecated in 2.0.0.** Use :meth:`cassandra.query.ordered_dict_factory` diff --git a/docs/api/cassandra/query.rst b/docs/api/cassandra/query.rst index 4b1497155d..94cf5df57a 100644 --- a/docs/api/cassandra/query.rst +++ b/docs/api/cassandra/query.rst @@ -1,5 +1,5 @@ -``cassandra.query`` - Prepared Statements and Query Policies -============================================================ +``cassandra.query`` - Prepared Statements, Batch Statements, Tracing, and Row Factories +======================================================================================= .. module:: cassandra.query diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 046dfb76d6..d2fb1de1d5 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -58,7 +58,7 @@ now: * :attr:`cassandra.decoder.tuple_factory` has moved to :attr:`cassandra.query.tuple_factory` * :attr:`cassandra.decoder.named_tuple_factory` has moved to - :attr:`cassandra.query.named_tuple_factory + :attr:`cassandra.query.named_tuple_factory` * :attr:`cassandra.decoder.dict_factory` has moved to :attr:`cassandra.query.dict_factory` * :attr:`cassandra.decoder.ordered_dict_factory` has moved to From 7835e37a36d68e250ba0b3b452dc09c7ef66d8c8 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 14:43:28 -0500 Subject: [PATCH 0696/3961] Make next version in CHANGELOG 2.0.0 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f05e86d6b..9615ed0306 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,5 @@ -2.0.0b2 -======= +2.0.0 +===== In Progress Features From 66a5cca5abe8214e1bfd827e2f2714479ea1e41a Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 14:53:52 -0500 Subject: [PATCH 0697/3961] Mention Cluster.shutdown() in the upgrading guide --- docs/upgrading.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index d2fb1de1d5..78af601436 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -12,7 +12,16 @@ in 2.1 are not yet supported. Using the v2 Native Protocol ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default, the driver will attempt to use version 2 of Cassandra's -native protocol. When working with Cassandra 1.2, you will need to +native protocol. You can explicitly set the protocol version to +2, though: + +.. code-block:: python + + from cassandra.cluster import Cluster + + cluster = Cluster(protocol_version=2) + +When working with Cassandra 1.2, you will need to explicitly set the :attr:`~.Cluster.protocol_version` to 1: .. code-block:: python @@ -49,6 +58,13 @@ Lightweight Transactions to your CQL queries and set the :attr:`~.Statement.serial_consistency_level` on your statements. +Calling Cluster.shutdown() +^^^^^^^^^^^^^^^^^^^^^^^^^^ +In order to fix some issues around garbage collection and unclean interpreter +shutdowns, version 2.0 of the driver requires you to call :meth:`.Cluster.shutdown()` +on your :class:`~.Cluster` objects when you are through with them. +This helps to guarantee a clean shutdown. + Deprecations ^^^^^^^^^^^^ The following functions have moved from ``cassandra.decoder`` to ``cassandra.query``. From 74d030edb2d9c02cd6f4e04f42da56ddcca823da Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 15:19:20 -0500 Subject: [PATCH 0698/3961] Release version 2.0.0 --- CHANGELOG.rst | 2 +- README.rst | 6 ++---- cassandra/__init__.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9615ed0306..d6a97667c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,6 @@ 2.0.0 ===== -In Progress +May 28, 2014 Features -------- diff --git a/README.rst b/README.rst index fa4eb2854c..a159dbea7a 100644 --- a/README.rst +++ b/README.rst @@ -21,16 +21,14 @@ For more complete installation instructions, see the Documentation ------------- +The documentation can be found online `here `_. A couple of links for getting up to speed: * `Installation `_ * `Getting started guide `_ * `API docs `_ - -You can also find some -`notes about the performance `_ -on the `documentation page `_. +* `Performance tips `_ Reporting Problems ------------------ diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 4c2831e5bf..c976db917f 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 0, '0b1', 'post') +__version_info__ = (2, 0, 0) __version__ = '.'.join(map(str, __version_info__)) From fc974a70d924856a748a646bbd3bbc53f5426271 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 15:21:13 -0500 Subject: [PATCH 0699/3961] Make version 2.0.0.post --- cassandra/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index c976db917f..a11e8feecd 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 0, 0) +__version_info__ = (2, 0, 0, 'post') __version__ = '.'.join(map(str, __version_info__)) From e1af5b3fb559e0c0917131ccce508bf5bbb77337 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 15:24:28 -0500 Subject: [PATCH 0700/3961] Add missing steps to README-dev for publishing docs --- README-dev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README-dev.rst b/README-dev.rst index 3495eb7c74..9a81395d8f 100644 --- a/README-dev.rst +++ b/README-dev.rst @@ -41,6 +41,8 @@ For example:: python setup.py doc cp -R docs/_build/1.0.0-beta1/* ~/python-driver-docs/ cd ~/python-driver-docs + git add --all + git commit -m 'Update docs' git push origin gh-pages Running the Tests From 09ea6030d91ad38ce8663be0346e4453219ecc74 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 17:59:55 -0500 Subject: [PATCH 0701/3961] Use v3 collection serialization inside UDTs --- cassandra/cqltypes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 09bf680d8c..749c23ffc3 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -800,7 +800,9 @@ def deserialize_safe(cls, byts, protocol_version): p += cls.FIELD_LENGTH item = byts[p:p + itemlen] p += itemlen - result.append(col_type.from_binary(item, protocol_version)) + # collections inside UDTs are always encoded with at least the + # version 3 format + result.append(col_type.from_binary(item, max(3, protocol_version))) if len(result) < len(cls.subtypes): nones = [None] * (len(cls.subtypes) - len(result)) From e8d6a577565b160c0a0e9953b9bcf283fd18d3d4 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 18:17:47 -0500 Subject: [PATCH 0702/3961] Serial consistency level support for BatchStatements --- cassandra/cluster.py | 2 +- cassandra/protocol.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index bc708b489f..c8f0d9e9a9 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1184,7 +1184,7 @@ def _create_response_future(self, query, parameters, trace): "2 or higher (supported in Cassandra 2.0 and higher). Consider " "setting Cluster.protocol_version to 2 to support this operation.") message = BatchMessage( - query.batch_type, query._statements_and_parameters, cl) + query.batch_type, query._statements_and_parameters, cl, query.serial_consistency_level) if trace: message.tracing = True diff --git a/cassandra/protocol.py b/cassandra/protocol.py index a146b26110..82318be019 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -699,13 +699,19 @@ class BatchMessage(_MessageType): opcode = 0x0D name = 'BATCH' - def __init__(self, batch_type, queries, consistency_level): + def __init__(self, batch_type, queries, consistency_level, serial_consistency_level=None): self.batch_type = batch_type self.queries = queries self.consistency_level = consistency_level + self.serial_consistency_level = serial_consistency_level def send_body(self, f, protocol_version): write_byte(f, self.batch_type.value) + if protocol_version >= 3: + flags = 0 + if self.serial_consistency_level: + flags |= _WITH_SERIAL_CONSISTENCY_FLAG + write_byte(f, flags) write_short(f, len(self.queries)) for prepared, string_or_query_id, params in self.queries: if not prepared: @@ -720,6 +726,8 @@ def send_body(self, f, protocol_version): write_value(f, param) write_consistency_level(f, self.consistency_level) + if protocol_version >= 3 and self.serial_consistency_level: + write_consistency_level(f, self.serial_consistency_level) known_event_types = frozenset(( From 3713aa85b47b4453f1e5ba159ff27f3e281bd28d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 18:35:49 -0500 Subject: [PATCH 0703/3961] Support for protocol-level timestamps --- cassandra/cluster.py | 23 ++++++++++++++++++++--- cassandra/protocol.py | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index c8f0d9e9a9..0b799a8964 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1035,6 +1035,16 @@ class Session(object): .. versionadded:: 2.0.0 """ + use_client_timestamp = True + """ + When using protocol version 3 or higher, write timestamps may be supplied + client-side at the protocol level. (Normally they are generated + server-side by the coordinator node.) Note that timestamps specified + within a CQL query will override this timestamp. + + .. versionadded:: 2.1.0 + """ + _lock = None _pools = None _load_balancer = None @@ -1165,17 +1175,23 @@ def _create_response_future(self, query, parameters, trace): if not fetch_size and self._protocol_version >= 2: fetch_size = self.default_fetch_size + if self._protocol_version >= 3 and self.use_client_timestamp: + timestamp = int(time.time() * 1e6) + else: + timestamp = None + if isinstance(query, SimpleStatement): query_string = query.query_string if parameters: query_string = bind_params(query.query_string, parameters) message = QueryMessage( query_string, cl, query.serial_consistency_level, - fetch_size=fetch_size) + fetch_size, timestamp=timestamp) elif isinstance(query, BoundStatement): message = ExecuteMessage( query.prepared_statement.query_id, query.values, cl, - query.serial_consistency_level, fetch_size=fetch_size) + query.serial_consistency_level, fetch_size, + timestamp=timestamp) prepared_statement = query.prepared_statement elif isinstance(query, BatchStatement): if self._protocol_version < 2: @@ -1184,7 +1200,8 @@ def _create_response_future(self, query, parameters, trace): "2 or higher (supported in Cassandra 2.0 and higher). Consider " "setting Cluster.protocol_version to 2 to support this operation.") message = BatchMessage( - query.batch_type, query._statements_and_parameters, cl, query.serial_consistency_level) + query.batch_type, query._statements_and_parameters, cl, + query.serial_consistency_level, timestamp) if trace: message.tracing = True diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 82318be019..a36b4ebc35 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -23,7 +23,7 @@ AlreadyExists, InvalidRequest, Unauthorized, UnsupportedOperation) from cassandra.marshal import (int32_pack, int32_unpack, uint16_pack, uint16_unpack, - int8_pack, int8_unpack, header_pack) + int8_pack, int8_unpack, uint64_pack, header_pack) from cassandra.cqltypes import (AsciiType, BytesType, BooleanType, CounterColumnType, DateType, DecimalType, DoubleType, FloatType, Int32Type, @@ -439,6 +439,7 @@ def recv_body(cls, f, protocol_version): _PAGE_SIZE_FLAG = 0x04 _WITH_PAGING_STATE_FLAG = 0x08 _WITH_SERIAL_CONSISTENCY_FLAG = 0x10 +_PROTOCOL_TIMESTAMP = 0x20 class QueryMessage(_MessageType): @@ -446,12 +447,13 @@ class QueryMessage(_MessageType): name = 'QUERY' def __init__(self, query, consistency_level, serial_consistency_level=None, - fetch_size=None, paging_state=None): + fetch_size=None, paging_state=None, timestamp=None): self.query = query self.consistency_level = consistency_level self.serial_consistency_level = serial_consistency_level self.fetch_size = fetch_size self.paging_state = paging_state + self.timestamp = timestamp def send_body(self, f, protocol_version): write_longstring(f, self.query) @@ -482,6 +484,9 @@ def send_body(self, f, protocol_version): "Automatic query paging may only be used with protocol version " "2 or higher. Consider setting Cluster.protocol_version to 2.") + if self.timestamp is not None: + flags |= _PROTOCOL_TIMESTAMP + write_byte(f, flags) if self.fetch_size: write_int(f, self.fetch_size) @@ -489,6 +494,8 @@ def send_body(self, f, protocol_version): write_longstring(f, self.paging_state) if self.serial_consistency_level: write_consistency_level(f, self.serial_consistency_level) + if self.timestamp is not None: + write_long(f, self.timestamp) CUSTOM_TYPE = object() @@ -650,13 +657,14 @@ class ExecuteMessage(_MessageType): def __init__(self, query_id, query_params, consistency_level, serial_consistency_level=None, fetch_size=None, - paging_state=None): + paging_state=None, timestamp=None): self.query_id = query_id self.query_params = query_params self.consistency_level = consistency_level self.serial_consistency_level = serial_consistency_level self.fetch_size = fetch_size self.paging_state = paging_state + self.timestamp = timestamp def send_body(self, f, protocol_version): write_string(f, self.query_id) @@ -683,6 +691,13 @@ def send_body(self, f, protocol_version): flags |= _PAGE_SIZE_FLAG if self.paging_state: flags |= _WITH_PAGING_STATE_FLAG + if self.timestamp is not None: + if protocol_version >= 3: + flags |= _PROTOCOL_TIMESTAMP + else: + raise UnsupportedOperation( + "Protocol-level timestamps may only be used with protocol version " + "3 or higher. Consider setting Cluster.protocol_version to 3.") write_byte(f, flags) write_short(f, len(self.query_params)) for param in self.query_params: @@ -693,17 +708,21 @@ def send_body(self, f, protocol_version): write_longstring(f, self.paging_state) if self.serial_consistency_level: write_consistency_level(f, self.serial_consistency_level) + if self.timestamp is not None: + write_long(f, self.timestamp) class BatchMessage(_MessageType): opcode = 0x0D name = 'BATCH' - def __init__(self, batch_type, queries, consistency_level, serial_consistency_level=None): + def __init__(self, batch_type, queries, consistency_level, + serial_consistency_level=None, timestamp=None): self.batch_type = batch_type self.queries = queries self.consistency_level = consistency_level self.serial_consistency_level = serial_consistency_level + self.timestamp = timestamp def send_body(self, f, protocol_version): write_byte(f, self.batch_type.value) @@ -711,6 +730,8 @@ def send_body(self, f, protocol_version): flags = 0 if self.serial_consistency_level: flags |= _WITH_SERIAL_CONSISTENCY_FLAG + if self.timestamp is not None: + flags |= _PROTOCOL_TIMESTAMP write_byte(f, flags) write_short(f, len(self.queries)) for prepared, string_or_query_id, params in self.queries: @@ -728,6 +749,8 @@ def send_body(self, f, protocol_version): write_consistency_level(f, self.consistency_level) if protocol_version >= 3 and self.serial_consistency_level: write_consistency_level(f, self.serial_consistency_level) + if self.timestamp is not None: + write_long(f, self.timestamp) known_event_types = frozenset(( @@ -811,6 +834,10 @@ def write_int(f, i): f.write(int32_pack(i)) +def write_long(f, i): + f.write(uint64_pack(i)) + + def read_short(f): return uint16_unpack(f.read(2)) From 84682187d898bde8a2bd0830eba58cba97a311ea Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 18:37:22 -0500 Subject: [PATCH 0704/3961] Fix is_shutdown check when running in executor --- CHANGELOG.rst | 9 +++++++++ cassandra/cluster.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d6a97667c3..b42f9bad2b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,12 @@ +2.0.1 +===== +May 28, 2014 + +Bug Fixes +--------- +* Fix check for Cluster.is_shutdown in in @run_in_executor + decorator + 2.0.0 ===== May 28, 2014 diff --git a/cassandra/cluster.py b/cassandra/cluster.py index b6b3d5c666..d3989d7a4a 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -130,7 +130,7 @@ def run_in_executor(f): @wraps(f) def new_f(self, *args, **kwargs): - if self._is_shutdown: + if self.is_shutdown: return try: future = self.executor.submit(f, self, *args, **kwargs) From ceaece0b6343bc1a951d4760980713ab70c4392c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 18:38:15 -0500 Subject: [PATCH 0705/3961] Release 2.0.1 --- cassandra/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index a11e8feecd..9c4d7a551d 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 0, 0, 'post') +__version_info__ = (2, 0, 1) __version__ = '.'.join(map(str, __version_info__)) From 5103be2f2cc585289d62cd86f84673d7b9b38b5d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 18:41:26 -0500 Subject: [PATCH 0706/3961] Make version 2.0.1.post --- cassandra/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 9c4d7a551d..800e74e11e 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 0, 1) +__version_info__ = (2, 0, 1, 'post') __version__ = '.'.join(map(str, __version_info__)) From 47e79ac6686447fa9f8034d2c8e889b16e634ddf Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 28 May 2014 18:53:45 -0500 Subject: [PATCH 0707/3961] Handle v3 schema change messages --- cassandra/protocol.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index a36b4ebc35..4171da1d5d 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -608,11 +608,21 @@ def recv_results_metadata(cls, f): return paging_state, column_metadata @classmethod - def recv_results_schema_change(cls, f): + def recv_results_schema_change(cls, f, protocol_version): change_type = read_string(f) - keyspace = read_string(f) - table = read_string(f) - return dict(change_type=change_type, keyspace=keyspace, table=table) + if protocol_version >= 3: + target = read_string(f) + keyspace = read_string(f) + if target != "KEYSPACE": + table_or_type = read_string(f) + return {'change_type': change_type, 'keyspace': keyspace, target.lower(): table_or_type} + else: + return {'change_type': change_type, 'keyspace': keyspace} + + else: + keyspace = read_string(f) + table = read_string(f) + return {'change_type': change_type, 'keyspace': keyspace, 'table': table} @classmethod def read_type(cls, f): From 9f9a414ebb1a4f540873c5ed28b5af96baa9fcc5 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 29 May 2014 10:32:22 -0500 Subject: [PATCH 0708/3961] Add "six >=1.6" to requirements.txt Fixes PYTHON-77 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index b5cca97d7c..c067e65ee3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ futures +six >=1.6 From 420c8f9c960f32c786dda0a8c3cbae5e002aae12 Mon Sep 17 00:00:00 2001 From: Colin Stolley Date: Thu, 29 May 2014 22:29:07 -0500 Subject: [PATCH 0709/3961] Fix mixed statements and declarations, memory leak Cleaned up mixed statements and declarations which offend -Werror=declaration-after-statement (under python 3.4). Also fixed memory leak in Async_dealloc(). --- cassandra/io/libevwrapper.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cassandra/io/libevwrapper.c b/cassandra/io/libevwrapper.c index ced7220869..ab38ff169b 100644 --- a/cassandra/io/libevwrapper.c +++ b/cassandra/io/libevwrapper.c @@ -133,7 +133,8 @@ IO_init(libevwrapper_IO *self, PyObject *args, PyObject *kwds) { PyObject *socket; PyObject *callback; PyObject *loop; - int io_flags = 0; + int io_flags = 0, fd = -1; + struct ev_io *io = NULL; if (!PyArg_ParseTuple(args, "OiOO", &socket, &io_flags, &loop, &callback)) { return -1; @@ -154,14 +155,14 @@ IO_init(libevwrapper_IO *self, PyObject *args, PyObject *kwds) { self->callback = callback; } - int fd = PyObject_AsFileDescriptor(socket); + fd = PyObject_AsFileDescriptor(socket); if (fd == -1) { PyErr_SetString(PyExc_TypeError, "unable to get file descriptor from socket"); Py_XDECREF(callback); Py_XDECREF(loop); return -1; } - struct ev_io *io = &(self->io); + io = &(self->io); ev_io_init(io, io_callback, fd, io_flags); self->io.data = self; return 0; @@ -246,6 +247,7 @@ typedef struct libevwrapper_Async { static void Async_dealloc(libevwrapper_Async *self) { + Py_XDECREF(self->loop); Py_TYPE(self)->tp_free((PyObject *)self); }; @@ -254,8 +256,9 @@ static void async_callback(EV_P_ ev_async *watcher, int revents) {}; static int Async_init(libevwrapper_Async *self, PyObject *args, PyObject *kwds) { PyObject *loop; - static char *kwlist[] = {"loop", NULL}; + struct ev_async *async = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &loop)) { PyErr_SetString(PyExc_TypeError, "unable to get file descriptor from socket"); return -1; @@ -267,7 +270,7 @@ Async_init(libevwrapper_Async *self, PyObject *args, PyObject *kwds) { } else { return -1; } - struct ev_async *async = &(self->async); + async = &(self->async); ev_async_init(async, async_callback); return 0; }; @@ -345,11 +348,11 @@ Prepare_dealloc(libevwrapper_Prepare *self) { static void prepare_callback(struct ev_loop *loop, ev_prepare *watcher, int revents) { libevwrapper_Prepare *self = watcher->data; - + PyObject *result = NULL; PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - PyObject *result = PyObject_CallFunction(self->callback, "O", self); + gstate = PyGILState_Ensure(); + result = PyObject_CallFunction(self->callback, "O", self); if (!result) { PyErr_WriteUnraisable(self->callback); } @@ -362,6 +365,7 @@ static int Prepare_init(libevwrapper_Prepare *self, PyObject *args, PyObject *kwds) { PyObject *callback; PyObject *loop; + struct ev_prepare *prepare = NULL; if (!PyArg_ParseTuple(args, "OO", &loop, &callback)) { return -1; @@ -383,7 +387,7 @@ Prepare_init(libevwrapper_Prepare *self, PyObject *args, PyObject *kwds) { Py_INCREF(callback); self->callback = callback; } - struct ev_prepare *prepare = &(self->prepare); + prepare = &(self->prepare); ev_prepare_init(prepare, prepare_callback); self->prepare.data = self; return 0; @@ -478,6 +482,8 @@ void initlibevwrapper(void) #endif { + PyObject *module = NULL; + if (PyType_Ready(&libevwrapper_LoopType) < 0) INITERROR; @@ -494,9 +500,9 @@ initlibevwrapper(void) INITERROR; # if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); + module = PyModule_Create(&moduledef); # else - PyObject *module = Py_InitModule3("libevwrapper", module_methods, module_doc); + module = Py_InitModule3("libevwrapper", module_methods, module_doc); # endif if (module == NULL) From d1f579b3ce71a9a62b1de8ad873be3c8c764cba5 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Fri, 30 May 2014 13:28:46 -0400 Subject: [PATCH 0710/3961] Can blind update a map column such that it becomes an empty map. --- cqlengine/columns.py | 3 +++ cqlengine/statements.py | 27 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 96ac7cc6c6..83caac6b85 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -799,6 +799,9 @@ def get(self, key): def keys(self): return self.value.keys() + def items(self): + return self.value.items() + def __init__(self, key_type, value_type, default=dict, **kwargs): """ :param key_type: a column class indicating the types of the key diff --git a/cqlengine/statements.py b/cqlengine/statements.py index c7b06945d2..7ad022de8e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -320,7 +320,10 @@ def _analyze(self): if self._operation == "update": self._updates = self.value.keys() else: - self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None + if self.value.items(): + self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None + else: + self._updates = [] self._analyzed = True def get_context_size(self): @@ -330,20 +333,26 @@ def get_context_size(self): def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id - for key in self._updates or []: - val = self.value.get(key) - ctx[str(ctx_id)] = self._column.key_col.to_database(key) if self._column else key - ctx[str(ctx_id + 1)] = self._column.value_col.to_database(val) if self._column else val - ctx_id += 2 + if self._updates is None or self._updates: + for key in self._updates or []: + val = self.value.get(key) + ctx[str(ctx_id)] = self._column.key_col.to_database(key) if self._column else key + ctx[str(ctx_id + 1)] = self._column.value_col.to_database(val) if self._column else val + ctx_id += 2 + else: + ctx[str(ctx_id)] = {} def __unicode__(self): if not self._analyzed: self._analyze() qs = [] ctx_id = self.context_id - for _ in self._updates or []: - qs += ['"{}"[:{}] = :{}'.format(self.field, ctx_id, ctx_id + 1)] - ctx_id += 2 + if self._updates is None or self._updates: + for _ in self._updates or []: + qs += ['"{}"[:{}] = :{}'.format(self.field, ctx_id, ctx_id + 1)] + ctx_id += 2 + else: + qs += ['"int_map" = :1'] return ', '.join(qs) From 011e491d5fa07579749878acb54bdfecd6c617af Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Fri, 30 May 2014 14:14:28 -0400 Subject: [PATCH 0711/3961] Can blind update a set column such that it becomes an empty set. --- cqlengine/statements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 7ad022de8e..c2ff767428 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -169,6 +169,8 @@ def __init__(self, field, value, operation=None, previous=None, column=None): def __unicode__(self): qs = [] ctx_id = self.context_id + if self.previous is None and not (self._assignments or self._additions or self._removals): + qs += ['"{}" = :{}'.format(self.field, ctx_id)] if self._assignments: qs += ['"{}" = :{}'.format(self.field, ctx_id)] ctx_id += 1 @@ -203,6 +205,8 @@ def get_context_size(self): def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id + if self.previous is None and not (self._assignments or self._additions or self._removals): + ctx[str(ctx_id)] = self._to_database({}) if self._assignments: ctx[str(ctx_id)] = self._to_database(self._assignments) ctx_id += 1 From 0f11c430a7f3c74c432db5c2ef99493e6caeff7c Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Fri, 30 May 2014 14:19:25 -0400 Subject: [PATCH 0712/3961] test for blind update of empty list --- cqlengine/tests/columns/test_container_columns.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index d1531e7c53..838f2eb298 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -370,6 +370,20 @@ def test_insert_none(self): with self.assertRaises(ValidationError): TestListModel.create(partition=pkey, int_list=[None]) + def test_blind_list_updates_from_none(self): + """ Tests that updates from None work as expected """ + m = TestListModel.create(int_list=None) + expected = [1, 2] + m.int_list = expected + m.save() + + m2 = TestListModel.get(partition=m.partition) + assert m2.int_list == expected + + TestListModel.objects(partition=m.partition).update(int_list=[]) + + m3 = TestListModel.get(partition=m.partition) + assert m3.int_list == [] class TestMapModel(Model): From 635879df6315e5a7f31a1c022dc6c89c68b67dd1 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Fri, 30 May 2014 14:28:49 -0400 Subject: [PATCH 0713/3961] slight change to way blind update to empty handled in MapUpdateClause --- cqlengine/statements.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index c2ff767428..8530015389 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -318,45 +318,47 @@ class MapUpdateClause(ContainerUpdateClause): def __init__(self, field, value, operation=None, previous=None, column=None): super(MapUpdateClause, self).__init__(field, value, operation, previous, column=column) self._updates = None - self.previous = self.previous or {} + self.previous = self.previous def _analyze(self): if self._operation == "update": self._updates = self.value.keys() else: - if self.value.items(): - self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None + if self.previous is None: + self._updates = sorted([k for k, v in self.value.items()]) else: - self._updates = [] + self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None self._analyzed = True def get_context_size(self): if not self._analyzed: self._analyze() + if self.previous is None: + return 1 return len(self._updates or []) * 2 def update_context(self, ctx): if not self._analyzed: self._analyze() ctx_id = self.context_id - if self._updates is None or self._updates: + if self.previous is None and not self._updates: + ctx[str(ctx_id)] = {} + else: for key in self._updates or []: val = self.value.get(key) ctx[str(ctx_id)] = self._column.key_col.to_database(key) if self._column else key ctx[str(ctx_id + 1)] = self._column.value_col.to_database(val) if self._column else val ctx_id += 2 - else: - ctx[str(ctx_id)] = {} def __unicode__(self): if not self._analyzed: self._analyze() qs = [] ctx_id = self.context_id - if self._updates is None or self._updates: + if self.previous is None and not self._updates: + qs += ['"int_map" = :1'] + else: for _ in self._updates or []: qs += ['"{}"[:{}] = :{}'.format(self.field, ctx_id, ctx_id + 1)] ctx_id += 2 - else: - qs += ['"int_map" = :1'] return ', '.join(qs) From b00f84e284d9a36d4a827668b8c98fc13d240926 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Fri, 30 May 2014 14:30:24 -0400 Subject: [PATCH 0714/3961] fixing get_context_size for blind update to None in SetUpdateClause --- cqlengine/statements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 8530015389..dbae45ba15 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -200,6 +200,8 @@ def _analyze(self): def get_context_size(self): if not self._analyzed: self._analyze() + if self.previous is None and not (self._assignments or self._additions or self._removals): + return 1 return int(bool(self._assignments)) + int(bool(self._additions)) + int(bool(self._removals)) def update_context(self, ctx): From 2695377da9aab231d8e9d2ee3d8767cf3145e0c8 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Fri, 30 May 2014 14:37:38 -0400 Subject: [PATCH 0715/3961] slight fix to MapUpdateClause context size. Fix MapUpdateClause assignment clause test. --- cqlengine/statements.py | 2 +- cqlengine/tests/statements/test_assignment_clauses.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index dbae45ba15..31d1ff088f 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -334,7 +334,7 @@ def _analyze(self): def get_context_size(self): if not self._analyzed: self._analyze() - if self.previous is None: + if self.previous is None and not self._updates: return 1 return len(self._updates or []) * 2 diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 64b35f892b..126dd9869a 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -235,7 +235,7 @@ def test_shrinking_list_update(self): class MapUpdateTests(TestCase): def test_update(self): - c = MapUpdateClause('s', {3: 0, 5: 6}, {5: 0, 3: 4}) + c = MapUpdateClause('s', {3: 0, 5: 6}, previous={5: 0, 3: 4}) c._analyze() c.set_context_id(0) From 98ef13169a1ec3256e122b2152305f98aaee3bc8 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 30 May 2014 15:41:06 -0500 Subject: [PATCH 0716/3961] 2 byte request IDs + queue-less req ID management --- cassandra/cluster.py | 4 +- cassandra/connection.py | 87 +++++++++++++++++-------- cassandra/io/asyncorereactor.py | 12 ++-- cassandra/io/libevreactor.py | 10 +-- cassandra/marshal.py | 16 ++--- cassandra/pool.py | 14 ++-- cassandra/protocol.py | 12 ++-- tests/unit/io/test_asyncorereactor.py | 11 ++-- tests/unit/io/test_libevreactor.py | 8 +-- tests/unit/test_connection.py | 8 --- tests/unit/test_host_connection_pool.py | 29 +++++---- tests/unit/test_response_future.py | 56 ++++++++++++---- 12 files changed, 164 insertions(+), 103 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 7473c58882..f9a6014269 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -2143,8 +2143,8 @@ def _query(self, host, message=None, cb=None): connection = None try: # TODO get connectTimeout from cluster settings - connection = pool.borrow_connection(timeout=2.0) - request_id = connection.send_msg(message, cb=cb) + connection, request_id = pool.borrow_connection(timeout=2.0) + connection.send_msg(message, request_id, cb=cb) except NoConnectionsAvailable as exc: log.debug("All connections for host %s are at capacity, moving to the next host", host) self._errors[host] = exc diff --git a/cassandra/connection.py b/cassandra/connection.py index 0fcf36c600..ec2c7c41bb 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -29,7 +29,7 @@ from six.moves import range from cassandra import ConsistencyLevel, AuthenticationFailed, OperationTimedOut -from cassandra.marshal import int32_pack, header_unpack +from cassandra.marshal import int32_pack, header_unpack, v3_header_unpack from cassandra.protocol import (ReadyMessage, AuthenticateMessage, OptionsMessage, StartupMessage, ErrorMessage, CredentialsMessage, QueryMessage, ResultMessage, decode_response, @@ -79,8 +79,6 @@ def decompress(byts): locally_supported_compressions['snappy'] = (snappy.compress, decompress) -MAX_STREAM_PER_CONNECTION = 127 - PROTOCOL_VERSION_MASK = 0x7f HEADER_DIRECTION_FROM_CLIENT = 0x00 @@ -153,6 +151,7 @@ class Connection(object): ssl_options = None last_error = None in_flight = 0 + current_request_id = 0 is_defunct = False is_closed = False lock = None @@ -172,10 +171,27 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None, self.protocol_version = protocol_version self.is_control_connection = is_control_connection self._push_watchers = defaultdict(set) - - self._id_queue = Queue(MAX_STREAM_PER_CONNECTION) - for i in range(MAX_STREAM_PER_CONNECTION): - self._id_queue.put_nowait(i) + if protocol_version >= 3: + self._header_unpack = v3_header_unpack + self._header_length = 5 + self.max_request_id = (2 ** 15) - 1 + else: + self._header_unpack = header_unpack + self._header_length = 4 + self.max_request_id = (2 ** 7) - 1 + + # 0 8 16 24 32 40 + # +---------+---------+---------+---------+---------+ + # | version | flags | stream | opcode | + # +---------+---------+---------+---------+---------+ + # | length | + # +---------+---------+---------+---------+ + # | | + # . ... body ... . + # . . + # . . + # +---------------------------------------- + self._full_header_length = self._header_length + 4 self.lock = RLock() @@ -210,6 +226,11 @@ def error_all_callbacks(self, exc): "failed connection (%s) to host %s:", id(self), self.host, exc_info=True) + def get_request_id(self): + current = self.current_request_id + self.current_request_id = (current + 1) % self.max_request_id + return current + def handle_pushed(self, response): log.debug("Message pushed from server: %r", response) for cb in self._push_watchers.get(response.event_type, []): @@ -218,21 +239,12 @@ def handle_pushed(self, response): except Exception: log.exception("Pushed event handler errored, ignoring:") - def send_msg(self, msg, cb, wait_for_id=False): + def send_msg(self, msg, request_id, cb): if self.is_defunct: raise ConnectionShutdown("Connection to %s is defunct" % self.host) elif self.is_closed: raise ConnectionShutdown("Connection to %s is closed" % self.host) - if not wait_for_id: - try: - request_id = self._id_queue.get_nowait() - except Empty: - raise ConnectionBusy( - "Connection to %s is at the max number of requests" % self.host) - else: - request_id = self._id_queue.get() - self._callbacks[request_id] = cb self.push(msg.to_binary(request_id, self.protocol_version, compression=self.compressor)) return request_id @@ -251,11 +263,15 @@ def wait_for_responses(self, *msgs, **kwargs): while True: needed = len(msgs) - messages_sent with self.lock: - available = min(needed, MAX_STREAM_PER_CONNECTION - self.in_flight) + available = min(needed, self.max_request_id - self.in_flight) + start_request_id = self.current_request_id + self.current_request_id = (self.current_request_id + available) % self.max_request_id self.in_flight += available - for i in range(messages_sent, messages_sent + available): - self.send_msg(msgs[i], partial(waiter.got_response, index=i), wait_for_id=True) + for i in range(available): + self.send_msg(msgs[messages_sent + i], + (start_request_id + i) % self.max_request_id, + partial(waiter.got_response, index=messages_sent + i)) messages_sent += available if messages_sent == len(msgs): @@ -287,12 +303,11 @@ def control_conn_disposed(self): @defunct_on_error def process_msg(self, msg, body_len): - version, flags, stream_id, opcode = header_unpack(msg[:4]) + version, flags, stream_id, opcode = self._header_unpack(msg[:self._header_length]) if stream_id < 0: callback = None else: callback = self._callbacks.pop(stream_id, None) - self._id_queue.put_nowait(stream_id) body = None try: @@ -344,7 +359,7 @@ def _send_options_message(self): self._send_startup_message() else: log.debug("Sending initial options message for new connection (%s) to %s", id(self), self.host) - self.send_msg(OptionsMessage(), self._handle_options_response) + self.send_msg(OptionsMessage(), 0, self._handle_options_response) @defunct_on_error def _handle_options_response(self, options_response): @@ -411,11 +426,13 @@ def _handle_options_response(self, options_response): @defunct_on_error def _send_startup_message(self, compression=None): + log.debug("Sending StartupMessage on %s", self) opts = {} if compression: opts['COMPRESSION'] = compression sm = StartupMessage(cqlversion=self.cql_version, options=opts) - self.send_msg(sm, cb=self._handle_startup_response) + self.send_msg(sm, 0, cb=self._handle_startup_response) + log.debug("Sent StartupMessage on %s", self) @defunct_on_error def _handle_startup_response(self, startup_response, did_authenticate=False): @@ -439,12 +456,12 @@ def _handle_startup_response(self, startup_response, did_authenticate=False): log.debug("Sending credentials-based auth response on %s", self) cm = CredentialsMessage(creds=self.authenticator) callback = partial(self._handle_startup_response, did_authenticate=True) - self.send_msg(cm, cb=callback) + self.send_msg(cm, 0, cb=callback) else: log.debug("Sending SASL-based auth response on %s", self) initial_response = self.authenticator.initial_response() initial_response = "" if initial_response is None else initial_response.encode('utf-8') - self.send_msg(AuthResponseMessage(initial_response), self._handle_auth_response) + self.send_msg(AuthResponseMessage(initial_response), 0, self._handle_auth_response) elif isinstance(startup_response, ErrorMessage): log.debug("Received ErrorMessage on new connection (%s) from %s: %s", id(self), self.host, startup_response.summary_msg()) @@ -479,7 +496,7 @@ def _handle_auth_response(self, auth_response): response = self.authenticator.evaluate_challenge(auth_response.challenge) msg = AuthResponseMessage("" if response is None else response) log.debug("Responding to auth challenge on %s", self) - self.send_msg(msg, self._handle_auth_response) + self.send_msg(msg, 0, self._handle_auth_response) elif isinstance(auth_response, ErrorMessage): log.debug("Received ErrorMessage on new connection (%s) from %s: %s", id(self), self.host, auth_response.summary_msg()) @@ -543,7 +560,21 @@ def process_result(result): callback(self, self.defunct(ConnectionException( "Problem while setting keyspace: %r" % (result,), self.host))) - self.send_msg(query, process_result, wait_for_id=True) + request_id = None + # we use a busy wait on the lock here because: + # - we'll only spin if the connection is at max capacity, which is very + # unlikely for a set_keyspace call + # - it allows us to avoid signaling a condition every time a request completes + while True: + with self.lock: + if self.in_flight < self.max_request_id: + request_id = self.get_request_id() + self.in_flight += 1 + break + + time.sleep(0.001) + + self.send_msg(query, request_id, process_result) def __str__(self): status = "" diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index 758458e04d..de1429c414 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -229,7 +229,7 @@ def handle_error(self): self.defunct(sys.exc_info()[1]) def handle_close(self): - log.debug("connection (%s) to %s closed by server", id(self), self.host) + log.debug("Connection %s closed by server", self) self.close() def handle_write(self): @@ -277,24 +277,24 @@ def handle_read(self): if self._iobuf.tell(): while True: pos = self._iobuf.tell() - if pos < 8 or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): + if pos < self._full_header_length or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): # we don't have a complete header yet or we # already saw a header, but we don't have a # complete message yet break else: # have enough for header, read body len from header - self._iobuf.seek(4) + self._iobuf.seek(self._header_length) body_len = int32_unpack(self._iobuf.read(4)) # seek to end to get length of current buffer self._iobuf.seek(0, os.SEEK_END) pos = self._iobuf.tell() - if pos >= body_len + 8: + if pos >= body_len + self._full_header_length: # read message header and body self._iobuf.seek(0) - msg = self._iobuf.read(8 + body_len) + msg = self._iobuf.read(self._full_header_length + body_len) # leave leftover in current buffer leftover = self._iobuf.read() @@ -304,7 +304,7 @@ def handle_read(self): self._total_reqd_bytes = 0 self.process_msg(msg, body_len) else: - self._total_reqd_bytes = body_len + 8 + self._total_reqd_bytes = body_len + self._full_header_length break if not self._callbacks and not self.is_control_connection: diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 02eff6ffba..bd000e78ed 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -305,24 +305,24 @@ def handle_read(self, watcher, revents, errno=None): if self._iobuf.tell(): while True: pos = self._iobuf.tell() - if pos < 8 or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): + if pos < self._full_header_length or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): # we don't have a complete header yet or we # already saw a header, but we don't have a # complete message yet break else: # have enough for header, read body len from header - self._iobuf.seek(4) + self._iobuf.seek(self._header_length) body_len = int32_unpack(self._iobuf.read(4)) # seek to end to get length of current buffer self._iobuf.seek(0, os.SEEK_END) pos = self._iobuf.tell() - if pos >= body_len + 8: + if pos >= body_len + self._full_header_length: # read message header and body self._iobuf.seek(0) - msg = self._iobuf.read(8 + body_len) + msg = self._iobuf.read(self._full_header_length + body_len) # leave leftover in current buffer leftover = self._iobuf.read() @@ -332,7 +332,7 @@ def handle_read(self, watcher, revents, errno=None): self._total_reqd_bytes = 0 self.process_msg(msg, body_len) else: - self._total_reqd_bytes = body_len + 8 + self._total_reqd_bytes = body_len + self._full_header_length break else: log.debug("Connection %s closed by server", self) diff --git a/cassandra/marshal.py b/cassandra/marshal.py index 447e4ef14c..1a78a97a8f 100644 --- a/cassandra/marshal.py +++ b/cassandra/marshal.py @@ -17,14 +17,9 @@ def _make_packer(format_string): - try: - packer = struct.Struct(format_string) # new in Python 2.5 - except AttributeError: - pack = lambda x: struct.pack(format_string, x) - unpack = lambda s: struct.unpack(format_string, s) - else: - pack = packer.pack - unpack = lambda s: packer.unpack(s)[0] + packer = struct.Struct(format_string) + pack = packer.pack + unpack = lambda s: packer.unpack(s)[0] return pack, unpack int64_pack, int64_unpack = _make_packer('>q') @@ -43,6 +38,11 @@ def _make_packer(format_string): header_pack = header_struct.pack header_unpack = header_struct.unpack +# in protocol version 3 and higher, the stream ID is two bytes +v3_header_struct = struct.Struct('>BBhB') +v3_header_pack = v3_header_struct.pack +v3_header_unpack = v3_header_struct.unpack + if six.PY3: def varint_unpack(term): diff --git a/cassandra/pool.py b/cassandra/pool.py index c2cf4236dc..f99121d737 100644 --- a/cassandra/pool.py +++ b/cassandra/pool.py @@ -28,7 +28,7 @@ from cassandra.util import WeakSet # NOQA from cassandra import AuthenticationFailed -from cassandra.connection import MAX_STREAM_PER_CONNECTION, ConnectionException +from cassandra.connection import ConnectionException log = logging.getLogger(__name__) @@ -349,6 +349,7 @@ def borrow_connection(self, timeout): max_conns = self._session.cluster.get_max_connections_per_host(self.host_distance) least_busy = min(conns, key=lambda c: c.in_flight) + request_id = None # to avoid another thread closing this connection while # trashing it (through the return_connection process), hold # the connection lock from this point until we've incremented @@ -356,15 +357,16 @@ def borrow_connection(self, timeout): need_to_wait = False with least_busy.lock: - if least_busy.in_flight >= MAX_STREAM_PER_CONNECTION: + if least_busy.in_flight >= least_busy.max_request_id: # once we release the lock, wait for another connection need_to_wait = True else: least_busy.in_flight += 1 + request_id = least_busy.get_request_id() if need_to_wait: # wait_for_conn will increment in_flight on the conn - least_busy = self._wait_for_conn(timeout) + least_busy, request_id = self._wait_for_conn(timeout) # if we have too many requests on this connection but we still # have space to open a new connection against this host, go ahead @@ -372,7 +374,7 @@ def borrow_connection(self, timeout): if least_busy.in_flight >= max_reqs and len(self._connections) < max_conns: self._maybe_spawn_new_connection() - return least_busy + return least_busy, request_id def _maybe_spawn_new_connection(self): with self._lock: @@ -461,9 +463,9 @@ def _wait_for_conn(self, timeout): if conns: least_busy = min(conns, key=lambda c: c.in_flight) with least_busy.lock: - if least_busy.in_flight < MAX_STREAM_PER_CONNECTION: + if least_busy.in_flight < least_busy.max_request_id: least_busy.in_flight += 1 - return least_busy + return least_busy, least_busy.get_request_id() remaining = timeout - (time.time() - start) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 4171da1d5d..99979c32cf 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -23,7 +23,8 @@ AlreadyExists, InvalidRequest, Unauthorized, UnsupportedOperation) from cassandra.marshal import (int32_pack, int32_unpack, uint16_pack, uint16_unpack, - int8_pack, int8_unpack, uint64_pack, header_pack) + int8_pack, int8_unpack, uint64_pack, header_pack, + v3_header_pack) from cassandra.cqltypes import (AsciiType, BytesType, BooleanType, CounterColumnType, DateType, DecimalType, DoubleType, FloatType, Int32Type, @@ -80,11 +81,7 @@ def to_binary(self, stream_id, protocol_version, compression=None): flags |= TRACING_FLAG msg = six.BytesIO() - write_header( - msg, - protocol_version | HEADER_DIRECTION_FROM_CLIENT, - flags, stream_id, self.opcode, len(body) - ) + write_header(msg, protocol_version, flags, stream_id, self.opcode, len(body)) msg.write(body) return msg.getvalue() @@ -824,7 +821,8 @@ def write_header(f, version, flags, stream_id, opcode, length): """ Write a CQL protocol frame header. """ - f.write(header_pack(version, flags, stream_id, opcode)) + pack = v3_header_pack if version >= 3 else header_pack + f.write(pack(version | HEADER_DIRECTION_FROM_CLIENT, flags, stream_id, opcode)) write_int(f, length) diff --git a/tests/unit/io/test_asyncorereactor.py b/tests/unit/io/test_asyncorereactor.py index 179fe63781..8b358e6dea 100644 --- a/tests/unit/io/test_asyncorereactor.py +++ b/tests/unit/io/test_asyncorereactor.py @@ -46,6 +46,9 @@ def setUpClass(cls): cls.mock_socket = cls.socket_patcher.start() cls.mock_socket().connect_ex.return_value = 0 cls.mock_socket().getsockopt.return_value = 0 + cls.mock_socket().fileno.return_value = 100 + + AsyncoreConnection.add_channel = lambda *args, **kwargs: None @classmethod def tearDownClass(cls): @@ -97,7 +100,7 @@ def test_successful_connection(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ReadyMessage, stream_id=1) + header = self.make_header_prefix(ReadyMessage, stream_id=0) c.socket.recv.return_value = self.make_msg(header) c.handle_read() @@ -169,7 +172,7 @@ def test_error_message_on_startup(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ServerError, stream_id=1) + header = self.make_header_prefix(ServerError, stream_id=0) body = self.make_error_body(ServerError.error_code, ServerError.summary) c.socket.recv.return_value = self.make_msg(header, body) c.handle_read() @@ -251,7 +254,7 @@ def test_partial_header_read(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ReadyMessage, stream_id=1) + header = self.make_header_prefix(ReadyMessage, stream_id=0) c.socket.recv.return_value = self.make_msg(header) c.handle_read() @@ -278,7 +281,7 @@ def test_partial_message_read(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ReadyMessage, stream_id=1) + header = self.make_header_prefix(ReadyMessage, stream_id=0) c.socket.recv.return_value = self.make_msg(header) c.handle_read() diff --git a/tests/unit/io/test_libevreactor.py b/tests/unit/io/test_libevreactor.py index 5fed53b499..a7b060b146 100644 --- a/tests/unit/io/test_libevreactor.py +++ b/tests/unit/io/test_libevreactor.py @@ -97,7 +97,7 @@ def test_successful_connection(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ReadyMessage, stream_id=1) + header = self.make_header_prefix(ReadyMessage, stream_id=0) c._socket.recv.return_value = self.make_msg(header) c.handle_read(None, 0) @@ -169,7 +169,7 @@ def test_error_message_on_startup(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ServerError, stream_id=1) + header = self.make_header_prefix(ServerError, stream_id=0) body = self.make_error_body(ServerError.error_code, ServerError.summary) c._socket.recv.return_value = self.make_msg(header, body) c.handle_read(None, 0) @@ -252,7 +252,7 @@ def test_partial_header_read(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ReadyMessage, stream_id=1) + header = self.make_header_prefix(ReadyMessage, stream_id=0) c._socket.recv.return_value = self.make_msg(header) c.handle_read(None, 0) @@ -279,7 +279,7 @@ def test_partial_message_read(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ReadyMessage, stream_id=1) + header = self.make_header_prefix(ReadyMessage, stream_id=0) c._socket.recv.return_value = self.make_msg(header) c.handle_read(None, 0) diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py index 708c3e0be8..6bc50c2282 100644 --- a/tests/unit/test_connection.py +++ b/tests/unit/test_connection.py @@ -68,7 +68,6 @@ def make_msg(self, header, body=""): def test_bad_protocol_version(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = Mock() c.defunct = Mock() @@ -85,7 +84,6 @@ def test_bad_protocol_version(self, *args): def test_bad_header_direction(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = Mock() c.defunct = Mock() @@ -107,7 +105,6 @@ def test_bad_header_direction(self, *args): def test_negative_body_length(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = Mock() c.defunct = Mock() @@ -124,7 +121,6 @@ def test_negative_body_length(self, *args): def test_unsupported_cql_version(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = {0: c._handle_options_response} c.defunct = Mock() c.cql_version = "3.0.3" @@ -149,7 +145,6 @@ def test_unsupported_cql_version(self, *args): def test_prefer_lz4_compression(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = {0: c._handle_options_response} c.defunct = Mock() c.cql_version = "3.0.3" @@ -176,7 +171,6 @@ def test_prefer_lz4_compression(self, *args): def test_requested_compression_not_available(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = {0: c._handle_options_response} c.defunct = Mock() # request lz4 compression @@ -208,7 +202,6 @@ def test_requested_compression_not_available(self, *args): def test_use_requested_compression(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = {0: c._handle_options_response} c.defunct = Mock() # request snappy compression @@ -237,7 +230,6 @@ def test_use_requested_compression(self, *args): def test_disable_compression(self, *args): c = self.make_connection() - c._id_queue.get_nowait() c._callbacks = {0: c._handle_options_response} c.defunct = Mock() # disable compression diff --git a/tests/unit/test_host_connection_pool.py b/tests/unit/test_host_connection_pool.py index 20c8830f5b..6c32bc4777 100644 --- a/tests/unit/test_host_connection_pool.py +++ b/tests/unit/test_host_connection_pool.py @@ -21,7 +21,7 @@ from threading import Thread, Event from cassandra.cluster import Session -from cassandra.connection import Connection, MAX_STREAM_PER_CONNECTION +from cassandra.connection import Connection from cassandra.pool import Host, HostConnectionPool, NoConnectionsAvailable from cassandra.policies import HostDistance, SimpleConvictionPolicy @@ -38,13 +38,13 @@ def make_session(self): def test_borrow_and_return(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = HostConnectionPool(host, HostDistance.LOCAL, session) session.cluster.connection_factory.assert_called_once_with(host.address) - c = pool.borrow_connection(timeout=0.01) + c, request_id = pool.borrow_connection(timeout=0.01) self.assertIs(c, conn) self.assertEqual(1, conn.in_flight) conn.set_keyspace_blocking.assert_called_once_with('foobarkeyspace') @@ -56,7 +56,7 @@ def test_borrow_and_return(self): def test_failed_wait_for_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = HostConnectionPool(host, HostDistance.LOCAL, session) @@ -65,7 +65,7 @@ def test_failed_wait_for_connection(self): pool.borrow_connection(timeout=0.01) self.assertEqual(1, conn.in_flight) - conn.in_flight = MAX_STREAM_PER_CONNECTION + conn.in_flight = conn.max_request_id # we're already at the max number of requests for this connection, # so we this should fail @@ -74,7 +74,7 @@ def test_failed_wait_for_connection(self): def test_successful_wait_for_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = HostConnectionPool(host, HostDistance.LOCAL, session) @@ -84,7 +84,7 @@ def test_successful_wait_for_connection(self): self.assertEqual(1, conn.in_flight) def get_second_conn(): - c = pool.borrow_connection(1.0) + c, request_id = pool.borrow_connection(1.0) self.assertIs(conn, c) pool.return_connection(c) @@ -98,7 +98,7 @@ def get_second_conn(): def test_all_connections_trashed(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn session.cluster.get_core_connections_per_host.return_value = 1 @@ -118,7 +118,7 @@ def fire_event(*args, **kwargs): def get_conn(): conn.reset_mock() - c = pool.borrow_connection(1.0) + c, request_id = pool.borrow_connection(1.0) self.assertIs(conn, c) self.assertEqual(1, conn.in_flight) conn.set_keyspace_blocking.assert_called_once_with('foobarkeyspace') @@ -140,7 +140,8 @@ def get_conn(): def test_spawn_when_at_max(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) + conn.max_request_id = 100 session.cluster.connection_factory.return_value = conn # core conns = 1, max conns = 2 @@ -153,7 +154,7 @@ def test_spawn_when_at_max(self): self.assertEqual(1, conn.in_flight) # make this conn full - conn.in_flight = MAX_STREAM_PER_CONNECTION + conn.in_flight = conn.max_request_id # we don't care about making this borrow_connection call succeed for the # purposes of this test, as long as it results in a new connection @@ -164,7 +165,7 @@ def test_spawn_when_at_max(self): def test_return_defunct_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = HostConnectionPool(host, HostDistance.LOCAL, session) @@ -183,7 +184,7 @@ def test_return_defunct_connection(self): def test_return_defunct_connection_on_down_host(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = HostConnectionPool(host, HostDistance.LOCAL, session) @@ -203,7 +204,7 @@ def test_return_defunct_connection_on_down_host(self): def test_return_closed_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=True) + conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=True, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = HostConnectionPool(host, HostDistance.LOCAL, session) diff --git a/tests/unit/test_response_future.py b/tests/unit/test_response_future.py index e9e0b68645..ca97a729f1 100644 --- a/tests/unit/test_response_future.py +++ b/tests/unit/test_response_future.py @@ -21,7 +21,7 @@ from cassandra import ConsistencyLevel from cassandra.cluster import Session, ResponseFuture, NoHostAvailable -from cassandra.connection import ConnectionException +from cassandra.connection import Connection, ConnectionException from cassandra.protocol import (ReadTimeoutErrorMessage, WriteTimeoutErrorMessage, UnavailableErrorMessage, ResultMessage, QueryMessage, OverloadedErrorMessage, IsBootstrappingErrorMessage, @@ -58,13 +58,16 @@ def test_result_message(self): pool = session._pools.get.return_value pool.is_shutdown = False + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + rf = self.make_response_future(session) rf.send_request() rf.session._pools.get.assert_called_once_with('ip1') pool.borrow_connection.assert_called_once_with(timeout=ANY) - connection = pool.borrow_connection.return_value - connection.send_msg.assert_called_once_with(rf.message, cb=ANY) + + connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY) rf._set_result(self.make_mock_response([{'col': 'val'}])) result = rf.result() @@ -72,6 +75,10 @@ def test_result_message(self): def test_unknown_result_class(self): session = self.make_session() + pool = session._pools.get.return_value + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + rf = self.make_response_future(session) rf.send_request() rf._set_result(object()) @@ -168,18 +175,21 @@ def test_retry_policy_says_ignore(self): def test_retry_policy_says_retry(self): session = self.make_session() pool = session._pools.get.return_value + query = SimpleStatement("INSERT INFO foo (a, b) VALUES (1, 2)") query.retry_policy = Mock() query.retry_policy.on_unavailable.return_value = (RetryPolicy.RETRY, ConsistencyLevel.ONE) message = QueryMessage(query=query, consistency_level=ConsistencyLevel.QUORUM) + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + rf = ResponseFuture(session, message, query) rf.send_request() rf.session._pools.get.assert_called_once_with('ip1') pool.borrow_connection.assert_called_once_with(timeout=ANY) - connection = pool.borrow_connection.return_value - connection.send_msg.assert_called_once_with(rf.message, cb=ANY) + connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY) result = Mock(spec=UnavailableErrorMessage, info={}) rf._set_result(result) @@ -187,6 +197,9 @@ def test_retry_policy_says_retry(self): session.submit.assert_called_once_with(rf._retry_task, True) self.assertEqual(1, rf._query_retries) + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 2) + # simulate the executor running this rf._retry_task(True) @@ -194,21 +207,22 @@ def test_retry_policy_says_retry(self): # an UnavailableException rf.session._pools.get.assert_called_with('ip1') pool.borrow_connection.assert_called_with(timeout=ANY) - connection = pool.borrow_connection.return_value - connection.send_msg.assert_called_with(rf.message, cb=ANY) + connection.send_msg.assert_called_with(rf.message, 2, cb=ANY) def test_retry_with_different_host(self): session = self.make_session() pool = session._pools.get.return_value + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + rf = self.make_response_future(session) rf.message.consistency_level = ConsistencyLevel.QUORUM rf.send_request() rf.session._pools.get.assert_called_once_with('ip1') pool.borrow_connection.assert_called_once_with(timeout=ANY) - connection = pool.borrow_connection.return_value - connection.send_msg.assert_called_once_with(rf.message, cb=ANY) + connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY) self.assertEqual(ConsistencyLevel.QUORUM, rf.message.consistency_level) result = Mock(spec=OverloadedErrorMessage, info={}) @@ -218,20 +232,24 @@ def test_retry_with_different_host(self): # query_retries does not get incremented for Overloaded/Bootstrapping errors self.assertEqual(0, rf._query_retries) + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 2) # simulate the executor running this rf._retry_task(False) # it should try with a different host rf.session._pools.get.assert_called_with('ip2') pool.borrow_connection.assert_called_with(timeout=ANY) - connection = pool.borrow_connection.return_value - connection.send_msg.assert_called_with(rf.message, cb=ANY) + connection.send_msg.assert_called_with(rf.message, 2, cb=ANY) # the consistency level should be the same self.assertEqual(ConsistencyLevel.QUORUM, rf.message.consistency_level) def test_all_retries_fail(self): session = self.make_session() + pool = session._pools.get.return_value + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) rf = self.make_response_future(session) rf.send_request() @@ -287,7 +305,11 @@ def test_timeout_getting_connection_from_pool(self): exc = NoConnectionsAvailable() first_pool = Mock(is_shutdown=False) first_pool.borrow_connection.side_effect = exc + + # the second pool will return a connection second_pool = Mock(is_shutdown=False) + connection = Mock(spec=Connection) + second_pool.borrow_connection.return_value = (connection, 1) session._pools.get.side_effect = [first_pool, second_pool] @@ -317,6 +339,10 @@ def test_callback(self): def test_errback(self): session = self.make_session() + pool = session._pools.get.return_value + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + query = SimpleStatement("INSERT INFO foo (a, b) VALUES (1, 2)") query.retry_policy = Mock() query.retry_policy.on_unavailable.return_value = (RetryPolicy.RETHROW, None) @@ -366,6 +392,10 @@ def test_add_callbacks(self): def test_prepared_query_not_found(self): session = self.make_session() + pool = session._pools.get.return_value + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + rf = self.make_response_future(session) rf.send_request() @@ -386,6 +416,10 @@ def test_prepared_query_not_found(self): def test_prepared_query_not_found_bad_keyspace(self): session = self.make_session() + pool = session._pools.get.return_value + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + rf = self.make_response_future(session) rf.send_request() From 375283feb8cce1977f456a1e73f9ee11b6adba2d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 30 May 2014 16:18:37 -0500 Subject: [PATCH 0717/3961] Don't require proto version to be passed to bind() --- cassandra/cluster.py | 5 ++-- cassandra/protocol.py | 2 +- cassandra/query.py | 23 ++++++++++++------- tests/integration/standard/test_connection.py | 13 +++++++++-- tests/unit/test_parameter_binding.py | 7 +++--- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index f9a6014269..7451dd3db8 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1168,7 +1168,7 @@ def _create_response_future(self, query, parameters, trace): if isinstance(query, six.string_types): query = SimpleStatement(query) elif isinstance(query, PreparedStatement): - query = query.bind(parameters, self._protocol_version) + query = query.bind(parameters) cl = query.consistency_level if query.consistency_level is not None else self.default_consistency_level fetch_size = query.fetch_size @@ -1246,7 +1246,8 @@ def prepare(self, query): raise prepared_statement = PreparedStatement.from_message( - query_id, column_metadata, self.cluster.metadata, query, self.keyspace) + query_id, column_metadata, self.cluster.metadata, query, self.keyspace, + self._protocol_version) host = future._current_host try: diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 99979c32cf..c37478be46 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -558,7 +558,7 @@ def recv_body(cls, f, protocol_version): elif kind == RESULT_KIND_PREPARED: results = cls.recv_results_prepared(f) elif kind == RESULT_KIND_SCHEMA_CHANGE: - results = cls.recv_results_schema_change(f) + results = cls.recv_results_schema_change(f, protocol_version) return cls(kind, results, paging_state) @classmethod diff --git a/cassandra/query.py b/cassandra/query.py index ce7a92e56c..fcd24d2672 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -313,21 +313,25 @@ class PreparedStatement(object): consistency_level = None serial_consistency_level = None + _protocol_version = None + def __init__(self, column_metadata, query_id, routing_key_indexes, query, keyspace, - consistency_level=None, serial_consistency_level=None, fetch_size=None): + protocol_version, consistency_level=None, serial_consistency_level=None, + fetch_size=None): self.column_metadata = column_metadata self.query_id = query_id self.routing_key_indexes = routing_key_indexes self.query_string = query self.keyspace = keyspace + self._protocol_version = protocol_version self.consistency_level = consistency_level self.serial_consistency_level = serial_consistency_level self.fetch_size = fetch_size @classmethod - def from_message(cls, query_id, column_metadata, cluster_metadata, query, keyspace): + def from_message(cls, query_id, column_metadata, cluster_metadata, query, keyspace, protocol_version): if not column_metadata: - return PreparedStatement(column_metadata, query_id, None, query, keyspace) + return PreparedStatement(column_metadata, query_id, None, query, keyspace, protocol_version) partition_key_columns = None routing_key_indexes = None @@ -350,15 +354,16 @@ def from_message(cls, query_id, column_metadata, cluster_metadata, query, keyspa pass # we're missing a partition key component in the prepared # statement; just leave routing_key_indexes as None - return PreparedStatement(column_metadata, query_id, routing_key_indexes, query, keyspace) + return PreparedStatement(column_metadata, query_id, routing_key_indexes, + query, keyspace, protocol_version) - def bind(self, values, protocol_version): + def bind(self, values): """ Creates and returns a :class:`BoundStatement` instance using `values`. The `values` parameter **must** be a sequence, such as a tuple or list, even if there is only one value to bind. """ - return BoundStatement(self).bind(values, protocol_version) + return BoundStatement(self).bind(values) def __str__(self): consistency = ConsistencyLevel.value_to_name.get(self.consistency_level, 'Not Set') @@ -397,7 +402,7 @@ def __init__(self, prepared_statement, *args, **kwargs): Statement.__init__(self, *args, **kwargs) - def bind(self, values, protocol_version): + def bind(self, values): """ Binds a sequence of values for the prepared statement parameters and returns this instance. Note that `values` *must* be: @@ -408,6 +413,8 @@ def bind(self, values, protocol_version): values = () col_meta = self.prepared_statement.column_metadata + proto_version = self.prepared_statement._protocol_version + # special case for binding dicts if isinstance(values, dict): dict_values = values @@ -457,7 +464,7 @@ def bind(self, values, protocol_version): col_type = col_spec[-1] try: - self.values.append(col_type.serialize(value, protocol_version)) + self.values.append(col_type.serialize(value, proto_version)) except (TypeError, struct.error): col_name = col_spec[2] expected_type = col_type diff --git a/tests/integration/standard/test_connection.py b/tests/integration/standard/test_connection.py index 79b0fde11e..fca1e6fa57 100644 --- a/tests/integration/standard/test_connection.py +++ b/tests/integration/standard/test_connection.py @@ -77,10 +77,12 @@ def cb(count, *args, **kwargs): else: conn.send_msg( QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE), + request_id=0, cb=partial(cb, count)) conn.send_msg( QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE), + request_id=0, cb=partial(cb, 0)) event.wait() @@ -102,6 +104,7 @@ def cb(response_list, request_num, *args, **kwargs): for i in range(100): conn.send_msg( QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE), + request_id=i, cb=partial(cb, responses, i)) event.wait() @@ -122,11 +125,13 @@ def cb(event, conn, count, *args, **kwargs): else: conn.send_msg( QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE), + request_id=count, cb=partial(cb, event, conn, count)) for event, conn in zip(events, conns): conn.send_msg( QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE), + request_id=0, cb=partial(cb, event, conn, 0)) for event in events: @@ -153,7 +158,9 @@ def cb(all_responses, thread_responses, request_num, *args, **kwargs): def send_msgs(all_responses, thread_responses): for i in range(num_requests_per_conn): qmsg = QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE) - conn.send_msg(qmsg, cb=partial(cb, all_responses, thread_responses, i)) + with conn.lock: + request_id = conn.get_request_id() + conn.send_msg(qmsg, request_id, cb=partial(cb, all_responses, thread_responses, i)) all_responses = [] threads = [] @@ -192,7 +199,9 @@ def send_msgs(conn, event): thread_responses = [False] * num_requests_per_conn for i in range(num_requests_per_conn): qmsg = QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE) - conn.send_msg(qmsg, cb=partial(cb, conn, event, thread_responses, i)) + with conn.lock: + request_id = conn.get_request_id() + conn.send_msg(qmsg, request_id, cb=partial(cb, conn, event, thread_responses, i)) event.wait() diff --git a/tests/unit/test_parameter_binding.py b/tests/unit/test_parameter_binding.py index fad61e2ee9..3f79c6b548 100644 --- a/tests/unit/test_parameter_binding.py +++ b/tests/unit/test_parameter_binding.py @@ -83,13 +83,14 @@ def test_invalid_argument_type(self): query_id=None, routing_key_indexes=[], query=None, - keyspace=keyspace) + keyspace=keyspace, + protocol_version=2) bound_statement = BoundStatement(prepared_statement=prepared_statement) values = ['nonint', 1] try: - bound_statement.bind(values, protocol_version=1) + bound_statement.bind(values) except TypeError as e: self.assertIn('foo1', str(e)) self.assertIn('Int32Type', str(e)) @@ -100,7 +101,7 @@ def test_invalid_argument_type(self): values = [1, ['1', '2']] try: - bound_statement.bind(values, protocol_version=1) + bound_statement.bind(values) except TypeError as e: self.assertIn('foo2', str(e)) self.assertIn('Int32Type', str(e)) From e64a45f5ee8335d42ba7775000ad10ab0556b4a0 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 30 May 2014 16:50:05 -0500 Subject: [PATCH 0718/3961] Avoid KeyError in schema refresh when dropping keyspaces --- CHANGELOG.rst | 9 +++++++++ cassandra/metadata.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b42f9bad2b..311693f96a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,12 @@ +2.0.2 +===== + +Bug Fixes +--------- +* Add six to requirements.txt +* Avoid KeyError during schema refresh when a keyspace is dropped + and TokenAwarePolicy is not in use + 2.0.1 ===== May 28, 2014 diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 9597c4180f..3e3807cead 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -970,7 +970,7 @@ def replica_map_for_keyspace(self, ks_metadata): return None def remove_keyspace(self, keyspace): - del self.tokens_to_hosts_by_ks[keyspace] + self.tokens_to_hosts_by_ks.pop(keyspace, None) def get_replicas(self, keyspace, token): """ From d69b8e62a5205ca76f4124b1994e54fb426f193c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 30 May 2014 17:21:01 -0500 Subject: [PATCH 0719/3961] Avoid registering exit cleanups multiple times --- CHANGELOG.rst | 2 ++ cassandra/io/asyncorereactor.py | 28 +++++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 311693f96a..35be84210c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Bug Fixes * Add six to requirements.txt * Avoid KeyError during schema refresh when a keyspace is dropped and TokenAwarePolicy is not in use +* Avoid registering multiple atexit() cleanup functions will the + asyncore event loop is restarted multiple times 2.0.1 ===== diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index 758458e04d..9b7240dcd5 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -20,6 +20,7 @@ import socket import sys from threading import Event, Lock, Thread +import weakref from six import BytesIO from six.moves import range @@ -46,6 +47,15 @@ log = logging.getLogger(__name__) +def _cleanup(loop_weakref): + try: + loop = loop_weakref() + except ReferenceError: + return + + loop._cleanup() + + class AsyncoreLoop(object): def __init__(self): @@ -55,6 +65,8 @@ def __init__(self): self._conns_lock = Lock() self._conns = WeakSet() + self._thread = None + atexit.register(partial(_cleanup, weakref.ref(self))) def maybe_start(self): should_start = False @@ -69,10 +81,9 @@ def maybe_start(self): self._loop_lock.release() if should_start: - thread = Thread(target=self._run_loop, name="cassandra_driver_event_loop") - thread.daemon = True - thread.start() - atexit.register(partial(self._cleanup, thread)) + self._thread = Thread(target=self._run_loop, name="cassandra_driver_event_loop") + self._thread.daemon = True + self._thread.start() def _run_loop(self): log.debug("Starting asyncore event loop") @@ -95,11 +106,14 @@ def _run_loop(self): log.debug("Asyncore event loop ended") - def _cleanup(self, thread): + def _cleanup(self): self._shutdown = True + if not self._thread: + return + log.debug("Waiting for event loop thread to join...") - thread.join(timeout=1.0) - if thread.is_alive(): + self._thread.join(timeout=1.0) + if self._thread.is_alive(): log.warning( "Event loop thread could not be joined, so shutdown may not be clean. " "Please call Cluster.shutdown() to avoid this.") From d075a487c79f73e94414c143ee356e146f0214a8 Mon Sep 17 00:00:00 2001 From: Caleb Rackliffe Date: Tue, 3 Jun 2014 01:55:00 -0700 Subject: [PATCH 0720/3961] A ModelDefinitionException is now thrown from the metaclass at model definition time if the model contains a column whose name conflicts with a built-in attribute or method. (See https://github.com/cqlengine/cqlengine/issues/193) --- cqlengine/models.py | 7 +++++-- cqlengine/tests/model/test_model.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index a0df093c75..7791da0ec8 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -649,9 +649,12 @@ def _get_polymorphic_base(bases): has_partition_keys = any(v.partition_key for (k, v) in column_definitions) - #TODO: check that the defined columns don't conflict with any of the Model API's existing attributes/methods #transform column definitions for k, v in column_definitions: + # don't allow a column with the same name as a built-in attribute or method + if k in BaseModel.__dict__: + raise ModelDefinitionException("column '{}' conflicts with built-in attribute/method".format(k)) + # counter column primary keys are not allowed if (v.primary_key or v.partition_key) and isinstance(v, (columns.Counter, columns.BaseContainerColumn)): raise ModelDefinitionException('counter columns and container columns cannot be used as primary keys') @@ -661,7 +664,7 @@ def _get_polymorphic_base(bases): if not has_partition_keys and v.primary_key: v.partition_key = True has_partition_keys = True - _transform_column(k,v) + _transform_column(k, v) partition_keys = OrderedDict(k for k in primary_keys.items() if k[1].partition_key) clustering_keys = OrderedDict(k for k in primary_keys.items() if not k[1].partition_key) diff --git a/cqlengine/tests/model/test_model.py b/cqlengine/tests/model/test_model.py index a0a6929a27..1e18359ec0 100644 --- a/cqlengine/tests/model/test_model.py +++ b/cqlengine/tests/model/test_model.py @@ -1,6 +1,6 @@ from unittest import TestCase -from cqlengine.models import Model +from cqlengine.models import Model, ModelDefinitionException from cqlengine import columns @@ -31,3 +31,21 @@ class EqualityModel1(Model): self.assertEqual(m0, m0) self.assertNotEqual(m0, m1) + + +class BuiltInAttributeConflictTest(TestCase): + """tests Model definitions that conflict with built-in attributes/methods""" + + def test_model_with_attribute_name_conflict(self): + """should raise exception when model defines column that conflicts with built-in attribute""" + with self.assertRaises(ModelDefinitionException): + class IllegalTimestampColumnModel(Model): + my_primary_key = columns.Integer(primary_key=True) + timestamp = columns.BigInt() + + def test_model_with_method_name_conflict(self): + """should raise exception when model defines column that conflicts with built-in method""" + with self.assertRaises(ModelDefinitionException): + class IllegalFilterColumnModel(Model): + my_primary_key = columns.Integer(primary_key=True) + filter = columns.Text() \ No newline at end of file From d3ca9614fda54ce0539b6fce3eae82a3dbe7e266 Mon Sep 17 00:00:00 2001 From: Caleb Rackliffe Date: Tue, 3 Jun 2014 02:26:19 -0700 Subject: [PATCH 0721/3961] The pip plumbing for the sure library seems to be horribly broken in 1.2.7, so I propose pegging to the stable version and getting the Travis tests working again. See https://github.com/gabrielfalcao/sure/pull/60 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1fd68cdd9c..d813898e67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ ipython==0.13.1 ipdb==0.7 Sphinx==1.1.3 mock==1.0.1 -sure +sure==1.2.5 From b6d45894332498552cb733d5ba7caf9dd1d02945 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 3 Jun 2014 14:30:36 -0500 Subject: [PATCH 0722/3961] Move libev loop components into a class --- cassandra/io/libevreactor.py | 236 ++++++++++++++++------------- tests/unit/io/test_libevreactor.py | 2 +- 2 files changed, 128 insertions(+), 110 deletions(-) diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 02eff6ffba..5173fc8007 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -19,6 +19,7 @@ import os import socket from threading import Event, Lock, Thread +import weakref from six import BytesIO @@ -46,120 +47,118 @@ log = logging.getLogger(__name__) -class LibevConnection(Connection): - """ - An implementation of :class:`.Connection` that uses libev for its event loop. - """ - _loop = libev.Loop() - _loop_notifier = libev.Async(_loop) - _loop_notifier.start() +def _cleanup(loop_weakref): + try: + loop = loop_weakref() + except ReferenceError: + return - # prevent _loop_notifier from keeping the loop from returning - _loop.unref() + loop._cleanup() - _loop_started = None - _loop_lock = Lock() - _loop_shutdown = False - @classmethod - def _run_loop(cls): +class LibevLoop(object): + + def __init__(self): + self._loop = libev.Loop() + self._notifier = libev.Async(self._loop) + self._notifier.start() + + # prevent _notifier from keeping the loop from returning + self._loop.unref() + + self._started = False + self._shutdown = False + self._lock = Lock() + + self._thread = None + + # set of all connections; only replaced with a new copy + # while holding _conn_set_lock, never modified in place + self._live_conns = set() + # newly created connections that need their write/read watcher started + self._new_conns = set() + # recently closed connections that need their write/read watcher stopped + self._closed_conns = set() + self._conn_set_lock = Lock() + + self._preparer = libev.Prepare(self._loop, self._loop_will_run) + # prevent _preparer from keeping the loop from returning + self._loop.unref() + self._preparer.start() + + atexit.register(partial(_cleanup, weakref.ref(self))) + + def notify(self): + self._notifier.send() + + def maybe_start(self): + should_start = False + with self._lock: + if not self._started: + log.debug("Starting libev event loop") + self._started = True + should_start = True + + if should_start: + self._thread = Thread(target=self._run_loop, name="event_loop") + self._thread.daemon = True + self._thread.start() + + self._notifier.send() + + def _run_loop(self): while True: - end_condition = cls._loop.start() + end_condition = self._loop.start() # there are still active watchers, no deadlock - with cls._loop_lock: - if not cls._loop_shutdown and (end_condition or cls._live_conns): + with self._lock: + if not self._shutdown and (end_condition or self._live_conns): log.debug("Restarting event loop") continue else: # all Connections have been closed, no active watchers log.debug("All Connections currently closed, event loop ended") - cls._loop_started = False + self._started = False break - @classmethod - def _maybe_start_loop(cls): - should_start = False - with cls._loop_lock: - if not cls._loop_started: - log.debug("Starting libev event loop") - cls._loop_started = True - should_start = True - - if should_start: - t = Thread(target=cls._run_loop, name="event_loop") - t.daemon = True - t.start() - atexit.register(partial(cls._cleanup, t)) - - return should_start + def _cleanup(self): + self._shutdown = True + if not self._thread: + return - @classmethod - def _cleanup(cls, thread): - cls._loop_shutdown = True log.debug("Waiting for event loop thread to join...") - thread.join(timeout=1.0) - if thread.is_alive(): + self._thread.join(timeout=1.0) + if self._thread.is_alive(): log.warning( "Event loop thread could not be joined, so shutdown may not be clean. " "Please call Cluster.shutdown() to avoid this.") log.debug("Event loop thread was joined") - # class-level set of all connections; only replaced with a new copy - # while holding _conn_set_lock, never modified in place - _live_conns = set() - # newly created connections that need their write/read watcher started - _new_conns = set() - # recently closed connections that need their write/read watcher stopped - _closed_conns = set() - _conn_set_lock = Lock() - - _write_watcher_is_active = False - - _total_reqd_bytes = 0 - _read_watcher = None - _write_watcher = None - _socket = None - - @classmethod - def factory(cls, *args, **kwargs): - timeout = kwargs.pop('timeout', 5.0) - conn = cls(*args, **kwargs) - conn.connected_event.wait(timeout) - if conn.last_error: - raise conn.last_error - elif not conn.connected_event.is_set(): - conn.close() - raise OperationTimedOut("Timed out creating new connection") - else: - return conn - - @classmethod - def _connection_created(cls, conn): - with cls._conn_set_lock: - new_live_conns = cls._live_conns.copy() + def connection_created(self, conn): + with self._conn_set_lock: + new_live_conns = self._live_conns.copy() new_live_conns.add(conn) - cls._live_conns = new_live_conns + self._live_conns = new_live_conns - new_new_conns = cls._new_conns.copy() + new_new_conns = self._new_conns.copy() new_new_conns.add(conn) - cls._new_conns = new_new_conns + self._new_conns = new_new_conns - @classmethod - def _connection_destroyed(cls, conn): - with cls._conn_set_lock: - new_live_conns = cls._live_conns.copy() + def connection_destroyed(self, conn): + with self._conn_set_lock: + new_live_conns = self._live_conns.copy() new_live_conns.discard(conn) - cls._live_conns = new_live_conns + self._live_conns = new_live_conns - new_closed_conns = cls._closed_conns.copy() + new_closed_conns = self._closed_conns.copy() new_closed_conns.add(conn) - cls._closed_conns = new_closed_conns + self._closed_conns = new_closed_conns - @classmethod - def loop_will_run(cls, prepare): + self._notifier.send() + + def _loop_will_run(self, prepare): changed = False - for conn in cls._live_conns: + for conn in self._live_conns: if not conn.deque and conn._write_watcher_is_active: if conn._write_watcher: conn._write_watcher.stop() @@ -170,20 +169,20 @@ def loop_will_run(cls, prepare): conn._write_watcher_is_active = True changed = True - if cls._new_conns: - with cls._conn_set_lock: - to_start = cls._new_conns - cls._new_conns = set() + if self._new_conns: + with self._conn_set_lock: + to_start = self._new_conns + self._new_conns = set() for conn in to_start: conn._read_watcher.start() changed = True - if cls._closed_conns: - with cls._conn_set_lock: - to_stop = cls._closed_conns - cls._closed_conns = set() + if self._closed_conns: + with self._conn_set_lock: + to_stop = self._closed_conns + self._closed_conns = set() for conn in to_stop: if conn._write_watcher: @@ -194,7 +193,33 @@ def loop_will_run(cls, prepare): changed = True if changed: - cls._loop_notifier.send() + self._notifier.send() + + +class LibevConnection(Connection): + """ + An implementation of :class:`.Connection` that uses libev for its event loop. + """ + _libevloop = LibevLoop() + + _write_watcher_is_active = False + _total_reqd_bytes = 0 + _read_watcher = None + _write_watcher = None + _socket = None + + @classmethod + def factory(cls, *args, **kwargs): + timeout = kwargs.pop('timeout', 5.0) + conn = cls(*args, **kwargs) + conn.connected_event.wait(timeout) + if conn.last_error: + raise conn.last_error + elif not conn.connected_event.is_set(): + conn.close() + raise OperationTimedOut("Timed out creating new connection") + else: + return conn def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) @@ -219,17 +244,16 @@ def __init__(self, *args, **kwargs): for args in self.sockopts: self._socket.setsockopt(*args) - with self._loop_lock: - self._read_watcher = libev.IO(self._socket.fileno(), libev.EV_READ, self._loop, self.handle_read) - self._write_watcher = libev.IO(self._socket.fileno(), libev.EV_WRITE, self._loop, self.handle_write) + with self._libevloop._lock: + self._read_watcher = libev.IO(self._socket.fileno(), libev.EV_READ, self._libevloop._loop, self.handle_read) + self._write_watcher = libev.IO(self._socket.fileno(), libev.EV_WRITE, self._libevloop._loop, self.handle_write) self._send_options_message() - self.__class__._connection_created(self) + self._libevloop.connection_created(self) # start the global event loop if needed - self._maybe_start_loop() - self._loop_notifier.send() + self._libevloop.maybe_start() def close(self): with self.lock: @@ -238,9 +262,9 @@ def close(self): self.is_closed = True log.debug("Closing connection (%s) to %s", id(self), self.host) - self.__class__._connection_destroyed(self) - self._loop_notifier.send() + self._libevloop.connection_destroyed(self) self._socket.close() + log.debug("Closed socket to %s", self.host) # don't leave in-progress operations hanging if not self.is_defunct: @@ -349,7 +373,7 @@ def push(self, data): with self._deque_lock: self.deque.extend(chunks) - self._loop_notifier.send() + self._libevloop.notify() def register_watcher(self, event_type, callback, register_timeout=None): self._push_watchers[event_type].add(callback) @@ -361,9 +385,3 @@ def register_watchers(self, type_callback_dict, register_timeout=None): self._push_watchers[event_type].add(callback) self.wait_for_response( RegisterMessage(event_list=type_callback_dict.keys()), timeout=register_timeout) - - -_preparer = libev.Prepare(LibevConnection._loop, LibevConnection.loop_will_run) -# prevent _preparer from keeping the loop from returning -LibevConnection._loop.unref() -_preparer.start() diff --git a/tests/unit/io/test_libevreactor.py b/tests/unit/io/test_libevreactor.py index 5fed53b499..ce3858df0c 100644 --- a/tests/unit/io/test_libevreactor.py +++ b/tests/unit/io/test_libevreactor.py @@ -44,7 +44,7 @@ @patch('cassandra.io.libevwrapper.IO') @patch('cassandra.io.libevwrapper.Prepare') @patch('cassandra.io.libevwrapper.Async') -@patch('cassandra.io.libevreactor.LibevConnection._maybe_start_loop') +@patch('cassandra.io.libevreactor.LibevLoop.maybe_start') class LibevConnectionTest(unittest.TestCase): def setUp(self): From 251188d572cb2ecb378a1936459bd2bbac771280 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 3 Jun 2014 14:54:07 -0500 Subject: [PATCH 0723/3961] Delay initialization of shared connection state This makes it much simpler to avoid sharing connection state across multiple processes. Fixes PYTHON-60 --- cassandra/cluster.py | 1 + cassandra/connection.py | 8 ++++++++ cassandra/io/asyncorereactor.py | 7 ++++++- cassandra/io/libevreactor.py | 8 ++++++-- tests/integration/standard/test_connection.py | 5 +++++ tests/unit/io/test_asyncorereactor.py | 1 + tests/unit/io/test_libevreactor.py | 1 + 7 files changed, 28 insertions(+), 3 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index d3989d7a4a..bc5f081405 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -530,6 +530,7 @@ def connect(self, keyspace=None): raise Exception("Cluster is already shut down") if not self._is_setup: + self.connection_class.initialize_reactor() atexit.register(partial(_shutdown_cluster, self)) for address in self.contact_points: host = self.add_host(address, signal=False) diff --git a/cassandra/connection.py b/cassandra/connection.py index a06adc2e68..11a17f29fa 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -179,6 +179,14 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None, self.lock = RLock() + @classmethod + def initialize_reactor(self): + """ + Called once by Cluster.connect(). This should be used by implementations + to set up any resources that will be shared across connections. + """ + pass + def close(self): raise NotImplementedError() diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index 9b7240dcd5..8c021a88de 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -135,12 +135,17 @@ class AsyncoreConnection(Connection, asyncore.dispatcher): module in the Python standard library for its event loop. """ - _loop = AsyncoreLoop() + _loop = None _total_reqd_bytes = 0 _writable = False _readable = False + @classmethod + def initialize_reactor(cls): + if not cls._loop: + cls._loop = AsyncoreLoop() + @classmethod def factory(cls, *args, **kwargs): timeout = kwargs.pop('timeout', 5.0) diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 5173fc8007..b1ed25b160 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -200,14 +200,18 @@ class LibevConnection(Connection): """ An implementation of :class:`.Connection` that uses libev for its event loop. """ - _libevloop = LibevLoop() - + _libevloop = None _write_watcher_is_active = False _total_reqd_bytes = 0 _read_watcher = None _write_watcher = None _socket = None + @classmethod + def initialize_reactor(cls): + if not cls._libevloop: + cls._libevloop = LibevLoop() + @classmethod def factory(cls, *args, **kwargs): timeout = kwargs.pop('timeout', 5.0) diff --git a/tests/integration/standard/test_connection.py b/tests/integration/standard/test_connection.py index 79b0fde11e..4d4fb8f308 100644 --- a/tests/integration/standard/test_connection.py +++ b/tests/integration/standard/test_connection.py @@ -39,6 +39,9 @@ class ConnectionTest(object): klass = None + def setUp(self): + self.klass.initialize_reactor() + def get_connection(self): """ Helper method to solve automated testing issues within Jenkins. @@ -216,6 +219,7 @@ class AsyncoreConnectionTest(ConnectionTest, unittest.TestCase): def setUp(self): if 'gevent.monkey' in sys.modules: raise unittest.SkipTest("Can't test libev with gevent monkey patching") + ConnectionTest.setUp(self) class LibevConnectionTest(ConnectionTest, unittest.TestCase): @@ -228,3 +232,4 @@ def setUp(self): if LibevConnection is None: raise unittest.SkipTest( 'libev does not appear to be installed properly') + ConnectionTest.setUp(self) diff --git a/tests/unit/io/test_asyncorereactor.py b/tests/unit/io/test_asyncorereactor.py index 179fe63781..bab800fa4a 100644 --- a/tests/unit/io/test_asyncorereactor.py +++ b/tests/unit/io/test_asyncorereactor.py @@ -42,6 +42,7 @@ class AsyncoreConnectionTest(unittest.TestCase): @classmethod def setUpClass(cls): + AsyncoreConnection.initialize_reactor() cls.socket_patcher = patch('socket.socket', spec=socket.socket) cls.mock_socket = cls.socket_patcher.start() cls.mock_socket().connect_ex.return_value = 0 diff --git a/tests/unit/io/test_libevreactor.py b/tests/unit/io/test_libevreactor.py index ce3858df0c..3b677ab547 100644 --- a/tests/unit/io/test_libevreactor.py +++ b/tests/unit/io/test_libevreactor.py @@ -50,6 +50,7 @@ class LibevConnectionTest(unittest.TestCase): def setUp(self): if LibevConnection is None: raise unittest.SkipTest('libev does not appear to be installed correctly') + LibevConnection.initialize_reactor() def make_connection(self): c = LibevConnection('1.2.3.4', cql_version='3.0.1') From d7cb0bc0a686bb7223e49bfafbe57080ad3649ba Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 3 Jun 2014 14:57:26 -0500 Subject: [PATCH 0724/3961] Update changelog --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 35be84210c..913a4fb6ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,8 @@ Bug Fixes and TokenAwarePolicy is not in use * Avoid registering multiple atexit() cleanup functions will the asyncore event loop is restarted multiple times +* Delay initialization of reactors in order to avoid problems + with shared state when using multiprocessing (PYTHON-60) 2.0.1 ===== From a848d3bf81ebcaad4887b0b8c0557bf69c34d8ef Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 3 Jun 2014 15:18:10 -0500 Subject: [PATCH 0725/3961] Update debian dependencies for 2.0.x Fixes #133 --- CHANGELOG.rst | 2 ++ debian/changelog | 6 ++++++ debian/control | 8 ++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 913a4fb6ef..121f72ae56 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,8 @@ Bug Fixes asyncore event loop is restarted multiple times * Delay initialization of reactors in order to avoid problems with shared state when using multiprocessing (PYTHON-60) +* Add python-six to debian dependencies, move python-blist to + recommends 2.0.1 ===== diff --git a/debian/changelog b/debian/changelog index c98ea8c94f..2acd0df496 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cassandra-driver (2.0.2~prerelease-1) unstable; urgency=low + + * Update dependencies for 2.0.x + + -- Tyler Hobbs Tue, 03 Jun 2014 15:16:53 -0500 + python-cassandra-driver (1.1.0~prerelease-1) unstable; urgency=low * Initial packaging diff --git a/debian/control b/debian/control index c13f4e863d..6a698a8fb8 100644 --- a/debian/control +++ b/debian/control @@ -6,16 +6,16 @@ Build-Depends: python-all-dev (>= 2.6.6-3), python-all-dbg, debhelper (>= 9), python-sphinx (>= 1.0.7+dfsg) | python3-sphinx, libev-dev, python-concurrent.futures | python-futures, python-setuptools, python-nose, python-mock, python-yaml, python-gevent, - python-blist, python-tz + python-blist, python-tz, python-six (>= 1.6) X-Python-Version: >= 2.7 Standards-Version: 3.9.4 Package: python-cassandra-driver Architecture: any -Depends: ${misc:Depends}, ${python:Depends}, ${shlibs:Depends}, python-blist, - python-concurrent.futures | python-futures +Depends: ${misc:Depends}, ${python:Depends}, ${shlibs:Depends}, + python-concurrent.futures | python-futures, python-six (>= 1.6) Provides: ${python:Provides} -Recommends: python-scales +Recommends: python-scales, python-blist Suggests: python-cassandra-driver-doc Description: Python driver for Apache Cassandra This driver works exclusively with the Cassandra Query Language v3 (CQL3) From 58b3c717d742bdc63b96cb61d5f2cb830fcfec62 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 3 Jun 2014 15:20:07 -0500 Subject: [PATCH 0726/3961] Add debian/control step to README-dev release steps Relates to #133 --- README-dev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README-dev.rst b/README-dev.rst index 9a81395d8f..8c6a13bb0a 100644 --- a/README-dev.rst +++ b/README-dev.rst @@ -1,6 +1,8 @@ Releasing ========= * Run the tests and ensure they all pass +* If dependencies have changed, make sure ``debian/control`` + is up to date * Update CHANGELOG.rst * Update the version in ``cassandra/__init__.py`` * Commit the changelog and version changes From a6d3c06a2aa98a5014e52bfff2384e25f56383cb Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Fri, 6 Jun 2014 20:00:12 -0500 Subject: [PATCH 0727/3961] Adding wait_other_notice=True for ccm --- tests/integration/__init__.py | 4 ++-- tests/integration/standard/test_metrics.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 3124009389..320e5a4602 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -109,7 +109,7 @@ def setup_package(): cluster.populate(3) log.debug("Starting ccm test cluster") - cluster.start(wait_for_binary_proto=True) + cluster.start(wait_for_binary_proto=True, wait_other_notice=True) except Exception: log.exception("Failed to start ccm cluster:") raise @@ -134,7 +134,7 @@ def use_multidc(dc_list): cluster.populate(dc_list) log.debug("Starting ccm test cluster") - cluster.start(wait_for_binary_proto=True) + cluster.start(wait_for_binary_proto=True, wait_other_notice=True) except Exception: log.exception("Failed to start ccm cluster:") raise diff --git a/tests/integration/standard/test_metrics.py b/tests/integration/standard/test_metrics.py index aa39843a93..9793aa228c 100644 --- a/tests/integration/standard/test_metrics.py +++ b/tests/integration/standard/test_metrics.py @@ -51,7 +51,7 @@ def test_connection_error(self): # Ensure the nodes are actually down self.assertRaises(NoHostAvailable, session.execute, "USE test3rf") finally: - get_cluster().start(wait_for_binary_proto=True) + get_cluster().start(wait_for_binary_proto=True, wait_other_notice=True) self.assertGreater(cluster.metrics.stats.connection_errors, 0) From f8fd7942334775f138323b6478a1b07430b3e2b8 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 9 Jun 2014 14:34:15 -0700 Subject: [PATCH 0728/3961] updated changelog --- changelog | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/changelog b/changelog index c2d7aa2055..12e5e527a3 100644 --- a/changelog +++ b/changelog @@ -1,15 +1,18 @@ CHANGELOG -0.14.0-dev +0.14.0 + +* fix for setting map to empty (Lifto) +* report when creating models with attributes that conflict with cqlengine (maedhroz) +* use stable version of sure package (maedhroz) +* performance improvements -* Check number of elements in collections. 0.13.0 * adding support for post batch callbacks (thanks Daniel Dotsenko github.com/dvdotsenko) * fixing sync table for tables with multiple keys (thanks Daniel Dotsenko github.com/dvdotsenko) * fixing bug in Bytes column (thanks Netanel Cohen-Tzemach github.com/natict) * fixing bug with timestamps and DST (thanks Netanel Cohen-Tzemach github.com/natict) -* performance improvements (thanks Jon Haddad) 0.12.0 From 4eb4e74bf7b55d9c0d36cf91cc4bc5fcf93cafae Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 9 Jun 2014 15:14:53 -0700 Subject: [PATCH 0729/3961] added native driver to requirements --- requirements.txt | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d813898e67..62eba662b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ ipdb==0.7 Sphinx==1.1.3 mock==1.0.1 sure==1.2.5 +cassandra-driver>=2.0.0 diff --git a/setup.py b/setup.py index 2965a6d73a..dfbcdf05f1 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "Topic :: Software Development :: Libraries :: Python Modules", ], keywords='cassandra,cql,orm', - install_requires = ['cql'], + install_requires = ['cql', 'cassandra-driver'], author='Blake Eggleston, Jon Haddad', author_email='bdeggleston@gmail.com', url='https://github.com/cqlengine/cqlengine', From 6180d5fda323e13c3956ce235b63c57f299755fe Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 9 Jun 2014 17:49:33 -0700 Subject: [PATCH 0730/3961] getting native driver setup --- cqlengine/connection.py | 12 +++++++++--- cqlengine/management.py | 1 - cqlengine/tests/base.py | 2 +- upgrading.txt | 4 ++++ 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 upgrading.txt diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 1274e3e7fa..aa5941314b 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -3,6 +3,8 @@ #http://cassandra.apache.org/doc/cql/CQL.html from collections import namedtuple +from cassandra.cluster import Cluster + try: import Queue as queue except ImportError: @@ -33,7 +35,8 @@ class CQLConnectionError(CQLEngineException): pass # global connection pool connection_pool = None - +cluster = None +session = None class CQLConnectionError(CQLEngineException): pass @@ -76,8 +79,11 @@ def setup( :type timeout: int or long """ - global _max_connections - global connection_pool + global _max_connections, connection_pool, cluster, session + + cluster = Cluster(hosts) + session = cluster.connect() + _max_connections = max_connections if default_keyspace: diff --git a/cqlengine/management.py b/cqlengine/management.py index ed31eb5759..1e0401a43a 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -17,7 +17,6 @@ # system keyspaces schema_columnfamilies = NamedTable('system', 'schema_columnfamilies') - def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, durable_writes=True, **replication_values): """ creates a keyspace diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index 98e34eaa7e..7700456b7b 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -6,7 +6,7 @@ if os.environ.get('CASSANDRA_TEST_HOST'): CASSANDRA_TEST_HOST = os.environ['CASSANDRA_TEST_HOST'] else: - CASSANDRA_TEST_HOST = 'localhost:9160' + CASSANDRA_TEST_HOST = 'localhost' class BaseCassEngTestCase(TestCase): diff --git a/upgrading.txt b/upgrading.txt new file mode 100644 index 0000000000..5f4f89d2aa --- /dev/null +++ b/upgrading.txt @@ -0,0 +1,4 @@ +0.15 Upgrade to use Datastax Native Driver + + + From 882fffb463240b655da3e08e38a6d370ac5d28fc Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 10 Jun 2014 11:24:23 -0500 Subject: [PATCH 0731/3961] Better messages and docs for Read/WriteTimeouts --- cassandra/__init__.py | 10 ++++++++++ cassandra/protocol.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 800e74e11e..48f21947db 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -172,6 +172,11 @@ def __init__(self, message, consistency=None, required_responses=None, received_ class ReadTimeout(Timeout): """ A subclass of :exc:`Timeout` for read operations. + + This indicates that the replicas failed to respond to the coordinator + node before the configured timeout. This timeout is configured in + ``cassandra.yaml`` with the ``read_request_timeout_in_ms`` + and ``range_request_timeout_in_ms`` options. """ data_retrieved = None @@ -189,6 +194,11 @@ def __init__(self, message, data_retrieved=None, **kwargs): class WriteTimeout(Timeout): """ A subclass of :exc:`Timeout` for write operations. + + This indicates that the replicas failed to respond to the coordinator + node before the configured timeout. This timeout is configured in + ``cassandra.yaml`` with the ``write_request_timeout_in_ms`` + option. """ write_type = None diff --git a/cassandra/protocol.py b/cassandra/protocol.py index e9cb59f4de..18b6d904b4 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -230,7 +230,7 @@ class TruncateError(RequestExecutionException): class WriteTimeoutErrorMessage(RequestExecutionException): - summary = 'Timeout during write request' + summary = 'Coordinator timeout waiting for replica response' error_code = 0x1100 @staticmethod @@ -247,7 +247,7 @@ def to_exception(self): class ReadTimeoutErrorMessage(RequestExecutionException): - summary = 'Timeout during read request' + summary = 'Coordinator timeout waiting for replica response' error_code = 0x1200 @staticmethod From d0e040d7dde8edcb3e1162a9ed3af63ebb26ba78 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 10 Jun 2014 14:45:38 -0500 Subject: [PATCH 0732/3961] Fix memory leak when destroying libev connections The IO objects held a reference to their LibevConnection instances through the callback. This reference cycle was preventing the connection from being garbage collected. Fixes #93 --- CHANGELOG.rst | 2 ++ cassandra/io/libevreactor.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 121f72ae56..396d2b2ef2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,8 @@ Bug Fixes with shared state when using multiprocessing (PYTHON-60) * Add python-six to debian dependencies, move python-blist to recommends +* Fix memory leak when libev connections are created and + destroyed (github #93) 2.0.1 ===== diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index b1ed25b160..c386e457ac 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -187,8 +187,12 @@ def _loop_will_run(self, prepare): for conn in to_stop: if conn._write_watcher: conn._write_watcher.stop() + # clear reference cycles from IO callback + del conn._write_watcher if conn._read_watcher: conn._read_watcher.stop() + # clear reference cycles from IO callback + del conn._read_watcher changed = True From db98dde36cc2bdbe6371f35640ee9bbbd5de4269 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 10 Jun 2014 14:49:16 -0500 Subject: [PATCH 0733/3961] Minor tweak to Read/WriteTimeout error summaries --- cassandra/protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 18b6d904b4..8b98d20ee0 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -230,7 +230,7 @@ class TruncateError(RequestExecutionException): class WriteTimeoutErrorMessage(RequestExecutionException): - summary = 'Coordinator timeout waiting for replica response' + summary = "Coordinator node timed out waiting for replica nodes' responses" error_code = 0x1100 @staticmethod @@ -247,7 +247,7 @@ def to_exception(self): class ReadTimeoutErrorMessage(RequestExecutionException): - summary = 'Coordinator timeout waiting for replica response' + summary = "Coordinator node timed out waiting for replica nodes' responses" error_code = 0x1200 @staticmethod From 616eb26ae9ab7729eab8d0a05447feb7e793bf08 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 10 Jun 2014 15:57:55 -0500 Subject: [PATCH 0734/3961] Ensure token map is rebuilt when nodes are removed --- CHANGELOG.rst | 2 ++ cassandra/cluster.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 396d2b2ef2..2cb09b7678 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,8 @@ Bug Fixes recommends * Fix memory leak when libev connections are created and destroyed (github #93) +* Ensure token map is rebuilt when hosts are removed from + the cluster 2.0.1 ===== diff --git a/cassandra/cluster.py b/cassandra/cluster.py index bc5f081405..ff7936ea3e 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1691,17 +1691,18 @@ def _refresh_schema(self, connection, keyspace=None, table=None, preloaded_resul else: self._cluster.metadata.rebuild_schema(ks_result, cf_result, col_result) - def refresh_node_list_and_token_map(self): + def refresh_node_list_and_token_map(self, force_token_rebuild=False): try: if self._connection: - self._refresh_node_list_and_token_map(self._connection) + self._refresh_node_list_and_token_map(self._connection, force_token_rebuild=force_token_rebuild) except ReferenceError: pass # our weak reference to the Cluster is no good except Exception: log.debug("[control connection] Error refreshing node list and token map", exc_info=True) self._signal_error() - def _refresh_node_list_and_token_map(self, connection, preloaded_results=None): + def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, + force_token_rebuild=False): if preloaded_results: log.debug("[control connection] Refreshing node list and token map using preloaded results") peers_result = preloaded_results[0] @@ -1736,7 +1737,7 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None): if partitioner and tokens: token_map[host] = tokens - should_rebuild_token_map = False + should_rebuild_token_map = force_token_rebuild found_hosts = set() for row in peers_result: addr = row.get("rpc_address") @@ -1762,12 +1763,11 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None): token_map[host] = tokens for old_host in self._cluster.metadata.all_hosts(): - if old_host.address != connection.host and \ - old_host.address not in found_hosts and \ - old_host.address not in self._cluster.contact_points: - log.debug("[control connection] Found host that has been removed: %r", old_host) + if old_host.address != connection.host and old_host.address not in found_hosts: should_rebuild_token_map = True - self._cluster.remove_host(old_host) + if old_host.address not in self._cluster.contact_points: + log.debug("[control connection] Found host that has been removed: %r", old_host) + self._cluster.remove_host(old_host) log.debug("[control connection] Finished fetching ring info") if partitioner and should_rebuild_token_map: @@ -1946,10 +1946,10 @@ def on_down(self, host): self.reconnect() def on_add(self, host): - self.refresh_node_list_and_token_map() + self.refresh_node_list_and_token_map(force_token_rebuild=True) def on_remove(self, host): - self.refresh_node_list_and_token_map() + self.refresh_node_list_and_token_map(force_token_rebuild=True) def _stop_scheduler(scheduler, thread): From 88a7b5993f099197555577fdf3bf810624442e99 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 10 Jun 2014 16:22:54 -0500 Subject: [PATCH 0735/3961] Release 2.0.2 --- CHANGELOG.rst | 1 + cassandra/__init__.py | 2 +- debian/changelog | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2cb09b7678..319534ec04 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,6 @@ 2.0.2 ===== +June 10, 2014 Bug Fixes --------- diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 48f21947db..8d7c70314d 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 0, 1, 'post') +__version_info__ = (2, 0, 2) __version__ = '.'.join(map(str, __version_info__)) diff --git a/debian/changelog b/debian/changelog index 2acd0df496..0360d9064f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cassandra-driver (2.0.2-1) unstable; urgency=low + + * Release 2.0.2 + + -- Tyler Hobbs Tue, 10 Jun 2014 16:22:23 -0500 + python-cassandra-driver (2.0.2~prerelease-1) unstable; urgency=low * Update dependencies for 2.0.x From 04273cde9947de4025e06a74a9d30f9d286b307b Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 10 Jun 2014 16:25:00 -0500 Subject: [PATCH 0736/3961] Make version 2.0.2.post --- cassandra/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 8d7c70314d..1462df37fe 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 0, 2) +__version_info__ = (2, 0, 2, 'post') __version__ = '.'.join(map(str, __version_info__)) From 0ecc91ff25676a9bd1ff6320467520b8a5b8fd70 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 10 Jun 2014 16:32:05 -0500 Subject: [PATCH 0737/3961] Fix changelog typo --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 319534ec04..fb54562998 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,7 +7,7 @@ Bug Fixes * Add six to requirements.txt * Avoid KeyError during schema refresh when a keyspace is dropped and TokenAwarePolicy is not in use -* Avoid registering multiple atexit() cleanup functions will the +* Avoid registering multiple atexit cleanup functions when the asyncore event loop is restarted multiple times * Delay initialization of reactors in order to avoid problems with shared state when using multiprocessing (PYTHON-60) From 5476e8288553131b4698a46aa7cfb3e533503eff Mon Sep 17 00:00:00 2001 From: Dmitry Belaventsev Date: Wed, 11 Jun 2014 11:45:56 +0700 Subject: [PATCH 0738/3961] dict_factory docstring fix --- cassandra/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/query.py b/cassandra/query.py index 74d565ae07..52418a09c3 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -109,7 +109,7 @@ def dict_factory(colnames, rows): Example:: - >>> from cassandra.query import named_tuple_factory + >>> from cassandra.query import dict_factory >>> session = cluster.connect('mykeyspace') >>> session.row_factory = dict_factory >>> rows = session.execute("SELECT name, age FROM users LIMIT 1") From 321cf779e849f6d8a1ad2ae73ffb85dd4b68828f Mon Sep 17 00:00:00 2001 From: Dmitry Belaventsev Date: Wed, 11 Jun 2014 11:55:19 +0700 Subject: [PATCH 0739/3961] docstring fix --- cassandra/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/query.py b/cassandra/query.py index 52418a09c3..dddcd5578b 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -114,7 +114,7 @@ def dict_factory(colnames, rows): >>> session.row_factory = dict_factory >>> rows = session.execute("SELECT name, age FROM users LIMIT 1") >>> print rows[0] - {'age': 42, 'name': 'Bob'} + {u'age': 42, u'name': u'Bob'} .. versionchanged:: 2.0.0 moved from ``cassandra.decoder`` to ``cassandra.query`` From a199debd2f00c25abdea41faa89509717bf2892c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 11 Jun 2014 00:09:14 -0700 Subject: [PATCH 0740/3961] Document exceptions moving from cassandra.decoder to cassandra.protocol --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fb54562998..2d0c07f5b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -114,6 +114,9 @@ now: * cassandra.decoder.dict_factory has moved to cassandra.query.dict_factory * cassandra.decoder.ordered_dict_factory has moved to cassandra.query.ordered_dict_factory +Exceptions that were in cassandra.decoder have been moved to cassandra.protocol. If +you handle any of these exceptions, you must adjust the code accordingly. + 1.1.2 ===== May 8, 2014 From 43e4ebf40a8046610b274c2b1a1bfaf05870888d Mon Sep 17 00:00:00 2001 From: Colin Stolley Date: Fri, 6 Jun 2014 23:57:04 -0500 Subject: [PATCH 0741/3961] Added twisted-based event loop This should address PYTHON-8. - added unit tests - import twistedreactor in cluster if libev is not available - fixed unittest import and added copyright notice --- cassandra/cluster.py | 7 +- cassandra/io/twistedreactor.py | 279 +++++++++++++++++++++++++++ tests/unit/io/test_twistedreactor.py | 199 +++++++++++++++++++ 3 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 cassandra/io/twistedreactor.py create mode 100644 tests/unit/io/test_twistedreactor.py diff --git a/cassandra/cluster.py b/cassandra/cluster.py index bc5f081405..0c6a94eef4 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -66,14 +66,17 @@ named_tuple_factory, dict_factory) # default to gevent when we are monkey patched, otherwise if libev is available, use that as the -# default because it's faster than asyncore +# default because it's fastest. Try twisted otherwise, then fallback to asyncore. if 'gevent.monkey' in sys.modules: from cassandra.io.geventreactor import GeventConnection as DefaultConnection else: try: from cassandra.io.libevreactor import LibevConnection as DefaultConnection # NOQA except ImportError: - from cassandra.io.asyncorereactor import AsyncoreConnection as DefaultConnection # NOQA + try: + from cassandra.io.twistedreactor import TwistedConnection as DefaultConnection # NOQA + except ImportError: + from cassandra.io.asyncorereactor import AsyncoreConnection as DefaultConnection # NOQA # Forces load of utf8 encoding module to avoid deadlock that occurs # if code that is being imported tries to import the module in a seperate diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py new file mode 100644 index 0000000000..442200c911 --- /dev/null +++ b/cassandra/io/twistedreactor.py @@ -0,0 +1,279 @@ +# Copyright 2013-2014 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Module that implements an event loop based on twisted +( https://twistedmatrix.com ). +""" +from twisted.internet import reactor, protocol +from threading import Event, Thread +from functools import partial +import logging +import weakref +import atexit +import os + +from six import BytesIO + +from cassandra import OperationTimedOut +from cassandra.connection import Connection, ConnectionShutdown +from cassandra.protocol import RegisterMessage +from cassandra.marshal import int32_unpack + + +log = logging.getLogger(__name__) + + +def _cleanup(cleanup_weakref): + try: + cleanup_weakref() + except ReferenceError: + return + + +class TwistedConnectionProtocol(protocol.Protocol): + """ + Twisted Protocol class for handling data received and connection + made events. + """ + def dataReceived(self, data): + """ + Callback function that is called when data has been received + on the connection. + + Reaches back to the Connection object and queues the data for + processing. + """ + self.transport.connector.factory.conn._iobuf.write(data) + self.transport.connector.factory.conn.handle_read() + + def connectionMade(self): + """ + Callback function that is called when a connection has succeeded. + + Reaches back to the Connection object and confirms that the connection + is ready. + """ + self.transport.connector.factory.conn.client_connection_made() + + +class TwistedConnectionClientFactory(protocol.ClientFactory): + def __init__(self, connection): + # ClientFactory does not define __init__() in parent classes + # and does not inherit from object. + self.conn = connection + + def buildProtocol(self, addr): + """ + Twisted function that defines which kind of protocol to use + in the ClientFactory. + """ + return TwistedConnectionProtocol() + + def clientConnectionFailed(self, connector, reason): + """ + Overridden twisted callback which is called when the + connection attempt fails. + """ + log.debug("Connect failed: %s", reason) + self.conn.close() + + def clientConnectionLost(self, connector, reason): + """ + Overridden twisted callback which is called when the + connection goes away (cleanly or otherwise). + """ + log.debug("Connect lost: %s", reason) + self.conn.close() + + +class TwistedConnection(Connection): + """ + An implementation of :class:`.Connection` that utilizes the + Twisted event loop. + """ + + _total_reqd_bytes = 0 + + @classmethod + def factory(cls, *args, **kwargs): + """ + A factory function which returns connections which have + succeeded in connecting and are ready for service (or + raises an exception otherwise). + """ + timeout = kwargs.pop('timeout', 5.0) + conn = cls(*args, **kwargs) + conn.connected_event.wait(timeout) + if conn.last_error: + raise conn.last_error + elif not conn.connected_event.is_set(): + conn.close() + raise OperationTimedOut("Timed out creating connection") + else: + return conn + + def __init__(self, *args, **kwargs): + """ + Initialization method. + + Note that we can't call reactor methods directly here because + it's not thread-safe, so we schedule the reactor/connection + stuff to be run from the event loop thread when it gets the + chance. + """ + Connection.__init__(self, *args, **kwargs) + + self.connected_event = Event() + self._iobuf = BytesIO() + self._thread = None + self.is_closed = True + self.connector = None + + self._callbacks = {} + reactor.callFromThread(self.add_connection) + + if not reactor.running: # XXX: might want a lock here? + self._thread = Thread(target=reactor.run, + name="cassandra_driver_event_loop", + kwargs={'installSignalHandlers': False}) + self._thread.daemon = True + self._thread.start() + atexit.register(partial(_cleanup, weakref.ref(self))) + + def add_connection(self): + """ + Convenience function to connect and store the resulting + connector. + """ + self.connector = reactor.connectTCP( + host=self.host, port=self.port, + factory=TwistedConnectionClientFactory(self)) + + def client_connection_made(self): + """ + Called by twisted protocol when a connection attempt has + succeeded. + """ + with self.lock: + self.is_closed = False + self._send_options_message() + + def close(self): + """ + Disconnect and error-out all callbacks. + """ + with self.lock: + if self.is_closed: + return + self.is_closed = True + + log.debug("Closing connection (%s) to %s", id(self), self.host) + self.connector.disconnect() + log.debug("Closed socket to %s", self.host) + + if not self.is_defunct: + self.error_all_callbacks( + ConnectionShutdown("Connection to %s was closed" % self.host)) + # don't leave in-progress operations hanging + self.connected_event.set() + + def handle_close(self): + """ + Not used, but kept for consistency with other reactors. + """ + self.close() + + def handle_write(self): + """ + This function is not needed, so if it is called, blow up. + """ + raise RuntimeError("handle_write() should not be called" + "in TwistedConnection") + + def handle_read(self): + """ + Process the incoming data buffer. + """ + while True: + pos = self._iobuf.tell() + if pos < 8 or (self._total_reqd_bytes > 0 and + pos < self._total_reqd_bytes): + # we don't have a complete header yet or we + # already saw a header, but we don't have a + # complete message yet + return + else: + # have enough for header, read body len from header + self._iobuf.seek(4) + body_len = int32_unpack(self._iobuf.read(4)) + + # seek to end to get length of current buffer + self._iobuf.seek(0, os.SEEK_END) + pos = self._iobuf.tell() + + if pos >= body_len + 8: + # read message header and body + self._iobuf.seek(0) + msg = self._iobuf.read(8 + body_len) + + # leave leftover in current buffer + leftover = self._iobuf.read() + self._iobuf = BytesIO() + self._iobuf.write(leftover) + + self._total_reqd_bytes = 0 + self.process_msg(msg, body_len) + else: + self._total_reqd_bytes = body_len + 8 + return + + def push(self, data): + """ + This function is called when outgoing data should be queued + for sending. + + Note that we can't call transport.write() directly because + it is not thread-safe, so we schedule it to run from within + the event loop when it gets the chance. + """ + reactor.callFromThread(self.connector.transport.write, data) + + def _cleanup(self): + if self._thread: + reactor.callFromThread(reactor.stop) + self._thread.join(timeout=1.0) + if self._thread.is_alive(): + log.warning("Event loop thread could not be joined, so " + "shutdown may not be clean. Please call " + "Cluster.shutdown() to avoid this.") + log.debug("Event loop thread was joined") + + def register_watcher(self, event_type, callback, register_timeout=None): + """ + Register a callback for a given event type. + """ + self._push_watchers[event_type].add(callback) + self.wait_for_response( + RegisterMessage(event_list=[event_type]), + timeout=register_timeout) + + def register_watchers(self, type_callback_dict, register_timeout=None): + """ + Register multiple callback/event type pairs, expressed as a dict. + """ + for event_type, callback in type_callback_dict.items(): + self._push_watchers[event_type].add(callback) + self.wait_for_response( + RegisterMessage(event_list=type_callback_dict.keys()), + timeout=register_timeout) diff --git a/tests/unit/io/test_twistedreactor.py b/tests/unit/io/test_twistedreactor.py new file mode 100644 index 0000000000..6772a35a56 --- /dev/null +++ b/tests/unit/io/test_twistedreactor.py @@ -0,0 +1,199 @@ +# Copyright 2013-2014 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + import unittest2 as unittest +except ImportError: + import unittest +from mock import Mock, patch + +try: + from twisted.test import proto_helpers + from cassandra.io import twistedreactor +except ImportError: + twistedreactor = None + + +class TestTwistedProtocol(unittest.TestCase): + def setUp(self): + if twistedreactor is None: + raise unittest.SkipTest("Twisted libraries not available") + self.tr = proto_helpers.StringTransportWithDisconnection() + self.tr.connector = Mock() + self.mock_connection = Mock() + self.tr.connector.factory = twistedreactor.TwistedConnectionClientFactory( + self.mock_connection) + self.obj_ut = twistedreactor.TwistedConnectionProtocol() + self.tr.protocol = self.obj_ut + + def tearDown(self): + pass + + def test_makeConnection(self): + """ + Verify that the protocol class notifies the connection + object that a successful connection was made. + """ + self.obj_ut.makeConnection(self.tr) + self.assertTrue(self.mock_connection.client_connection_made.called) + + def test_receiving_data(self): + """ + Verify that the dataReceived() callback writes the data to + the connection object's buffer and calls handle_read(). + """ + self.obj_ut.makeConnection(self.tr) + self.obj_ut.dataReceived('foobar') + self.assertTrue(self.mock_connection.handle_read.called) + self.mock_connection._iobuf.write.assert_called_with("foobar") + + +class TestTwistedClientFactory(unittest.TestCase): + def setUp(self): + if twistedreactor is None: + raise unittest.SkipTest("Twisted libraries not available") + self.mock_connection = Mock() + self.obj_ut = twistedreactor.TwistedConnectionClientFactory( + self.mock_connection) + + def test_client_connection_failed(self): + """ + Verify that connection failed causes the connection object to close. + """ + self.obj_ut.clientConnectionFailed(None, 'a test') + self.mock_connection.close.assert_called_with() + + def test_client_connection_lost(self): + """ + Verify that connection lost causes the connection object to close. + """ + self.obj_ut.clientConnectionLost(None, 'a test') + self.mock_connection.close.assert_called_with() + + +class TestTwistedConnection(unittest.TestCase): + def setUp(self): + if twistedreactor is None: + raise unittest.SkipTest("Twisted libraries not available") + self.reactor_cft_patcher = patch( + 'twisted.internet.reactor.callFromThread') + self.reactor_running_patcher = patch( + 'twisted.internet.reactor.running', False) + self.reactor_run_patcher = patch('twisted.internet.reactor.run') + self.mock_reactor_cft = self.reactor_cft_patcher.start() + self.mock_reactor_run = self.reactor_run_patcher.start() + self.obj_ut = twistedreactor.TwistedConnection('1.2.3.4', + cql_version='3.0.1') + + def tearDown(self): + self.reactor_cft_patcher.stop() + self.reactor_run_patcher.stop() + if self.obj_ut._thread: + self.obj_ut._thread.join() + + def test_connection_initialization(self): + """ + Verify that __init__() works correctly. + """ + self.mock_reactor_cft.assert_called_with(self.obj_ut.add_connection) + self.obj_ut._thread.join() # make sure thread exits before checking + self.mock_reactor_run.assert_called_with(installSignalHandlers=False) + + @patch('twisted.internet.reactor.connectTCP') + def test_add_connection(self, mock_connectTCP): + """ + Verify that add_connection() gives us a valid twisted connector. + """ + self.obj_ut.add_connection() + self.assertTrue(self.obj_ut.connector is not None) + self.assertTrue(mock_connectTCP.called) + + def test_client_connection_made(self): + """ + Verifiy that _send_options_message() is called in + client_connection_made() + """ + self.obj_ut._send_options_message = Mock() + self.obj_ut.client_connection_made() + self.obj_ut._send_options_message.assert_called_with() + + @patch('twisted.internet.reactor.connectTCP') + def test_close(self, mock_connectTCP): + """ + Verify that close() disconnects the connector and errors callbacks. + """ + self.obj_ut.error_all_callbacks = Mock() + self.obj_ut.add_connection() + self.obj_ut.is_closed = False + self.obj_ut.close() + self.obj_ut.connector.disconnect.assert_called_with() + self.assertTrue(self.obj_ut.connected_event.is_set()) + self.assertTrue(self.obj_ut.error_all_callbacks.called) + + def test_handle_close(self): + """ + Skipped for now, since it just calls close() and isn't really used. + """ + + def test_handle_write(self): + """ + Verify that this raises an exception if called. + """ + self.assertRaises(RuntimeError, self.obj_ut.handle_write) + + def test_handle_read__incomplete(self): + """ + Verify that handle_read() processes incomplete messages properly. + """ + self.obj_ut.process_msg = Mock() + self.assertEqual(self.obj_ut._iobuf.getvalue(), '') # buf starts empty + # incomplete header + self.obj_ut._iobuf.write('\xff\x00\x00\x00') + self.obj_ut.handle_read() + self.assertEqual(self.obj_ut._iobuf.getvalue(), '\xff\x00\x00\x00') + + # full header, but incomplete body + self.obj_ut._iobuf.write('\x00\x00\x00\x15') + self.obj_ut.handle_read() + self.assertEqual(self.obj_ut._iobuf.getvalue(), + '\xff\x00\x00\x00\x00\x00\x00\x15') + self.assertEqual(self.obj_ut._total_reqd_bytes, 29) + + # verify we never attempted to process the incomplete message + self.assertFalse(self.obj_ut.process_msg.called) + + def test_handle_read__fullmessage(self): + """ + Verify that handle_read() processes complete messages properly. + """ + self.obj_ut.process_msg = Mock() + self.assertEqual(self.obj_ut._iobuf.getvalue(), '') # buf starts empty + + # write a complete message, plus 'NEXT' (to simulate next message) + self.obj_ut._iobuf.write( + '\xff\x00\x00\x00\x00\x00\x00\x15this is the drum rollNEXT') + self.obj_ut.handle_read() + self.assertEqual(self.obj_ut._iobuf.getvalue(), 'NEXT') + self.obj_ut.process_msg.assert_called_with( + '\xff\x00\x00\x00\x00\x00\x00\x15this is the drum roll', 21) + + @patch('twisted.internet.reactor.connectTCP') + def test_push(self, mock_connectTCP): + """ + Verifiy that push() calls transport.write(data). + """ + self.obj_ut.add_connection() + self.obj_ut.push('123 pickup') + self.mock_reactor_cft.assert_called_with( + self.obj_ut.connector.transport.write, '123 pickup') From fd3ae26c043bc43d4052f7768dcdadca3c22a84e Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 11 Jun 2014 13:12:55 -0500 Subject: [PATCH 0742/3961] Use six.moves for references to xrange Fixes #138 --- CHANGELOG.rst | 9 +++++++++ cassandra/io/geventreactor.py | 1 + cassandra/io/libevreactor.py | 1 + 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2d0c07f5b1..e532a448ae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,12 @@ +2.0.3 +===== +In Progress + +Bug Fixes +--------- +* Fix references to xrange that do not go through "six" in + libevreactor and geventreactor (github #138) + 2.0.2 ===== June 10, 2014 diff --git a/cassandra/io/geventreactor.py b/cassandra/io/geventreactor.py index cefe4b8e0e..27f2432854 100644 --- a/cassandra/io/geventreactor.py +++ b/cassandra/io/geventreactor.py @@ -22,6 +22,7 @@ import logging import os +from six.moves import xrange try: from cStringIO import StringIO except ImportError: diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index c386e457ac..05b6863b35 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -22,6 +22,7 @@ import weakref from six import BytesIO +from six.moves import xrange from cassandra import OperationTimedOut from cassandra.connection import Connection, ConnectionShutdown, NONBLOCKING From fe9d0f2d26afdb23190f190ceef078c508a35899 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 11 Jun 2014 18:57:45 -0500 Subject: [PATCH 0743/3961] BoundStmt should inherit fetch_size from PreparedStmt Fixes PYTHON-80 --- CHANGELOG.rst | 2 ++ cassandra/query.py | 1 + tests/unit/test_parameter_binding.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e532a448ae..eb774c3a40 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Bug Fixes --------- * Fix references to xrange that do not go through "six" in libevreactor and geventreactor (github #138) +* Make BoundStatements inherit fetch_size from their parent + PreparedStatement (PYTHON-80) 2.0.2 ===== diff --git a/cassandra/query.py b/cassandra/query.py index dddcd5578b..c3cc8de625 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -392,6 +392,7 @@ def __init__(self, prepared_statement, *args, **kwargs): """ self.consistency_level = prepared_statement.consistency_level self.serial_consistency_level = prepared_statement.serial_consistency_level + self.fetch_size = prepared_statement.fetch_size self.prepared_statement = prepared_statement self.values = [] diff --git a/tests/unit/test_parameter_binding.py b/tests/unit/test_parameter_binding.py index b9edbbe3fb..01934caa6b 100644 --- a/tests/unit/test_parameter_binding.py +++ b/tests/unit/test_parameter_binding.py @@ -107,3 +107,21 @@ def test_invalid_argument_type(self): self.assertIn('list', str(e)) else: self.fail('Passed invalid type but exception was not thrown') + + def test_inherit_fetch_size(self): + keyspace = 'keyspace1' + column_family = 'cf1' + + column_metadata = [ + (keyspace, column_family, 'foo1', Int32Type), + (keyspace, column_family, 'foo2', Int32Type) + ] + + prepared_statement = PreparedStatement(column_metadata=column_metadata, + query_id=None, + routing_key_indexes=[], + query=None, + keyspace=keyspace, + fetch_size=1234) + bound_statement = BoundStatement(prepared_statement=prepared_statement) + self.assertEqual(1234, bound_statement.fetch_size) From f060384b6adff5cc4443d6a85508c72d3c012203 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 12 Jun 2014 13:01:08 -0500 Subject: [PATCH 0744/3961] Use ev_loop_new and ev_loop_destroy Related to #141 --- cassandra/io/libevwrapper.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cassandra/io/libevwrapper.c b/cassandra/io/libevwrapper.c index ab38ff169b..cbac83b277 100644 --- a/cassandra/io/libevwrapper.c +++ b/cassandra/io/libevwrapper.c @@ -8,6 +8,7 @@ typedef struct libevwrapper_Loop { static void Loop_dealloc(libevwrapper_Loop *self) { + ev_loop_destroy(self->loop); Py_TYPE(self)->tp_free((PyObject *)self); }; @@ -17,9 +18,9 @@ Loop_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { self = (libevwrapper_Loop *)type->tp_alloc(type, 0); if (self != NULL) { - self->loop = ev_default_loop(EVBACKEND_SELECT); + self->loop = ev_loop_new(EVBACKEND_SELECT); if (!self->loop) { - PyErr_SetString(PyExc_Exception, "Error getting default ev loop"); + PyErr_SetString(PyExc_Exception, "Error getting new ev loop"); Py_DECREF(self); return NULL; } From c277202ac906961b62061736abca5317cdecaf93 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 12 Jun 2014 13:07:33 -0500 Subject: [PATCH 0745/3961] Cleanup live watchers during libev loop cleanup Relates to #141 --- cassandra/io/libevreactor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 05b6863b35..141ddd570e 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -126,6 +126,15 @@ def _cleanup(self): if not self._thread: return + for conn in self._live_conns | self._new_conns | self._closed_conns: + conn.close() + if conn._write_watcher: + conn._write_watcher.stop() + del conn._write_watcher + if conn._read_watcher: + conn._read_watcher.stop() + del conn._read_watcher + log.debug("Waiting for event loop thread to join...") self._thread.join(timeout=1.0) if self._thread.is_alive(): @@ -134,6 +143,7 @@ def _cleanup(self): "Please call Cluster.shutdown() to avoid this.") log.debug("Event loop thread was joined") + self._loop = None def connection_created(self, conn): with self._conn_set_lock: From cd9ff7077b1ca91e9661f744cab9d20545c788dd Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 12 Jun 2014 13:08:01 -0500 Subject: [PATCH 0746/3961] Add methods for cleaning up after forking Relates to #141 --- cassandra/connection.py | 8 ++++++++ cassandra/io/asyncorereactor.py | 6 ++++++ cassandra/io/libevreactor.py | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/cassandra/connection.py b/cassandra/connection.py index 11a17f29fa..39c58ed4a7 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -187,6 +187,14 @@ def initialize_reactor(self): """ pass + @classmethod + def handle_fork(self): + """ + Called after a forking. This should cleanup any remaining reactor state + from the parent process. + """ + pass + def close(self): raise NotImplementedError() diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index 8c021a88de..6d4a0ba45e 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -146,6 +146,12 @@ def initialize_reactor(cls): if not cls._loop: cls._loop = AsyncoreLoop() + @classmethod + def handle_fork(cls): + if cls._loop: + cls._loop._cleanup() + cls._loop = None + @classmethod def factory(cls, *args, **kwargs): timeout = kwargs.pop('timeout', 5.0) diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 141ddd570e..fd90e6a906 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -227,6 +227,12 @@ def initialize_reactor(cls): if not cls._libevloop: cls._libevloop = LibevLoop() + @classmethod + def handle_fork(cls): + if cls._libevloop: + cls._libevloop._cleanup() + cls._libevloop = None + @classmethod def factory(cls, *args, **kwargs): timeout = kwargs.pop('timeout', 5.0) From 5b57a85f82f6b5c50ae7e8842db5ebdf246e0c85 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 12 Jun 2014 13:41:22 -0500 Subject: [PATCH 0747/3961] Detect fork in child proc, reinitialize reactor state --- CHANGELOG.rst | 3 +++ cassandra/io/asyncorereactor.py | 7 +++++++ cassandra/io/libevreactor.py | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eb774c3a40..e835948b61 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,9 @@ Bug Fixes libevreactor and geventreactor (github #138) * Make BoundStatements inherit fetch_size from their parent PreparedStatement (PYTHON-80) +* Clear reactor state in child process after forking + to prevent errors with multiprocessing when the parent + process has connected a Cluster before forking (github #141) 2.0.2 ===== diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index 6d4a0ba45e..be73187bc5 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -59,6 +59,7 @@ def _cleanup(loop_weakref): class AsyncoreLoop(object): def __init__(self): + self._pid = os.getpid() self._loop_lock = Lock() self._started = False self._shutdown = False @@ -145,6 +146,12 @@ class AsyncoreConnection(Connection, asyncore.dispatcher): def initialize_reactor(cls): if not cls._loop: cls._loop = AsyncoreLoop() + else: + current_pid = os.getpid() + if cls._loop._pid != current_pid: + log.debug("Detected fork, clearing and reinitializing reactor state") + cls.handle_fork() + cls._loop = AsyncoreLoop() @classmethod def handle_fork(cls): diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index fd90e6a906..23da912d37 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -60,6 +60,7 @@ def _cleanup(loop_weakref): class LibevLoop(object): def __init__(self): + self._pid = os.getpid() self._loop = libev.Loop() self._notifier = libev.Async(self._loop) self._notifier.start() @@ -226,6 +227,11 @@ class LibevConnection(Connection): def initialize_reactor(cls): if not cls._libevloop: cls._libevloop = LibevLoop() + else: + if cls._libevloop._pid != os.getpid(): + log.debug("Detected fork, clearing and reinitializing reactor state") + cls.handle_fork() + cls._libevloop = LibevLoop() @classmethod def handle_fork(cls): From 38306470bf0fc3f4499bd7e1c2d1eba987325647 Mon Sep 17 00:00:00 2001 From: Colin Stolley Date: Sat, 14 Jun 2014 13:09:14 -0500 Subject: [PATCH 0748/3961] Replace six.BytesIO with io.BytesIO for speed-up On python 2.6 and 2.7, six.BytesIO maps to StringIO, which is slow: https://github.com/kelp404/six/blob/master/six.py#L478 Python 2.6 and 2.7 provide the io module (for compatability with the Python 3 io module), which defines a faster BytesIO implementation. cProfile reveals a significant speedup when using the io module. Using six.BytesIO: ncalls tottime percall cumtime percall filename:lineno(function) ... 2277792 9.736 0.000 13.088 0.000 /usr/lib64/python2.6/StringIO.py:208(write) However with io.BytesIO: 2277792 0.920 0.000 0.920 0.000 {method 'write' of '_bytesio._BytesIO' objects} This will break on Python 2.5, but I think that's ok. --- cassandra/protocol.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 8b98d20ee0..9b17eb6800 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import # to enable import io from stdlib import logging import socket from uuid import UUID import six from six.moves import range +import io from cassandra import (Unavailable, WriteTimeout, ReadTimeout, AlreadyExists, InvalidRequest, Unauthorized, @@ -67,7 +69,7 @@ class _MessageType(object): tracing = False def to_binary(self, stream_id, protocol_version, compression=None): - body = six.BytesIO() + body = io.BytesIO() self.send_body(body, protocol_version) body = body.getvalue() @@ -78,7 +80,7 @@ def to_binary(self, stream_id, protocol_version, compression=None): if self.tracing: flags |= TRACING_FLAG - msg = six.BytesIO() + msg = io.BytesIO() write_header( msg, protocol_version | HEADER_DIRECTION_FROM_CLIENT, @@ -107,7 +109,7 @@ def decode_response(stream_id, flags, opcode, body, decompressor=None): body = decompressor(body) flags ^= COMPRESSED_FLAG - body = six.BytesIO(body) + body = io.BytesIO(body) if flags & TRACING_FLAG: trace_id = UUID(bytes=body.read(16)) flags ^= TRACING_FLAG From 5053fb241aed055933cb9d3b6bbde9fa0f715779 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Mon, 16 Jun 2014 08:21:06 -0400 Subject: [PATCH 0749/3961] removing redundant assignment in MapUpdateClause --- cqlengine/statements.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 31d1ff088f..090fc62680 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -320,7 +320,6 @@ class MapUpdateClause(ContainerUpdateClause): def __init__(self, field, value, operation=None, previous=None, column=None): super(MapUpdateClause, self).__init__(field, value, operation, previous, column=column) self._updates = None - self.previous = self.previous def _analyze(self): if self._operation == "update": From 78d1654478777ec5959b83774d9ff74d696e4b18 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 14:49:14 -0700 Subject: [PATCH 0750/3961] removed cqlengine based exceptios in favor of native driver exceptions --- cqlengine/connection.py | 7 ++++++ cqlengine/tests/base.py | 8 +++---- .../tests/connections/test_connection_pool.py | 22 ------------------- .../connections/test_connection_setup.py | 22 ------------------- upgrading.txt | 3 ++- 5 files changed, 13 insertions(+), 49 deletions(-) delete mode 100644 cqlengine/tests/connections/test_connection_pool.py delete mode 100644 cqlengine/tests/connections/test_connection_setup.py diff --git a/cqlengine/connection.py b/cqlengine/connection.py index aa5941314b..dbf92ecd07 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -261,3 +261,10 @@ def connection_manager(): # tmp = connection_pool.get() yield connection_pool # connection_pool.put(tmp) + + +def execute_native(query, params, consistency_level): + # TODO use consistency level + prepared = session.prepare(query) + result = session.execute(prepared, params) + return result diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index 7700456b7b..35b7cca97a 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -8,13 +8,13 @@ else: CASSANDRA_TEST_HOST = 'localhost' +connection.setup([CASSANDRA_TEST_HOST], default_keyspace='cqlengine_test') class BaseCassEngTestCase(TestCase): - @classmethod - def setUpClass(cls): - super(BaseCassEngTestCase, cls).setUpClass() - connection.setup([CASSANDRA_TEST_HOST], default_keyspace='cqlengine_test') + # @classmethod + # def setUpClass(cls): + # super(BaseCassEngTestCase, cls).setUpClass() def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), diff --git a/cqlengine/tests/connections/test_connection_pool.py b/cqlengine/tests/connections/test_connection_pool.py deleted file mode 100644 index b34ea47182..0000000000 --- a/cqlengine/tests/connections/test_connection_pool.py +++ /dev/null @@ -1,22 +0,0 @@ -from unittest import TestCase -from cql import OperationalError -from mock import MagicMock, patch, Mock - -from cqlengine import ONE -from cqlengine.connection import ConnectionPool, Host - - -class OperationalErrorLoggingTest(TestCase): - def test_logging(self): - p = ConnectionPool([Host('127.0.0.1', '9160')]) - - class MockConnection(object): - host = 'localhost' - port = 6379 - def cursor(self): - raise OperationalError('test') - - - with patch.object(p, 'get', return_value=MockConnection()): - with self.assertRaises(OperationalError): - p.execute("select * from system.peers", {}, ONE) diff --git a/cqlengine/tests/connections/test_connection_setup.py b/cqlengine/tests/connections/test_connection_setup.py deleted file mode 100644 index e07820f93c..0000000000 --- a/cqlengine/tests/connections/test_connection_setup.py +++ /dev/null @@ -1,22 +0,0 @@ -from unittest import TestCase -from mock import patch - -from cqlengine.connection import setup as setup_connection -from cqlengine.connection import CQLConnectionError, Host - - -class OperationalErrorLoggingTest(TestCase): - - @patch('cqlengine.connection.ConnectionPool', return_value=None, autospec=True) - def test_setup_hosts(self, PatchedConnectionPool): - with self.assertRaises(CQLConnectionError): - setup_connection(hosts=['localhost:abcd']) - self.assertEqual(len(PatchedConnectionPool.mock_calls), 0) - - with self.assertRaises(CQLConnectionError): - setup_connection(hosts=['localhost:9160:abcd']) - self.assertEqual(len(PatchedConnectionPool.mock_calls), 0) - - setup_connection(hosts=['localhost:9161', 'remotehost']) - self.assertEqual(len(PatchedConnectionPool.mock_calls), 1) - self.assertEqual(PatchedConnectionPool.call_args[0][0], [Host('localhost', 9161), Host('remotehost', 9160)]) diff --git a/upgrading.txt b/upgrading.txt index 5f4f89d2aa..cf82097bff 100644 --- a/upgrading.txt +++ b/upgrading.txt @@ -1,4 +1,5 @@ 0.15 Upgrade to use Datastax Native Driver - +Calls to execute should no longer use named placeholders. +We no longer raise cqlengine based OperationalError when connection fails. Now using the exception thrown in the native driver. From 0bb08e22ea7b95d6095426c7cc421f17cb1c352d Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 15:23:49 -0700 Subject: [PATCH 0751/3961] starting to use native driver for syncing tables --- cqlengine/connection.py | 3 +++ cqlengine/management.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index dbf92ecd07..a3ff3ba11a 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -246,6 +246,7 @@ def execute(self, query, params, consistency_level=None): def execute(query, params=None, consistency_level=None): + if isinstance(query, BaseCQLStatement): params = query.get_context() query = str(query) @@ -254,6 +255,8 @@ def execute(query, params=None, consistency_level=None): consistency_level = connection_pool._consistency return connection_pool.execute(query, params, consistency_level) + #return execute_native(query, params, consistency_level) + @contextmanager def connection_manager(): """ :rtype: ConnectionPool """ diff --git a/cqlengine/management.py b/cqlengine/management.py index 1e0401a43a..c926601d3b 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -4,7 +4,7 @@ from cqlengine import ONE from cqlengine.named import NamedTable -from cqlengine.connection import connection_manager, execute +from cqlengine.connection import connection_manager, execute, execute_native from cqlengine.exceptions import CQLEngineException import logging @@ -87,13 +87,12 @@ def sync_table(model, create_missing_keyspace=True): if create_missing_keyspace: create_keyspace(ks_name) - with connection_manager() as con: - tables = con.execute( - "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", - {'ks_name': ks_name}, - ONE - ) - tables = [x[0] for x in tables.results] + tables = execute_native( + "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = ?", + [ks_name], ONE + ) + + tables = [x.columnfamily_name for x in tables] #check for an existing column family if raw_cf_name not in tables: From eb19c3e3e722f4bd9fda970bd760a6a8350969e8 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 16:25:34 -0700 Subject: [PATCH 0752/3961] creating table with native driver --- cqlengine/connection.py | 2 +- cqlengine/management.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index a3ff3ba11a..5db546c0a0 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -266,7 +266,7 @@ def connection_manager(): # connection_pool.put(tmp) -def execute_native(query, params, consistency_level): +def execute_native(query, params=None, consistency_level=None): # TODO use consistency level prepared = session.prepare(query) result = session.execute(prepared, params) diff --git a/cqlengine/management.py b/cqlengine/management.py index c926601d3b..c12b109f7f 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -99,7 +99,7 @@ def sync_table(model, create_missing_keyspace=True): qs = get_create_table(model) try: - execute(qs) + execute_native(qs) except CQLEngineException as ex: # 1.2 doesn't return cf names, so we have to examine the exception # and ignore if it says the column family already exists @@ -246,6 +246,8 @@ def get_fields(model): tmp = con.execute(query, {'ks_name': ks_name, 'col_family': col_family}, ONE) + #import ipdb; ipdb.set_trace() + # Tables containing only primary keys do not appear to create # any entries in system.schema_columns, as only non-primary-key attributes # appear to be inserted into the schema_columns table From 0c011a66d5866d994fa1987723d540af20be4b27 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 16:29:13 -0700 Subject: [PATCH 0753/3961] performing table alterations using native driver --- cqlengine/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index c12b109f7f..3fdfa61eab 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -116,7 +116,7 @@ def sync_table(model, create_missing_keyspace=True): # add missing column using the column def query = "ALTER TABLE {} add {}".format(cf_name, col.get_column_def()) logger.debug(query) - execute(query) + execute_native(query) update_compaction(model) From bccca12f13e39f13939d6b53681c19f72340dca1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 16:45:43 -0700 Subject: [PATCH 0754/3961] reworking get_fields to use much simpler native driver calls --- cqlengine/management.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 3fdfa61eab..f81e591352 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -108,7 +108,7 @@ def sync_table(model, create_missing_keyspace=True): else: # see if we're missing any columns fields = get_fields(model) - field_names = [x.name for x in fields] + field_names = [x.name for x in fields] # [Field(name=u'name', type=u'org.apache.cassandra.db.marshal.UTF8Type')] for name, col in model._columns.items(): if col.primary_key or col.partition_key: continue # we can't mess with the PK if col.db_field_name in field_names: continue # skip columns already defined @@ -239,28 +239,19 @@ def get_fields(model): col_family = model.column_family_name(include_keyspace=False) with connection_manager() as con: - query = "SELECT * FROM system.schema_columns \ - WHERE keyspace_name = :ks_name AND columnfamily_name = :col_family" - - logger.debug("get_fields %s %s", ks_name, col_family) - - tmp = con.execute(query, {'ks_name': ks_name, 'col_family': col_family}, ONE) - - #import ipdb; ipdb.set_trace() + query = "select * from system.schema_columns where keyspace_name = ? and columnfamily_name = ?" + tmp = execute_native(query, [ks_name, col_family]) # Tables containing only primary keys do not appear to create # any entries in system.schema_columns, as only non-primary-key attributes # appear to be inserted into the schema_columns table - if not tmp.results: + if not tmp: return [] - column_name_positon = tmp.columns.index('column_name') - validator_positon = tmp.columns.index('validator') try: - type_position = tmp.columns.index('type') - return [Field(x[column_name_positon], x[validator_positon]) for x in tmp.results if x[type_position] == 'regular'] + return [Field(x.column_name, x.validator) for x in tmp if x.type == 'regular'] except ValueError: - return [Field(x[column_name_positon], x[validator_positon]) for x in tmp.results] + return [Field(x.column_name, x.validator) for x in tmp] # convert to Field named tuples From bbbb3a1fbb116cef29a196b8360f57faafeb1020 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 16:51:32 -0700 Subject: [PATCH 0755/3961] get_fields now using native driver --- cqlengine/management.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index f81e591352..329e1baa62 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -108,7 +108,7 @@ def sync_table(model, create_missing_keyspace=True): else: # see if we're missing any columns fields = get_fields(model) - field_names = [x.name for x in fields] # [Field(name=u'name', type=u'org.apache.cassandra.db.marshal.UTF8Type')] + field_names = [x.name for x in fields] for name, col in model._columns.items(): if col.primary_key or col.partition_key: continue # we can't mess with the PK if col.db_field_name in field_names: continue # skip columns already defined @@ -238,9 +238,8 @@ def get_fields(model): ks_name = model._get_keyspace() col_family = model.column_family_name(include_keyspace=False) - with connection_manager() as con: - query = "select * from system.schema_columns where keyspace_name = ? and columnfamily_name = ?" - tmp = execute_native(query, [ks_name, col_family]) + query = "select * from system.schema_columns where keyspace_name = ? and columnfamily_name = ?" + tmp = execute_native(query, [ks_name, col_family]) # Tables containing only primary keys do not appear to create # any entries in system.schema_columns, as only non-primary-key attributes From 434590cb850c10c6877aebb62da2724b1f08fe97 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 17:15:48 -0700 Subject: [PATCH 0756/3961] setting compaction options via native --- cqlengine/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 329e1baa62..c5bd694871 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -300,7 +300,7 @@ def update_compaction(model): cf_name = model.column_family_name() query = "ALTER TABLE {} with compaction = {}".format(cf_name, options) logger.debug(query) - execute(query) + execute_native(query) return True return False From 60d58c193044d8498696675af73f67d4382f38c1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 17:17:37 -0700 Subject: [PATCH 0757/3961] delete keyspace now uses native driver --- cqlengine/management.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index c5bd694871..dee113fd5f 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -54,10 +54,9 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, def delete_keyspace(name): - with connection_manager() as con: - _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}, ONE) - if name in [r[0] for r in keyspaces]: - execute("DROP KEYSPACE {}".format(name)) + keyspaces = execute_native("""SELECT keyspace_name FROM system.schema_keyspaces""", {}, ONE) + if name in [r.keyspace_name for r in keyspaces]: + execute_native("DROP KEYSPACE {}".format(name)) def create_table(model, create_missing_keyspace=True): warnings.warn("create_table has been deprecated in favor of sync_table and will be removed in a future release", DeprecationWarning) From f1f2ef0356e24d46db7bd82407e8a23495b4a085 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 17:33:55 -0700 Subject: [PATCH 0758/3961] dropping tables natively --- cqlengine/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index dee113fd5f..d2fac363f1 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -325,6 +325,6 @@ def drop_table(model): return cf_name = model.column_family_name() - execute('drop table {};'.format(cf_name)) + execute_native('drop table {};'.format(cf_name)) From 2dd3a388a0b659b481ac9d95a73261957bfebe3b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 18:15:04 -0700 Subject: [PATCH 0759/3961] drop table now uses the native driver metadata instead of trying to do crazy raw calls --- cqlengine/connection.py | 6 ++++++ cqlengine/management.py | 20 +++++++++----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 5db546c0a0..2768db1077 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -271,3 +271,9 @@ def execute_native(query, params=None, consistency_level=None): prepared = session.prepare(query) result = session.execute(prepared, params) return result + +def get_session(): + return session + +def get_cluster(): + return cluster diff --git a/cqlengine/management.py b/cqlengine/management.py index d2fac363f1..dc3f086200 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -4,7 +4,7 @@ from cqlengine import ONE from cqlengine.named import NamedTable -from cqlengine.connection import connection_manager, execute, execute_native +from cqlengine.connection import connection_manager, execute, execute_native, get_session, get_cluster from cqlengine.exceptions import CQLEngineException import logging @@ -313,18 +313,16 @@ def delete_table(model): def drop_table(model): # don't try to delete non existant tables + meta = get_cluster().metadata + ks_name = model._get_keyspace() - with connection_manager() as con: - _, tables = con.execute( - "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = :ks_name", - {'ks_name': ks_name}, - ONE - ) raw_cf_name = model.column_family_name(include_keyspace=False) - if raw_cf_name not in [t[0] for t in tables]: - return - cf_name = model.column_family_name() - execute_native('drop table {};'.format(cf_name)) + try: + table = meta.keyspaces[ks_name].tables[raw_cf_name] + execute_native('drop table {};'.format(model.column_family_name(include_keyspace=True))) + except KeyError: + pass + From 232e5e5558b86dd7cac5d9e5d3d3492bcf26a991 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 18:19:08 -0700 Subject: [PATCH 0760/3961] switched to cluster metadata for creating keyspaces --- cqlengine/management.py | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index dc3f086200..678ff89597 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -27,30 +27,30 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, :param durable_writes: 1.2 only, write log is bypassed if set to False :param **replication_values: 1.2 only, additional values to ad to the replication data map """ - with connection_manager() as con: - _, keyspaces = con.execute("""SELECT keyspace_name FROM system.schema_keyspaces""", {}, ONE) - if name not in [r[0] for r in keyspaces]: - #try the 1.2 method - replication_map = { - 'class': strategy_class, - 'replication_factor':replication_factor - } - replication_map.update(replication_values) - if strategy_class.lower() != 'simplestrategy': - # Although the Cassandra documentation states for `replication_factor` - # that it is "Required if class is SimpleStrategy; otherwise, - # not used." we get an error if it is present. - replication_map.pop('replication_factor', None) - - query = """ - CREATE KEYSPACE {} - WITH REPLICATION = {} - """.format(name, json.dumps(replication_map).replace('"', "'")) - - if strategy_class != 'SimpleStrategy': - query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false') - - execute(query) + cluster = get_cluster() + + if name not in cluster.metadata.keyspaces: + #try the 1.2 method + replication_map = { + 'class': strategy_class, + 'replication_factor':replication_factor + } + replication_map.update(replication_values) + if strategy_class.lower() != 'simplestrategy': + # Although the Cassandra documentation states for `replication_factor` + # that it is "Required if class is SimpleStrategy; otherwise, + # not used." we get an error if it is present. + replication_map.pop('replication_factor', None) + + query = """ + CREATE KEYSPACE {} + WITH REPLICATION = {} + """.format(name, json.dumps(replication_map).replace('"', "'")) + + if strategy_class != 'SimpleStrategy': + query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false') + + execute_native(query) def delete_keyspace(name): From a5b4535a8c3bf0152c077fa8a62df7b462da6869 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 18:38:58 -0700 Subject: [PATCH 0761/3961] removed a bunch of raw CQL calls in favor of the driver --- cqlengine/management.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 678ff89597..8ddf8de128 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -54,8 +54,8 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, def delete_keyspace(name): - keyspaces = execute_native("""SELECT keyspace_name FROM system.schema_keyspaces""", {}, ONE) - if name in [r.keyspace_name for r in keyspaces]: + cluster = get_cluster() + if name in cluster.metadata.keyspaces: execute_native("DROP KEYSPACE {}".format(name)) def create_table(model, create_missing_keyspace=True): @@ -86,12 +86,9 @@ def sync_table(model, create_missing_keyspace=True): if create_missing_keyspace: create_keyspace(ks_name) - tables = execute_native( - "SELECT columnfamily_name from system.schema_columnfamilies WHERE keyspace_name = ?", - [ks_name], ONE - ) + cluster = get_cluster() - tables = [x.columnfamily_name for x in tables] + tables = cluster.metadata.keyspaces[ks_name].tables #check for an existing column family if raw_cf_name not in tables: From e4a763ceb3b2bc4526ec7c4da6a6b5e368d6c936 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 16 Jun 2014 19:24:46 -0700 Subject: [PATCH 0762/3961] logic for indexes now using native driver and metadata --- cqlengine/management.py | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 8ddf8de128..820a70139b 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -88,7 +88,8 @@ def sync_table(model, create_missing_keyspace=True): cluster = get_cluster() - tables = cluster.metadata.keyspaces[ks_name].tables + keyspace = cluster.metadata.keyspaces[ks_name] + tables = keyspace.tables #check for an existing column family if raw_cf_name not in tables: @@ -117,31 +118,19 @@ def sync_table(model, create_missing_keyspace=True): update_compaction(model) - #get existing index names, skip ones that already exist - with connection_manager() as con: - _, idx_names = con.execute( - "SELECT index_name from system.\"IndexInfo\" WHERE table_name=:table_name", - {'table_name': raw_cf_name}, - ONE - ) - - idx_names = [i[0] for i in idx_names] - idx_names = filter(None, idx_names) + table = cluster.metadata.keyspaces[ks_name].tables[raw_cf_name] indexes = [c for n,c in model._columns.items() if c.index] - if indexes: - for column in indexes: - if column.db_index_name in idx_names: continue - qs = ['CREATE INDEX index_{}_{}'.format(raw_cf_name, column.db_field_name)] - qs += ['ON {}'.format(cf_name)] - qs += ['("{}")'.format(column.db_field_name)] - qs = ' '.join(qs) - - try: - execute(qs) - except CQLEngineException: - # index already exists - pass + + for column in indexes: + if table.columns[column.db_field_name].index: + continue + + qs = ['CREATE INDEX index_{}_{}'.format(raw_cf_name, column.db_field_name)] + qs += ['ON {}'.format(cf_name)] + qs += ['("{}")'.format(column.db_field_name)] + qs = ' '.join(qs) + execute_native(qs) def get_create_table(model): cf_name = model.column_family_name() From 2badda4a5aaad4c9b80ae9117b6d5c2c9c81e7ab Mon Sep 17 00:00:00 2001 From: Erik Forsberg Date: Wed, 4 Jun 2014 14:31:14 +0200 Subject: [PATCH 0763/3961] #66 IPv6 support in asyncreactor. --- cassandra/io/asyncorereactor.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index be73187bc5..952998b7ca 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -185,8 +185,19 @@ def __init__(self, *args, **kwargs): self._loop.connection_created(self) - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - self.connect((self.host, self.port)) + sockerr = None + for (af, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): + try: + self.create_socket(af, socktype) + self.connect(sockaddr) + sockerr = None + break + except socket.error as err: + sockerr = err + if sockerr: + raise socket.error(sockerr.errno, "Tried connecting to all addresses. Last error was %s" % sockerr.message) + + self.add_channel() if self.sockopts: From f53d9b144e793af542b205ea26b73ddc4f4de3fe Mon Sep 17 00:00:00 2001 From: Erik Forsberg Date: Thu, 5 Jun 2014 11:16:20 +0200 Subject: [PATCH 0764/3961] #66: Rudimentary IPv6 support for libevreactor. --- cassandra/io/libevreactor.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 23da912d37..6062c6b41d 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -262,13 +262,22 @@ def __init__(self, *args, **kwargs): self.deque = deque() self._deque_lock = Lock() - self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + addrinfo = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM) + # Only try connecting to the first address listed. This is not + # the proper way to do it, but I'm a bit too lazy to figure + # out the event loop callback stuff required to try all + # entries. + # At least try to warn the user about this limitation. + if len(addrinfo) > 1: + log.warning("This version of python-driver will only try the first address returned by getaddrinfo") + (af, socktype, proto, cannoname, sockaddr) = addrinfo[0] + self._socket = socket.socket(af, socktype, proto) if self.ssl_options: if not ssl: raise Exception("This version of Python was not compiled with SSL support") self._socket = ssl.wrap_socket(self._socket, **self.ssl_options) self._socket.settimeout(1.0) # TODO potentially make this value configurable - self._socket.connect((self.host, self.port)) + self._socket.connect(sockaddr) self._socket.setblocking(0) if self.sockopts: From 18886ce8b33121f400de8d1b0328e8e5ef4d49a2 Mon Sep 17 00:00:00 2001 From: Erik Forsberg Date: Mon, 16 Jun 2014 14:09:51 +0000 Subject: [PATCH 0765/3961] #66: Rudimentary IPv6 support for geventreactor. --- cassandra/io/geventreactor.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cassandra/io/geventreactor.py b/cassandra/io/geventreactor.py index 27f2432854..af74837c64 100644 --- a/cassandra/io/geventreactor.py +++ b/cassandra/io/geventreactor.py @@ -79,9 +79,21 @@ def __init__(self, *args, **kwargs): self._callbacks = {} self._push_watchers = defaultdict(set) - self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + addrinfo = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM) + + # Only try connecting to the first address listed. This is not + # the proper way to do it, but I'm a bit too lazy to figure + # out the event loop callback stuff required to try all + # entries. + # At least try to warn the user about this limitation. + if len(addrinfo) > 1: + log.warning("This version of python-driver will only try the first address returned by getaddrinfo") + + (af, socktype, proto, cannoname, sockaddr) = addrinfo[0] + + self._socket = socket.socket(af, socktype, proto) self._socket.settimeout(1.0) - self._socket.connect((self.host, self.port)) + self._socket.connect(sockaddr) if self.sockopts: for args in self.sockopts: From 48582f447698307725156e9196369dab083c730a Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 11:02:32 -0500 Subject: [PATCH 0766/3961] Use BytesIO for reduced CPU consumption Relates to #143 --- CHANGELOG.rst | 4 ++++ cassandra/io/asyncorereactor.py | 7 ++++--- cassandra/io/geventreactor.py | 10 ++++------ cassandra/io/libevreactor.py | 7 ++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e835948b61..b4854f6828 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,10 @@ ===== In Progress +Features +-------- +* Use io.BytesIO for reduced CPU consumption (github #143) + Bug Fixes --------- * Fix references to xrange that do not go through "six" in diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index be73187bc5..0cbad004d6 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import # to enable import io from stdlib import atexit from collections import deque from functools import partial +import io import logging import os import socket @@ -22,7 +24,6 @@ from threading import Event, Lock, Thread import weakref -from six import BytesIO from six.moves import range from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, EINVAL, EISCONN, errorcode @@ -177,7 +178,7 @@ def __init__(self, *args, **kwargs): asyncore.dispatcher.__init__(self) self.connected_event = Event() - self._iobuf = BytesIO() + self._iobuf = io.BytesIO() self._callbacks = {} self.deque = deque() @@ -330,7 +331,7 @@ def handle_read(self): # leave leftover in current buffer leftover = self._iobuf.read() - self._iobuf = BytesIO() + self._iobuf = io.BytesIO() self._iobuf.write(leftover) self._total_reqd_bytes = 0 diff --git a/cassandra/io/geventreactor.py b/cassandra/io/geventreactor.py index 27f2432854..93ebab8243 100644 --- a/cassandra/io/geventreactor.py +++ b/cassandra/io/geventreactor.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import # to enable import io from stdlib import gevent from gevent import select, socket from gevent.event import Event @@ -19,14 +20,11 @@ from collections import defaultdict from functools import partial +import io import logging import os from six.moves import xrange -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO # ignore flake8 warning: # NOQA from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, EINVAL @@ -73,7 +71,7 @@ def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) self.connected_event = Event() - self._iobuf = StringIO() + self._iobuf = io.BytesIO() self._write_queue = Queue() self._callbacks = {} @@ -179,7 +177,7 @@ def handle_read(self): # leave leftover in current buffer leftover = self._iobuf.read() - self._iobuf = StringIO() + self._iobuf = io.BytesIO() self._iobuf.write(leftover) self._total_reqd_bytes = 0 diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 23da912d37..9c2d93d9de 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import # to enable import io from stdlib import atexit from collections import deque from functools import partial +import io import logging import os import socket from threading import Event, Lock, Thread import weakref -from six import BytesIO from six.moves import xrange from cassandra import OperationTimedOut @@ -256,7 +257,7 @@ def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) self.connected_event = Event() - self._iobuf = BytesIO() + self._iobuf = io.BytesIO() self._callbacks = {} self.deque = deque() @@ -381,7 +382,7 @@ def handle_read(self, watcher, revents, errno=None): # leave leftover in current buffer leftover = self._iobuf.read() - self._iobuf = BytesIO() + self._iobuf = io.BytesIO() self._iobuf.write(leftover) self._total_reqd_bytes = 0 From cf5dbbda848b243c8df2cba0bec8b1dd367728ce Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 17 Jun 2014 09:48:57 -0700 Subject: [PATCH 0767/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 54d1a4f2a4..a803cc227f 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.13.0 +0.14.0 From c9c8bb4b430f758e820b52bf762dd650ea7fcad3 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 17 Jun 2014 12:27:38 -0700 Subject: [PATCH 0768/3961] breaking everything, more or less --- cqlengine/statements.py | 28 +++++++-------- .../statements/test_assignment_clauses.py | 34 +++++++++---------- .../tests/statements/test_delete_statement.py | 4 +-- .../tests/statements/test_insert_statement.py | 4 +-- .../tests/statements/test_update_statement.py | 4 +-- .../tests/statements/test_where_clause.py | 4 +-- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 31d1ff088f..8a92fb6697 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -101,7 +101,7 @@ def __init__(self, field, operator, value, quote_field=True): def __unicode__(self): field = ('"{}"' if self.quote_field else '{}').format(self.field) - return u'{} {} {}'.format(field, self.operator, unicode(self.query_value)) + return u'{} {} ?'.format(field, self.operator) def __hash__(self): return super(WhereClause, self).__hash__() ^ hash(self.operator) @@ -129,7 +129,7 @@ class AssignmentClause(BaseClause): """ a single variable st statement """ def __unicode__(self): - return u'"{}" = :{}'.format(self.field, self.context_id) + return u'"{}" = ?'.format(self.field) def insert_tuple(self): return self.field, self.context_id @@ -170,15 +170,15 @@ def __unicode__(self): qs = [] ctx_id = self.context_id if self.previous is None and not (self._assignments or self._additions or self._removals): - qs += ['"{}" = :{}'.format(self.field, ctx_id)] + qs += ['"{}" = ?'.format(self.field)] if self._assignments: - qs += ['"{}" = :{}'.format(self.field, ctx_id)] + qs += ['"{}" = ?'.format(self.field)] ctx_id += 1 if self._additions: - qs += ['"{0}" = "{0}" + :{1}'.format(self.field, ctx_id)] + qs += ['"{0}" = "{0}" + ?'.format(self.field)] ctx_id += 1 if self._removals: - qs += ['"{0}" = "{0}" - :{1}'.format(self.field, ctx_id)] + qs += ['"{0}" = "{0}" - ?'.format(self.field)] return ', '.join(qs) @@ -232,15 +232,15 @@ def __unicode__(self): qs = [] ctx_id = self.context_id if self._assignments is not None: - qs += ['"{}" = :{}'.format(self.field, ctx_id)] + qs += ['"{}" = ?'.format(self.field)] ctx_id += 1 if self._prepend: - qs += ['"{0}" = :{1} + "{0}"'.format(self.field, ctx_id)] + qs += ['"{0}" = ? + "{0}"'.format(self.field)] ctx_id += 1 if self._append: - qs += ['"{0}" = "{0}" + :{1}'.format(self.field, ctx_id)] + qs += ['"{0}" = "{0}" + ?'.format(self.field)] return ', '.join(qs) @@ -356,10 +356,10 @@ def __unicode__(self): ctx_id = self.context_id if self.previous is None and not self._updates: - qs += ['"int_map" = :1'] + qs += ['"int_map" = ?'] else: for _ in self._updates or []: - qs += ['"{}"[:{}] = :{}'.format(self.field, ctx_id, ctx_id + 1)] + qs += ['"{}"[?] = ?'.format(self.field)] ctx_id += 2 return ', '.join(qs) @@ -380,7 +380,7 @@ def update_context(self, ctx): def __unicode__(self): delta = self.value - self.previous sign = '-' if delta < 0 else '+' - return '"{0}" = "{0}" {1} :{2}'.format(self.field, sign, self.context_id) + return '"{0}" = "{0}" {1} ?'.format(self.field, sign) class BaseDeleteClause(BaseClause): @@ -428,7 +428,7 @@ def get_context_size(self): def __unicode__(self): if not self._analyzed: self._analyze() - return ', '.join(['"{}"[:{}]'.format(self.field, self.context_id + i) for i in range(len(self._removals))]) + return ', '.join(['"{}"[?]'.format(self.field) for i in range(len(self._removals))]) class BaseCQLStatement(object): @@ -626,7 +626,7 @@ def __unicode__(self): qs += ["({})".format(', '.join(['"{}"'.format(c) for c in columns]))] qs += ['VALUES'] - qs += ["({})".format(', '.join([':{}'.format(v) for v in values]))] + qs += ["({})".format(', '.join(['?' for v in values]))] if self.ttl: qs += ["USING TTL {}".format(self.ttl)] diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 126dd9869a..48f58edc5a 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -25,7 +25,7 @@ def test_update_from_none(self): self.assertIsNone(c._removals) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = :0') + self.assertEqual(str(c), '"s" = ?') ctx = {} c.update_context(ctx) @@ -75,7 +75,7 @@ def test_additions(self): self.assertIsNone(c._removals) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" + :0') + self.assertEqual(str(c), '"s" = "s" + ?') ctx = {} c.update_context(ctx) @@ -91,7 +91,7 @@ def test_removals(self): self.assertEqual(c._removals, {3}) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" - :0') + self.assertEqual(str(c), '"s" = "s" - ?') ctx = {} c.update_context(ctx) @@ -107,7 +107,7 @@ def test_additions_and_removals(self): self.assertEqual(c._removals, {1}) self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s" = "s" + :0, "s" = "s" - :1') + self.assertEqual(str(c), '"s" = "s" + ?, "s" = "s" - ?') ctx = {} c.update_context(ctx) @@ -126,7 +126,7 @@ def test_update_from_none(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = :0') + self.assertEqual(str(c), '"s" = ?') ctx = {} c.update_context(ctx) @@ -142,7 +142,7 @@ def test_update_from_empty(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = :0') + self.assertEqual(str(c), '"s" = ?') ctx = {} c.update_context(ctx) @@ -158,7 +158,7 @@ def test_update_from_different_list(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = :0') + self.assertEqual(str(c), '"s" = ?') ctx = {} c.update_context(ctx) @@ -174,7 +174,7 @@ def test_append(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" + :0') + self.assertEqual(str(c), '"s" = "s" + ?') ctx = {} c.update_context(ctx) @@ -190,7 +190,7 @@ def test_prepend(self): self.assertEqual(c._prepend, [1, 2]) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = :0 + "s"') + self.assertEqual(str(c), '"s" = ? + "s"') ctx = {} c.update_context(ctx) @@ -207,7 +207,7 @@ def test_append_and_prepend(self): self.assertEqual(c._prepend, [1, 2]) self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s" = :0 + "s", "s" = "s" + :1') + self.assertEqual(str(c), '"s" = ? + "s", "s" = "s" + ?') ctx = {} c.update_context(ctx) @@ -225,7 +225,7 @@ def test_shrinking_list_update(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = :0') + self.assertEqual(str(c), '"s" = ?') ctx = {} c.update_context(ctx) @@ -241,7 +241,7 @@ def test_update(self): self.assertEqual(c._updates, [3, 5]) self.assertEqual(c.get_context_size(), 4) - self.assertEqual(str(c), '"s"[:0] = :1, "s"[:2] = :3') + self.assertEqual(str(c), '"s"[?] = ?, "s"[?] = ?') ctx = {} c.update_context(ctx) @@ -254,7 +254,7 @@ def test_update_from_null(self): self.assertEqual(c._updates, [3, 5]) self.assertEqual(c.get_context_size(), 4) - self.assertEqual(str(c), '"s"[:0] = :1, "s"[:2] = :3') + self.assertEqual(str(c), '"s"[?] = ?, "s"[?] = ?') ctx = {} c.update_context(ctx) @@ -275,7 +275,7 @@ def test_positive_update(self): c.set_context_id(5) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" + :5') + self.assertEqual(str(c), '"a" = "a" + ?') ctx = {} c.update_context(ctx) @@ -286,7 +286,7 @@ def test_negative_update(self): c.set_context_id(3) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" - :3') + self.assertEqual(str(c), '"a" = "a" - ?') ctx = {} c.update_context(ctx) @@ -297,7 +297,7 @@ def noop_update(self): c.set_context_id(5) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" + :5') + self.assertEqual(str(c), '"a" = "a" + ?') ctx = {} c.update_context(ctx) @@ -313,7 +313,7 @@ def test_update(self): self.assertEqual(c._removals, [1, 5]) self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s"[:0], "s"[:1]') + self.assertEqual(str(c), '"s"[?], "s"[?]') ctx = {} c.update_context(ctx) diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py index 9ac9554383..ada5b7209f 100644 --- a/cqlengine/tests/statements/test_delete_statement.py +++ b/cqlengine/tests/statements/test_delete_statement.py @@ -31,7 +31,7 @@ def test_table_rendering(self): def test_where_clause_rendering(self): ds = DeleteStatement('table', None) ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ds), 'DELETE FROM table WHERE "a" = :0', unicode(ds)) + self.assertEqual(unicode(ds), 'DELETE FROM table WHERE "a" = ?', unicode(ds)) def test_context_update(self): ds = DeleteStatement('table', None) @@ -39,7 +39,7 @@ def test_context_update(self): ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) ds.update_context_id(7) - self.assertEqual(unicode(ds), 'DELETE "d"[:8] FROM table WHERE "a" = :7') + self.assertEqual(unicode(ds), 'DELETE "d"[?] FROM table WHERE "a" = ?') self.assertEqual(ds.get_context(), {'7': 'b', '8': 3}) def test_context(self): diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py index 5e22d7d99e..eeffe556d9 100644 --- a/cqlengine/tests/statements/test_insert_statement.py +++ b/cqlengine/tests/statements/test_insert_statement.py @@ -17,7 +17,7 @@ def test_statement(self): self.assertEqual( unicode(ist), - 'INSERT INTO table ("a", "c") VALUES (:0, :1)' + 'INSERT INTO table ("a", "c") VALUES (?, ?)' ) def test_context_update(self): @@ -28,7 +28,7 @@ def test_context_update(self): ist.update_context_id(4) self.assertEqual( unicode(ist), - 'INSERT INTO table ("a", "c") VALUES (:4, :5)' + 'INSERT INTO table ("a", "c") VALUES (?, ?)' ) ctx = ist.get_context() self.assertEqual(ctx, {'4': 'b', '5': 'd'}) diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py index 86dbc8be44..9fe32a1aeb 100644 --- a/cqlengine/tests/statements/test_update_statement.py +++ b/cqlengine/tests/statements/test_update_statement.py @@ -16,7 +16,7 @@ def test_rendering(self): us.add_assignment_clause(AssignmentClause('a', 'b')) us.add_assignment_clause(AssignmentClause('c', 'd')) us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) - self.assertEqual(unicode(us), 'UPDATE table SET "a" = :0, "c" = :1 WHERE "a" = :2', unicode(us)) + self.assertEqual(unicode(us), 'UPDATE table SET "a" = ?, "c" = ? WHERE "a" = ?', unicode(us)) def test_context(self): us = UpdateStatement('table') @@ -31,7 +31,7 @@ def test_context_update(self): us.add_assignment_clause(AssignmentClause('c', 'd')) us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) us.update_context_id(3) - self.assertEqual(unicode(us), 'UPDATE table SET "a" = :4, "c" = :5 WHERE "a" = :3') + self.assertEqual(unicode(us), 'UPDATE table SET "a" = ?, "c" = ? WHERE "a" = ?') self.assertEqual(us.get_context(), {'4': 'b', '5': 'd', '3': 'x'}) def test_additional_rendering(self): diff --git a/cqlengine/tests/statements/test_where_clause.py b/cqlengine/tests/statements/test_where_clause.py index 938a2b4919..083f154234 100644 --- a/cqlengine/tests/statements/test_where_clause.py +++ b/cqlengine/tests/statements/test_where_clause.py @@ -14,8 +14,8 @@ def test_where_clause_rendering(self): """ tests that where clauses are rendered properly """ wc = WhereClause('a', EqualsOperator(), 'c') wc.set_context_id(5) - self.assertEqual('"a" = :5', unicode(wc)) - self.assertEqual('"a" = :5', str(wc)) + self.assertEqual('"a" = ?', unicode(wc)) + self.assertEqual('"a" = ?', str(wc)) def test_equality_method(self): """ tests that 2 identical where clauses evaluate as == """ From 01bd654516e486491852524ff8dae6ded204d934 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 17 Jun 2014 12:32:32 -0700 Subject: [PATCH 0769/3961] shouldn't be any more old school statements in there anymore --- cqlengine/tests/statements/test_select_statement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index f358c6ae5a..7caafbd796 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -30,12 +30,12 @@ def test_table_rendering(self): def test_where_clause_rendering(self): ss = SelectStatement('table') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = :0', unicode(ss)) + self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = ?', unicode(ss)) def test_count(self): ss = SelectStatement('table', count=True, limit=10, order_by='d') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = :0', unicode(ss)) + self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = ?', unicode(ss)) self.assertNotIn('LIMIT', unicode(ss)) self.assertNotIn('ORDER', unicode(ss)) @@ -49,11 +49,11 @@ def test_context_id_update(self): ss = SelectStatement('table') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(ss.get_context(), {'0': 'b'}) - self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = :0') + self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = ?') ss.update_context_id(5) self.assertEqual(ss.get_context(), {'5': 'b'}) - self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = :5') + self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = ?') def test_additional_rendering(self): ss = SelectStatement( From 5202ec9ad1a97b2fb9f0c86f3d9edf5ac917740b Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 15:20:11 -0500 Subject: [PATCH 0770/3961] Collect UDT info, handle UDT schema change pushes --- cassandra/cluster.py | 100 +++++++++++++++++++++++++----------------- cassandra/metadata.py | 58 ++++++++++++++++-------- 2 files changed, 99 insertions(+), 59 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 3ef4245d20..c0b0300a6d 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1326,7 +1326,7 @@ def run_add_or_renew_pool(): # the host itself will still be marked down, so we need to pass # a special flag to make sure the reconnector is created self.cluster.signal_connection_failure( - host, conn_exc, is_host_addition, expect_host_to_be_down=True) + host, conn_exc, is_host_addition, expect_host_to_be_down=True) return False previous = self._pools.get(host) @@ -1484,6 +1484,7 @@ class ControlConnection(object): _SELECT_KEYSPACES = "SELECT * FROM system.schema_keyspaces" _SELECT_COLUMN_FAMILIES = "SELECT * FROM system.schema_columnfamilies" _SELECT_COLUMNS = "SELECT * FROM system.schema_columns" + _SELECT_TYPES = "SELECT * FROM system.schema_types" _SELECT_PEERS = "SELECT peer, data_center, rack, tokens, rpc_address, schema_version FROM system.peers" _SELECT_LOCAL = "SELECT cluster_name, data_center, rack, tokens, partitioner, schema_version FROM system.local WHERE key='local'" @@ -1493,6 +1494,7 @@ class ControlConnection(object): _is_shutdown = False _timeout = None + _protocol_version = None # for testing purposes _time = time @@ -1514,6 +1516,7 @@ def connect(self): if self._is_shutdown: return + self._protocol_version = self._cluster.protocol_version self._set_new_connection(self._reconnect_internal()) def _set_new_connection(self, conn): @@ -1655,59 +1658,77 @@ def shutdown(self): self._connection.close() del self._connection - def refresh_schema(self, keyspace=None, table=None): + def refresh_schema(self, keyspace=None, table=None, usertype=None): try: if self._connection: - self._refresh_schema(self._connection, keyspace, table) + self._refresh_schema(self._connection, keyspace, table, usertype) except ReferenceError: pass # our weak reference to the Cluster is no good except Exception: log.debug("[control connection] Error refreshing schema", exc_info=True) self._signal_error() - def _refresh_schema(self, connection, keyspace=None, table=None, preloaded_results=None): + def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, preloaded_results=None): if self._cluster.is_shutdown: return + assert table is None or usertype is None + agreed = self.wait_for_schema_agreement(connection, preloaded_results=preloaded_results) if not agreed: log.debug("Skipping schema refresh due to lack of schema agreement") return - where_clause = "" - if keyspace: - where_clause = " WHERE keyspace_name = '%s'" % (keyspace,) - if table: - where_clause += " AND columnfamily_name = '%s'" % (table,) - cl = ConsistencyLevel.ONE if table: - ks_query = None - else: - ks_query = QueryMessage(query=self._SELECT_KEYSPACES + where_clause, consistency_level=cl) - cf_query = QueryMessage(query=self._SELECT_COLUMN_FAMILIES + where_clause, consistency_level=cl) - col_query = QueryMessage(query=self._SELECT_COLUMNS + where_clause, consistency_level=cl) + # a particular table changed + where_clause = " WHERE keyspace_name = '%s' AND columnfamily_name = '%s'" % (keyspace, table) + cf_query = QueryMessage(query=self._SELECT_COLUMN_FAMILIES + where_clause, consistency_level=cl) + col_query = QueryMessage(query=self._SELECT_COLUMNS + where_clause, consistency_level=cl) + cf_result, col_result = connection.wait_for_responses( + cf_query, col_query) - if ks_query: - ks_result, cf_result, col_result = connection.wait_for_responses( - ks_query, cf_query, col_query, timeout=self._timeout) - ks_result = dict_factory(*ks_result.results) + log.debug("[control connection] Fetched table info for %s.%s, rebuilding metadata", (keyspace, table)) cf_result = dict_factory(*cf_result.results) col_result = dict_factory(*col_result.results) + self._cluster.metadata.table_changed(keyspace, table, cf_result, col_result) + elif usertype: + # user defined types within this keyspace changed + where_clause = " WHERE keyspace_name = '%s' AND type_name = '%s'" % (keyspace, usertype) + types_query = QueryMessage(query=self._SELECT_USERTYPES + where_clause, consistency_level=cl) + types_result = connection.wait_for_response(types_query) + log.debug("[control connection] Fetched user type info for %s.%s, rebuilding metadata", (keyspace, usertype)) + types_result = dict_factory(*types_result) + self._cluster.metadata.usertype_changed(keyspace, usertype, types_result) + elif keyspace: + # only the keyspace itself changed (such as replication settings) + where_clause = " WHERE keyspace_name = '%s'" % (keyspace,) + ks_query = QueryMessage(query=self._SELECT_KEYSPACES + where_clause, consistency_level=cl) + ks_result = connection.wait_for_response(ks_query) + log.debug("[control connection] Fetched keyspace info for %s, rebuilding metadata", (keyspace,)) + ks_result = dict_factory(*types_result) + self._cluster.metadata.keyspace_changed(keyspace, ks_result) else: - ks_result = None - cf_result, col_result = connection.wait_for_responses( - cf_query, col_query, timeout=self._timeout) + # build everything from scratch + queries = [ + QueryMessage(query=self._SELECT_KEYSPACES, consistency_level=cl), + QueryMessage(query=self._SELECT_COLUMN_FAMILIES, consistency_level=cl), + QueryMessage(query=self._SELECT_COLUMNS, consistency_level=cl) + ] + if self._protocol_version >= 3: + queries.append(QueryMessage(query=self._SELECT_USERTYPES, consistency_level=cl)) + ks_result, cf_result, col_result, types_result = connection.wait_for_responses(*queries) + types_result = dict_factory(*types_result) + else: + ks_result, cf_result, col_result = connection.wait_for_responses(*queries) + types_result = {} + + ks_result = dict_factory(*types_result) cf_result = dict_factory(*cf_result.results) col_result = dict_factory(*col_result.results) - log.debug("[control connection] Fetched schema, rebuilding metadata") - if table: - self._cluster.metadata.table_changed(keyspace, table, cf_result, col_result) - elif keyspace: - self._cluster.metadata.keyspace_changed(keyspace, ks_result, cf_result, col_result) - else: - self._cluster.metadata.rebuild_schema(ks_result, cf_result, col_result) + log.debug("[control connection] Fetched schema, rebuilding metadata") + self._cluster.metadata.rebuild_schema(ks_result, types_result, cf_result, col_result) def refresh_node_list_and_token_map(self, force_token_rebuild=False): try: @@ -1837,12 +1858,9 @@ def _handle_status_change(self, event): def _handle_schema_change(self, event): keyspace = event['keyspace'] or None - table = event['table'] or None - if event['change_type'] in ("CREATED", "DROPPED"): - keyspace = keyspace if table else None - self._submit(self.refresh_schema, keyspace) - elif event['change_type'] == "UPDATED": - self._submit(self.refresh_schema, keyspace, table) + table = event.get('table') or None + usertype = event.get('type') + self._submit(self.refresh_schema, keyspace, table, usertype) def wait_for_schema_agreement(self, connection=None, preloaded_results=None): # Each schema change typically generates two schema refreshes, one @@ -1879,7 +1897,7 @@ def wait_for_schema_agreement(self, connection=None, preloaded_results=None): peers_result, local_result = connection.wait_for_responses( peers_query, local_query, timeout=timeout) except OperationTimedOut as timeout: - log.debug("[control connection] Timed out waiting for " \ + log.debug("[control connection] Timed out waiting for " "response during schema agreement check: %s", timeout) elapsed = self._time.time() - start continue @@ -2045,12 +2063,13 @@ def _log_if_failed(self, future): exc_info=exc) -def refresh_schema_and_set_result(keyspace, table, control_conn, response_future): +def refresh_schema_and_set_result(keyspace, table, usertype, control_conn, response_future): try: - control_conn._refresh_schema(response_future._connection, keyspace, table) + control_conn._refresh_schema(response_future._connection, keyspace, table, usertype) except Exception: log.exception("Exception refreshing schema in response to schema change:") - response_future.session.submit(control_conn.refresh_schema, keyspace, table) + response_future.session.submit( + control_conn.refresh_schema, keyspace, table, usertype) finally: response_future._set_final_result(None) @@ -2231,7 +2250,8 @@ def _set_result(self, response): self.session.submit( refresh_schema_and_set_result, response.results['keyspace'], - response.results['table'], + response.results.get('table'), + response.results.get('type'), self.session.cluster.control_connection, self) else: diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 3e3807cead..dceccb9f4f 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -88,7 +88,7 @@ def export_schema_as_string(self): """ return "\n".join(ks.export_as_string() for ks in self.keyspaces.values()) - def rebuild_schema(self, ks_results, cf_results, col_results): + def rebuild_schema(self, ks_results, type_results, cf_results, col_results): """ Rebuild the view of the current schema from a fresh set of rows from the system schema tables. @@ -97,6 +97,7 @@ def rebuild_schema(self, ks_results, cf_results, col_results): """ cf_def_rows = defaultdict(list) col_def_rows = defaultdict(lambda: defaultdict(list)) + usertype_rows = defaultdict(list) for row in cf_results: cf_def_rows[row["keyspace_name"]].append(row) @@ -106,6 +107,9 @@ def rebuild_schema(self, ks_results, cf_results, col_results): cfname = row["columnfamily_name"] col_def_rows[ksname][cfname].append(row) + for row in type_results: + usertype_rows[row["keyspace_name"]].append(row) + current_keyspaces = set() for row in ks_results: keyspace_meta = self._build_keyspace_metadata(row) @@ -114,6 +118,10 @@ def rebuild_schema(self, ks_results, cf_results, col_results): keyspace_meta, table_row, col_def_rows[keyspace_meta.name]) keyspace_meta.tables[table_meta.name] = table_meta + for usertype_row in usertype_rows.get(keyspace_meta.name, []): + usertype = self._build_usertype(usertype_row) + keyspace_meta.user_types[usertype.name] = usertype + current_keyspaces.add(keyspace_meta.name) old_keyspace_meta = self.keyspaces.get(keyspace_meta.name, None) self.keyspaces[keyspace_meta.name] = keyspace_meta @@ -130,29 +138,15 @@ def rebuild_schema(self, ks_results, cf_results, col_results): for ksname in removed_keyspaces: self._keyspace_removed(ksname) - def keyspace_changed(self, keyspace, ks_results, cf_results, col_results): + def keyspace_changed(self, keyspace, ks_results): if not ks_results: if keyspace in self.keyspaces: del self.keyspaces[keyspace] self._keyspace_removed(keyspace) return - col_def_rows = defaultdict(list) - for row in col_results: - cfname = row["columnfamily_name"] - col_def_rows[cfname].append(row) - keyspace_meta = self._build_keyspace_metadata(ks_results[0]) old_keyspace_meta = self.keyspaces.get(keyspace, None) - - new_table_metas = {} - for table_row in cf_results: - table_meta = self._build_table_metadata( - keyspace_meta, table_row, col_def_rows) - new_table_metas[table_meta.name] = table_meta - - keyspace_meta.tables = new_table_metas - self.keyspaces[keyspace] = keyspace_meta if old_keyspace_meta: if (keyspace_meta.replication_strategy != old_keyspace_meta.replication_strategy): @@ -160,6 +154,10 @@ def keyspace_changed(self, keyspace, ks_results, cf_results, col_results): else: self._keyspace_added(keyspace) + def usertype_changed(self, keyspace, name, type_results): + new_usertype = self._build_usertype(keyspace, type_results[0]) + self.user_types[name] = new_usertype + def table_changed(self, keyspace, table, cf_results, col_results): try: keyspace_meta = self.keyspaces[keyspace] @@ -175,7 +173,7 @@ def table_changed(self, keyspace, table, cf_results, col_results): else: assert len(cf_results) == 1 keyspace_meta.tables[table] = self._build_table_metadata( - keyspace_meta, cf_results[0], {table: col_results}) + keyspace_meta, cf_results[0], {table: col_results}) def _keyspace_added(self, ksname): if self.token_map: @@ -196,6 +194,11 @@ def _build_keyspace_metadata(self, row): strategy_options = json.loads(row["strategy_options"]) return KeyspaceMetadata(name, durable_writes, strategy_class, strategy_options) + def _build_usertype(self, keyspace, usertype_row): + type_classes = map(types.lookup_casstype, usertype_row['field_types']) + return UserType(usertype_row['keyspace_name'], usertype_row['type_name'], + usertype_row['field_names'], type_classes) + def _build_table_metadata(self, keyspace_metadata, row, col_rows): cfname = row["columnfamily_name"] @@ -339,7 +342,7 @@ def rebuild_token_map(self, partitioner, token_map): all_tokens = sorted(ring) self.token_map = TokenMap( - token_class, token_to_host_owner, all_tokens, self) + token_class, token_to_host_owner, all_tokens, self) def get_replicas(self, keyspace, key): """ @@ -461,7 +464,7 @@ class NetworkTopologyStrategy(ReplicationStrategy): def __init__(self, dc_replication_factors): self.dc_replication_factors = dict( - (str(k), int(v)) for k, v in dc_replication_factors.items()) + (str(k), int(v)) for k, v in dc_replication_factors.items()) def make_token_replica_map(self, token_to_host_owner, ring): # note: this does not account for hosts having different racks @@ -569,11 +572,14 @@ class KeyspaceMetadata(object): A map from table names to instances of :class:`~.TableMetadata`. """ + user_types = None + def __init__(self, name, durable_writes, strategy_class, strategy_options): self.name = name self.durable_writes = durable_writes self.replication_strategy = ReplicationStrategy.create(strategy_class, strategy_options) self.tables = {} + self.user_types = {} def export_as_string(self): return "\n".join([self.as_cql_query()] + [t.export_as_string() for t in self.tables.values()]) @@ -585,6 +591,20 @@ def as_cql_query(self): return ret + (' AND durable_writes = %s;' % ("true" if self.durable_writes else "false")) +class UserType(object): + + keyspace = None + name = None + field_names = None + field_types = None + + def __init__(self, keyspace, name, field_names, field_types): + self.keyspace = keyspace + self.name = name + self.field_names = field_names + self.field_types = field_types + + class TableMetadata(object): """ A representation of the schema for a single table. From 65dc463d9f8d7db411082b768e1afe9e4dcf3511 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 15:53:36 -0500 Subject: [PATCH 0771/3961] Define serializer for UDTs --- cassandra/cqltypes.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 749c23ffc3..34c5a3b015 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -27,9 +27,11 @@ # for example), these classes would be a good place to tack on # .from_cql_literal() and .as_cql_literal() classmethods (or whatever). +from __future__ import absolute_import # to enable import io from stdlib import calendar from collections import namedtuple from decimal import Decimal +import io import re import socket import time @@ -583,7 +585,8 @@ def serialize(v, protocol_version): if not _have_warned_about_timestamps: _have_warned_about_timestamps = True - warnings.warn("timestamp columns in Cassandra hold a number of " + warnings.warn( + "timestamp columns in Cassandra hold a number of " "milliseconds since the unix epoch. Currently, when executing " "prepared statements, this driver multiplies timestamp " "values by 1000 so that the result of time.time() " @@ -790,6 +793,7 @@ def cql_parameterized_type(cls): @classmethod def deserialize_safe(cls, byts, protocol_version): + proto_version = max(3, protocol_version) p = 0 Result = namedtuple(cls.typename, cls.fieldnames) result = [] @@ -802,7 +806,7 @@ def deserialize_safe(cls, byts, protocol_version): p += itemlen # collections inside UDTs are always encoded with at least the # version 3 format - result.append(col_type.from_binary(item, max(3, protocol_version))) + result.append(col_type.from_binary(item, proto_version)) if len(result) < len(cls.subtypes): nones = [None] * (len(cls.subtypes) - len(result)) @@ -810,6 +814,16 @@ def deserialize_safe(cls, byts, protocol_version): return Result(*result) + @classmethod + def serialize_safe(cls, val, protocol_version): + proto_version = max(3, protocol_version) + buf = io.BytesIO() + for subtype, item in zip(cls.subtypes, val): + packed_item = subtype.to_binary(item, proto_version) + buf.write(int32_pack(len(packed_item))) + buf.write(packed_item) + return buf.getvalue() + class CompositeType(_ParameterizedType): typename = "'org.apache.cassandra.db.marshal.CompositeType'" From e7187abfb4289e3767faeb84a550100fe2f3e74b Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 15:55:00 -0500 Subject: [PATCH 0772/3961] Use io.BytesIO for collection serialization --- cassandra/cqltypes.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index e79b881518..7298287c0e 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -27,8 +27,10 @@ # for example), these classes would be a good place to tack on # .from_cql_literal() and .as_cql_literal() classmethods (or whatever). +from __future__ import absolute_import # to enable import io from stdlib import calendar from decimal import Decimal +import io import re import socket import time @@ -560,7 +562,8 @@ def serialize(v): if not _have_warned_about_timestamps: _have_warned_about_timestamps = True - warnings.warn("timestamp columns in Cassandra hold a number of " + warnings.warn( + "timestamp columns in Cassandra hold a number of " "milliseconds since the unix epoch. Currently, when executing " "prepared statements, this driver multiplies timestamp " "values by 1000 so that the result of time.time() " @@ -666,7 +669,7 @@ def serialize_safe(cls, items): raise TypeError("Received a string for a type that expects a sequence") subtype, = cls.subtypes - buf = six.BytesIO() + buf = io.BytesIO() buf.write(uint16_pack(len(items))) for item in items: itembytes = subtype.to_binary(item) @@ -719,7 +722,7 @@ def deserialize_safe(cls, byts): @classmethod def serialize_safe(cls, themap): subkeytype, subvaltype = cls.subtypes - buf = six.BytesIO() + buf = io.BytesIO() buf.write(uint16_pack(len(themap))) try: items = six.iteritems(themap) From aba577a2f98489a7da2a8c69d52a1e9054fda605 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 17 Jun 2014 14:11:54 -0700 Subject: [PATCH 0773/3961] updating the compaction options codepath to use native metadata rather than query system tables --- cqlengine/management.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 820a70139b..995b27b2c4 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -4,7 +4,7 @@ from cqlengine import ONE from cqlengine.named import NamedTable -from cqlengine.connection import connection_manager, execute, execute_native, get_session, get_cluster +from cqlengine.connection import execute_native, get_cluster from cqlengine.exceptions import CQLEngineException import logging @@ -240,10 +240,13 @@ def get_fields(model): def get_table_settings(model): - return schema_columnfamilies.objects.consistency(ONE).get( - keyspace_name=model._get_keyspace(), - columnfamily_name=model.column_family_name(include_keyspace=False)) + # returns the table as provided by the native driver for a given model + cluster = get_cluster() + ks = model._get_keyspace() + table = model.column_family_name(include_keyspace=False) + table = cluster.metadata.keyspaces[ks].tables[table] + return table def update_compaction(model): """Updates the compaction options for the given model if necessary. @@ -255,21 +258,11 @@ def update_compaction(model): :rtype: bool """ logger.debug("Checking %s for compaction differences", model) - row = get_table_settings(model) - # check compaction_strategy_class - if not model.__compaction__: - return - - do_update = not row['compaction_strategy_class'].endswith(model.__compaction__) - - existing_options = json.loads(row['compaction_strategy_options']) - # The min/max thresholds are stored differently in the system data dictionary - existing_options.update({ - 'min_threshold': str(row['min_compaction_threshold']), - 'max_threshold': str(row['max_compaction_threshold']), - }) + table = get_table_settings(model) + existing_options = table.compaction_options.copy() desired_options = get_compaction_options(model) + desired_options.pop('class', None) for k, v in desired_options.items(): From 5c32d6c6aa870031a61fef2a6f16bbf82da2e84e Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 17 Jun 2014 14:15:13 -0700 Subject: [PATCH 0774/3961] removed code for testing connection pool, which is being removed --- cqlengine/tests/management/test_management.py | 71 ++++--------------- 1 file changed, 14 insertions(+), 57 deletions(-) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 0ba27cd5a1..b578b15897 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,54 +1,11 @@ -from mock import MagicMock, patch -from cqlengine import ONE -from cqlengine.exceptions import CQLEngineException -from cqlengine.management import create_table, delete_table, get_fields, sync_table +from cqlengine.management import get_fields, sync_table, drop_table from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.connection import ConnectionPool, Host from cqlengine import management from cqlengine.tests.query.test_queryset import TestModel from cqlengine.models import Model from cqlengine import columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy -class ConnectionPoolFailoverTestCase(BaseCassEngTestCase): - """Test cassandra connection pooling.""" - - def setUp(self): - self.host = Host('127.0.0.1', '9160') - self.pool = ConnectionPool([self.host]) - - def test_totally_dead_pool(self): - # kill the con - with patch('cqlengine.connection.cql.connect') as mock: - mock.side_effect=CQLEngineException - with self.assertRaises(CQLEngineException): - self.pool.execute("select * from system.peers", {}, ONE) - - def test_dead_node(self): - """ - tests that a single dead node doesn't mess up the pool - """ - self.pool._hosts.append(self.host) - - # cursor mock needed so set_cql_version doesn't crap out - ok_cur = MagicMock() - - ok_conn = MagicMock() - ok_conn.return_value = ok_cur - - - returns = [CQLEngineException(), ok_conn] - - def side_effect(*args, **kwargs): - result = returns.pop(0) - if isinstance(result, Exception): - raise result - return result - - with patch('cqlengine.connection.cql.connect') as mock: - mock.side_effect = side_effect - conn = self.pool._create_connection() - class CreateKeyspaceTest(BaseCassEngTestCase): def test_create_succeeeds(self): @@ -61,10 +18,10 @@ def test_multiple_deletes_dont_fail(self): """ """ - create_table(TestModel) + sync_table(TestModel) - delete_table(TestModel) - delete_table(TestModel) + drop_table(TestModel) + drop_table(TestModel) class LowercaseKeyModel(Model): first_key = columns.Integer(primary_key=True) @@ -87,11 +44,11 @@ class CapitalizedKeyTest(BaseCassEngTestCase): def test_table_definition(self): """ Tests that creating a table with capitalized column names succeedso """ - create_table(LowercaseKeyModel) - create_table(CapitalizedKeyModel) + sync_table(LowercaseKeyModel) + sync_table(CapitalizedKeyModel) - delete_table(LowercaseKeyModel) - delete_table(CapitalizedKeyModel) + drop_table(LowercaseKeyModel) + drop_table(CapitalizedKeyModel) class FirstModel(Model): @@ -125,25 +82,25 @@ class FourthModel(Model): class AddColumnTest(BaseCassEngTestCase): def setUp(self): - delete_table(FirstModel) + drop_table(FirstModel) def test_add_column(self): - create_table(FirstModel) + sync_table(FirstModel) fields = get_fields(FirstModel) # this should contain the second key self.assertEqual(len(fields), 2) # get schema - create_table(SecondModel) + sync_table(SecondModel) fields = get_fields(FirstModel) self.assertEqual(len(fields), 3) - create_table(ThirdModel) + sync_table(ThirdModel) fields = get_fields(FirstModel) self.assertEqual(len(fields), 4) - create_table(FourthModel) + sync_table(FourthModel) fields = get_fields(FirstModel) self.assertEqual(len(fields), 4) @@ -151,7 +108,7 @@ def test_add_column(self): class SyncTableTests(BaseCassEngTestCase): def setUp(self): - delete_table(PrimaryKeysOnlyModel) + drop_table(PrimaryKeysOnlyModel) def test_sync_table_works_with_primary_keys_only_tables(self): From 17640917036954fcac017b4c2f0353afaaff31db Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 16:38:30 -0500 Subject: [PATCH 0775/3961] Don't share prepared stmt lock across Cluster instances --- cassandra/cluster.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index ff7936ea3e..7b18998912 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -332,7 +332,7 @@ def auth_provider(self, value): is_shutdown = False _is_setup = False _prepared_statements = None - _prepared_statement_lock = Lock() + _prepared_statement_lock = None _listeners = None _listener_lock = None @@ -410,6 +410,7 @@ def __init__(self, self.metadata = Metadata(self) self.control_connection = None self._prepared_statements = WeakValueDictionary() + self._prepared_statement_lock = Lock() self._min_requests_per_connection = { HostDistance.LOCAL: DEFAULT_MIN_REQUESTS, @@ -1308,7 +1309,7 @@ def run_add_or_renew_pool(): # the host itself will still be marked down, so we need to pass # a special flag to make sure the reconnector is created self.cluster.signal_connection_failure( - host, conn_exc, is_host_addition, expect_host_to_be_down=True) + host, conn_exc, is_host_addition, expect_host_to_be_down=True) return False previous = self._pools.get(host) @@ -1861,7 +1862,7 @@ def wait_for_schema_agreement(self, connection=None, preloaded_results=None): peers_result, local_result = connection.wait_for_responses( peers_query, local_query, timeout=timeout) except OperationTimedOut as timeout: - log.debug("[control connection] Timed out waiting for " \ + log.debug("[control connection] Timed out waiting for " "response during schema agreement check: %s", timeout) elapsed = self._time.time() - start continue From 499359afa20bf15bf9fd272cbe0a606d553a3559 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 16:39:10 -0500 Subject: [PATCH 0776/3961] Update changelog --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b4854f6828..d6ab3dc614 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,8 @@ Bug Fixes * Clear reactor state in child process after forking to prevent errors with multiprocessing when the parent process has connected a Cluster before forking (github #141) +* Don't share prepared statement lock across Cluster + instances 2.0.2 ===== From c4ef43c26c5b41f5d546a939967e353d9b3c1131 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 17:50:24 -0500 Subject: [PATCH 0777/3961] Register classes for UDT deserialization --- cassandra/cluster.py | 11 +++++++ cassandra/connection.py | 14 +++++--- cassandra/cqltypes.py | 38 ++++++++++++++-------- cassandra/protocol.py | 47 +++++++++++++++++---------- tests/unit/test_control_connection.py | 18 ++-------- tests/unit/test_response_future.py | 2 +- 6 files changed, 77 insertions(+), 53 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 1015f637b3..7e9ba2810b 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -334,6 +334,11 @@ def auth_provider(self, value): _prepared_statements = None _prepared_statement_lock = None + _user_types = None + """ + A map of {keyspace: {type_name: UserType}} + """ + _listeners = None _listener_lock = None @@ -412,6 +417,8 @@ def __init__(self, self._prepared_statements = WeakValueDictionary() self._prepared_statement_lock = Lock() + self._user_types = defaultdict(dict) + self._min_requests_per_connection = { HostDistance.LOCAL: DEFAULT_MIN_REQUESTS, HostDistance.REMOTE: DEFAULT_MIN_REQUESTS @@ -444,6 +451,9 @@ def __init__(self, self.control_connection = ControlConnection( self, self.control_connection_timeout) + def register_type_class(self, keyspace, user_type, klass): + self._user_types[keyspace][user_type] = klass + def get_min_requests_per_connection(self, host_distance): return self._min_requests_per_connection[host_distance] @@ -517,6 +527,7 @@ def _make_connection_kwargs(self, address, kwargs_dict): kwargs_dict['ssl_options'] = self.ssl_options kwargs_dict['cql_version'] = self.cql_version kwargs_dict['protocol_version'] = self.protocol_version + kwargs_dict['user_type_map'] = self._user_types return kwargs_dict diff --git a/cassandra/connection.py b/cassandra/connection.py index d2bfdfb428..6f90da26c1 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -155,12 +155,14 @@ class Connection(object): is_defunct = False is_closed = False lock = None + user_type_map = None is_control_connection = False def __init__(self, host='127.0.0.1', port=9042, authenticator=None, ssl_options=None, sockopts=None, compression=True, - cql_version=None, protocol_version=2, is_control_connection=False): + cql_version=None, protocol_version=2, is_control_connection=False, + user_type_map=None): self.host = host self.port = port self.authenticator = authenticator @@ -170,6 +172,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None, self.cql_version = cql_version self.protocol_version = protocol_version self.is_control_connection = is_control_connection + self.user_type_map = user_type_map self._push_watchers = defaultdict(set) if protocol_version >= 3: self._header_unpack = v3_header_unpack @@ -347,7 +350,8 @@ def process_msg(self, msg, body_len): else: raise ProtocolError("Got negative body length: %r" % body_len) - response = decode_response(given_version, stream_id, flags, opcode, body, self.decompressor) + response = decode_response(given_version, self.user_type_map, stream_id, + flags, opcode, body, self.decompressor) except Exception as exc: log.exception("Error decoding response from Cassandra. " "opcode: %04x; message contents: %r", opcode, msg) @@ -386,10 +390,10 @@ def _handle_options_response(self, options_response): if isinstance(options_response, ConnectionException): raise options_response else: - log.error("Did not get expected SupportedMessage response; " \ + log.error("Did not get expected SupportedMessage response; " "instead, got: %s", options_response) - raise ConnectionException("Did not get expected SupportedMessage " \ - "response; instead, got: %s" \ + raise ConnectionException("Did not get expected SupportedMessage " + "response; instead, got: %s" % (options_response,)) log.debug("Received options response on new connection (%s) from %s", diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 29ae451ce6..e399381602 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -778,14 +778,21 @@ class UserDefinedType(_ParameterizedType): FIELD_LENGTH = 4 + _cache = {} + @classmethod - def apply_parameters(cls, subtypes, names): - newname = subtypes[1].cassname.decode("hex") - field_names = [encoded_name.decode("hex") for encoded_name in names[2:]] - return type(newname, (cls,), {'subtypes': subtypes[2:], - 'cassname': cls.cassname, - 'typename': newname, - 'fieldnames': field_names}) + def apply_parameters(cls, udt_name, names_and_types, mapped_class): + try: + return cls._cache[udt_name] + except KeyError: + fieldnames, types = zip(*names_and_types) + instance = type(udt_name, (cls,), {'subtypes': types, + 'cassname': cls.cassname, + 'typename': udt_name, + 'fieldnames': fieldnames, + 'mapped_class': mapped_class}) + cls._cache[udt_name] = instance + return instance @classmethod def cql_parameterized_type(cls): @@ -795,8 +802,7 @@ def cql_parameterized_type(cls): def deserialize_safe(cls, byts, protocol_version): proto_version = max(3, protocol_version) p = 0 - Result = namedtuple(cls.typename, cls.fieldnames) - result = [] + values = [] for col_type in cls.subtypes: if p == len(byts): break @@ -806,13 +812,17 @@ def deserialize_safe(cls, byts, protocol_version): p += itemlen # collections inside UDTs are always encoded with at least the # version 3 format - result.append(col_type.from_binary(item, proto_version)) + values.append(col_type.from_binary(item, proto_version)) - if len(result) < len(cls.subtypes): - nones = [None] * (len(cls.subtypes) - len(result)) - result = result + nones + if len(values) < len(cls.subtypes): + nones = [None] * (len(cls.subtypes) - len(values)) + values = values + nones - return Result(*result) + if cls.mapped_class: + return cls.mapped_class(**dict(zip(cls.fieldnames, values))) + else: + Result = namedtuple(cls.typename, cls.fieldnames) + return Result(*values) @classmethod def serialize_safe(cls, val, protocol_version): diff --git a/cassandra/protocol.py b/cassandra/protocol.py index eb37d765da..0ca4077173 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -100,7 +100,8 @@ def _get_params(message_obj): ) -def decode_response(protocol_version, stream_id, flags, opcode, body, decompressor=None): +def decode_response(protocol_version, user_type_map, stream_id, flags, opcode, body, + decompressor=None): if flags & COMPRESSED_FLAG: if decompressor is None: raise Exception("No de-compressor available for compressed frame!") @@ -118,7 +119,7 @@ def decode_response(protocol_version, stream_id, flags, opcode, body, decompress log.warning("Unknown protocol flags set: %02x. May cause problems.", flags) msg_class = _message_types_by_opcode[opcode] - msg = msg_class.recv_body(body, protocol_version) + msg = msg_class.recv_body(body, protocol_version, user_type_map) msg.stream_id = stream_id msg.trace_id = trace_id return msg @@ -138,7 +139,7 @@ def __init__(self, code, message, info): self.info = info @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): code = read_int(f) msg = read_string(f) subcls = error_classes.get(code, cls) @@ -338,7 +339,7 @@ class ReadyMessage(_MessageType): name = 'READY' @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): return cls() @@ -350,7 +351,7 @@ def __init__(self, authenticator): self.authenticator = authenticator @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): authname = read_string(f) return cls(authenticator=authname) @@ -382,7 +383,7 @@ def __init__(self, challenge): self.challenge = challenge @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): return cls(read_longstring(f)) @@ -405,7 +406,7 @@ def __init__(self, token): self.token = token @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): return cls(read_longstring(f)) @@ -426,7 +427,7 @@ def __init__(self, cql_versions, options): self.options = options @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): options = read_stringmultimap(f) cql_versions = options.pop('CQL_VERSION') return cls(cql_versions=cql_versions, options=options) @@ -547,13 +548,14 @@ def __init__(self, kind, results, paging_state=None): self.paging_state = paging_state @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): kind = read_int(f) paging_state = None if kind == RESULT_KIND_VOID: results = None elif kind == RESULT_KIND_ROWS: - paging_state, results = cls.recv_results_rows(f, protocol_version) + paging_state, results = cls.recv_results_rows( + f, protocol_version, user_type_map) elif kind == RESULT_KIND_SET_KEYSPACE: ksname = read_string(f) results = ksname @@ -564,16 +566,17 @@ def recv_body(cls, f, protocol_version): return cls(kind, results, paging_state) @classmethod - def recv_results_rows(cls, f, protocol_version): + def recv_results_rows(cls, f, protocol_version, user_type_map): paging_state, column_metadata = cls.recv_results_metadata(f) rowcount = read_int(f) rows = [cls.recv_row(f, len(column_metadata)) for _ in range(rowcount)] colnames = [c[2] for c in column_metadata] coltypes = [c[3] for c in column_metadata] - return ( - paging_state, - (colnames, [tuple(ctype.from_binary(val, protocol_version) for ctype, val in zip(coltypes, row)) - for row in rows])) + parsed_rows = [ + tuple(ctype.from_binary(val, protocol_version) + for ctype, val in zip(coltypes, row)) + for row in rows] + return (paging_state, (colnames, parsed_rows)) @classmethod def recv_results_prepared(cls, f): @@ -582,7 +585,7 @@ def recv_results_prepared(cls, f): return (query_id, column_metadata) @classmethod - def recv_results_metadata(cls, f): + def recv_results_metadata(cls, f, user_type_map): flags = read_int(f) glob_tblspec = bool(flags & cls._FLAGS_GLOBAL_TABLES_SPEC) colcount = read_int(f) @@ -624,7 +627,7 @@ def recv_results_schema_change(cls, f, protocol_version): return {'change_type': change_type, 'keyspace': keyspace, 'table': table} @classmethod - def read_type(cls, f): + def read_type(cls, f, user_type_map): optid = read_short(f) try: typeclass = cls._type_codes[optid] @@ -638,6 +641,14 @@ def read_type(cls, f): keysubtype = cls.read_type(f) valsubtype = cls.read_type(f) typeclass = typeclass.apply_parameters((keysubtype, valsubtype)) + elif typeclass == UserDefinedType: + ks = cls.read_string(f) + udt_name = cls.read_string(f) + num_fields = cls.read_short(f) + names_and_types = ((cls.read_string(f), cls.read_type(f)) + for _ in xrange(num_fields)) + mapped_class = user_type_map.get(ks, {}).get(udt_name) + typeclass = typeclass.apply_parameters(udt_name, names_and_types, mapped_class) elif typeclass == CUSTOM_TYPE: classname = read_string(f) typeclass = lookup_casstype(classname) @@ -789,7 +800,7 @@ def __init__(self, event_type, event_args): self.event_args = event_args @classmethod - def recv_body(cls, f, protocol_version): + def recv_body(cls, f, protocol_version, user_type_map): event_type = read_string(f).upper() if event_type in known_event_types: read_method = getattr(cls, 'recv_' + event_type.lower()) diff --git a/tests/unit/test_control_connection.py b/tests/unit/test_control_connection.py index e552c8e363..3ca2e8160d 100644 --- a/tests/unit/test_control_connection.py +++ b/tests/unit/test_control_connection.py @@ -397,27 +397,15 @@ def test_handle_status_change(self): def test_handle_schema_change(self): - for change_type in ('CREATED', 'DROPPED'): + for change_type in ('CREATED', 'DROPPED', 'UPDATED'): event = { 'change_type': change_type, 'keyspace': 'ks1', 'table': 'table1' } self.control_connection._handle_schema_change(event) - self.cluster.executor.submit.assert_called_with(self.control_connection.refresh_schema, 'ks1') + self.cluster.executor.submit.assert_called_with(self.control_connection.refresh_schema, 'ks1', 'table1', None) event['table'] = None self.control_connection._handle_schema_change(event) - self.cluster.executor.submit.assert_called_with(self.control_connection.refresh_schema, None) - - event = { - 'change_type': 'UPDATED', - 'keyspace': 'ks1', - 'table': 'table1' - } - self.control_connection._handle_schema_change(event) - self.cluster.executor.submit.assert_called_with(self.control_connection.refresh_schema, 'ks1', 'table1') - - event['table'] = None - self.control_connection._handle_schema_change(event) - self.cluster.executor.submit.assert_called_with(self.control_connection.refresh_schema, 'ks1', None) + self.cluster.executor.submit.assert_called_with(self.control_connection.refresh_schema, 'ks1', None, None) diff --git a/tests/unit/test_response_future.py b/tests/unit/test_response_future.py index ca97a729f1..32c635bfb1 100644 --- a/tests/unit/test_response_future.py +++ b/tests/unit/test_response_future.py @@ -105,7 +105,7 @@ def test_schema_change_result(self): kind=RESULT_KIND_SCHEMA_CHANGE, results={'keyspace': "keyspace1", "table": "table1"}) rf._set_result(result) - session.submit.assert_called_once_with(ANY, 'keyspace1', 'table1', ANY, rf) + session.submit.assert_called_once_with(ANY, 'keyspace1', 'table1', None, ANY, rf) def test_other_result_message_kind(self): session = self.make_session() From 42b6c8e5a8a8189d7988b44872cd88c36b4660bf Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 18:10:59 -0500 Subject: [PATCH 0778/3961] Include keyspace in UDT class cache --- cassandra/cqltypes.py | 6 +++--- cassandra/protocol.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index e399381602..b9d369b94d 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -781,9 +781,9 @@ class UserDefinedType(_ParameterizedType): _cache = {} @classmethod - def apply_parameters(cls, udt_name, names_and_types, mapped_class): + def apply_parameters(cls, keyspace, udt_name, names_and_types, mapped_class): try: - return cls._cache[udt_name] + return cls._cache[(keyspace, udt_name)] except KeyError: fieldnames, types = zip(*names_and_types) instance = type(udt_name, (cls,), {'subtypes': types, @@ -791,7 +791,7 @@ def apply_parameters(cls, udt_name, names_and_types, mapped_class): 'typename': udt_name, 'fieldnames': fieldnames, 'mapped_class': mapped_class}) - cls._cache[udt_name] = instance + cls._cache[(keyspace, udt_name)] = instance return instance @classmethod diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 0ca4077173..69bc68f1c4 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -648,7 +648,8 @@ def read_type(cls, f, user_type_map): names_and_types = ((cls.read_string(f), cls.read_type(f)) for _ in xrange(num_fields)) mapped_class = user_type_map.get(ks, {}).get(udt_name) - typeclass = typeclass.apply_parameters(udt_name, names_and_types, mapped_class) + typeclass = typeclass.apply_parameters( + ks, udt_name, names_and_types, mapped_class) elif typeclass == CUSTOM_TYPE: classname = read_string(f) typeclass = lookup_casstype(classname) From 4875e0a26c9a2a2d3f839b6e59d4d200c4c24674 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 17 Jun 2014 18:18:07 -0500 Subject: [PATCH 0779/3961] Use fieldname instead of assuming tuples --- cassandra/cqltypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index b9d369b94d..c42ad33fb6 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -828,8 +828,8 @@ def deserialize_safe(cls, byts, protocol_version): def serialize_safe(cls, val, protocol_version): proto_version = max(3, protocol_version) buf = io.BytesIO() - for subtype, item in zip(cls.subtypes, val): - packed_item = subtype.to_binary(item, proto_version) + for fieldname, subtype in zip(cls.fieldnames, cls.subtypes): + packed_item = subtype.to_binary(getattr(val, fieldname), proto_version) buf.write(int32_pack(len(packed_item))) buf.write(packed_item) return buf.getvalue() From 9a799c76d9eb2cb03261441623b9902be520cd10 Mon Sep 17 00:00:00 2001 From: Colin Stolley Date: Wed, 18 Jun 2014 13:16:20 -0500 Subject: [PATCH 0780/3961] Use io.BytesIO instead of six.BytesIO --- cassandra/io/twistedreactor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py index 442200c911..6b33f46121 100644 --- a/cassandra/io/twistedreactor.py +++ b/cassandra/io/twistedreactor.py @@ -23,7 +23,7 @@ import atexit import os -from six import BytesIO +from io import BytesIO from cassandra import OperationTimedOut from cassandra.connection import Connection, ConnectionShutdown From 6bc5f944433e89769e8e4f4bf9afdfbde473b680 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:03:05 -0500 Subject: [PATCH 0781/3961] Remove unused methods --- cassandra/io/twistedreactor.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py index 6b33f46121..8821b00346 100644 --- a/cassandra/io/twistedreactor.py +++ b/cassandra/io/twistedreactor.py @@ -188,19 +188,6 @@ def close(self): # don't leave in-progress operations hanging self.connected_event.set() - def handle_close(self): - """ - Not used, but kept for consistency with other reactors. - """ - self.close() - - def handle_write(self): - """ - This function is not needed, so if it is called, blow up. - """ - raise RuntimeError("handle_write() should not be called" - "in TwistedConnection") - def handle_read(self): """ Process the incoming data buffer. From ac37c33909e2940059f9397e5c013025c5c24cc0 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 12:03:28 -0700 Subject: [PATCH 0782/3961] fixing broken tests --- cqlengine/management.py | 8 +++++--- cqlengine/tests/management/test_compaction_settings.py | 2 +- cqlengine/tests/management/test_management.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 995b27b2c4..5810c12f12 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -244,7 +244,6 @@ def get_table_settings(model): cluster = get_cluster() ks = model._get_keyspace() table = model.column_family_name(include_keyspace=False) - table = cluster.metadata.keyspaces[ks].tables[table] return table @@ -259,12 +258,15 @@ def update_compaction(model): """ logger.debug("Checking %s for compaction differences", model) table = get_table_settings(model) - existing_options = table.compaction_options.copy() - desired_options = get_compaction_options(model) + existing_options = table.options.copy() + + desired_options = get_compaction_options(model).get("compaction_strategy_options", {}) desired_options.pop('class', None) + do_update = False + for k, v in desired_options.items(): val = existing_options.pop(k, None) if val != v: diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index e2147e6ccd..aef0d5facf 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -136,7 +136,7 @@ def test_alter_actually_alters(self): table_settings = get_table_settings(tmp) - self.assertRegexpMatches(table_settings['compaction_strategy_class'], '.*SizeTieredCompactionStrategy$') + self.assertRegexpMatches(table_settings.options['compaction_strategy_class'], '.*SizeTieredCompactionStrategy$') def test_alter_options(self): diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index b578b15897..67e77a356f 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -122,7 +122,8 @@ def test_sync_table_works_with_primary_keys_only_tables(self): # blows up with DoesNotExist if table does not exist table_settings = management.get_table_settings(PrimaryKeysOnlyModel) # let make sure the flag we care about - assert LeveledCompactionStrategy in table_settings['compaction_strategy_class'] + + assert LeveledCompactionStrategy in table_settings.options['compaction_strategy_class'] # Now we are "updating" the table: @@ -138,4 +139,4 @@ def test_sync_table_works_with_primary_keys_only_tables(self): sync_table(PrimaryKeysOnlyModel) table_settings = management.get_table_settings(PrimaryKeysOnlyModel) - assert SizeTieredCompactionStrategy in table_settings['compaction_strategy_class'] + assert SizeTieredCompactionStrategy in table_settings.options['compaction_strategy_class'] From e989bcf42fe161427d6c224b880f86effbd78c33 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 12:15:11 -0700 Subject: [PATCH 0783/3961] fixing broken tests --- cqlengine/tests/management/test_compaction_settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index aef0d5facf..3da83da7b7 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -196,13 +196,15 @@ class AllSizeTieredOptionsModel(Model): drop_table(AllSizeTieredOptionsModel) sync_table(AllSizeTieredOptionsModel) - settings = get_table_settings(AllSizeTieredOptionsModel) - options = json.loads(settings['compaction_strategy_options']) + options = get_table_settings(AllSizeTieredOptionsModel).options['compaction_strategy_options'] + options = json.loads(options) + expected = {u'min_threshold': u'2', u'bucket_low': u'0.3', u'tombstone_compaction_interval': u'86400', u'bucket_high': u'2', u'max_threshold': u'64'} + self.assertDictEqual(options, expected) From 7e4f90c90830dca9f96d0d99750a5a5a3f6df300 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:16:36 -0500 Subject: [PATCH 0784/3961] Minor refactor of loop state handling --- cassandra/io/twistedreactor.py | 61 ++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py index 8821b00346..9ab5765d19 100644 --- a/cassandra/io/twistedreactor.py +++ b/cassandra/io/twistedreactor.py @@ -16,7 +16,7 @@ ( https://twistedmatrix.com ). """ from twisted.internet import reactor, protocol -from threading import Event, Thread +from threading import Event, Thread, Lock from functools import partial import logging import weakref @@ -36,7 +36,7 @@ def _cleanup(cleanup_weakref): try: - cleanup_weakref() + cleanup_weakref()._cleanup() except ReferenceError: return @@ -46,6 +46,7 @@ class TwistedConnectionProtocol(protocol.Protocol): Twisted Protocol class for handling data received and connection made events. """ + def dataReceived(self, data): """ Callback function that is called when data has been received @@ -68,6 +69,7 @@ def connectionMade(self): class TwistedConnectionClientFactory(protocol.ClientFactory): + def __init__(self, connection): # ClientFactory does not define __init__() in parent classes # and does not inherit from object. @@ -97,14 +99,49 @@ def clientConnectionLost(self, connector, reason): self.conn.close() +class TwistedLoop(object): + + _lock = None + _thread = None + + def __init__(self): + self._lock = Lock() + + def maybe_start(self): + with self._lock: + if not reactor.running: + self._thread = Thread(target=reactor.run, + name="cassandra_driver_event_loop", + kwargs={'installSignalHandlers': False}) + self._thread.daemon = True + self._thread.start() + atexit.register(partial(_cleanup, weakref.ref(self))) + + def _cleanup(self): + if self._thread: + reactor.callFromThread(reactor.stop) + self._thread.join(timeout=1.0) + if self._thread.is_alive(): + log.warning("Event loop thread could not be joined, so " + "shutdown may not be clean. Please call " + "Cluster.shutdown() to avoid this.") + log.debug("Event loop thread was joined") + + class TwistedConnection(Connection): """ An implementation of :class:`.Connection` that utilizes the Twisted event loop. """ + _loop = None _total_reqd_bytes = 0 + @classmethod + def initialize_reactor(cls): + if not cls._loop: + cls._loop = TwistedLoop() + @classmethod def factory(cls, *args, **kwargs): """ @@ -136,20 +173,12 @@ def __init__(self, *args, **kwargs): self.connected_event = Event() self._iobuf = BytesIO() - self._thread = None self.is_closed = True self.connector = None self._callbacks = {} reactor.callFromThread(self.add_connection) - - if not reactor.running: # XXX: might want a lock here? - self._thread = Thread(target=reactor.run, - name="cassandra_driver_event_loop", - kwargs={'installSignalHandlers': False}) - self._thread.daemon = True - self._thread.start() - atexit.register(partial(_cleanup, weakref.ref(self))) + self._loop.maybe_start() def add_connection(self): """ @@ -236,16 +265,6 @@ def push(self, data): """ reactor.callFromThread(self.connector.transport.write, data) - def _cleanup(self): - if self._thread: - reactor.callFromThread(reactor.stop) - self._thread.join(timeout=1.0) - if self._thread.is_alive(): - log.warning("Event loop thread could not be joined, so " - "shutdown may not be clean. Please call " - "Cluster.shutdown() to avoid this.") - log.debug("Event loop thread was joined") - def register_watcher(self, event_type, callback, register_timeout=None): """ Register a callback for a given event type. From 57a47995f5aca652ffe523f39489fe9058245d13 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:44:40 -0500 Subject: [PATCH 0785/3961] Defunct where appropriate --- cassandra/io/twistedreactor.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py index 9ab5765d19..9ca401dd6d 100644 --- a/cassandra/io/twistedreactor.py +++ b/cassandra/io/twistedreactor.py @@ -67,6 +67,10 @@ def connectionMade(self): """ self.transport.connector.factory.conn.client_connection_made() + def connectionLost(self, reason): + # reason is a Failure instance + self.transport.connector.factory.conn.defunct(reason.value) + class TwistedConnectionClientFactory(protocol.ClientFactory): @@ -88,15 +92,21 @@ def clientConnectionFailed(self, connector, reason): connection attempt fails. """ log.debug("Connect failed: %s", reason) - self.conn.close() + self.conn.defunct(reason.value) def clientConnectionLost(self, connector, reason): """ Overridden twisted callback which is called when the connection goes away (cleanly or otherwise). + + It should be safe to call defunct() here instead of just close, because + we can assume that if the connection was closed cleanly, there are no + callbacks to error out. If this assumption turns out to be false, we + can call close() instead of defunct() when "reason" is an appropriate + type. """ log.debug("Connect lost: %s", reason) - self.conn.close() + self.conn.defunct(reason.value) class TwistedLoop(object): From 83c019ea4b7ef5d4d0886a6efc0560f17456ac54 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:44:55 -0500 Subject: [PATCH 0786/3961] Add twisted reactor to benchmarking tools --- benchmarks/base.py | 70 +++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/benchmarks/base.py b/benchmarks/base.py index 6b719ef846..8355fcf231 100644 --- a/benchmarks/base.py +++ b/benchmarks/base.py @@ -26,6 +26,7 @@ sys.path.append(dirname) sys.path.append(os.path.join(dirname, '..')) +import cassandra from cassandra.cluster import Cluster from cassandra.io.asyncorereactor import AsyncoreConnection from cassandra.policies import HostDistance @@ -44,39 +45,47 @@ except ImportError as exc: pass -KEYSPACE = "testkeyspace" +have_twisted = False +try: + from cassandra.io.twistedreactor import TwistedConnection + have_twisted = True + supported_reactors.append(TwistedConnection) +except ImportError as exc: + log.exception("Error importing twisted") + pass + +KEYSPACE = "testkeyspace" + str(int(time.time())) TABLE = "testtable" def setup(hosts): + log.info("Using 'cassandra' package from %s", cassandra.__path__) cluster = Cluster(hosts) cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) - session = cluster.connect() - - rows = session.execute("SELECT keyspace_name FROM system.schema_keyspaces") - if KEYSPACE in [row[0] for row in rows]: - log.debug("dropping existing keyspace...") - session.execute("DROP KEYSPACE " + KEYSPACE) - - log.debug("Creating keyspace...") - session.execute(""" - CREATE KEYSPACE %s - WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': '2' } - """ % KEYSPACE) - - log.debug("Setting keyspace...") - session.set_keyspace(KEYSPACE) - - log.debug("Creating table...") - session.execute(""" - CREATE TABLE %s ( - thekey text, - col1 text, - col2 text, - PRIMARY KEY (thekey, col1) - ) - """ % TABLE) + try: + session = cluster.connect() + + log.debug("Creating keyspace...") + session.execute(""" + CREATE KEYSPACE %s + WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': '2' } + """ % KEYSPACE) + + log.debug("Setting keyspace...") + session.set_keyspace(KEYSPACE) + + log.debug("Creating table...") + session.execute(""" + CREATE TABLE %s ( + thekey text, + col1 text, + col2 text, + PRIMARY KEY (thekey, col1) + ) + """ % TABLE) + finally: + cluster.shutdown() def teardown(hosts): @@ -84,6 +93,7 @@ def teardown(hosts): cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) session = cluster.connect() session.execute("DROP KEYSPACE " + KEYSPACE) + cluster.shutdown() def benchmark(thread_class): @@ -124,6 +134,7 @@ def benchmark(thread_class): end = time.time() finally: + cluster.shutdown() teardown(options.hosts) total = end - start @@ -164,6 +175,8 @@ def parse_options(): help='only benchmark with asyncore connections') parser.add_option('--libev-only', action='store_true', dest='libev_only', help='only benchmark with libev connections') + parser.add_option('--twisted-only', action='store_true', dest='twisted_only', + help='only benchmark with Twisted connections') parser.add_option('-m', '--metrics', action='store_true', dest='enable_metrics', help='enable and print metrics for operations') parser.add_option('-l', '--log-level', default='info', @@ -184,6 +197,11 @@ def parse_options(): log.error("libev is not available") sys.exit(1) options.supported_reactors = [LibevConnection] + elif options.twisted_only: + if not have_twisted: + log.error("Twisted is not available") + sys.exit(1) + options.supported_reactors = [TwistedConnection] else: options.supported_reactors = supported_reactors if not have_libev: From f58c315fcd9306751ca03025795ba594483afdd2 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:45:32 -0500 Subject: [PATCH 0787/3961] Prefer asyncore instead of Twisted --- cassandra/cluster.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index c4c32e73c3..f9d04ad256 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -66,17 +66,14 @@ named_tuple_factory, dict_factory) # default to gevent when we are monkey patched, otherwise if libev is available, use that as the -# default because it's fastest. Try twisted otherwise, then fallback to asyncore. +# default because it's fastest. Otherwise, use asyncore. if 'gevent.monkey' in sys.modules: from cassandra.io.geventreactor import GeventConnection as DefaultConnection else: try: from cassandra.io.libevreactor import LibevConnection as DefaultConnection # NOQA except ImportError: - try: - from cassandra.io.twistedreactor import TwistedConnection as DefaultConnection # NOQA - except ImportError: - from cassandra.io.asyncorereactor import AsyncoreConnection as DefaultConnection # NOQA + from cassandra.io.asyncorereactor import AsyncoreConnection as DefaultConnection # NOQA # Forces load of utf8 encoding module to avoid deadlock that occurs # if code that is being imported tries to import the module in a seperate From 0e620db28a61bf8413b89733e10ffb36e5d78fce Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:47:01 -0500 Subject: [PATCH 0788/3961] Update docs about available reactors --- cassandra/cluster.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index f9d04ad256..1456ad8840 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -309,6 +309,8 @@ def auth_provider(self, value): * :class:`cassandra.io.asyncorereactor.AsyncoreConnection` * :class:`cassandra.io.libevreactor.LibevConnection` + * :class:`cassandra.io.libevreactor.GeventConnection` (requires monkey-patching) + * :class:`cassandra.io.libevreactor.TwistedConnection` By default, ``AsyncoreConnection`` will be used, which uses the ``asyncore`` module in the Python standard library. The @@ -316,6 +318,9 @@ def auth_provider(self, value): supported on a wider range of systems. If ``libev`` is installed, ``LibevConnection`` will be used instead. + + If gevent monkey-patching of the standard library is detected, + GeventConnection will be used automatically. """ control_connection_timeout = 2.0 From 0edfe56d773be0cbfed3e60c58488147ef4a7bac Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:48:31 -0500 Subject: [PATCH 0789/3961] Update changelog --- CHANGELOG.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d6ab3dc614..17feba9a78 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,10 +1,13 @@ -2.0.3 +2.1.0 ===== In Progress Features -------- * Use io.BytesIO for reduced CPU consumption (github #143) +* Support Twisted as a reactor. Note that a Twisted-compatible + API is not exposed (so no Deferreds), this is just a reactor + implementation. (github #135, PYTHON-8) Bug Fixes --------- From 2d425af0699a32b4eecc7d80db6d4602593cf57c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 14:53:37 -0500 Subject: [PATCH 0790/3961] Update twisted reactor unit tests --- tests/unit/io/test_twistedreactor.py | 33 ++++++++++++---------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/unit/io/test_twistedreactor.py b/tests/unit/io/test_twistedreactor.py index 6772a35a56..de22415b4c 100644 --- a/tests/unit/io/test_twistedreactor.py +++ b/tests/unit/io/test_twistedreactor.py @@ -20,15 +20,18 @@ try: from twisted.test import proto_helpers + from twisted.python.failure import Failure from cassandra.io import twistedreactor except ImportError: - twistedreactor = None + twistedreactor = None # NOQA class TestTwistedProtocol(unittest.TestCase): + def setUp(self): if twistedreactor is None: raise unittest.SkipTest("Twisted libraries not available") + twistedreactor.TwistedConnection.initialize_reactor() self.tr = proto_helpers.StringTransportWithDisconnection() self.tr.connector = Mock() self.mock_connection = Mock() @@ -63,6 +66,7 @@ class TestTwistedClientFactory(unittest.TestCase): def setUp(self): if twistedreactor is None: raise unittest.SkipTest("Twisted libraries not available") + twistedreactor.TwistedConnection.initialize_reactor() self.mock_connection = Mock() self.obj_ut = twistedreactor.TwistedConnectionClientFactory( self.mock_connection) @@ -71,21 +75,24 @@ def test_client_connection_failed(self): """ Verify that connection failed causes the connection object to close. """ - self.obj_ut.clientConnectionFailed(None, 'a test') - self.mock_connection.close.assert_called_with() + exc = Exception('a test') + self.obj_ut.clientConnectionFailed(None, Failure(exc)) + self.mock_connection.defunct.assert_called_with(exc) def test_client_connection_lost(self): """ Verify that connection lost causes the connection object to close. """ - self.obj_ut.clientConnectionLost(None, 'a test') - self.mock_connection.close.assert_called_with() + exc = Exception('a test') + self.obj_ut.clientConnectionLost(None, Failure(exc)) + self.mock_connection.defunct.assert_called_with(exc) class TestTwistedConnection(unittest.TestCase): def setUp(self): if twistedreactor is None: raise unittest.SkipTest("Twisted libraries not available") + twistedreactor.TwistedConnection.initialize_reactor() self.reactor_cft_patcher = patch( 'twisted.internet.reactor.callFromThread') self.reactor_running_patcher = patch( @@ -99,15 +106,14 @@ def setUp(self): def tearDown(self): self.reactor_cft_patcher.stop() self.reactor_run_patcher.stop() - if self.obj_ut._thread: - self.obj_ut._thread.join() + self.obj_ut._loop._cleanup() def test_connection_initialization(self): """ Verify that __init__() works correctly. """ self.mock_reactor_cft.assert_called_with(self.obj_ut.add_connection) - self.obj_ut._thread.join() # make sure thread exits before checking + self.obj_ut._loop._cleanup() self.mock_reactor_run.assert_called_with(installSignalHandlers=False) @patch('twisted.internet.reactor.connectTCP') @@ -141,17 +147,6 @@ def test_close(self, mock_connectTCP): self.assertTrue(self.obj_ut.connected_event.is_set()) self.assertTrue(self.obj_ut.error_all_callbacks.called) - def test_handle_close(self): - """ - Skipped for now, since it just calls close() and isn't really used. - """ - - def test_handle_write(self): - """ - Verify that this raises an exception if called. - """ - self.assertRaises(RuntimeError, self.obj_ut.handle_write) - def test_handle_read__incomplete(self): """ Verify that handle_read() processes incomplete messages properly. From f709878577e78739491ce41ea8de98dd417f192b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 12:56:28 -0700 Subject: [PATCH 0791/3961] fixed all management tests --- cqlengine/management.py | 11 ++++++++++- .../tests/management/test_compaction_settings.py | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 5810c12f12..b5c6419acb 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -261,12 +261,21 @@ def update_compaction(model): existing_options = table.options.copy() - desired_options = get_compaction_options(model).get("compaction_strategy_options", {}) + existing_compaction_strategy = existing_options['compaction_strategy_class'] + + existing_options = json.loads(existing_options['compaction_strategy_options']) + + desired_options = get_compaction_options(model) + + desired_compact_strategy = desired_options.get('class', SizeTieredCompactionStrategy) desired_options.pop('class', None) do_update = False + if desired_compact_strategy not in existing_compaction_strategy: + do_update = True + for k, v in desired_options.items(): val = existing_options.pop(k, None) if val != v: diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index 3da83da7b7..b34a5cafe0 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -220,7 +220,8 @@ class AllLeveledOptionsModel(Model): drop_table(AllLeveledOptionsModel) sync_table(AllLeveledOptionsModel) - settings = get_table_settings(AllLeveledOptionsModel) + settings = get_table_settings(AllLeveledOptionsModel).options + options = json.loads(settings['compaction_strategy_options']) self.assertDictEqual(options, {u'sstable_size_in_mb': u'64'}) From 4cab5851f0a56ca23a1ad7bbd34c8145d2dc37cc Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 13:39:40 -0700 Subject: [PATCH 0792/3961] making sure we're 1.2 compatible --- cqlengine/connection.py | 8 ++++++-- cqlengine/management.py | 2 +- cqlengine/query.py | 4 ++-- cqlengine/tests/statements/test_base_clause.py | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 2768db1077..5e89c60dc5 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -268,8 +268,12 @@ def connection_manager(): def execute_native(query, params=None, consistency_level=None): # TODO use consistency level - prepared = session.prepare(query) - result = session.execute(prepared, params) + if isinstance(query, BaseCQLStatement): + params = query.get_context() + query = str(query) + params = params or {} + + result = session.execute(query, params) return result def get_session(): diff --git a/cqlengine/management.py b/cqlengine/management.py index b5c6419acb..c0562bab89 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -223,7 +223,7 @@ def get_fields(model): ks_name = model._get_keyspace() col_family = model.column_family_name(include_keyspace=False) - query = "select * from system.schema_columns where keyspace_name = ? and columnfamily_name = ?" + query = "select * from system.schema_columns where keyspace_name = %s and columnfamily_name = %s" tmp = execute_native(query, [ks_name, col_family]) # Tables containing only primary keys do not appear to create diff --git a/cqlengine/query.py b/cqlengine/query.py index 383b9c0510..4ea884ee6b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,7 +4,7 @@ from cqlengine import BaseContainerColumn, Map, columns from cqlengine.columns import Counter, List, Set -from cqlengine.connection import execute, RowResult +from cqlengine.connection import execute, RowResult, execute_native from cqlengine.exceptions import CQLEngineException, ValidationError from cqlengine.functions import Token, BaseQueryFunction, QueryValue @@ -775,7 +775,7 @@ def _execute(self, q): if self._batch: return self._batch.add_query(q) else: - return execute(q, consistency_level=self._consistency) + return execute_native(q, consistency_level=self._consistency) def batch(self, batch_obj): if batch_obj is not None and not isinstance(batch_obj, BatchQuery): diff --git a/cqlengine/tests/statements/test_base_clause.py b/cqlengine/tests/statements/test_base_clause.py index c5bbeb402d..4fd6718df7 100644 --- a/cqlengine/tests/statements/test_base_clause.py +++ b/cqlengine/tests/statements/test_base_clause.py @@ -8,9 +8,9 @@ def test_context_updating(self): ss = BaseClause('a', 'b') assert ss.get_context_size() == 1 - ctx = {} + ctx = [] ss.set_context_id(10) ss.update_context(ctx) - assert ctx == {'10': 'b'} + assert ctx == ['10': 'b'] From 7a838e2350d0da617b4f928e03960aea0914a0d7 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 16:08:22 -0500 Subject: [PATCH 0793/3961] Pass user type map to decode fns as necessary --- cassandra/protocol.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 69bc68f1c4..502507ecb4 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -605,7 +605,7 @@ def recv_results_metadata(cls, f, user_type_map): colksname = read_string(f) colcfname = read_string(f) colname = read_string(f) - coltype = cls.read_type(f) + coltype = cls.read_type(f, user_type_map) column_metadata.append((colksname, colcfname, colname, coltype)) return paging_state, column_metadata @@ -635,17 +635,17 @@ def read_type(cls, f, user_type_map): raise NotSupportedError("Unknown data type code 0x%04x. Have to skip" " entire result set." % (optid,)) if typeclass in (ListType, SetType): - subtype = cls.read_type(f) + subtype = cls.read_type(f, user_type_map) typeclass = typeclass.apply_parameters((subtype,)) elif typeclass == MapType: - keysubtype = cls.read_type(f) - valsubtype = cls.read_type(f) + keysubtype = cls.read_type(f, user_type_map) + valsubtype = cls.read_type(f, user_type_map) typeclass = typeclass.apply_parameters((keysubtype, valsubtype)) elif typeclass == UserDefinedType: ks = cls.read_string(f) udt_name = cls.read_string(f) num_fields = cls.read_short(f) - names_and_types = ((cls.read_string(f), cls.read_type(f)) + names_and_types = ((cls.read_string(f), cls.read_type(f, user_type_map)) for _ in xrange(num_fields)) mapped_class = user_type_map.get(ks, {}).get(udt_name) typeclass = typeclass.apply_parameters( From f02692ff27539bc60cab38800f8c05ea62e804fa Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:19:28 -0700 Subject: [PATCH 0794/3961] switching from the prepared statements to parameterized statements --- cqlengine/functions.py | 2 +- cqlengine/statements.py | 26 +++++++++---------- .../tests/statements/test_base_clause.py | 2 +- .../tests/statements/test_update_statement.py | 2 +- .../tests/statements/test_where_clause.py | 5 ++-- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 5796ba4ae4..d70b129710 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -9,7 +9,7 @@ class QueryValue(object): be passed into .filter() keyword args """ - format_string = ':{}' + format_string = '%({})s' def __init__(self, value): self.value = value diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 8a92fb6697..dcd9c410a7 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -101,7 +101,7 @@ def __init__(self, field, operator, value, quote_field=True): def __unicode__(self): field = ('"{}"' if self.quote_field else '{}').format(self.field) - return u'{} {} ?'.format(field, self.operator) + return u'{} {} {}'.format(field, self.operator, unicode(self.query_value)) def __hash__(self): return super(WhereClause, self).__hash__() ^ hash(self.operator) @@ -129,7 +129,7 @@ class AssignmentClause(BaseClause): """ a single variable st statement """ def __unicode__(self): - return u'"{}" = ?'.format(self.field) + return u'"{}" = %({})s'.format(self.field, self.context_id) def insert_tuple(self): return self.field, self.context_id @@ -170,15 +170,15 @@ def __unicode__(self): qs = [] ctx_id = self.context_id if self.previous is None and not (self._assignments or self._additions or self._removals): - qs += ['"{}" = ?'.format(self.field)] + qs += ['"{}" = %({})s'.format(self.field, ctx_id)] if self._assignments: - qs += ['"{}" = ?'.format(self.field)] + qs += ['"{}" = %({})s'.format(self.field, ctx_id)] ctx_id += 1 if self._additions: - qs += ['"{0}" = "{0}" + ?'.format(self.field)] + qs += ['"{0}" = "{0}" + %({1})s'.format(self.field, ctx_id)] ctx_id += 1 if self._removals: - qs += ['"{0}" = "{0}" - ?'.format(self.field)] + qs += ['"{0}" = "{0}" - %({1})s'.format(self.field, ctx_id)] return ', '.join(qs) @@ -232,15 +232,15 @@ def __unicode__(self): qs = [] ctx_id = self.context_id if self._assignments is not None: - qs += ['"{}" = ?'.format(self.field)] + qs += ['"{}" = %({})s'.format(self.field, ctx_id)] ctx_id += 1 if self._prepend: - qs += ['"{0}" = ? + "{0}"'.format(self.field)] + qs += ['"{0}" = %({1})s + "{0}"'.format(self.field, ctx_id)] ctx_id += 1 if self._append: - qs += ['"{0}" = "{0}" + ?'.format(self.field)] + qs += ['"{0}" = "{0}" + %({})s'.format(self.field, ctx_id)] return ', '.join(qs) @@ -356,10 +356,10 @@ def __unicode__(self): ctx_id = self.context_id if self.previous is None and not self._updates: - qs += ['"int_map" = ?'] + qs += ['"int_map" = %({})s'] else: for _ in self._updates or []: - qs += ['"{}"[?] = ?'.format(self.field)] + qs += ['"{}"[%({})s] = %({})s'.format(self.field, ctx_id, ctx_id + 1)] ctx_id += 2 return ', '.join(qs) @@ -380,7 +380,7 @@ def update_context(self, ctx): def __unicode__(self): delta = self.value - self.previous sign = '-' if delta < 0 else '+' - return '"{0}" = "{0}" {1} ?'.format(self.field, sign) + return '"{0}" = "{0}" {1} %({})s'.format(self.field, sign, self.context_id) class BaseDeleteClause(BaseClause): @@ -428,7 +428,7 @@ def get_context_size(self): def __unicode__(self): if not self._analyzed: self._analyze() - return ', '.join(['"{}"[?]'.format(self.field) for i in range(len(self._removals))]) + return ', '.join(['"{}"[%({})s]'.format(self.field) for i in range(len(self._removals))]) class BaseCQLStatement(object): diff --git a/cqlengine/tests/statements/test_base_clause.py b/cqlengine/tests/statements/test_base_clause.py index 4fd6718df7..746cafae31 100644 --- a/cqlengine/tests/statements/test_base_clause.py +++ b/cqlengine/tests/statements/test_base_clause.py @@ -11,6 +11,6 @@ def test_context_updating(self): ctx = [] ss.set_context_id(10) ss.update_context(ctx) - assert ctx == ['10': 'b'] + assert ctx == {'10': 'b'} diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py index 9fe32a1aeb..b98cfae9b0 100644 --- a/cqlengine/tests/statements/test_update_statement.py +++ b/cqlengine/tests/statements/test_update_statement.py @@ -16,7 +16,7 @@ def test_rendering(self): us.add_assignment_clause(AssignmentClause('a', 'b')) us.add_assignment_clause(AssignmentClause('c', 'd')) us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) - self.assertEqual(unicode(us), 'UPDATE table SET "a" = ?, "c" = ? WHERE "a" = ?', unicode(us)) + self.assertEqual(unicode(us), 'UPDATE table SET "a" = %(0)s, "c" = %(1)s WHERE "a" = %(2)s', unicode(us)) def test_context(self): us = UpdateStatement('table') diff --git a/cqlengine/tests/statements/test_where_clause.py b/cqlengine/tests/statements/test_where_clause.py index 083f154234..be81b63e6c 100644 --- a/cqlengine/tests/statements/test_where_clause.py +++ b/cqlengine/tests/statements/test_where_clause.py @@ -14,8 +14,9 @@ def test_where_clause_rendering(self): """ tests that where clauses are rendered properly """ wc = WhereClause('a', EqualsOperator(), 'c') wc.set_context_id(5) - self.assertEqual('"a" = ?', unicode(wc)) - self.assertEqual('"a" = ?', str(wc)) + + self.assertEqual('"a" = %(5)s', unicode(wc), unicode(wc)) + self.assertEqual('"a" = %(5)s', str(wc), type(wc)) def test_equality_method(self): """ tests that 2 identical where clauses evaluate as == """ From 81081e709ba6a4062602e9734b526bb3b376a626 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:24:12 -0700 Subject: [PATCH 0795/3961] fixed select queries, can use params now --- cqlengine/tests/statements/test_select_statement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 7caafbd796..3afe34e2d9 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -30,12 +30,12 @@ def test_table_rendering(self): def test_where_clause_rendering(self): ss = SelectStatement('table') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = ?', unicode(ss)) + self.assertEqual(unicode(ss), 'SELECT * FROM table WHERE "a" = %(0)s', unicode(ss)) def test_count(self): ss = SelectStatement('table', count=True, limit=10, order_by='d') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = ?', unicode(ss)) + self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = %(0)s', unicode(ss)) self.assertNotIn('LIMIT', unicode(ss)) self.assertNotIn('ORDER', unicode(ss)) @@ -49,11 +49,11 @@ def test_context_id_update(self): ss = SelectStatement('table') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) self.assertEqual(ss.get_context(), {'0': 'b'}) - self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = ?') + self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = %(0)s') ss.update_context_id(5) self.assertEqual(ss.get_context(), {'5': 'b'}) - self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = ?') + self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = %(5)s') def test_additional_rendering(self): ss = SelectStatement( From 8480123f4005b7848bbd74d0969e80d92ff7a11c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 16:29:05 -0500 Subject: [PATCH 0796/3961] Use registered UDTs for non-prepared encoding --- cassandra/cluster.py | 40 +++++++++++++++++++++------- cassandra/protocol.py | 8 +++--- cassandra/query.py | 16 ++++++----- tests/unit/test_parameter_binding.py | 19 ++++++------- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 7e9ba2810b..37b86b4cea 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -44,6 +44,7 @@ from cassandra import (ConsistencyLevel, AuthenticationFailed, OperationTimedOut, UnsupportedOperation) from cassandra.connection import ConnectionException, ConnectionShutdown +from cassandra.encoder import cql_encode_all_types, cql_encoders from cassandra.protocol import (QueryMessage, ResultMessage, ErrorMessage, ReadTimeoutErrorMessage, WriteTimeoutErrorMessage, @@ -409,8 +410,7 @@ def __init__(self, self._listener_lock = Lock() # let Session objects be GC'ed (and shutdown) when the user no longer - # holds a reference. Normally the cycle detector would handle this, - # but implementing __del__ prevents that. + # holds a reference. self.sessions = WeakSet() self.metadata = Metadata(self) self.control_connection = None @@ -451,8 +451,10 @@ def __init__(self, self.control_connection = ControlConnection( self, self.control_connection_timeout) - def register_type_class(self, keyspace, user_type, klass): + def register_user_type(self, keyspace, user_type, klass): self._user_types[keyspace][user_type] = klass + for session in self.sessions: + self.session.user_type_registered(keyspace, user_type, klass) def get_min_requests_per_connection(self, host_distance): return self._min_requests_per_connection[host_distance] @@ -602,6 +604,9 @@ def shutdown(self): def _new_session(self): session = Session(self, self.metadata.all_hosts()) + for keyspace, type_map in six.iteritems(self._user_types): + for udt_name, klass in six.iteritems(type_map): + session.user_type_registered(keyspace, udt_name, klass) self.sessions.add(session) return session @@ -1064,6 +1069,19 @@ class Session(object): _metrics = None _protocol_version = None + encoders = None + + def user_type_registered(self, keyspace, user_type, klass): + type_meta = self.cluster.metadata.keyspaces[keyspace].user_types[user_type] + + def encode(val): + return '{ %s }' % ' , '.join('%s : %s' % ( + field_name, + cql_encode_all_types(getattr(val, field_name)) + ) for field_name in type_meta.field_names) + + self._encoders[klass] = encode + def __init__(self, cluster, hosts): self.cluster = cluster self.hosts = hosts @@ -1074,6 +1092,8 @@ def __init__(self, cluster, hosts): self._metrics = cluster.metrics self._protocol_version = self.cluster.protocol_version + self._encoders = cql_encoders.copy() + # create connection pools in parallel futures = [] for host in hosts: @@ -1196,7 +1216,7 @@ def _create_response_future(self, query, parameters, trace): if isinstance(query, SimpleStatement): query_string = query.query_string if parameters: - query_string = bind_params(query.query_string, parameters) + query_string = bind_params(query.query_string, parameters, self._encoders) message = QueryMessage( query_string, cl, query.serial_consistency_level, fetch_size, timestamp=timestamp) @@ -1701,8 +1721,8 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, cf_query, col_query) log.debug("[control connection] Fetched table info for %s.%s, rebuilding metadata", (keyspace, table)) - cf_result = dict_factory(*cf_result.results) - col_result = dict_factory(*col_result.results) + cf_result = dict_factory(*cf_result.results) if cf_result else {} + col_result = dict_factory(*col_result.results) if col_result else {} self._cluster.metadata.table_changed(keyspace, table, cf_result, col_result) elif usertype: # user defined types within this keyspace changed @@ -1710,7 +1730,7 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, types_query = QueryMessage(query=self._SELECT_USERTYPES + where_clause, consistency_level=cl) types_result = connection.wait_for_response(types_query) log.debug("[control connection] Fetched user type info for %s.%s, rebuilding metadata", (keyspace, usertype)) - types_result = dict_factory(*types_result) + types_result = dict_factory(*types_result.results) if types_result.results else {} self._cluster.metadata.usertype_changed(keyspace, usertype, types_result) elif keyspace: # only the keyspace itself changed (such as replication settings) @@ -1718,7 +1738,7 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, ks_query = QueryMessage(query=self._SELECT_KEYSPACES + where_clause, consistency_level=cl) ks_result = connection.wait_for_response(ks_query) log.debug("[control connection] Fetched keyspace info for %s, rebuilding metadata", (keyspace,)) - ks_result = dict_factory(*types_result) + ks_result = dict_factory(*ks_result.results) if ks_result.results else {} self._cluster.metadata.keyspace_changed(keyspace, ks_result) else: # build everything from scratch @@ -1730,12 +1750,12 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, if self._protocol_version >= 3: queries.append(QueryMessage(query=self._SELECT_USERTYPES, consistency_level=cl)) ks_result, cf_result, col_result, types_result = connection.wait_for_responses(*queries) - types_result = dict_factory(*types_result) + types_result = dict_factory(*types_result.results) if types_result.results else {} else: ks_result, cf_result, col_result = connection.wait_for_responses(*queries) types_result = {} - ks_result = dict_factory(*types_result) + ks_result = dict_factory(*ks_result.results) cf_result = dict_factory(*cf_result.results) col_result = dict_factory(*col_result.results) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 502507ecb4..f48e39fb66 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -560,14 +560,14 @@ def recv_body(cls, f, protocol_version, user_type_map): ksname = read_string(f) results = ksname elif kind == RESULT_KIND_PREPARED: - results = cls.recv_results_prepared(f) + results = cls.recv_results_prepared(f, user_type_map) elif kind == RESULT_KIND_SCHEMA_CHANGE: results = cls.recv_results_schema_change(f, protocol_version) return cls(kind, results, paging_state) @classmethod def recv_results_rows(cls, f, protocol_version, user_type_map): - paging_state, column_metadata = cls.recv_results_metadata(f) + paging_state, column_metadata = cls.recv_results_metadata(f, user_type_map) rowcount = read_int(f) rows = [cls.recv_row(f, len(column_metadata)) for _ in range(rowcount)] colnames = [c[2] for c in column_metadata] @@ -579,9 +579,9 @@ def recv_results_rows(cls, f, protocol_version, user_type_map): return (paging_state, (colnames, parsed_rows)) @classmethod - def recv_results_prepared(cls, f): + def recv_results_prepared(cls, f, user_type_map): query_id = read_binary_string(f) - _, column_metadata = cls.recv_results_metadata(f) + _, column_metadata = cls.recv_results_metadata(f, user_type_map) return (query_id, column_metadata) @classmethod diff --git a/cassandra/query.py b/cassandra/query.py index 108f4da4d9..659ce6ecd2 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -567,9 +567,10 @@ class BatchStatement(Statement): """ _statements_and_parameters = None + _session = None def __init__(self, batch_type=BatchType.LOGGED, retry_policy=None, - consistency_level=None): + consistency_level=None, session=None): """ `batch_type` specifies The :class:`.BatchType` for the batch operation. Defaults to :attr:`.BatchType.LOGGED`. @@ -605,6 +606,7 @@ def __init__(self, batch_type=BatchType.LOGGED, retry_policy=None, """ self.batch_type = batch_type self._statements_and_parameters = [] + self._session = session Statement.__init__(self, retry_policy=retry_policy, consistency_level=consistency_level) def add(self, statement, parameters=None): @@ -617,7 +619,8 @@ def add(self, statement, parameters=None): """ if isinstance(statement, six.string_types): if parameters: - statement = bind_params(statement, parameters) + encoders = cql_encoders if self._session is None else self._session.encoders + statement = bind_params(statement, parameters, encoders) self._statements_and_parameters.append((False, statement, ())) elif isinstance(statement, PreparedStatement): query_id = statement.query_id @@ -635,7 +638,8 @@ def add(self, statement, parameters=None): # it must be a SimpleStatement query_string = statement.query_string if parameters: - query_string = bind_params(query_string, parameters) + encoders = cql_encoders if self._session is None else self._session.encoders + query_string = bind_params(query_string, parameters, encoders) self._statements_and_parameters.append((False, query_string, ())) return self @@ -677,11 +681,11 @@ def __str__(self): return cql_encode_sequence(self.sequence) -def bind_params(query, params): +def bind_params(query, params, encoders): if isinstance(params, dict): - return query % dict((k, cql_encoders.get(type(v), cql_encode_object)(v)) for k, v in six.iteritems(params)) + return query % dict((k, encoders.get(type(v), cql_encode_object)(v)) for k, v in six.iteritems(params)) else: - return query % tuple(cql_encoders.get(type(v), cql_encode_object)(v) for v in params) + return query % tuple(encoders.get(type(v), cql_encode_object)(v) for v in params) class TraceUnavailable(Exception): diff --git a/tests/unit/test_parameter_binding.py b/tests/unit/test_parameter_binding.py index 908556fc80..6c3452e76e 100644 --- a/tests/unit/test_parameter_binding.py +++ b/tests/unit/test_parameter_binding.py @@ -17,6 +17,7 @@ except ImportError: import unittest # noqa +from cassandra.encoder import cql_encoders from cassandra.query import bind_params, ValueSequence from cassandra.query import PreparedStatement, BoundStatement from cassandra.cqltypes import Int32Type @@ -28,31 +29,31 @@ class ParamBindingTest(unittest.TestCase): def test_bind_sequence(self): - result = bind_params("%s %s %s", (1, "a", 2.0)) + result = bind_params("%s %s %s", (1, "a", 2.0), cql_encoders) self.assertEqual(result, "1 'a' 2.0") def test_bind_map(self): - result = bind_params("%(a)s %(b)s %(c)s", dict(a=1, b="a", c=2.0)) + result = bind_params("%(a)s %(b)s %(c)s", dict(a=1, b="a", c=2.0), cql_encoders) self.assertEqual(result, "1 'a' 2.0") def test_sequence_param(self): - result = bind_params("%s", (ValueSequence((1, "a", 2.0)),)) + result = bind_params("%s", (ValueSequence((1, "a", 2.0)),), cql_encoders) self.assertEqual(result, "( 1 , 'a' , 2.0 )") def test_generator_param(self): - result = bind_params("%s", ((i for i in xrange(3)),)) + result = bind_params("%s", ((i for i in xrange(3)),), cql_encoders) self.assertEqual(result, "[ 0 , 1 , 2 ]") def test_none_param(self): - result = bind_params("%s", (None,)) + result = bind_params("%s", (None,), cql_encoders) self.assertEqual(result, "NULL") def test_list_collection(self): - result = bind_params("%s", (['a', 'b', 'c'],)) + result = bind_params("%s", (['a', 'b', 'c'],), cql_encoders) self.assertEqual(result, "[ 'a' , 'b' , 'c' ]") def test_set_collection(self): - result = bind_params("%s", (set(['a', 'b']),)) + result = bind_params("%s", (set(['a', 'b']),), cql_encoders) self.assertIn(result, ("{ 'a' , 'b' }", "{ 'b' , 'a' }")) def test_map_collection(self): @@ -60,11 +61,11 @@ def test_map_collection(self): vals['a'] = 'a' vals['b'] = 'b' vals['c'] = 'c' - result = bind_params("%s", (vals,)) + result = bind_params("%s", (vals,), cql_encoders) self.assertEqual(result, "{ 'a' : 'a' , 'b' : 'b' , 'c' : 'c' }") def test_quote_escaping(self): - result = bind_params("%s", ("""'ef''ef"ef""ef'""",)) + result = bind_params("%s", ("""'ef''ef"ef""ef'""",), cql_encoders) self.assertEqual(result, """'''ef''''ef"ef""ef'''""") From 47018ee65dc4e1c9e8ba5056a8501fce0cbc8749 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:29:19 -0700 Subject: [PATCH 0797/3961] fixing tests --- .../statements/test_assignment_clauses.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 48f58edc5a..b344404e03 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -25,7 +25,7 @@ def test_update_from_none(self): self.assertIsNone(c._removals) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = ?') + self.assertEqual(str(c), '"s" = %(0)s') ctx = {} c.update_context(ctx) @@ -75,7 +75,7 @@ def test_additions(self): self.assertIsNone(c._removals) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" + ?') + self.assertEqual(str(c), '"s" = "s" + %(0)s') ctx = {} c.update_context(ctx) @@ -91,7 +91,7 @@ def test_removals(self): self.assertEqual(c._removals, {3}) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" - ?') + self.assertEqual(str(c), '"s" = "s" - %(0)s') ctx = {} c.update_context(ctx) @@ -107,7 +107,7 @@ def test_additions_and_removals(self): self.assertEqual(c._removals, {1}) self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s" = "s" + ?, "s" = "s" - ?') + self.assertEqual(str(c), '"s" = "s" + %(0)s, "s" = "s" - %(1)s') ctx = {} c.update_context(ctx) @@ -126,7 +126,7 @@ def test_update_from_none(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = ?') + self.assertEqual(str(c), '"s" = %(0)s') ctx = {} c.update_context(ctx) @@ -142,7 +142,7 @@ def test_update_from_empty(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = ?') + self.assertEqual(str(c), '"s" = %(0)s') ctx = {} c.update_context(ctx) @@ -158,7 +158,7 @@ def test_update_from_different_list(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = ?') + self.assertEqual(str(c), '"s" = %(0)s') ctx = {} c.update_context(ctx) @@ -254,7 +254,7 @@ def test_update_from_null(self): self.assertEqual(c._updates, [3, 5]) self.assertEqual(c.get_context_size(), 4) - self.assertEqual(str(c), '"s"[?] = ?, "s"[?] = ?') + self.assertEqual(str(c), '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s') ctx = {} c.update_context(ctx) @@ -297,7 +297,7 @@ def noop_update(self): c.set_context_id(5) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" + ?') + self.assertEqual(str(c), '"a" = "a" + %(0)s') ctx = {} c.update_context(ctx) From 684a605acb03ba11b04ebdba59b20f3c15d27f68 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:33:40 -0700 Subject: [PATCH 0798/3961] fixed tuple assignment issue --- cqlengine/statements.py | 2 +- cqlengine/tests/statements/test_assignment_clauses.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index dcd9c410a7..169c5b2f83 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -240,7 +240,7 @@ def __unicode__(self): ctx_id += 1 if self._append: - qs += ['"{0}" = "{0}" + %({})s'.format(self.field, ctx_id)] + qs += ['"{0}" = "{{0}}" + %({1})s'.format(self.field, ctx_id)] return ', '.join(qs) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index b344404e03..72365800df 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -190,7 +190,7 @@ def test_prepend(self): self.assertEqual(c._prepend, [1, 2]) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = ? + "s"') + self.assertEqual(str(c), '"s" = %(0)s + "s"') ctx = {} c.update_context(ctx) @@ -225,7 +225,7 @@ def test_shrinking_list_update(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = ?') + self.assertEqual(str(c), '"s" = %(0)s') ctx = {} c.update_context(ctx) @@ -241,7 +241,7 @@ def test_update(self): self.assertEqual(c._updates, [3, 5]) self.assertEqual(c.get_context_size(), 4) - self.assertEqual(str(c), '"s"[?] = ?, "s"[?] = ?') + self.assertEqual(str(c), '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s') ctx = {} c.update_context(ctx) @@ -313,7 +313,7 @@ def test_update(self): self.assertEqual(c._removals, [1, 5]) self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s"[?], "s"[?]') + self.assertEqual(str(c), '"s"[%(0)s], "s"[%(1)s]') ctx = {} c.update_context(ctx) From 43454da71a8bb71d10f563ea45f9696f7eff9c93 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:36:45 -0700 Subject: [PATCH 0799/3961] fixed issue with counter placeholders --- cqlengine/statements.py | 2 +- cqlengine/tests/statements/test_assignment_clauses.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 169c5b2f83..e9d203b6ba 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -380,7 +380,7 @@ def update_context(self, ctx): def __unicode__(self): delta = self.value - self.previous sign = '-' if delta < 0 else '+' - return '"{0}" = "{0}" {1} %({})s'.format(self.field, sign, self.context_id) + return '"{0}" = "{0}" {1} %({2})s'.format(self.field, sign, self.context_id) class BaseDeleteClause(BaseClause): diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 72365800df..6c173029dc 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -207,7 +207,7 @@ def test_append_and_prepend(self): self.assertEqual(c._prepend, [1, 2]) self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s" = ? + "s", "s" = "s" + ?') + self.assertEqual(str(c), '"s" = %(0)s + "s", "s" = "s" + %(1)s') ctx = {} c.update_context(ctx) From 84f39e6b4898af11e2c810a2ebbd051af93c3058 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:41:39 -0700 Subject: [PATCH 0800/3961] fixing counter statements --- cqlengine/tests/statements/test_assignment_clauses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index 6c173029dc..c16d33eb8c 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -275,7 +275,7 @@ def test_positive_update(self): c.set_context_id(5) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" + ?') + self.assertEqual(str(c), '"a" = "a" + %(0)s') ctx = {} c.update_context(ctx) @@ -286,7 +286,7 @@ def test_negative_update(self): c.set_context_id(3) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" - ?') + self.assertEqual(str(c), '"a" = "a" - %(3)s') ctx = {} c.update_context(ctx) From 21d43392b1eea643b5d9290c8f4476f5031a6283 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:48:58 -0700 Subject: [PATCH 0801/3961] fixed list append --- cqlengine/statements.py | 2 +- cqlengine/tests/statements/test_assignment_clauses.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index e9d203b6ba..c055f8e452 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -240,7 +240,7 @@ def __unicode__(self): ctx_id += 1 if self._append: - qs += ['"{0}" = "{{0}}" + %({1})s'.format(self.field, ctx_id)] + qs += ['"{0}" = "{0}" + %({1})s'.format(self.field, ctx_id)] return ', '.join(qs) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index c16d33eb8c..efa7129c56 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -174,7 +174,7 @@ def test_append(self): self.assertIsNone(c._prepend) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" + ?') + self.assertEqual(str(c), '"s" = "s" + %(0)s') ctx = {} c.update_context(ctx) From 0b161a4b9c7843fa25f8e0df9c62215279097873 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:51:22 -0700 Subject: [PATCH 0802/3961] fixed counter test --- cqlengine/tests/statements/test_assignment_clauses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/statements/test_assignment_clauses.py b/cqlengine/tests/statements/test_assignment_clauses.py index efa7129c56..32da18b228 100644 --- a/cqlengine/tests/statements/test_assignment_clauses.py +++ b/cqlengine/tests/statements/test_assignment_clauses.py @@ -275,7 +275,7 @@ def test_positive_update(self): c.set_context_id(5) self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" + %(0)s') + self.assertEqual(str(c), '"a" = "a" + %(5)s') ctx = {} c.update_context(ctx) From d66ec78756fdd982ab5b6c78b9144ace8e4dbcbc Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 14:52:54 -0700 Subject: [PATCH 0803/3961] fixed string formatting --- cqlengine/statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index c055f8e452..7e39442369 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -428,7 +428,7 @@ def get_context_size(self): def __unicode__(self): if not self._analyzed: self._analyze() - return ', '.join(['"{}"[%({})s]'.format(self.field) for i in range(len(self._removals))]) + return ', '.join(['"{}"[%({})s]'.format(self.field, self.context_id + i) for i in range(len(self._removals))]) class BaseCQLStatement(object): From 52c736b57d2722d1d578086d68fec8ed2bae2cad Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 15:07:58 -0700 Subject: [PATCH 0804/3961] fixed update clause --- cqlengine/tests/statements/test_base_clause.py | 2 +- cqlengine/tests/statements/test_delete_statement.py | 4 ++-- cqlengine/tests/statements/test_update_statement.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/statements/test_base_clause.py b/cqlengine/tests/statements/test_base_clause.py index 746cafae31..c5bbeb402d 100644 --- a/cqlengine/tests/statements/test_base_clause.py +++ b/cqlengine/tests/statements/test_base_clause.py @@ -8,7 +8,7 @@ def test_context_updating(self): ss = BaseClause('a', 'b') assert ss.get_context_size() == 1 - ctx = [] + ctx = {} ss.set_context_id(10) ss.update_context(ctx) assert ctx == {'10': 'b'} diff --git a/cqlengine/tests/statements/test_delete_statement.py b/cqlengine/tests/statements/test_delete_statement.py index ada5b7209f..f3b0df9ffd 100644 --- a/cqlengine/tests/statements/test_delete_statement.py +++ b/cqlengine/tests/statements/test_delete_statement.py @@ -31,7 +31,7 @@ def test_table_rendering(self): def test_where_clause_rendering(self): ds = DeleteStatement('table', None) ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ds), 'DELETE FROM table WHERE "a" = ?', unicode(ds)) + self.assertEqual(unicode(ds), 'DELETE FROM table WHERE "a" = %(0)s', unicode(ds)) def test_context_update(self): ds = DeleteStatement('table', None) @@ -39,7 +39,7 @@ def test_context_update(self): ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) ds.update_context_id(7) - self.assertEqual(unicode(ds), 'DELETE "d"[?] FROM table WHERE "a" = ?') + self.assertEqual(unicode(ds), 'DELETE "d"[%(8)s] FROM table WHERE "a" = %(7)s') self.assertEqual(ds.get_context(), {'7': 'b', '8': 3}) def test_context(self): diff --git a/cqlengine/tests/statements/test_update_statement.py b/cqlengine/tests/statements/test_update_statement.py index b98cfae9b0..fae69bbd8e 100644 --- a/cqlengine/tests/statements/test_update_statement.py +++ b/cqlengine/tests/statements/test_update_statement.py @@ -31,7 +31,7 @@ def test_context_update(self): us.add_assignment_clause(AssignmentClause('c', 'd')) us.add_where_clause(WhereClause('a', EqualsOperator(), 'x')) us.update_context_id(3) - self.assertEqual(unicode(us), 'UPDATE table SET "a" = ?, "c" = ? WHERE "a" = ?') + self.assertEqual(unicode(us), 'UPDATE table SET "a" = %(4)s, "c" = %(5)s WHERE "a" = %(3)s') self.assertEqual(us.get_context(), {'4': 'b', '5': 'd', '3': 'x'}) def test_additional_rendering(self): From 82ebcac4ddd68b3fb6cad74a51777ba725b0092b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 15:17:17 -0700 Subject: [PATCH 0805/3961] actually executing against native proto now, need to gut out the remaining connection pool calls --- cqlengine/connection.py | 1 - cqlengine/statements.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 5e89c60dc5..b6e5d1f635 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -272,7 +272,6 @@ def execute_native(query, params=None, consistency_level=None): params = query.get_context() query = str(query) params = params or {} - result = session.execute(query, params) return result diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 7e39442369..46b4587590 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -626,7 +626,7 @@ def __unicode__(self): qs += ["({})".format(', '.join(['"{}"'.format(c) for c in columns]))] qs += ['VALUES'] - qs += ["({})".format(', '.join(['?' for v in values]))] + qs += ["({})".format(', '.join(['%({})s'.format(v) for v in values]))] if self.ttl: qs += ["USING TTL {}".format(self.ttl)] From 0761453e402c64a867d86832b6f22d7e74dd241c Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 15:29:26 -0700 Subject: [PATCH 0806/3961] trying to rig up the old RowResult --- cqlengine/connection.py | 3 ++- cqlengine/query.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index b6e5d1f635..1f3be21150 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -273,7 +273,8 @@ def execute_native(query, params=None, consistency_level=None): query = str(query) params = params or {} result = session.execute(query, params) - return result + import ipdb; ipdb.set_trace() + return ([], result) def get_session(): return session diff --git a/cqlengine/query.py b/cqlengine/query.py index 4ea884ee6b..ef92f92369 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -231,7 +231,9 @@ def _execute(self, q): if self._batch: return self._batch.add_query(q) else: - return execute(q, consistency_level=self._consistency) + result = execute_native(q, consistency_level=self._consistency) + import ipdb; ipdb.set_trace() + return result def __unicode__(self): return unicode(self._select_query()) @@ -775,7 +777,8 @@ def _execute(self, q): if self._batch: return self._batch.add_query(q) else: - return execute_native(q, consistency_level=self._consistency) + tmp = execute_native(q, consistency_level=self._consistency) + return tmp def batch(self, batch_obj): if batch_obj is not None and not isinstance(batch_obj, BatchQuery): From 91ef3c32f46554fb33d703e391860577fbe30f70 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 16:28:07 -0700 Subject: [PATCH 0807/3961] fixed TTL queries --- cqlengine/connection.py | 12 +++++++++-- cqlengine/management.py | 6 +++--- cqlengine/query.py | 1 - .../tests/statements/test_insert_statement.py | 4 ++-- cqlengine/tests/test_ttl.py | 20 ++++++++++++++----- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 1f3be21150..c7274e8170 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -24,6 +24,7 @@ from thrift.transport.TTransport import TTransportException from cqlengine.statements import BaseCQLStatement +from cassandra.query import dict_factory LOG = logging.getLogger('cqlengine.cql') @@ -83,6 +84,7 @@ def setup( cluster = Cluster(hosts) session = cluster.connect() + session.row_factory = dict_factory _max_connections = max_connections @@ -273,8 +275,14 @@ def execute_native(query, params=None, consistency_level=None): query = str(query) params = params or {} result = session.execute(query, params) - import ipdb; ipdb.set_trace() - return ([], result) + + if result: + keys = result[0].keys() + else: + keys = [] + + return QueryResult(keys, result) + def get_session(): return session diff --git a/cqlengine/management.py b/cqlengine/management.py index c0562bab89..14a0203fb7 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -229,13 +229,13 @@ def get_fields(model): # Tables containing only primary keys do not appear to create # any entries in system.schema_columns, as only non-primary-key attributes # appear to be inserted into the schema_columns table - if not tmp: + if not tmp[1]: return [] try: - return [Field(x.column_name, x.validator) for x in tmp if x.type == 'regular'] + return [Field(x['column_name'], x['validator']) for x in tmp[1] if x['type'] == 'regular'] except ValueError: - return [Field(x.column_name, x.validator) for x in tmp] + return [Field(x['column_name'], x['validator']) for x in tmp[1]] # convert to Field named tuples diff --git a/cqlengine/query.py b/cqlengine/query.py index ef92f92369..94ae98675b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -232,7 +232,6 @@ def _execute(self, q): return self._batch.add_query(q) else: result = execute_native(q, consistency_level=self._consistency) - import ipdb; ipdb.set_trace() return result def __unicode__(self): diff --git a/cqlengine/tests/statements/test_insert_statement.py b/cqlengine/tests/statements/test_insert_statement.py index eeffe556d9..32bb74350d 100644 --- a/cqlengine/tests/statements/test_insert_statement.py +++ b/cqlengine/tests/statements/test_insert_statement.py @@ -17,7 +17,7 @@ def test_statement(self): self.assertEqual( unicode(ist), - 'INSERT INTO table ("a", "c") VALUES (?, ?)' + 'INSERT INTO table ("a", "c") VALUES (%(0)s, %(1)s)' ) def test_context_update(self): @@ -28,7 +28,7 @@ def test_context_update(self): ist.update_context_id(4) self.assertEqual( unicode(ist), - 'INSERT INTO table ("a", "c") VALUES (?, ?)' + 'INSERT INTO table ("a", "c") VALUES (%(4)s, %(5)s)' ) ctx = ist.get_context() self.assertEqual(ctx, {'4': 'b', '5': 'd'}) diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index c08a1a19e0..b55254a5eb 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -4,7 +4,8 @@ from uuid import uuid4 from cqlengine import columns import mock -from cqlengine.connection import ConnectionPool +from cqlengine.connection import ConnectionPool, get_session + class TestTTLModel(Model): id = columns.UUID(primary_key=True, default=lambda:uuid4()) @@ -39,7 +40,9 @@ class TTLModelTests(BaseTTLTest): def test_ttl_included_on_create(self): """ tests that ttls on models work as expected """ - with mock.patch.object(ConnectionPool, 'execute') as m: + session = get_session() + + with mock.patch.object(session, 'execute') as m: TestTTLModel.ttl(60).create(text="hello blake") query = m.call_args[0][0] @@ -56,8 +59,10 @@ def test_queryset_is_returned_on_class(self): class TTLInstanceUpdateTest(BaseTTLTest): def test_update_includes_ttl(self): + session = get_session() + model = TestTTLModel.create(text="goodbye blake") - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(session, 'execute') as m: model.ttl(60).update(text="goodbye forever") query = m.call_args[0][0] @@ -84,22 +89,27 @@ def test_instance_is_returned(self): self.assertEqual(60, o._ttl) def test_ttl_is_include_with_query_on_update(self): + session = get_session() + o = TestTTLModel.create(text="whatever") o.text = "new stuff" o = o.ttl(60) - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(session, 'execute') as m: o.save() + query = m.call_args[0][0] self.assertIn("USING TTL", query) class TTLBlindUpdateTest(BaseTTLTest): def test_ttl_included_with_blind_update(self): + session = get_session() + o = TestTTLModel.create(text="whatever") tid = o.id - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(session, 'execute') as m: TestTTLModel.objects(id=tid).ttl(60).update(text="bacon") query = m.call_args[0][0] From f3001d82ada236cd309fe4993cbdb6d13fbd7978 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 18:42:46 -0500 Subject: [PATCH 0808/3961] Allow specifying cassandra dir with env variable --- tests/integration/__init__.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 320e5a4602..a6c9e95d46 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -39,13 +39,14 @@ MULTIDC_CLUSTER_NAME = 'multidc_test_cluster' CCM_CLUSTER = None +CASSANDRA_DIR = os.getenv('CASSANDRA_DIR', None) CASSANDRA_VERSION = os.getenv('CASSANDRA_VERSION', '2.0.6') if CASSANDRA_VERSION.startswith('1'): - DEFAULT_PROTOCOL_VERSION = 1 + default_protocol_version = 1 else: - DEFAULT_PROTOCOL_VERSION = 2 -PROTOCOL_VERSION = int(os.getenv('PROTOCOL_VERSION', DEFAULT_PROTOCOL_VERSION)) + default_protocol_version = 2 +PROTOCOL_VERSION = int(os.getenv('PROTOCOL_VERSION', default_protocol_version)) path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ccm') if not os.path.exists(path): @@ -100,10 +101,17 @@ def setup_package(): cluster = CCMCluster.load(path, CLUSTER_NAME) log.debug("Found existing ccm test cluster, clearing") cluster.clear() - cluster.set_cassandra_dir(cassandra_version=CASSANDRA_VERSION) + if CASSANDRA_DIR: + cluster.set_cassandra_dir(cassandra_dir=CASSANDRA_DIR) + else: + cluster.set_cassandra_dir(cassandra_version=CASSANDRA_VERSION) except Exception: - log.debug("Creating new ccm test cluster with version %s", CASSANDRA_VERSION) - cluster = CCMCluster(path, CLUSTER_NAME, cassandra_version=CASSANDRA_VERSION) + if CASSANDRA_DIR: + log.debug("Creating new ccm test cluster with cassandra dir %s", CASSANDRA_DIR) + cluster = CCMCluster(path, CLUSTER_NAME, cassandra_dir=CASSANDRA_DIR) + else: + log.debug("Creating new ccm test cluster with version %s", CASSANDRA_VERSION) + cluster = CCMCluster(path, CLUSTER_NAME, cassandra_version=CASSANDRA_VERSION) cluster.set_configuration_options({'start_native_transport': True}) common.switch_cluster(path, CLUSTER_NAME) cluster.populate(3) @@ -128,7 +136,10 @@ def use_multidc(dc_list): cluster.clear() except Exception: log.debug("Creating new ccm test multi-dc cluster") - cluster = CCMCluster(path, MULTIDC_CLUSTER_NAME, cassandra_version=CASSANDRA_VERSION) + if CASSANDRA_DIR: + cluster = CCMCluster(path, MULTIDC_CLUSTER_NAME, cassandra_dir=CASSANDRA_DIR) + else: + cluster = CCMCluster(path, MULTIDC_CLUSTER_NAME, cassandra_version=CASSANDRA_VERSION) cluster.set_configuration_options({'start_native_transport': True}) common.switch_cluster(path, MULTIDC_CLUSTER_NAME) cluster.populate(dc_list) From 901d9a16e4278458befe8303db0860b507705e36 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 18:43:56 -0500 Subject: [PATCH 0809/3961] Pushed schema changes are different from responses --- cassandra/protocol.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index f48e39fb66..e0dad3ef11 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -612,19 +612,9 @@ def recv_results_metadata(cls, f, user_type_map): @classmethod def recv_results_schema_change(cls, f, protocol_version): change_type = read_string(f) - if protocol_version >= 3: - target = read_string(f) - keyspace = read_string(f) - if target != "KEYSPACE": - table_or_type = read_string(f) - return {'change_type': change_type, 'keyspace': keyspace, target.lower(): table_or_type} - else: - return {'change_type': change_type, 'keyspace': keyspace} - - else: - keyspace = read_string(f) - table = read_string(f) - return {'change_type': change_type, 'keyspace': keyspace, 'table': table} + keyspace = read_string(f) + table = read_string(f) + return {'change_type': change_type, 'keyspace': keyspace, 'table': table} @classmethod def read_type(cls, f, user_type_map): @@ -805,30 +795,39 @@ def recv_body(cls, f, protocol_version, user_type_map): event_type = read_string(f).upper() if event_type in known_event_types: read_method = getattr(cls, 'recv_' + event_type.lower()) - return cls(event_type=event_type, event_args=read_method(f)) + return cls(event_type=event_type, event_args=read_method(f, protocol_version)) raise NotSupportedError('Unknown event type %r' % event_type) @classmethod - def recv_topology_change(cls, f): + def recv_topology_change(cls, f, protocol_version): # "NEW_NODE" or "REMOVED_NODE" change_type = read_string(f) address = read_inet(f) return dict(change_type=change_type, address=address) @classmethod - def recv_status_change(cls, f): + def recv_status_change(cls, f, protocol_version): # "UP" or "DOWN" change_type = read_string(f) address = read_inet(f) return dict(change_type=change_type, address=address) @classmethod - def recv_schema_change(cls, f): + def recv_schema_change(cls, f, protocol_version): # "CREATED", "DROPPED", or "UPDATED" change_type = read_string(f) - keyspace = read_string(f) - table = read_string(f) - return dict(change_type=change_type, keyspace=keyspace, table=table) + if protocol_version >= 3: + target = read_string(f) + keyspace = read_string(f) + if target != "KEYSPACE": + table_or_type = read_string(f) + return {'change_type': change_type, 'keyspace': keyspace, target.lower(): table_or_type} + else: + return {'change_type': change_type, 'keyspace': keyspace} + else: + keyspace = read_string(f) + table = read_string(f) + return {'change_type': change_type, 'keyspace': keyspace, 'table': table} def write_header(f, version, flags, stream_id, opcode, length): From 6d806ee4c855ed3e19cc0111ee7617e32f5dcb6b Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 18 Jun 2014 18:45:39 -0500 Subject: [PATCH 0810/3961] Integration tests for UDTs, related fixes --- cassandra/cluster.py | 37 ++-- cassandra/connection.py | 2 +- cassandra/cqltypes.py | 4 +- cassandra/metadata.py | 5 +- cassandra/protocol.py | 10 +- tests/integration/standard/test_udts.py | 229 ++++++++++++++++++++++++ 6 files changed, 261 insertions(+), 26 deletions(-) create mode 100644 tests/integration/standard/test_udts.py diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 2869ff97b5..da3733c31b 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -459,7 +459,7 @@ def __init__(self, def register_user_type(self, keyspace, user_type, klass): self._user_types[keyspace][user_type] = klass for session in self.sessions: - self.session.user_type_registered(keyspace, user_type, klass) + session.user_type_registered(keyspace, user_type, klass) def get_min_requests_per_connection(self, host_distance): return self._min_requests_per_connection[host_distance] @@ -1076,17 +1076,6 @@ class Session(object): encoders = None - def user_type_registered(self, keyspace, user_type, klass): - type_meta = self.cluster.metadata.keyspaces[keyspace].user_types[user_type] - - def encode(val): - return '{ %s }' % ' , '.join('%s : %s' % ( - field_name, - cql_encode_all_types(getattr(val, field_name)) - ) for field_name in type_meta.field_names) - - self._encoders[klass] = encode - def __init__(self, cluster, hosts): self.cluster = cluster self.hosts = hosts @@ -1457,6 +1446,22 @@ def pool_finished_setting_keyspace(pool, host_errors): for pool in self._pools.values(): pool._set_keyspace_for_all_conns(keyspace, pool_finished_setting_keyspace) + def user_type_registered(self, keyspace, user_type, klass): + """ + Called by the parent Cluster instance when the user registers a new + mapping from a user-defined type to a class. Intended for internal + use only. + """ + type_meta = self.cluster.metadata.keyspaces[keyspace].user_types[user_type] + + def encode(val): + return '{ %s }' % ' , '.join('%s : %s' % ( + field_name, + cql_encode_all_types(getattr(val, field_name)) + ) for field_name in type_meta.field_names) + + self._encoders[klass] = encode + def submit(self, fn, *args, **kwargs): """ Internal """ if not self.is_shutdown: @@ -1521,7 +1526,7 @@ class ControlConnection(object): _SELECT_KEYSPACES = "SELECT * FROM system.schema_keyspaces" _SELECT_COLUMN_FAMILIES = "SELECT * FROM system.schema_columnfamilies" _SELECT_COLUMNS = "SELECT * FROM system.schema_columns" - _SELECT_TYPES = "SELECT * FROM system.schema_types" + _SELECT_USERTYPES = "SELECT * FROM system.schema_usertypes" _SELECT_PEERS = "SELECT peer, data_center, rack, tokens, rpc_address, schema_version FROM system.peers" _SELECT_LOCAL = "SELECT cluster_name, data_center, rack, tokens, partitioner, schema_version FROM system.local WHERE key='local'" @@ -1725,7 +1730,7 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, cf_result, col_result = connection.wait_for_responses( cf_query, col_query) - log.debug("[control connection] Fetched table info for %s.%s, rebuilding metadata", (keyspace, table)) + log.debug("[control connection] Fetched table info for %s.%s, rebuilding metadata", keyspace, table) cf_result = dict_factory(*cf_result.results) if cf_result else {} col_result = dict_factory(*col_result.results) if col_result else {} self._cluster.metadata.table_changed(keyspace, table, cf_result, col_result) @@ -1734,7 +1739,7 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, where_clause = " WHERE keyspace_name = '%s' AND type_name = '%s'" % (keyspace, usertype) types_query = QueryMessage(query=self._SELECT_USERTYPES + where_clause, consistency_level=cl) types_result = connection.wait_for_response(types_query) - log.debug("[control connection] Fetched user type info for %s.%s, rebuilding metadata", (keyspace, usertype)) + log.debug("[control connection] Fetched user type info for %s.%s, rebuilding metadata", keyspace, usertype) types_result = dict_factory(*types_result.results) if types_result.results else {} self._cluster.metadata.usertype_changed(keyspace, usertype, types_result) elif keyspace: @@ -1742,7 +1747,7 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, where_clause = " WHERE keyspace_name = '%s'" % (keyspace,) ks_query = QueryMessage(query=self._SELECT_KEYSPACES + where_clause, consistency_level=cl) ks_result = connection.wait_for_response(ks_query) - log.debug("[control connection] Fetched keyspace info for %s, rebuilding metadata", (keyspace,)) + log.debug("[control connection] Fetched keyspace info for %s, rebuilding metadata", keyspace) ks_result = dict_factory(*ks_result.results) if ks_result.results else {} self._cluster.metadata.keyspace_changed(keyspace, ks_result) else: diff --git a/cassandra/connection.py b/cassandra/connection.py index 6f90da26c1..c94cf9724d 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -344,7 +344,7 @@ def process_msg(self, msg, body_len): % (opcode, stream_id)) if body_len > 0: - body = msg[8:] + body = msg[self._full_header_length:] elif body_len == 0: body = six.binary_type() else: diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index c42ad33fb6..a09eddbe0f 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -297,8 +297,7 @@ def apply_parameters(cls, subtypes, names=None): if cls.num_subtypes != 'UNKNOWN' and len(subtypes) != cls.num_subtypes: raise ValueError("%s types require %d subtypes (%d given)" % (cls.typename, cls.num_subtypes, len(subtypes))) - # newname = cls.cass_parameterized_type_with(subtypes).encode('utf8') - newname = cls.cass_parameterized_type_with(subtypes) + newname = cls.cass_parameterized_type_with(subtypes).encode('utf8') return type(newname, (cls,), {'subtypes': subtypes, 'cassname': cls.cassname}) @classmethod @@ -782,6 +781,7 @@ class UserDefinedType(_ParameterizedType): @classmethod def apply_parameters(cls, keyspace, udt_name, names_and_types, mapped_class): + udt_name = udt_name.encode('utf-8') try: return cls._cache[(keyspace, udt_name)] except KeyError: diff --git a/cassandra/metadata.py b/cassandra/metadata.py index dceccb9f4f..a9bda180e1 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -119,7 +119,7 @@ def rebuild_schema(self, ks_results, type_results, cf_results, col_results): keyspace_meta.tables[table_meta.name] = table_meta for usertype_row in usertype_rows.get(keyspace_meta.name, []): - usertype = self._build_usertype(usertype_row) + usertype = self._build_usertype(keyspace_meta.name, usertype_row) keyspace_meta.user_types[usertype.name] = usertype current_keyspaces.add(keyspace_meta.name) @@ -149,6 +149,7 @@ def keyspace_changed(self, keyspace, ks_results): old_keyspace_meta = self.keyspaces.get(keyspace, None) self.keyspaces[keyspace] = keyspace_meta if old_keyspace_meta: + keyspace_meta.user_types = old_keyspace_meta.user_types if (keyspace_meta.replication_strategy != old_keyspace_meta.replication_strategy): self._keyspace_updated(keyspace) else: @@ -156,7 +157,7 @@ def keyspace_changed(self, keyspace, ks_results): def usertype_changed(self, keyspace, name, type_results): new_usertype = self._build_usertype(keyspace, type_results[0]) - self.user_types[name] = new_usertype + self.keyspaces[keyspace].user_types[name] = new_usertype def table_changed(self, keyspace, table, cf_results, col_results): try: diff --git a/cassandra/protocol.py b/cassandra/protocol.py index e0dad3ef11..2509599249 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -632,11 +632,11 @@ def read_type(cls, f, user_type_map): valsubtype = cls.read_type(f, user_type_map) typeclass = typeclass.apply_parameters((keysubtype, valsubtype)) elif typeclass == UserDefinedType: - ks = cls.read_string(f) - udt_name = cls.read_string(f) - num_fields = cls.read_short(f) - names_and_types = ((cls.read_string(f), cls.read_type(f, user_type_map)) - for _ in xrange(num_fields)) + ks = read_string(f) + udt_name = read_string(f) + num_fields = read_short(f) + names_and_types = tuple((read_string(f), cls.read_type(f, user_type_map)) + for _ in xrange(num_fields)) mapped_class = user_type_map.get(ks, {}).get(udt_name) typeclass = typeclass.apply_parameters( ks, udt_name, names_and_types, mapped_class) diff --git a/tests/integration/standard/test_udts.py b/tests/integration/standard/test_udts.py new file mode 100644 index 0000000000..df2bddfcab --- /dev/null +++ b/tests/integration/standard/test_udts.py @@ -0,0 +1,229 @@ +# Copyright 2013-2014 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + import unittest2 as unittest +except ImportError: + import unittest # noqa + +import logging +log = logging.getLogger(__name__) + +from collections import namedtuple + +from cassandra.cluster import Cluster + +from tests.integration import get_server_versions, PROTOCOL_VERSION + +import time + + +class TypeTests(unittest.TestCase): + + def setUp(self): + if PROTOCOL_VERSION < 3: + raise unittest.SkipTest("v3 protocol is required for UDT tests") + + self._cass_version, self._cql_version = get_server_versions() + + def test_unprepared_registered_udts(self): + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + + s.execute(""" + CREATE KEYSPACE udt_test_unprepared_registered + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_unprepared_registered") + s.execute("CREATE TYPE user (age int, name text)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + time.sleep(1) + + User = namedtuple('user', ('age', 'name')) + c.register_user_type("udt_test_unprepared_registered", "user", User) + + s.execute("INSERT INTO mytable (a, b) VALUES (%s, %s)", (0, User(42, 'bob'))) + result = s.execute("SELECT b FROM mytable WHERE a=0") + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual(42, row.b.age) + self.assertEqual('bob', row.b.name) + self.assertTrue(type(row.b) is User) + + # use the same UDT name in a different keyspace + s.execute(""" + CREATE KEYSPACE udt_test_unprepared_registered2 + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_unprepared_registered2") + s.execute("CREATE TYPE user (state text, is_cool boolean)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + time.sleep(1) + + User = namedtuple('user', ('state', 'is_cool')) + c.register_user_type("udt_test_unprepared_registered2", "user", User) + + s.execute("INSERT INTO mytable (a, b) VALUES (%s, %s)", (0, User('Texas', True))) + result = s.execute("SELECT b FROM mytable WHERE a=0") + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual('Texas', row.b.state) + self.assertEqual(True, row.b.is_cool) + self.assertTrue(type(row.b) is User) + + def test_register_before_connecting(self): + User1 = namedtuple('user', ('age', 'name')) + User2 = namedtuple('user', ('state', 'is_cool')) + + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + + s.execute(""" + CREATE KEYSPACE udt_test_register_before_connecting + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_register_before_connecting") + s.execute("CREATE TYPE user (age int, name text)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + s.execute(""" + CREATE KEYSPACE udt_test_register_before_connecting2 + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_register_before_connecting2") + s.execute("CREATE TYPE user (state text, is_cool boolean)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + time.sleep(1) + + # now that types are defined, shutdown and re-create Cluster + c.shutdown() + c = Cluster(protocol_version=PROTOCOL_VERSION) + c.register_user_type("udt_test_register_before_connecting", "user", User1) + c.register_user_type("udt_test_register_before_connecting2", "user", User2) + + s = c.connect() + + s.set_keyspace("udt_test_register_before_connecting") + s.execute("INSERT INTO mytable (a, b) VALUES (%s, %s)", (0, User1(42, 'bob'))) + result = s.execute("SELECT b FROM mytable WHERE a=0") + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual(42, row.b.age) + self.assertEqual('bob', row.b.name) + self.assertTrue(type(row.b) is User1) + + # use the same UDT name in a different keyspace + s.set_keyspace("udt_test_register_before_connecting2") + s.execute("INSERT INTO mytable (a, b) VALUES (%s, %s)", (0, User2('Texas', True))) + result = s.execute("SELECT b FROM mytable WHERE a=0") + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual('Texas', row.b.state) + self.assertEqual(True, row.b.is_cool) + self.assertTrue(type(row.b) is User2) + + def test_prepared_unregistered_udts(self): + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + + s.execute(""" + CREATE KEYSPACE udt_test_prepared_unregistered + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_prepared_unregistered") + s.execute("CREATE TYPE user (age int, name text)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + User = namedtuple('user', ('age', 'name')) + insert = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") + s.execute(insert, (0, User(42, 'bob'))) + + select = s.prepare("SELECT b FROM mytable WHERE a=?") + result = s.execute(select, (0,)) + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual(42, row.b.age) + self.assertEqual('bob', row.b.name) + + # use the same UDT name in a different keyspace + s.execute(""" + CREATE KEYSPACE udt_test_prepared_unregistered2 + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_prepared_unregistered2") + s.execute("CREATE TYPE user (state text, is_cool boolean)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + User = namedtuple('user', ('state', 'is_cool')) + insert = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") + s.execute(insert, (0, User('Texas', True))) + + select = s.prepare("SELECT b FROM mytable WHERE a=?") + result = s.execute(select, (0,)) + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual('Texas', row.b.state) + self.assertEqual(True, row.b.is_cool) + + def test_prepared_registered_udts(self): + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + + s.execute(""" + CREATE KEYSPACE udt_test_prepared_registered + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_prepared_registered") + s.execute("CREATE TYPE user (age int, name text)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + User = namedtuple('user', ('age', 'name')) + c.register_user_type("udt_test_prepared_registered", "user", User) + + insert = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") + s.execute(insert, (0, User(42, 'bob'))) + + select = s.prepare("SELECT b FROM mytable WHERE a=?") + result = s.execute(select, (0,)) + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual(42, row.b.age) + self.assertEqual('bob', row.b.name) + self.assertTrue(type(row.b) is User) + + # use the same UDT name in a different keyspace + s.execute(""" + CREATE KEYSPACE udt_test_prepared_registered2 + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + """) + s.set_keyspace("udt_test_prepared_registered2") + s.execute("CREATE TYPE user (state text, is_cool boolean)") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + + User = namedtuple('user', ('state', 'is_cool')) + c.register_user_type("udt_test_prepared_registered2", "user", User) + + insert = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") + s.execute(insert, (0, User('Texas', True))) + + select = s.prepare("SELECT b FROM mytable WHERE a=?") + result = s.execute(select, (0,)) + self.assertEqual(1, len(result)) + row = result[0] + self.assertEqual('Texas', row.b.state) + self.assertEqual(True, row.b.is_cool) + self.assertTrue(type(row.b) is User) From fd236ff5d6df040dade65c2829d11b7af6511c51 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 17:02:21 -0700 Subject: [PATCH 0811/3961] trying to properly create objects again --- cqlengine/connection.py | 13 +++++++------ cqlengine/management.py | 4 ++-- cqlengine/tests/model/test_model_io.py | 1 + 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index c7274e8170..732fc0bd63 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -84,7 +84,7 @@ def setup( cluster = Cluster(hosts) session = cluster.connect() - session.row_factory = dict_factory + #session.row_factory = dict_factory _max_connections = max_connections @@ -262,10 +262,11 @@ def execute(query, params=None, consistency_level=None): @contextmanager def connection_manager(): """ :rtype: ConnectionPool """ - global connection_pool - # tmp = connection_pool.get() - yield connection_pool - # connection_pool.put(tmp) + raise Exception("deprecated") + # global connection_pool + # # tmp = connection_pool.get() + # yield connection_pool + # # connection_pool.put(tmp) def execute_native(query, params=None, consistency_level=None): @@ -277,7 +278,7 @@ def execute_native(query, params=None, consistency_level=None): result = session.execute(query, params) if result: - keys = result[0].keys() + keys = [x for x in result[0]._fields] else: keys = [] diff --git a/cqlengine/management.py b/cqlengine/management.py index 14a0203fb7..142ea7a7bb 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -233,9 +233,9 @@ def get_fields(model): return [] try: - return [Field(x['column_name'], x['validator']) for x in tmp[1] if x['type'] == 'regular'] + return [Field(x.column_name, x.validator) for x in tmp[1] if x.type == 'regular'] except ValueError: - return [Field(x['column_name'], x['validator']) for x in tmp[1]] + return [Field(x.column_name, xvalidator) for x in tmp[1]] # convert to Field named tuples diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index e20e588571..8a6306249c 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -98,6 +98,7 @@ def test_column_deleting_works_properly(self): tm.save() tm2 = TestModel.objects(id=tm.pk).first() + assert tm2.text is None assert tm2._values['text'].previous_value is None From c359ac918115e636ec9355e19cd6ac55a1964ade Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 17:46:57 -0700 Subject: [PATCH 0812/3961] additional checks for basic io, trying to track down why Rows aren't becoming models --- cqlengine/tests/model/test_model_io.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 8a6306249c..7c26bae4a3 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -39,7 +39,10 @@ def test_model_save_and_load(self): Tests that models can be saved and retrieved """ tm = TestModel.create(count=8, text='123456789') + self.assertIsInstance(tm, TestModel) + tm2 = TestModel.objects(id=tm.pk).first() + self.assertIsInstance(tm2, TestModel) for cname in tm._columns.keys(): self.assertEquals(getattr(tm, cname), getattr(tm2, cname)) @@ -98,6 +101,7 @@ def test_column_deleting_works_properly(self): tm.save() tm2 = TestModel.objects(id=tm.pk).first() + self.assertIsInstance(tm2, TestModel) assert tm2.text is None assert tm2._values['text'].previous_value is None @@ -286,7 +290,9 @@ def tearDownClass(cls): def test_query_with_date(self): uid = uuid4() day = date(2013, 11, 26) - TestQueryModel.create(test_id=uid, date=day, description=u'foo') + obj = TestQueryModel.create(test_id=uid, date=day, description=u'foo') + + self.assertEqual(obj.description, u'foo') inst = TestQueryModel.filter( TestQueryModel.test_id == uid, From 421119bb69463ba214cb5435f7c98c494d7cb381 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 18:43:15 -0700 Subject: [PATCH 0813/3961] fixed the model construction, cache is broken though --- cqlengine/connection.py | 13 ++++++------- cqlengine/management.py | 6 ++---- cqlengine/models.py | 11 +++++++++-- cqlengine/query.py | 13 +++++++------ 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 732fc0bd63..9983aeac57 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -84,7 +84,8 @@ def setup( cluster = Cluster(hosts) session = cluster.connect() - #session.row_factory = dict_factory + session.row_factory = dict_factory + return _max_connections = max_connections @@ -249,6 +250,8 @@ def execute(self, query, params, consistency_level=None): def execute(query, params=None, consistency_level=None): + raise Exception("shut up") + if isinstance(query, BaseCQLStatement): params = query.get_context() query = str(query) @@ -274,15 +277,11 @@ def execute_native(query, params=None, consistency_level=None): if isinstance(query, BaseCQLStatement): params = query.get_context() query = str(query) + params = params or {} result = session.execute(query, params) - if result: - keys = [x for x in result[0]._fields] - else: - keys = [] - - return QueryResult(keys, result) + return result def get_session(): diff --git a/cqlengine/management.py b/cqlengine/management.py index 142ea7a7bb..a0716002da 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -229,13 +229,11 @@ def get_fields(model): # Tables containing only primary keys do not appear to create # any entries in system.schema_columns, as only non-primary-key attributes # appear to be inserted into the schema_columns table - if not tmp[1]: - return [] try: - return [Field(x.column_name, x.validator) for x in tmp[1] if x.type == 'regular'] + return [Field(x['column_name'], x['validator']) for x in tmp if x['type'] == 'regular'] except ValueError: - return [Field(x.column_name, xvalidator) for x in tmp[1]] + return [Field(x['column_name'], x['validator']) for x in tmp] # convert to Field named tuples diff --git a/cqlengine/models.py b/cqlengine/models.py index 7791da0ec8..028ab658e6 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -307,12 +307,17 @@ def _get_model_by_polymorphic_key(cls, key): return cls._polymorphic_map.get(key) @classmethod - def _construct_instance(cls, names, values): + def _construct_instance(cls, values): """ method used to construct instances from query results this is where polymorphic deserialization occurs """ - field_dict = dict((cls._db_map.get(k, k), v) for k, v in zip(names, values)) + # we're going to take the values, which is from the DB as a dict + # and translate that into our local fields + # the db_map is a db_field -> model field map + items = values.items() + field_dict = dict([(cls._db_map.get(k, k),v) for k,v in items]) + if cls._is_polymorphic: poly_key = field_dict.get(cls._polymorphic_column_name) @@ -703,6 +708,8 @@ def _get_polymorphic_base(bases): attrs['_columns'] = column_dict attrs['_primary_keys'] = primary_keys attrs['_defined_columns'] = defined_columns + + # maps the database field to the models key attrs['_db_map'] = db_map attrs['_pk_name'] = pk_name attrs['_dynamic_columns'] = {} diff --git a/cqlengine/query.py b/cqlengine/query.py index 94ae98675b..87a7d17624 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -6,6 +6,7 @@ from cqlengine.connection import execute, RowResult, execute_native + from cqlengine.exceptions import CQLEngineException, ValidationError from cqlengine.functions import Token, BaseQueryFunction, QueryValue @@ -293,8 +294,8 @@ def _execute_query(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: - columns, self._result_cache = self._execute(self._select_query()) - self._construct_result = self._get_result_constructor(columns) + self._result_cache = self._execute(self._select_query()) + self._construct_result = self._get_result_constructor() def _fill_result_cache_to_idx(self, idx): self._execute_query() @@ -319,7 +320,7 @@ def __iter__(self): for idx in range(len(self._result_cache)): instance = self._result_cache[idx] - if isinstance(instance, RowResult): + if isinstance(instance, dict): self._fill_result_cache_to_idx(idx) yield self._result_cache[idx] @@ -350,7 +351,7 @@ def __getitem__(self, s): self._fill_result_cache_to_idx(s) return self._result_cache[s] - def _get_result_constructor(self, names): + def _get_result_constructor(self): """ Returns a function that will be used to instantiate query results """ @@ -650,10 +651,10 @@ def _select_fields(self): return [self.model._columns[f].db_field_name for f in fields] return super(ModelQuerySet, self)._select_fields() - def _get_result_constructor(self, names): + def _get_result_constructor(self): """ Returns a function that will be used to instantiate query results """ if not self._values_list: - return lambda values: self.model._construct_instance(names, values) + return lambda rows: self.model._construct_instance(rows) else: columns = [self.model._columns[n] for n in names] if self._flat_values_list: From 7d58249b69082948cc36ad7392dc42a57c80a0f9 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 18 Jun 2014 19:09:02 -0700 Subject: [PATCH 0814/3961] starting on fixing value lists --- cqlengine/query.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 87a7d17624..a1caba9de1 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -653,14 +653,19 @@ def _select_fields(self): def _get_result_constructor(self): """ Returns a function that will be used to instantiate query results """ - if not self._values_list: + if not self._values_list: # we want models return lambda rows: self.model._construct_instance(rows) + elif self._flat_values_list: # the user has requested flattened tuples + return lambda rows: self._get_tuple_list_flat(rows) else: - columns = [self.model._columns[n] for n in names] - if self._flat_values_list: - return lambda values: columns[0].to_python(values[0]) - else: - return lambda values: map(lambda (c, v): c.to_python(v), zip(columns, values)) + return lambda rows: self._get_tuple_list_nested(rows) + + + def _get_tuple_list_nested(self, rows): + return map(lambda (c, v): c.to_python(v), zip(columns, rows)) + + def _get_tuple_list_flat(self, rows): + return columns[0].to_python(rows[0]) def _get_ordering_condition(self, colname): colname, order_type = super(ModelQuerySet, self)._get_ordering_condition(colname) From 997c5a85c5bc7cf64f73fa2278d16a6bc8113370 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 19 Jun 2014 11:59:01 +0200 Subject: [PATCH 0815/3961] Dumb down table properties unit tests. --- cqlengine/tests/management/test_management.py | 95 +++++++++++++------ 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index f0d7c6e909..d09c9e5543 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -152,39 +152,78 @@ def test_add_column(self): self.assertEqual(len(fields), 4) -class TablePropertiesTests(BaseCassEngTestCase): +class ModelWithTableProperties(Model): + # Set random table properties + __bloom_filter_fp_chance__ = 0.76328 + __caching__ = ALL + __comment__ = 'TxfguvBdzwROQALmQBOziRMbkqVGFjqcJfVhwGR' + __default_time_to_live__ = 4756 + __gc_grace_seconds__ = 2063 + __index_interval__ = 98706 + __memtable_flush_period_in_ms__ = 43681 + __populate_io_cache_on_flush__ = True + __read_repair_chance__ = 0.17985 + __replicate_on_write__ = False + __dclocal_read_repair_chance__ = 0.50811 + + key = columns.UUID(primary_key=True) + - table_properties = { - 'bloom_filter_fp_chance': random.randint(0, 99999) / 100000.0, - 'caching': random.choice([ALL, KEYS_ONLY, ROWS_ONLY, NONE]), - 'comment': ''.join(random.choice(string.ascii_letters) - for i in range(random.randint(4, 99))), - 'dclocal_read_repair_chance': random.randint(0, 99999) / 100000.0, - 'default_time_to_live': random.randint(0, 99999), - 'gc_grace_seconds': random.randint(0, 99999), - 'index_interval': random.randint(0, 99999), - 'memtable_flush_period_in_ms': random.randint(0, 99999), - 'populate_io_cache_on_flush': random.choice([True, False]), - 'read_repair_chance': random.randint(0, 99999) / 100000.0, - 'replicate_on_write': random.choice([True, False]), - } +class TablePropertiesTests(BaseCassEngTestCase): def setUp(self): - delete_table(FirstModel) + delete_table(ModelWithTableProperties) def test_set_table_properties(self): - for property_name, property_value in self.table_properties.items(): - property_id = '__{}__'.format(property_name) - setattr(FirstModel, property_id, property_value) - sync_table(FirstModel) - table_settings = management.get_table_settings(FirstModel) - for property_name, property_value in self.table_properties.items(): - if property_name == 'dclocal_read_repair_chance': - # For some reason 'dclocal_read_repair_chance' in CQL is called - # just 'local_read_repair_chance' in the schema table. - # Source: https://issues.apache.org/jira/browse/CASSANDRA-6717 - property_name = 'local_read_repair_chance' - assert table_settings[property_name] == property_value + create_table(ModelWithTableProperties) + self.assertDictContainsSubset({ + 'bloom_filter_fp_chance': 0.76328, + 'caching': ALL, + 'comment': 'TxfguvBdzwROQALmQBOziRMbkqVGFjqcJfVhwGR', + 'default_time_to_live': 4756, + 'gc_grace_seconds': 2063, + 'index_interval': 98706, + 'memtable_flush_period_in_ms': 43681, + 'populate_io_cache_on_flush': True, + 'read_repair_chance': 0.17985, + 'replicate_on_write': False, + # For some reason 'dclocal_read_repair_chance' in CQL is called + # just 'local_read_repair_chance' in the schema table. + # Source: https://issues.apache.org/jira/browse/CASSANDRA-6717 + 'local_read_repair_chance': 0.50811, + }, management.get_table_settings(ModelWithTableProperties)) + + def test_table_property_update(self): + # Create vanilla table + #sync_table(ModelWithTableProperties) + + ModelWithTableProperties.__bloom_filter_fp_chance__ = 0.66778 + ModelWithTableProperties.__caching__ = NONE + ModelWithTableProperties.__comment__ = 'xirAkRWZVVvsmzRvXamiEcQkshkUIDINVJZgLYSdnGHweiBrAiJdLJkVohdRy' + ModelWithTableProperties.__default_time_to_live__ = 65178 + ModelWithTableProperties.__gc_grace_seconds__ = 96362 + ModelWithTableProperties.__index_interval__ = 94207 + ModelWithTableProperties.__memtable_flush_period_in_ms__ = 60210 + ModelWithTableProperties.__populate_io_cache_on_flush__ = False + ModelWithTableProperties.__read_repair_chance__ = 0.2989 + ModelWithTableProperties.__replicate_on_write__ = True + ModelWithTableProperties.__dclocal_read_repair_chance__ = 0.12732 + + sync_table(ModelWithTableProperties) + + self.assertDictContainsSubset({ + 'bloom_filter_fp_chance': 0.66778, + 'caching': NONE, + 'comment': 'xirAkRWZVVvsmzRvXamiEcQkshkUIDINVJZgLYSdnGHweiBrAiJdLJkVohdRy', + 'default_time_to_live': 65178, + 'gc_grace_seconds': 96362, + 'index_interval': 94207, + 'memtable_flush_period_in_ms': 60210, + 'populate_io_cache_on_flush': False, + 'read_repair_chance': 0.2989, + 'replicate_on_write': True, + 'local_read_repair_chance': 0.12732, + }, management.get_table_settings(ModelWithTableProperties)) class SyncTableTests(BaseCassEngTestCase): From 3c56fec1d8d79d6037d3b83703608d4889994012 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Thu, 19 Jun 2014 12:01:29 +0200 Subject: [PATCH 0816/3961] Remove dead code and unused imports. --- cqlengine/tests/management/test_management.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index d09c9e5543..49ede1142b 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,9 +1,6 @@ -import random -import string - from mock import MagicMock, patch -from cqlengine import ONE, ALL, KEYS_ONLY, ROWS_ONLY, NONE +from cqlengine import ONE, ALL, NONE from cqlengine.exceptions import CQLEngineException from cqlengine.management import create_table, delete_table, get_fields, sync_table from cqlengine.tests.base import BaseCassEngTestCase @@ -194,9 +191,6 @@ def test_set_table_properties(self): }, management.get_table_settings(ModelWithTableProperties)) def test_table_property_update(self): - # Create vanilla table - #sync_table(ModelWithTableProperties) - ModelWithTableProperties.__bloom_filter_fp_chance__ = 0.66778 ModelWithTableProperties.__caching__ = NONE ModelWithTableProperties.__comment__ = 'xirAkRWZVVvsmzRvXamiEcQkshkUIDINVJZgLYSdnGHweiBrAiJdLJkVohdRy' From 9ee72896faa16a0a5d4151d3634295ffccfb6e52 Mon Sep 17 00:00:00 2001 From: Oliver Bock Date: Thu, 19 Jun 2014 15:03:18 +0200 Subject: [PATCH 0817/3961] Updated Debian-specific patch (failed to apply) * Kept original patch author's in-line comment for reference * Updated debian/changelog accordingly, just to make sure * Added patch validation step to release protocol in README-dev --- README-dev.rst | 1 + debian/changelog | 6 ++++++ debian/patches/0001-don-t-use-ez_setup.patch | 7 ++++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README-dev.rst b/README-dev.rst index 8c6a13bb0a..5242e5d3d3 100644 --- a/README-dev.rst +++ b/README-dev.rst @@ -3,6 +3,7 @@ Releasing * Run the tests and ensure they all pass * If dependencies have changed, make sure ``debian/control`` is up to date +* Make sure all patches in ``debian/patches`` still apply cleanly * Update CHANGELOG.rst * Update the version in ``cassandra/__init__.py`` * Commit the changelog and version changes diff --git a/debian/changelog b/debian/changelog index 0360d9064f..1568c12f84 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cassandra-driver (2.0.2-2) unstable; urgency=low + + * Fixed debian/patches + + -- Carsten Aulbert Thu, 19 Jun 2014 14:07:27 +0200 + python-cassandra-driver (2.0.2-1) unstable; urgency=low * Release 2.0.2 diff --git a/debian/patches/0001-don-t-use-ez_setup.patch b/debian/patches/0001-don-t-use-ez_setup.patch index ece0bef856..7e73a67aca 100644 --- a/debian/patches/0001-don-t-use-ez_setup.patch +++ b/debian/patches/0001-don-t-use-ez_setup.patch @@ -18,7 +18,8 @@ diff --git a/setup.py b/setup.py index 0c28d3d..c0fd6c1 100644 --- a/setup.py +++ b/setup.py -@@ -1,8 +1,5 @@ +@@ -15,9 +15,6 @@ + from __future__ import print_function import sys -import ez_setup @@ -27,12 +28,12 @@ index 0c28d3d..c0fd6c1 100644 if __name__ == '__main__' and sys.argv[1] == "gevent_nosetests": from gevent.monkey import patch_all patch_all() -@@ -174,8 +171,8 @@ def run_setup(extensions): +@@ -192,8 +189,8 @@ author_email='tyler@datastax.com', packages=['cassandra', 'cassandra.io'], include_package_data=True, - install_requires=dependencies, -- tests_require=['nose', 'mock', 'ccm', 'unittest2', 'PyYAML', 'pytz'], +- tests_require=['nose', 'mock', 'PyYAML', 'pytz'], + install_requires=(), + tests_require=(), classifiers=[ From c80d463b8d4f19909e022233fb3412a3b5c21244 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:12:49 -0700 Subject: [PATCH 0818/3961] note in test so I know what exactly is supposed to come back --- cqlengine/tests/model/test_clustering_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cqlengine/tests/model/test_clustering_order.py b/cqlengine/tests/model/test_clustering_order.py index 92d8e067e8..528ea2e01b 100644 --- a/cqlengine/tests/model/test_clustering_order.py +++ b/cqlengine/tests/model/test_clustering_order.py @@ -32,4 +32,5 @@ def test_clustering_order(self): TestModel.create(id=1, clustering_key=i) values = list(TestModel.objects.values_list('clustering_key', flat=True)) + # [19L, 18L, 17L, 16L, 15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L, 0L] self.assertEquals(values, sorted(items, reverse=True)) From 3d7d97528ae9087269defa340cee902d17481caf Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:16:05 -0700 Subject: [PATCH 0819/3961] fixed docs around values lists while i'm in here --- docs/topics/queryset.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index a8eb2af576..ddb2437152 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -268,6 +268,17 @@ Values Lists There is a special QuerySet's method ``.values_list()`` - when called, QuerySet returns lists of values instead of model instances. It may significantly speedup things with lower memory footprint for large responses. Each tuple contains the value from the respective field passed into the ``values_list()`` call — so the first item is the first field, etc. For example: + .. code-block:: python + + items = list(range(20)) + random.shuffle(items) + for i in items: + TestModel.create(id=1, clustering_key=i) + + values = list(TestModel.objects.values_list('clustering_key', flat=True)) + # [19L, 18L, 17L, 16L, 15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L, 0L] + + Batch Queries ============= From ed2749f235655c8d8d66776671c37492f2e2bfb7 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:22:28 -0700 Subject: [PATCH 0820/3961] fixed docs error --- docs/topics/queryset.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index ddb2437152..a8dca85356 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -269,7 +269,7 @@ Values Lists Each tuple contains the value from the respective field passed into the ``values_list()`` call — so the first item is the first field, etc. For example: .. code-block:: python - + items = list(range(20)) random.shuffle(items) for i in items: From 5f2dce51a5269ba368f5ac71c366c22ea1fea27e Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:30:10 -0700 Subject: [PATCH 0821/3961] test around clustering order --- .../tests/model/test_clustering_order.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/model/test_clustering_order.py b/cqlengine/tests/model/test_clustering_order.py index 92d8e067e8..98e1277b87 100644 --- a/cqlengine/tests/model/test_clustering_order.py +++ b/cqlengine/tests/model/test_clustering_order.py @@ -1,7 +1,7 @@ import random from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.management import create_table +from cqlengine.management import sync_table from cqlengine.management import delete_table from cqlengine.models import Model from cqlengine import columns @@ -10,12 +10,17 @@ class TestModel(Model): id = columns.Integer(primary_key=True) clustering_key = columns.Integer(primary_key=True, clustering_order='desc') +class TestClusteringComplexModel(Model): + id = columns.Integer(primary_key=True) + clustering_key = columns.Integer(primary_key=True, clustering_order='desc') + some_value = columns.Integer() + class TestClusteringOrder(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestClusteringOrder, cls).setUpClass() - create_table(TestModel) + sync_table(TestModel) @classmethod def tearDownClass(cls): @@ -33,3 +38,19 @@ def test_clustering_order(self): values = list(TestModel.objects.values_list('clustering_key', flat=True)) self.assertEquals(values, sorted(items, reverse=True)) + + def test_clustering_order_more_complex(self): + """ + Tests that models can be saved and retrieved + """ + sync_table(TestClusteringComplexModel) + + items = list(range(20)) + random.shuffle(items) + for i in items: + TestClusteringComplexModel.create(id=1, clustering_key=i, some_value=2) + + values = list(TestClusteringComplexModel.objects.values_list('some_value', flat=True)) + + self.assertEquals([2] * 20, values) + delete_table(TestClusteringComplexModel) From 14594276ce584ce461934515c8e10db79a78f747 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 19 Jun 2014 11:34:04 -0500 Subject: [PATCH 0822/3961] Only utf8 encode type names in Python 2 --- cassandra/cqltypes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index a09eddbe0f..71a5811447 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -297,7 +297,9 @@ def apply_parameters(cls, subtypes, names=None): if cls.num_subtypes != 'UNKNOWN' and len(subtypes) != cls.num_subtypes: raise ValueError("%s types require %d subtypes (%d given)" % (cls.typename, cls.num_subtypes, len(subtypes))) - newname = cls.cass_parameterized_type_with(subtypes).encode('utf8') + newname = cls.cass_parameterized_type_with(subtypes) + if six.PY2 and isinstance(newname, unicode): + newname = newname.encode('utf-8') return type(newname, (cls,), {'subtypes': subtypes, 'cassname': cls.cassname}) @classmethod @@ -781,7 +783,9 @@ class UserDefinedType(_ParameterizedType): @classmethod def apply_parameters(cls, keyspace, udt_name, names_and_types, mapped_class): - udt_name = udt_name.encode('utf-8') + if six.PY2 and isinstance(udt_name, unicode): + udt_name = udt_name.encode('utf-8') + try: return cls._cache[(keyspace, udt_name)] except KeyError: From 4a2d3b9f9ce6b2eaf311daffd47f6c3e92f3ea44 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:35:42 -0700 Subject: [PATCH 0823/3961] fixed flat query --- cqlengine/query.py | 6 +----- cqlengine/tests/model/test_clustering_order.py | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index a1caba9de1..92f0823284 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -656,17 +656,13 @@ def _get_result_constructor(self): if not self._values_list: # we want models return lambda rows: self.model._construct_instance(rows) elif self._flat_values_list: # the user has requested flattened tuples - return lambda rows: self._get_tuple_list_flat(rows) + return lambda row: row.popitem()[1] else: return lambda rows: self._get_tuple_list_nested(rows) - def _get_tuple_list_nested(self, rows): return map(lambda (c, v): c.to_python(v), zip(columns, rows)) - def _get_tuple_list_flat(self, rows): - return columns[0].to_python(rows[0]) - def _get_ordering_condition(self, colname): colname, order_type = super(ModelQuerySet, self)._get_ordering_condition(colname) diff --git a/cqlengine/tests/model/test_clustering_order.py b/cqlengine/tests/model/test_clustering_order.py index d523daf8c6..6c94a7f6c9 100644 --- a/cqlengine/tests/model/test_clustering_order.py +++ b/cqlengine/tests/model/test_clustering_order.py @@ -2,7 +2,7 @@ from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.management import sync_table -from cqlengine.management import delete_table +from cqlengine.management import drop_table from cqlengine.models import Model from cqlengine import columns @@ -25,7 +25,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): super(TestClusteringOrder, cls).tearDownClass() - delete_table(TestModel) + drop_table(TestModel) def test_clustering_order(self): """ @@ -54,4 +54,4 @@ def test_clustering_order_more_complex(self): values = list(TestClusteringComplexModel.objects.values_list('some_value', flat=True)) self.assertEquals([2] * 20, values) - delete_table(TestClusteringComplexModel) + drop_table(TestClusteringComplexModel) From 879339c01600fbacb7632705cd97c9835d51d2e3 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:38:21 -0700 Subject: [PATCH 0824/3961] fixing value lists --- cqlengine/query.py | 2 +- .../model/{test_clustering_order.py => test_value_lists.py} | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) rename cqlengine/tests/model/{test_clustering_order.py => test_value_lists.py} (99%) diff --git a/cqlengine/query.py b/cqlengine/query.py index 92f0823284..95f6529706 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -655,7 +655,7 @@ def _get_result_constructor(self): """ Returns a function that will be used to instantiate query results """ if not self._values_list: # we want models return lambda rows: self.model._construct_instance(rows) - elif self._flat_values_list: # the user has requested flattened tuples + elif self._flat_values_list: # the user has requested flattened list (1 value per row) return lambda row: row.popitem()[1] else: return lambda rows: self._get_tuple_list_nested(rows) diff --git a/cqlengine/tests/model/test_clustering_order.py b/cqlengine/tests/model/test_value_lists.py similarity index 99% rename from cqlengine/tests/model/test_clustering_order.py rename to cqlengine/tests/model/test_value_lists.py index 6c94a7f6c9..8d4c76de5c 100644 --- a/cqlengine/tests/model/test_clustering_order.py +++ b/cqlengine/tests/model/test_value_lists.py @@ -6,6 +6,7 @@ from cqlengine.models import Model from cqlengine import columns + class TestModel(Model): id = columns.Integer(primary_key=True) clustering_key = columns.Integer(primary_key=True, clustering_order='desc') @@ -55,3 +56,4 @@ def test_clustering_order_more_complex(self): self.assertEquals([2] * 20, values) drop_table(TestClusteringComplexModel) + From eed5d3ee7d2b9e31757f89a9ae6cbdde7611aa27 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:51:43 -0700 Subject: [PATCH 0825/3961] now calling native execute in batch --- cqlengine/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 95f6529706..71f3345d54 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -174,7 +174,7 @@ def execute(self): query_list.append('APPLY BATCH;') - execute('\n'.join(query_list), parameters, self._consistency) + execute_native('\n'.join(query_list), parameters, self._consistency) self.queries = [] self._execute_callbacks() From f9baed3d99064b748c54c88bd2779fe117367408 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:53:36 -0700 Subject: [PATCH 0826/3961] gutting the pool out --- cqlengine/connection.py | 27 --------------------------- cqlengine/tests/test_batch_query.py | 1 - 2 files changed, 28 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 9983aeac57..90a54a975c 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -85,34 +85,7 @@ def setup( cluster = Cluster(hosts) session = cluster.connect() session.row_factory = dict_factory - return - _max_connections = max_connections - - if default_keyspace: - from cqlengine import models - models.DEFAULT_KEYSPACE = default_keyspace - - _hosts = [] - for host in hosts: - host = host.strip() - host = host.split(':') - if len(host) == 1: - port = 9160 - elif len(host) == 2: - try: - port = int(host[1]) - except ValueError: - raise CQLConnectionError("Can't parse port as int {}".format(':'.join(host))) - else: - raise CQLConnectionError("Can't parse host string {}".format(':'.join(host))) - - _hosts.append(Host(host[0], port)) - - if not _hosts: - raise CQLConnectionError("At least one host required") - - connection_pool = ConnectionPool(_hosts, username, password, consistency, timeout) class ConnectionPool(object): diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index 7066643d39..9c2d4140c8 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -1,7 +1,6 @@ from unittest import skip from uuid import uuid4 import random -from cqlengine.connection import ConnectionPool import mock import sure From fdf142b19f7069a69cedd230e211aab907028969 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 09:56:50 -0700 Subject: [PATCH 0827/3961] count fixed, mock switched from connection pool to session --- cqlengine/query.py | 4 ++-- cqlengine/tests/model/test_polymorphism.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 71f3345d54..3592400228 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -507,8 +507,8 @@ def count(self): if self._result_cache is None: query = self._select_query() query.count = True - _, result = self._execute(query) - return result[0][0] + result = self._execute(query) + return result[0]['count'] else: return len(self._result_cache) diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index f950bbcdf8..197fcf712a 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -3,7 +3,7 @@ from cqlengine import columns from cqlengine import models -from cqlengine.connection import ConnectionPool +from cqlengine.connection import ConnectionPool, get_session from cqlengine.tests.base import BaseCassEngTestCase from cqlengine import management @@ -122,8 +122,8 @@ def test_query_deserialization(self): def test_delete_on_polymorphic_subclass_does_not_include_polymorphic_key(self): p1 = Poly1.create() - - with mock.patch.object(ConnectionPool, 'execute') as m: + session = get_session() + with mock.patch.object(session, 'execute') as m: Poly1.objects(partition=p1.partition).delete() # make sure our polymorphic key isn't in the CQL From 794e8dfd8450759be0e6ef7e6839414ec1f68025 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 10:11:45 -0700 Subject: [PATCH 0828/3961] fixing empty map issue --- cqlengine/statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index ce5d5fc47c..ac4f281d6f 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -355,7 +355,7 @@ def __unicode__(self): ctx_id = self.context_id if self.previous is None and not self._updates: - qs += ['"int_map" = %({})s'] + qs += ['"int_map" = %({})s'.format(ctx_id)] else: for _ in self._updates or []: qs += ['"{}"[%({})s] = %({})s'.format(self.field, ctx_id, ctx_id + 1)] From 4eeed3c2dc55bfa5e4d71b4a817e1b36bb911e79 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 10:30:23 -0700 Subject: [PATCH 0829/3961] fix for values list --- cqlengine/query.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 3592400228..e0219db9a2 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -658,10 +658,13 @@ def _get_result_constructor(self): elif self._flat_values_list: # the user has requested flattened list (1 value per row) return lambda row: row.popitem()[1] else: - return lambda rows: self._get_tuple_list_nested(rows) + return lambda row: self._get_row_value_list(self._only_fields, row) - def _get_tuple_list_nested(self, rows): - return map(lambda (c, v): c.to_python(v), zip(columns, rows)) + def _get_row_value_list(self, fields, row): + result = [] + for x in fields: + result.append(row[x]) + return result def _get_ordering_condition(self, colname): colname, order_type = super(ModelQuerySet, self)._get_ordering_condition(colname) From 1baa2b7faa20b806f773106c52a05465a53301dc Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 10:37:57 -0700 Subject: [PATCH 0830/3961] fixed a few fails --- cqlengine/tests/base.py | 6 ++++++ cqlengine/tests/test_consistency.py | 14 +++++++------- cqlengine/tests/test_timestamp.py | 14 +++++++------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index 35b7cca97a..c8679c1b21 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -1,6 +1,7 @@ from unittest import TestCase from cqlengine import connection import os +from cqlengine.connection import get_session if os.environ.get('CASSANDRA_TEST_HOST'): @@ -15,6 +16,11 @@ class BaseCassEngTestCase(TestCase): # @classmethod # def setUpClass(cls): # super(BaseCassEngTestCase, cls).setUpClass() + session = None + + def setUp(self): + self.session = get_session() + super(BaseCassEngTestCase, self).setUp() def assertHasAttr(self, obj, attr): self.assertTrue(hasattr(obj, attr), diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 2f54ae243c..44dbd08ea4 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -29,7 +29,7 @@ class TestConsistency(BaseConsistencyTest): def test_create_uses_consistency(self): qs = TestConsistencyModel.consistency(ALL) - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(self.session, 'execute') as m: qs.create(text="i am not fault tolerant this way") args = m.call_args @@ -43,7 +43,7 @@ def test_update_uses_consistency(self): t = TestConsistencyModel.create(text="bacon and eggs") t.text = "ham sandwich" - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(self.session, 'execute') as m: t.consistency(ALL).save() args = m.call_args @@ -52,14 +52,14 @@ def test_update_uses_consistency(self): def test_batch_consistency(self): - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(self.session, 'execute') as m: with BatchQuery(consistency=ALL) as b: TestConsistencyModel.batch(b).create(text="monkey") args = m.call_args self.assertEqual(ALL, args[0][2]) - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(self.session, 'execute') as m: with BatchQuery() as b: TestConsistencyModel.batch(b).create(text="monkey") @@ -71,7 +71,7 @@ def test_blind_update(self): t.text = "ham sandwich" uid = t.id - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(self.session, 'execute') as m: TestConsistencyModel.objects(id=uid).consistency(ALL).update(text="grilled cheese") args = m.call_args @@ -84,10 +84,10 @@ def test_delete(self): t.text = "ham and cheese sandwich" uid = t.id - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(self.session, 'execute') as m: t.consistency(ALL).delete() - with mock.patch.object(ConnectionPool, 'execute') as m: + with mock.patch.object(self.session, 'execute') as m: TestConsistencyModel.objects(id=uid).consistency(ALL).delete() args = m.call_args diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 261564319f..e59466e999 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -7,7 +7,6 @@ import mock import sure from cqlengine import Model, columns, BatchQuery -from cqlengine.connection import ConnectionPool from cqlengine.management import sync_table from cqlengine.tests.base import BaseCassEngTestCase @@ -28,7 +27,7 @@ def setUpClass(cls): class BatchTest(BaseTimestampTest): def test_batch_is_included(self): - with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery(timestamp=timedelta(seconds=30)) as b: + with mock.patch.object(self.session, "execute") as m, BatchQuery(timestamp=timedelta(seconds=30)) as b: TestTimestampModel.batch(b).create(count=1) "USING TIMESTAMP".should.be.within(m.call_args[0][0]) @@ -37,7 +36,7 @@ def test_batch_is_included(self): class CreateWithTimestampTest(BaseTimestampTest): def test_batch(self): - with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery() as b: + with mock.patch.object(self.session, "execute") as m, BatchQuery() as b: TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1) query = m.call_args[0][0] @@ -46,7 +45,7 @@ def test_batch(self): query.should_not.match(r"TIMESTAMP.*INSERT") def test_timestamp_not_included_on_normal_create(self): - with mock.patch.object(ConnectionPool, "execute") as m: + with mock.patch.object(self.session, "execute") as m: TestTimestampModel.create(count=2) "USING TIMESTAMP".shouldnt.be.within(m.call_args[0][0]) @@ -62,7 +61,7 @@ def test_non_batch_syntax_integration(self): def test_non_batch_syntax_unit(self): - with mock.patch.object(ConnectionPool, "execute") as m: + with mock.patch.object(self.session, "execute") as m: TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) query = m.call_args[0][0] @@ -73,17 +72,18 @@ def test_non_batch_syntax_unit(self): class UpdateWithTimestampTest(BaseTimestampTest): def setUp(self): self.instance = TestTimestampModel.create(count=1) + super(UpdateWithTimestampTest, self).setUp() def test_instance_update_includes_timestamp_in_query(self): # not a batch - with mock.patch.object(ConnectionPool, "execute") as m: + with mock.patch.object(self.session, "execute") as m: self.instance.timestamp(timedelta(seconds=30)).update(count=2) "USING TIMESTAMP".should.be.within(m.call_args[0][0]) def test_instance_update_in_batch(self): - with mock.patch.object(ConnectionPool, "execute") as m, BatchQuery() as b: + with mock.patch.object(self.session, "execute") as m, BatchQuery() as b: self.instance.batch(b).timestamp(timedelta(seconds=30)).update(count=2) query = m.call_args[0][0] From 2250762395736d7b620262b36eb9045b36fc0962 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 10:43:52 -0700 Subject: [PATCH 0831/3961] fixing batching constructors --- cqlengine/query.py | 4 ++-- upgrading.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index e0219db9a2..3651ab29fd 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -614,12 +614,12 @@ class SimpleQuerySet(AbstractQuerySet): """ - def _get_result_constructor(self, names): + def _get_result_constructor(self): """ Returns a function that will be used to instantiate query results """ def _construct_instance(values): - return ResultObject(zip(names, values)) + return ResultObject(values) return _construct_instance diff --git a/upgrading.txt b/upgrading.txt index cf82097bff..4f941aa1c1 100644 --- a/upgrading.txt +++ b/upgrading.txt @@ -3,3 +3,4 @@ Calls to execute should no longer use named placeholders. We no longer raise cqlengine based OperationalError when connection fails. Now using the exception thrown in the native driver. +If you're calling setup() with the old thrift port in place, you will get connection errors. Either remove it (the default native port is assumed) or change to the default native port, 9042. From 5280d526aaac9d9d16243404b474640968479308 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 10:45:30 -0700 Subject: [PATCH 0832/3961] removing connection pool --- cqlengine/tests/model/test_updates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/model/test_updates.py b/cqlengine/tests/model/test_updates.py index c9ab45d9de..df60d5d779 100644 --- a/cqlengine/tests/model/test_updates.py +++ b/cqlengine/tests/model/test_updates.py @@ -69,11 +69,11 @@ def test_noop_model_update(self): """ tests that calling update on a model with no changes will do nothing. """ m0 = TestUpdateModel.create(count=5, text='monkey') - with patch.object(ConnectionPool, 'execute') as execute: + with patch.object(self.session, 'execute') as execute: m0.update() assert execute.call_count == 0 - with patch.object(ConnectionPool, 'execute') as execute: + with patch.object(self.session, 'execute') as execute: m0.update(count=5) assert execute.call_count == 0 From e30e99c6dcd0c7fdd3397f700c41a1f6d3e10af5 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 10:47:08 -0700 Subject: [PATCH 0833/3961] removed connection pool and all references --- cqlengine/connection.py | 134 --------------------- cqlengine/tests/columns/test_validation.py | 6 +- cqlengine/tests/model/test_polymorphism.py | 2 +- cqlengine/tests/model/test_updates.py | 1 - cqlengine/tests/test_consistency.py | 1 - cqlengine/tests/test_ttl.py | 2 +- 6 files changed, 5 insertions(+), 141 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 90a54a975c..5bf93e5d47 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -87,140 +87,6 @@ def setup( session.row_factory = dict_factory - -class ConnectionPool(object): - """Handles pooling of database connections.""" - - def __init__( - self, - hosts, - username=None, - password=None, - consistency=None, - timeout=None): - self._hosts = hosts - self._username = username - self._password = password - self._consistency = consistency - self._timeout = timeout - - self._queue = queue.Queue(maxsize=_max_connections) - - def clear(self): - """ - Force the connection pool to be cleared. Will close all internal - connections. - """ - try: - while not self._queue.empty(): - self._queue.get().close() - except: - pass - - def get(self): - """ - Returns a usable database connection. Uses the internal queue to - determine whether to return an existing connection or to create - a new one. - """ - try: - # get with block=False returns an item if one - # is immediately available, else raises the Empty exception - return self._queue.get(block=False) - except queue.Empty: - return self._create_connection() - - def put(self, conn): - """ - Returns a connection to the queue freeing it up for other queries to - use. - - :param conn: The connection to be released - :type conn: connection - """ - - try: - self._queue.put(conn, block=False) - except queue.Full: - conn.close() - - def _create_transport(self, host): - """ - Create a new Thrift transport for the given host. - - :param host: The host object - :type host: Host - - :rtype: thrift.TTransport.* - - """ - from thrift.transport import TSocket, TTransport - - thrift_socket = TSocket.TSocket(host.name, host.port) - - if self._timeout is not None: - thrift_socket.setTimeout(self._timeout) - - return TTransport.TFramedTransport(thrift_socket) - - def _create_connection(self): - """ - Creates a new connection for the connection pool. - - should only return a valid connection that it's actually connected to - """ - if not self._hosts: - raise CQLConnectionError("At least one host required") - - hosts = copy(self._hosts) - random.shuffle(hosts) - - for host in hosts: - try: - transport = self._create_transport(host) - new_conn = cql.connect( - host.name, - host.port, - user=self._username, - password=self._password, - consistency_level=self._consistency, - transport=transport - ) - new_conn.set_cql_version('3.0.0') - return new_conn - except Exception as exc: - logging.debug("Could not establish connection to" - " {}:{} ({!r})".format(host.name, host.port, exc)) - - raise CQLConnectionError("Could not connect to any server in cluster") - - def execute(self, query, params, consistency_level=None): - if not consistency_level: - consistency_level = self._consistency - - while True: - try: - con = self.get() - if not con: - raise CQLEngineException("Error calling execute without calling setup.") - LOG.debug('{} {}'.format(query, repr(params))) - cur = con.cursor() - cur.execute(query, params, consistency_level=consistency_level) - columns = [i[0] for i in cur.description or []] - results = [RowResult(r) for r in cur.fetchall()] - self.put(con) - return QueryResult(columns, results) - except CQLConnectionError as ex: - raise CQLEngineException("Could not execute query against the cluster") - except cql.ProgrammingError as ex: - raise CQLEngineException(unicode(ex)) - except TTransportException: - pass - except OperationalError as ex: - LOG.exception("Operational Error %s on %s:%s", ex, con.host, con.port) - raise ex - - def execute(query, params=None, consistency_level=None): raise Exception("shut up") diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 4232b4003c..57f7aaf20c 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -6,7 +6,7 @@ from unittest import TestCase from uuid import uuid4, uuid1 from cqlengine import ValidationError -from cqlengine.connection import execute +from cqlengine.connection import execute, execute_native from cqlengine.tests.base import BaseCassEngTestCase @@ -175,7 +175,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): super(TestUUID, cls).tearDownClass() - delete_table(cls.UUIDTest) + delete_table(cls.UUIDTest) def test_uuid_str_with_dashes(self): a_uuid = uuid4() @@ -298,7 +298,7 @@ def test_extra_field(self): drop_table(self.TestModel) sync_table(self.TestModel) self.TestModel.create() - execute("ALTER TABLE {} add blah int".format(self.TestModel.column_family_name(include_keyspace=True))) + execute_native("ALTER TABLE {} add blah int".format(self.TestModel.column_family_name(include_keyspace=True))) self.TestModel.objects().all() class TestTimeUUIDFromDatetime(TestCase): diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 197fcf712a..7db8bb4efd 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -3,7 +3,7 @@ from cqlengine import columns from cqlengine import models -from cqlengine.connection import ConnectionPool, get_session +from cqlengine.connection import get_session from cqlengine.tests.base import BaseCassEngTestCase from cqlengine import management diff --git a/cqlengine/tests/model/test_updates.py b/cqlengine/tests/model/test_updates.py index df60d5d779..6141436c8f 100644 --- a/cqlengine/tests/model/test_updates.py +++ b/cqlengine/tests/model/test_updates.py @@ -7,7 +7,6 @@ from cqlengine.models import Model from cqlengine import columns from cqlengine.management import sync_table, drop_table -from cqlengine.connection import ConnectionPool class TestUpdateModel(Model): diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 44dbd08ea4..9821080b12 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -4,7 +4,6 @@ from uuid import uuid4 from cqlengine import columns import mock -from cqlengine.connection import ConnectionPool from cqlengine import ALL, BatchQuery class TestConsistencyModel(Model): diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index b55254a5eb..3d906b78e5 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -4,7 +4,7 @@ from uuid import uuid4 from cqlengine import columns import mock -from cqlengine.connection import ConnectionPool, get_session +from cqlengine.connection import get_session class TestTTLModel(Model): From 281fe43ea4609b2ce7b6c0a2f8e8013d3d13b940 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 10:48:27 -0700 Subject: [PATCH 0834/3961] removed connection pool and all references --- upgrading.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/upgrading.txt b/upgrading.txt index 4f941aa1c1..6a13ce7acc 100644 --- a/upgrading.txt +++ b/upgrading.txt @@ -1,6 +1,10 @@ 0.15 Upgrade to use Datastax Native Driver -Calls to execute should no longer use named placeholders. We no longer raise cqlengine based OperationalError when connection fails. Now using the exception thrown in the native driver. If you're calling setup() with the old thrift port in place, you will get connection errors. Either remove it (the default native port is assumed) or change to the default native port, 9042. + +The cqlengine connection pool has been removed. Connections are now managed by the native driver. This should drastically reduce the socket overhead as the native driver can multiplex queries. + + + From 6cfa0df73e9d42013e1fdd2ea7adfd1489bac0f9 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 12:33:55 -0700 Subject: [PATCH 0835/3961] removed old context manager --- cqlengine/connection.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 5bf93e5d47..1891da2c8c 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -94,6 +94,7 @@ def execute(query, params=None, consistency_level=None): if isinstance(query, BaseCQLStatement): params = query.get_context() query = str(query) + params = params or {} if consistency_level is None: consistency_level = connection_pool._consistency @@ -101,15 +102,6 @@ def execute(query, params=None, consistency_level=None): #return execute_native(query, params, consistency_level) -@contextmanager -def connection_manager(): - """ :rtype: ConnectionPool """ - raise Exception("deprecated") - # global connection_pool - # # tmp = connection_pool.get() - # yield connection_pool - # # connection_pool.put(tmp) - def execute_native(query, params=None, consistency_level=None): # TODO use consistency level From faffdfe912f7b511db47fee319b445de964b8b75 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 12:46:26 -0700 Subject: [PATCH 0836/3961] removed unused imports --- cqlengine/connection.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 1891da2c8c..3e14faaa8a 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -10,19 +10,11 @@ except ImportError: # python 3 import queue -import random -import cql import logging -from copy import copy from cqlengine.exceptions import CQLEngineException -from cql import OperationalError - -from contextlib import contextmanager - -from thrift.transport.TTransport import TTransportException from cqlengine.statements import BaseCQLStatement from cassandra.query import dict_factory @@ -94,7 +86,7 @@ def execute(query, params=None, consistency_level=None): if isinstance(query, BaseCQLStatement): params = query.get_context() query = str(query) - + params = params or {} if consistency_level is None: consistency_level = connection_pool._consistency From 1d58e740bd06e6ee68ca325b1ee16ad6a322c3ad Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 13:37:57 -0700 Subject: [PATCH 0837/3961] fixing consistency level --- cqlengine/__init__.py | 19 +++++++++-------- cqlengine/connection.py | 33 ++++++----------------------- cqlengine/query.py | 2 +- cqlengine/tests/test_consistency.py | 12 +++++------ upgrading.txt | 3 +-- 5 files changed, 25 insertions(+), 44 deletions(-) diff --git a/cqlengine/__init__.py b/cqlengine/__init__.py index cd9f8f3557..c6e3965350 100644 --- a/cqlengine/__init__.py +++ b/cqlengine/__init__.py @@ -1,5 +1,7 @@ import pkg_resources +from cassandra import ConsistencyLevel + from cqlengine.columns import * from cqlengine.functions import * from cqlengine.models import Model @@ -15,12 +17,11 @@ LeveledCompactionStrategy = "LeveledCompactionStrategy" -ANY = "ANY" -ONE = "ONE" -TWO = "TWO" -THREE = "THREE" -QUORUM = "QUORUM" -LOCAL_QUORUM = "LOCAL_QUORUM" -EACH_QUORUM = "EACH_QUORUM" -ALL = "ALL" - +ANY = ConsistencyLevel.ANY +ONE = ConsistencyLevel.ONE +TWO = ConsistencyLevel.TWO +THREE = ConsistencyLevel.THREE +QUORUM = ConsistencyLevel.QUORUM +LOCAL_QUORUM = ConsistencyLevel.LOCAL_QUORUM +EACH_QUORUM = ConsistencyLevel.EACH_QUORUM +ALL = ConsistencyLevel.ALL diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 3e14faaa8a..66aa55870d 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -4,6 +4,7 @@ from collections import namedtuple from cassandra.cluster import Cluster +from cassandra.query import SimpleStatement, Statement try: import Queue as queue @@ -24,10 +25,6 @@ class CQLConnectionError(CQLEngineException): pass Host = namedtuple('Host', ['name', 'port']) -_max_connections = 10 - -# global connection pool -connection_pool = None cluster = None session = None @@ -49,7 +46,6 @@ def setup( hosts, username=None, password=None, - max_connections=10, default_keyspace=None, consistency='ONE', timeout=None): @@ -62,8 +58,6 @@ def setup( :type username: str :param password: The cassandra password :type password: str - :param max_connections: The maximum number of connections to service - :type max_connections: int or long :param default_keyspace: The default keyspace to use :type default_keyspace: str :param consistency: The global consistency level @@ -72,34 +66,21 @@ def setup( :type timeout: int or long """ - global _max_connections, connection_pool, cluster, session + global cluster, session cluster = Cluster(hosts) session = cluster.connect() session.row_factory = dict_factory - -def execute(query, params=None, consistency_level=None): - - raise Exception("shut up") - - if isinstance(query, BaseCQLStatement): - params = query.get_context() - query = str(query) - - params = params or {} - if consistency_level is None: - consistency_level = connection_pool._consistency - return connection_pool.execute(query, params, consistency_level) - - #return execute_native(query, params, consistency_level) - - def execute_native(query, params=None, consistency_level=None): # TODO use consistency level - if isinstance(query, BaseCQLStatement): + if isinstance(query, Statement): + pass + + elif isinstance(query, BaseCQLStatement): params = query.get_context() query = str(query) + query = SimpleStatement(query, consistency_level=consistency_level) params = params or {} result = session.execute(query, params) diff --git a/cqlengine/query.py b/cqlengine/query.py index 3651ab29fd..a3f43e0f6b 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,7 +4,7 @@ from cqlengine import BaseContainerColumn, Map, columns from cqlengine.columns import Counter, List, Set -from cqlengine.connection import execute, RowResult, execute_native +from cqlengine.connection import RowResult, execute_native from cqlengine.exceptions import CQLEngineException, ValidationError diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 9821080b12..465001082b 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -32,7 +32,7 @@ def test_create_uses_consistency(self): qs.create(text="i am not fault tolerant this way") args = m.call_args - self.assertEqual(ALL, args[0][2]) + self.assertEqual(ALL, args[0][0].consistency_level) def test_queryset_is_returned_on_create(self): qs = TestConsistencyModel.consistency(ALL) @@ -46,7 +46,7 @@ def test_update_uses_consistency(self): t.consistency(ALL).save() args = m.call_args - self.assertEqual(ALL, args[0][2]) + self.assertEqual(ALL, args[0][0].consistency_level) def test_batch_consistency(self): @@ -56,14 +56,14 @@ def test_batch_consistency(self): TestConsistencyModel.batch(b).create(text="monkey") args = m.call_args - self.assertEqual(ALL, args[0][2]) + self.assertEqual(ALL, args[0][0].consistency_level) with mock.patch.object(self.session, 'execute') as m: with BatchQuery() as b: TestConsistencyModel.batch(b).create(text="monkey") args = m.call_args - self.assertNotEqual(ALL, args[0][2]) + self.assertNotEqual(ALL, args[0][0].consistency_level) def test_blind_update(self): t = TestConsistencyModel.create(text="bacon and eggs") @@ -74,7 +74,7 @@ def test_blind_update(self): TestConsistencyModel.objects(id=uid).consistency(ALL).update(text="grilled cheese") args = m.call_args - self.assertEqual(ALL, args[0][2]) + self.assertEqual(ALL, args[0][0].consistency_level) def test_delete(self): @@ -90,4 +90,4 @@ def test_delete(self): TestConsistencyModel.objects(id=uid).consistency(ALL).delete() args = m.call_args - self.assertEqual(ALL, args[0][2]) + self.assertEqual(ALL, args[0][0].consistency_level) diff --git a/upgrading.txt b/upgrading.txt index 6a13ce7acc..59e3a7c189 100644 --- a/upgrading.txt +++ b/upgrading.txt @@ -6,5 +6,4 @@ If you're calling setup() with the old thrift port in place, you will get connec The cqlengine connection pool has been removed. Connections are now managed by the native driver. This should drastically reduce the socket overhead as the native driver can multiplex queries. - - +If you were previously manually using "ALL", "QUORUM", etc, to specificy consistency levels, you will need to migrate to the cqlengine.ALL, QUORUM, etc instead. If you had been using the module level constants before, nothing should need to change. From 3b6a3a739d8d8028795e8977f951dc4877637a62 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 14:13:17 -0700 Subject: [PATCH 0838/3961] fixed multiple broken tests due to simple statement wrapper --- cqlengine/functions.py | 4 ++-- cqlengine/tests/columns/test_validation.py | 2 +- cqlengine/tests/model/test_polymorphism.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index d70b129710..838273cb6b 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -69,7 +69,7 @@ class MaxTimeUUID(BaseQueryFunction): http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun """ - format_string = 'MaxTimeUUID(:{})' + format_string = 'MaxTimeUUID(%({})s)' def __init__(self, value): """ @@ -109,7 +109,7 @@ def get_context_size(self): return len(self.value) def __unicode__(self): - token_args = ', '.join(':{}'.format(self.context_id + i) for i in range(self.get_context_size())) + token_args = ', '.join('%({})s'.format(self.context_id + i) for i in range(self.get_context_size())) return "token({})".format(token_args) def update_context(self, ctx): diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 57f7aaf20c..ad256062b2 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -6,7 +6,7 @@ from unittest import TestCase from uuid import uuid4, uuid1 from cqlengine import ValidationError -from cqlengine.connection import execute, execute_native +from cqlengine.connection import execute_native from cqlengine.tests.base import BaseCassEngTestCase diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 7db8bb4efd..28c3dccb82 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -130,7 +130,7 @@ def test_delete_on_polymorphic_subclass_does_not_include_polymorphic_key(self): # not sure how we would even get here if it was in there # since the CQL would fail. - self.assertNotIn("row_type", m.call_args[0][0]) + self.assertNotIn("row_type", m.call_args[0][0].query_string) From ca2d0e27f850f823f8f68a5476586e1627047e74 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 14:23:36 -0700 Subject: [PATCH 0839/3961] fixed brokenness around timeuuids --- cqlengine/functions.py | 2 +- cqlengine/tests/query/test_queryoperators.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/functions.py b/cqlengine/functions.py index 838273cb6b..b5f6a5af71 100644 --- a/cqlengine/functions.py +++ b/cqlengine/functions.py @@ -42,7 +42,7 @@ class MinTimeUUID(BaseQueryFunction): http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun """ - format_string = 'MinTimeUUID(:{})' + format_string = 'MinTimeUUID(%({})s)' def __init__(self, value): """ diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index dd70ef8300..1cedd6f367 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -32,7 +32,7 @@ def test_mintimeuuid_function(self): where = WhereClause('time', EqualsOperator(), functions.MinTimeUUID(now)) where.set_context_id(5) - self.assertEqual(str(where), '"time" = MinTimeUUID(:5)') + self.assertEqual(str(where), '"time" = MinTimeUUID(%(5)s)') ctx = {} where.update_context(ctx) self.assertEqual(ctx, {'5': DateTime().to_database(now)}) @@ -82,7 +82,7 @@ class TestModel(Model): q = TestModel.objects.filter(pk__token__gt=func) where = q._where[0] where.set_context_id(1) - self.assertEquals(str(where), 'token("p1", "p2") > token(:{}, :{})'.format(1, 2)) + self.assertEquals(str(where), 'token("p1", "p2") > token(%({})s, %({})s)'.format(1, 2)) # Verify that a SELECT query can be successfully generated str(q._select_query()) @@ -94,7 +94,7 @@ class TestModel(Model): q = TestModel.objects.filter(pk__token__gt=func) where = q._where[0] where.set_context_id(1) - self.assertEquals(str(where), 'token("p1", "p2") > token(:{}, :{})'.format(1, 2)) + self.assertEquals(str(where), 'token("p1", "p2") > token(%({})s, %({})s)'.format(1, 2)) str(q._select_query()) # The 'pk__token' virtual column may only be compared to a Token From 40061ce45cd9f42584d828496ef0b2dd8b947b5b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 14:24:58 -0700 Subject: [PATCH 0840/3961] fixed timeuuid. again --- cqlengine/tests/query/test_queryoperators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index 1cedd6f367..01cce27fc8 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -19,7 +19,7 @@ def test_maxtimeuuid_function(self): where = WhereClause('time', EqualsOperator(), functions.MaxTimeUUID(now)) where.set_context_id(5) - self.assertEqual(str(where), '"time" = MaxTimeUUID(:5)') + self.assertEqual(str(where), '"time" = MaxTimeUUID(%(5)s)') ctx = {} where.update_context(ctx) self.assertEqual(ctx, {'5': DateTime().to_database(now)}) From 2416d91a28b8796f6ccf36131cea1141daf51686 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 14:43:31 -0700 Subject: [PATCH 0841/3961] fixed batch consistency --- cqlengine/connection.py | 5 ++++- cqlengine/tests/test_consistency.py | 1 + cqlengine/tests/test_timestamp.py | 6 +++--- cqlengine/tests/test_ttl.py | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 66aa55870d..b2da6bf7d6 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -73,7 +73,6 @@ def setup( session.row_factory = dict_factory def execute_native(query, params=None, consistency_level=None): - # TODO use consistency level if isinstance(query, Statement): pass @@ -82,6 +81,10 @@ def execute_native(query, params=None, consistency_level=None): query = str(query) query = SimpleStatement(query, consistency_level=consistency_level) + elif isinstance(query, basestring): + query = SimpleStatement(query, consistency_level=consistency_level) + + params = params or {} result = session.execute(query, params) diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 465001082b..9f0f3f86b3 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -56,6 +56,7 @@ def test_batch_consistency(self): TestConsistencyModel.batch(b).create(text="monkey") args = m.call_args + self.assertEqual(ALL, args[0][0].consistency_level) with mock.patch.object(self.session, 'execute') as m: diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index e59466e999..6af7bf543e 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -48,7 +48,7 @@ def test_timestamp_not_included_on_normal_create(self): with mock.patch.object(self.session, "execute") as m: TestTimestampModel.create(count=2) - "USING TIMESTAMP".shouldnt.be.within(m.call_args[0][0]) + "USING TIMESTAMP".shouldnt.be.within(m.call_args[0][0].query_string) def test_timestamp_is_set_on_model_queryset(self): delta = timedelta(seconds=30) @@ -64,7 +64,7 @@ def test_non_batch_syntax_unit(self): with mock.patch.object(self.session, "execute") as m: TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) - query = m.call_args[0][0] + query = m.call_args[0][0].query_string "USING TIMESTAMP".should.be.within(query) @@ -80,7 +80,7 @@ def test_instance_update_includes_timestamp_in_query(self): with mock.patch.object(self.session, "execute") as m: self.instance.timestamp(timedelta(seconds=30)).update(count=2) - "USING TIMESTAMP".should.be.within(m.call_args[0][0]) + "USING TIMESTAMP".should.be.within(m.call_args[0][0].query_string) def test_instance_update_in_batch(self): with mock.patch.object(self.session, "execute") as m, BatchQuery() as b: diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 3d906b78e5..45f865e0e6 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -45,7 +45,7 @@ def test_ttl_included_on_create(self): with mock.patch.object(session, 'execute') as m: TestTTLModel.ttl(60).create(text="hello blake") - query = m.call_args[0][0] + query = m.call_args[0][0].query_string self.assertIn("USING TTL", query) def test_queryset_is_returned_on_class(self): @@ -65,7 +65,7 @@ def test_update_includes_ttl(self): with mock.patch.object(session, 'execute') as m: model.ttl(60).update(text="goodbye forever") - query = m.call_args[0][0] + query = m.call_args[0][0].query_string self.assertIn("USING TTL", query) def test_update_syntax_valid(self): @@ -98,7 +98,7 @@ def test_ttl_is_include_with_query_on_update(self): with mock.patch.object(session, 'execute') as m: o.save() - query = m.call_args[0][0] + query = m.call_args[0][0].query_string self.assertIn("USING TTL", query) @@ -112,7 +112,7 @@ def test_ttl_included_with_blind_update(self): with mock.patch.object(session, 'execute') as m: TestTTLModel.objects(id=tid).ttl(60).update(text="bacon") - query = m.call_args[0][0] + query = m.call_args[0][0].query_string self.assertIn("USING TTL", query) From bcec8a4d22b49d115d9941c8fc4d1c3426c2076c Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 14:45:14 -0700 Subject: [PATCH 0842/3961] fixed install requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dfbcdf05f1..a427f36bee 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "Topic :: Software Development :: Libraries :: Python Modules", ], keywords='cassandra,cql,orm', - install_requires = ['cql', 'cassandra-driver'], + install_requires = ['cassandra-driver >= 2.0.0'], author='Blake Eggleston, Jon Haddad', author_email='bdeggleston@gmail.com', url='https://github.com/cqlengine/cqlengine', From 24d8989069aea101d46c7c2b0a63324b4422c27d Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 19 Jun 2014 14:57:18 -0700 Subject: [PATCH 0843/3961] fixed the last of the broken tests --- cqlengine/tests/test_timestamp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index 6af7bf543e..b1a6a2860d 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -30,7 +30,7 @@ def test_batch_is_included(self): with mock.patch.object(self.session, "execute") as m, BatchQuery(timestamp=timedelta(seconds=30)) as b: TestTimestampModel.batch(b).create(count=1) - "USING TIMESTAMP".should.be.within(m.call_args[0][0]) + "USING TIMESTAMP".should.be.within(m.call_args[0][0].query_string) class CreateWithTimestampTest(BaseTimestampTest): @@ -39,7 +39,7 @@ def test_batch(self): with mock.patch.object(self.session, "execute") as m, BatchQuery() as b: TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1) - query = m.call_args[0][0] + query = m.call_args[0][0].query_string query.should.match(r"INSERT.*USING TIMESTAMP") query.should_not.match(r"TIMESTAMP.*INSERT") @@ -86,7 +86,7 @@ def test_instance_update_in_batch(self): with mock.patch.object(self.session, "execute") as m, BatchQuery() as b: self.instance.batch(b).timestamp(timedelta(seconds=30)).update(count=2) - query = m.call_args[0][0] + query = m.call_args[0][0].query_string "USING TIMESTAMP".should.be.within(query) From e5353dc1b1238530291e66f987436ac582098dbb Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 20 Jun 2014 11:21:57 -0500 Subject: [PATCH 0844/3961] Log when refreshing schema due to schema change --- cassandra/cluster.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index da3733c31b..d49d1c08eb 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -2107,6 +2107,8 @@ def _log_if_failed(self, future): def refresh_schema_and_set_result(keyspace, table, usertype, control_conn, response_future): try: + log.debug("Refreshing schema in response to schema change. Keyspace: %s; Table: %s, Type: %s", + keyspace, table, usertype) control_conn._refresh_schema(response_future._connection, keyspace, table, usertype) except Exception: log.exception("Exception refreshing schema in response to schema change:") From 47f74ca323dc78bff30c67d340cb6e4282e09631 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 20 Jun 2014 11:22:25 -0500 Subject: [PATCH 0845/3961] Handle UDT deletion correctly --- cassandra/metadata.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cassandra/metadata.py b/cassandra/metadata.py index a9bda180e1..76de53d7f4 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -156,8 +156,12 @@ def keyspace_changed(self, keyspace, ks_results): self._keyspace_added(keyspace) def usertype_changed(self, keyspace, name, type_results): - new_usertype = self._build_usertype(keyspace, type_results[0]) - self.keyspaces[keyspace].user_types[name] = new_usertype + if type_results: + new_usertype = self._build_usertype(keyspace, type_results[0]) + self.keyspaces[keyspace].user_types[name] = new_usertype + else: + # the type was deleted + self.keyspaces[keyspace].user_types.pop(name, None) def table_changed(self, keyspace, table, cf_results, col_results): try: From 60d4787f2155d203aea7bed994981e177196ed1a Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 20 Jun 2014 11:22:57 -0500 Subject: [PATCH 0846/3961] Update schema change handling for CASSANDRA-7413 --- cassandra/protocol.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 2509599249..60d08ed15b 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -611,10 +611,7 @@ def recv_results_metadata(cls, f, user_type_map): @classmethod def recv_results_schema_change(cls, f, protocol_version): - change_type = read_string(f) - keyspace = read_string(f) - table = read_string(f) - return {'change_type': change_type, 'keyspace': keyspace, 'table': table} + return EventMessage.recv_schema_change(f, protocol_version) @classmethod def read_type(cls, f, user_type_map): From b7e2631abfbf466d7bd63eb062dc9a5f65fee0db Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 20 Jun 2014 11:23:50 -0500 Subject: [PATCH 0847/3961] Remove sleeps in UDT integration tests --- tests/integration/standard/test_udts.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration/standard/test_udts.py b/tests/integration/standard/test_udts.py index df2bddfcab..4bf7f9bac0 100644 --- a/tests/integration/standard/test_udts.py +++ b/tests/integration/standard/test_udts.py @@ -26,8 +26,6 @@ from tests.integration import get_server_versions, PROTOCOL_VERSION -import time - class TypeTests(unittest.TestCase): @@ -49,8 +47,6 @@ def test_unprepared_registered_udts(self): s.execute("CREATE TYPE user (age int, name text)") s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") - time.sleep(1) - User = namedtuple('user', ('age', 'name')) c.register_user_type("udt_test_unprepared_registered", "user", User) @@ -71,8 +67,6 @@ def test_unprepared_registered_udts(self): s.execute("CREATE TYPE user (state text, is_cool boolean)") s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") - time.sleep(1) - User = namedtuple('user', ('state', 'is_cool')) c.register_user_type("udt_test_unprepared_registered2", "user", User) @@ -84,6 +78,8 @@ def test_unprepared_registered_udts(self): self.assertEqual(True, row.b.is_cool) self.assertTrue(type(row.b) is User) + c.shutdown() + def test_register_before_connecting(self): User1 = namedtuple('user', ('age', 'name')) User2 = namedtuple('user', ('state', 'is_cool')) @@ -107,8 +103,6 @@ def test_register_before_connecting(self): s.execute("CREATE TYPE user (state text, is_cool boolean)") s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") - time.sleep(1) - # now that types are defined, shutdown and re-create Cluster c.shutdown() c = Cluster(protocol_version=PROTOCOL_VERSION) @@ -136,6 +130,8 @@ def test_register_before_connecting(self): self.assertEqual(True, row.b.is_cool) self.assertTrue(type(row.b) is User2) + c.shutdown() + def test_prepared_unregistered_udts(self): c = Cluster(protocol_version=PROTOCOL_VERSION) s = c.connect() @@ -179,6 +175,8 @@ def test_prepared_unregistered_udts(self): self.assertEqual('Texas', row.b.state) self.assertEqual(True, row.b.is_cool) + c.shutdown() + def test_prepared_registered_udts(self): c = Cluster(protocol_version=PROTOCOL_VERSION) s = c.connect() @@ -189,11 +187,11 @@ def test_prepared_registered_udts(self): """) s.set_keyspace("udt_test_prepared_registered") s.execute("CREATE TYPE user (age int, name text)") - s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") - User = namedtuple('user', ('age', 'name')) c.register_user_type("udt_test_prepared_registered", "user", User) + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + insert = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") s.execute(insert, (0, User(42, 'bob'))) @@ -212,11 +210,11 @@ def test_prepared_registered_udts(self): """) s.set_keyspace("udt_test_prepared_registered2") s.execute("CREATE TYPE user (state text, is_cool boolean)") - s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") - User = namedtuple('user', ('state', 'is_cool')) c.register_user_type("udt_test_prepared_registered2", "user", User) + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b user)") + insert = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") s.execute(insert, (0, User('Texas', True))) @@ -227,3 +225,5 @@ def test_prepared_registered_udts(self): self.assertEqual('Texas', row.b.state) self.assertEqual(True, row.b.is_cool) self.assertTrue(type(row.b) is User) + + c.shutdown() From de57f366a86b0334377d7ad135bab54e828aeda9 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 20 Jun 2014 11:00:32 -0700 Subject: [PATCH 0848/3961] removed bool quoter --- cqlengine/columns.py | 10 ---------- cqlengine/tests/columns/test_value_io.py | 20 -------------------- 2 files changed, 30 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index 83caac6b85..c33304bace 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -451,23 +451,13 @@ def from_datetime(self, dt): class Boolean(Column): db_type = 'boolean' - class Quoter(ValueQuoter): - """ Cassandra 1.2.5 is stricter about boolean values """ - def __str__(self): - return 'true' if self.value else 'false' - def validate(self, value): """ Always returns a Python boolean. """ - if isinstance(value, self.Quoter): - value = value.value return bool(value) def to_python(self, value): return self.validate(value) - def to_database(self, value): - return self.Quoter(self.validate(value)) - class Float(Column): db_type = 'double' diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 4fa36d6cc1..121eb2580c 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -144,26 +144,6 @@ class TestTimeUUID(BaseColumnIOTest): def comparator_converter(self, val): return val if isinstance(val, UUID) else UUID(val) -class TestBooleanIO(BaseColumnIOTest): - - column = columns.Boolean - - pkey_val = True - data_val = False - - def comparator_converter(self, val): - return val.value if isinstance(val, columns.Boolean.Quoter) else val - -class TestBooleanQuoter(BaseColumnIOTest): - - column = columns.Boolean - - pkey_val = True - data_val = columns.Boolean.Quoter(False) - - def comparator_converter(self, val): - return val.value if isinstance(val, columns.Boolean.Quoter) else val - class TestFloatIO(BaseColumnIOTest): column = columns.Float From 6c4c6f1a3f7e0846667e860b3ec30d5c09fb2c59 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 12:06:01 -0700 Subject: [PATCH 0849/3961] kwargs on setup --- cqlengine/connection.py | 12 +++++++----- cqlengine/management.py | 18 +++++++++--------- cqlengine/query.py | 8 ++++---- cqlengine/tests/columns/test_validation.py | 4 ++-- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index b2da6bf7d6..592cdee9c3 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -44,11 +44,10 @@ def _column_tuple_factory(colnames, values): def setup( hosts, - username=None, - password=None, default_keyspace=None, consistency='ONE', - timeout=None): + timeout=None, + **kwargs): """ Records the hosts and connects to one of them @@ -68,11 +67,14 @@ def setup( """ global cluster, session - cluster = Cluster(hosts) + if 'username' in kwargs or 'password' in kwargs: + raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") + + cluster = Cluster(hosts, **kwargs) session = cluster.connect() session.row_factory = dict_factory -def execute_native(query, params=None, consistency_level=None): +def execute(query, params=None, consistency_level=None): if isinstance(query, Statement): pass diff --git a/cqlengine/management.py b/cqlengine/management.py index a0716002da..2eeaa310ef 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -4,7 +4,7 @@ from cqlengine import ONE from cqlengine.named import NamedTable -from cqlengine.connection import execute_native, get_cluster +from cqlengine.connection import execute, get_cluster from cqlengine.exceptions import CQLEngineException import logging @@ -50,13 +50,13 @@ def create_keyspace(name, strategy_class='SimpleStrategy', replication_factor=3, if strategy_class != 'SimpleStrategy': query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false') - execute_native(query) + execute(query) def delete_keyspace(name): cluster = get_cluster() if name in cluster.metadata.keyspaces: - execute_native("DROP KEYSPACE {}".format(name)) + execute("DROP KEYSPACE {}".format(name)) def create_table(model, create_missing_keyspace=True): warnings.warn("create_table has been deprecated in favor of sync_table and will be removed in a future release", DeprecationWarning) @@ -96,7 +96,7 @@ def sync_table(model, create_missing_keyspace=True): qs = get_create_table(model) try: - execute_native(qs) + execute(qs) except CQLEngineException as ex: # 1.2 doesn't return cf names, so we have to examine the exception # and ignore if it says the column family already exists @@ -113,7 +113,7 @@ def sync_table(model, create_missing_keyspace=True): # add missing column using the column def query = "ALTER TABLE {} add {}".format(cf_name, col.get_column_def()) logger.debug(query) - execute_native(query) + execute(query) update_compaction(model) @@ -130,7 +130,7 @@ def sync_table(model, create_missing_keyspace=True): qs += ['ON {}'.format(cf_name)] qs += ['("{}")'.format(column.db_field_name)] qs = ' '.join(qs) - execute_native(qs) + execute(qs) def get_create_table(model): cf_name = model.column_family_name() @@ -224,7 +224,7 @@ def get_fields(model): col_family = model.column_family_name(include_keyspace=False) query = "select * from system.schema_columns where keyspace_name = %s and columnfamily_name = %s" - tmp = execute_native(query, [ks_name, col_family]) + tmp = execute(query, [ks_name, col_family]) # Tables containing only primary keys do not appear to create # any entries in system.schema_columns, as only non-primary-key attributes @@ -287,7 +287,7 @@ def update_compaction(model): cf_name = model.column_family_name() query = "ALTER TABLE {} with compaction = {}".format(cf_name, options) logger.debug(query) - execute_native(query) + execute(query) return True return False @@ -308,7 +308,7 @@ def drop_table(model): try: table = meta.keyspaces[ks_name].tables[raw_cf_name] - execute_native('drop table {};'.format(model.column_family_name(include_keyspace=True))) + execute('drop table {};'.format(model.column_family_name(include_keyspace=True))) except KeyError: pass diff --git a/cqlengine/query.py b/cqlengine/query.py index a3f43e0f6b..1536456851 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,7 +4,7 @@ from cqlengine import BaseContainerColumn, Map, columns from cqlengine.columns import Counter, List, Set -from cqlengine.connection import RowResult, execute_native +from cqlengine.connection import RowResult, execute from cqlengine.exceptions import CQLEngineException, ValidationError @@ -174,7 +174,7 @@ def execute(self): query_list.append('APPLY BATCH;') - execute_native('\n'.join(query_list), parameters, self._consistency) + execute('\n'.join(query_list), parameters, self._consistency) self.queries = [] self._execute_callbacks() @@ -232,7 +232,7 @@ def _execute(self, q): if self._batch: return self._batch.add_query(q) else: - result = execute_native(q, consistency_level=self._consistency) + result = execute(q, consistency_level=self._consistency) return result def __unicode__(self): @@ -781,7 +781,7 @@ def _execute(self, q): if self._batch: return self._batch.add_query(q) else: - tmp = execute_native(q, consistency_level=self._consistency) + tmp = execute(q, consistency_level=self._consistency) return tmp def batch(self, batch_obj): diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index ad256062b2..ceaa9371aa 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -6,7 +6,7 @@ from unittest import TestCase from uuid import uuid4, uuid1 from cqlengine import ValidationError -from cqlengine.connection import execute_native +from cqlengine.connection import execute from cqlengine.tests.base import BaseCassEngTestCase @@ -298,7 +298,7 @@ def test_extra_field(self): drop_table(self.TestModel) sync_table(self.TestModel) self.TestModel.create() - execute_native("ALTER TABLE {} add blah int".format(self.TestModel.column_family_name(include_keyspace=True))) + execute("ALTER TABLE {} add blah int".format(self.TestModel.column_family_name(include_keyspace=True))) self.TestModel.objects().all() class TestTimeUUIDFromDatetime(TestCase): From 41072658b9c7bee68f24d63eae62ea8e17cb2a4b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 12:11:02 -0700 Subject: [PATCH 0850/3961] removed dead code --- cqlengine/connection.py | 10 ---------- cqlengine/query.py | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 592cdee9c3..5ed17131ef 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -32,16 +32,6 @@ class CQLConnectionError(CQLEngineException): pass class CQLConnectionError(CQLEngineException): pass -class RowResult(tuple): - pass - -QueryResult = namedtuple('RowResult', ('columns', 'results')) - - -def _column_tuple_factory(colnames, values): - return tuple(colnames), [RowResult(v) for v in values] - - def setup( hosts, default_keyspace=None, diff --git a/cqlengine/query.py b/cqlengine/query.py index 1536456851..320084ef00 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -4,8 +4,7 @@ from cqlengine import BaseContainerColumn, Map, columns from cqlengine.columns import Counter, List, Set -from cqlengine.connection import RowResult, execute - +from cqlengine.connection import execute from cqlengine.exceptions import CQLEngineException, ValidationError from cqlengine.functions import Token, BaseQueryFunction, QueryValue From 7779a032066360f86b5b2e505618f206298c0f5f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 12:27:46 -0700 Subject: [PATCH 0851/3961] fixes for consistency and setup -> native driver passthrough --- cqlengine/connection.py | 17 +++++++++-------- upgrading.txt | 4 ++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 5ed17131ef..b72b6a5ce7 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -15,7 +15,7 @@ import logging from cqlengine.exceptions import CQLEngineException - +from cassandra import ConsistencyLevel from cqlengine.statements import BaseCQLStatement from cassandra.query import dict_factory @@ -27,16 +27,12 @@ class CQLConnectionError(CQLEngineException): pass cluster = None session = None - - -class CQLConnectionError(CQLEngineException): pass - +default_consistency_level = None def setup( hosts, default_keyspace=None, - consistency='ONE', - timeout=None, + consistency=ConsistencyLevel.ONE, **kwargs): """ Records the hosts and connects to one of them @@ -55,16 +51,21 @@ def setup( :type timeout: int or long """ - global cluster, session + global cluster, session, default_consistency_level if 'username' in kwargs or 'password' in kwargs: raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") + default_consistency_level = consistency cluster = Cluster(hosts, **kwargs) session = cluster.connect() session.row_factory = dict_factory def execute(query, params=None, consistency_level=None): + + if consistency_level is None: + consistency_level = default_consistency_level + if isinstance(query, Statement): pass diff --git a/upgrading.txt b/upgrading.txt index 59e3a7c189..95727aa546 100644 --- a/upgrading.txt +++ b/upgrading.txt @@ -7,3 +7,7 @@ If you're calling setup() with the old thrift port in place, you will get connec The cqlengine connection pool has been removed. Connections are now managed by the native driver. This should drastically reduce the socket overhead as the native driver can multiplex queries. If you were previously manually using "ALL", "QUORUM", etc, to specificy consistency levels, you will need to migrate to the cqlengine.ALL, QUORUM, etc instead. If you had been using the module level constants before, nothing should need to change. + +No longer accepting username & password as arguments to setup. Use the native driver's authentication instead. See http://datastax.github.io/python-driver/api/cassandra/auth.html + + From cae504681f2794f31ffa1de8f108203a0cd7b72a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 14:38:20 -0700 Subject: [PATCH 0852/3961] removed old fields --- cqlengine/connection.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index b72b6a5ce7..af1afd7b5a 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -39,14 +39,10 @@ def setup( :param hosts: list of hosts, strings in the :, or just :type hosts: list - :param username: The cassandra username - :type username: str - :param password: The cassandra password - :type password: str :param default_keyspace: The default keyspace to use :type default_keyspace: str :param consistency: The global consistency level - :type consistency: str + :type consistency: int :param timeout: The connection timeout in milliseconds :type timeout: int or long From 82f3d4ed11396bff254028136c0e279d2be3af5b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 14:43:07 -0700 Subject: [PATCH 0853/3961] fixing docs --- cqlengine/connection.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index af1afd7b5a..b080792c93 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -37,15 +37,12 @@ def setup( """ Records the hosts and connects to one of them - :param hosts: list of hosts, strings in the :, or just + :param hosts: list of hosts, see http://datastax.github.io/python-driver/api/cassandra/cluster.html :type hosts: list :param default_keyspace: The default keyspace to use :type default_keyspace: str :param consistency: The global consistency level :type consistency: int - :param timeout: The connection timeout in milliseconds - :type timeout: int or long - """ global cluster, session, default_consistency_level From 41b4cf10aec623ca244a4c173d83465f88a6ac49 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 14:45:51 -0700 Subject: [PATCH 0854/3961] updated documentation --- docs/index.rst | 5 ++++- docs/topics/connection.rst | 13 +++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index d748f39fd1..82e5cb3743 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,7 +51,10 @@ Getting Started #next, setup the connection to your cassandra server(s)... >>> from cqlengine import connection - >>> connection.setup(['127.0.0.1:9160']) + + # see http://datastax.github.io/python-driver/api/cassandra/cluster.html for options + # the list of hosts will be passed to create a Cluster() instance + >>> connection.setup(['127.0.0.1']) #...and create your CQL table >>> from cqlengine.management import sync_table diff --git a/docs/topics/connection.rst b/docs/topics/connection.rst index 53eaeafad6..11eeda39e0 100644 --- a/docs/topics/connection.rst +++ b/docs/topics/connection.rst @@ -11,22 +11,15 @@ Connection The setup function in `cqlengine.connection` records the Cassandra servers to connect to. If there is a problem with one of the servers, cqlengine will try to connect to each of the other connections before failing. -.. function:: setup(hosts [, username=None, password=None, consistency='ONE']) +.. function:: setup(hosts) :param hosts: list of hosts, strings in the :, or just :type hosts: list - :param username: a username, if required - :type username: str - - :param password: a password, if required - :type password: str - :param consistency: the consistency level of the connection, defaults to 'ONE' - :type consistency: str + :type consistency: int - :param timeout: the connection timeout in milliseconds - :type timeout: int or long + # see http://datastax.github.io/python-driver/api/cassandra.html#cassandra.ConsistencyLevel Records the hosts and connects to one of them From 877a89d09a42872b28539dc59fa575079c659a4b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 15:03:26 -0700 Subject: [PATCH 0855/3961] adding protocol version as option in tests. need to get 1.2 working properly --- cqlengine/tests/base.py | 4 +++- docs/index.rst | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cqlengine/tests/base.py b/cqlengine/tests/base.py index c8679c1b21..3250de1471 100644 --- a/cqlengine/tests/base.py +++ b/cqlengine/tests/base.py @@ -9,7 +9,9 @@ else: CASSANDRA_TEST_HOST = 'localhost' -connection.setup([CASSANDRA_TEST_HOST], default_keyspace='cqlengine_test') +protocol_version = int(os.environ.get("CASSANDRA_PROTOCOL_VERSION", 2)) + +connection.setup([CASSANDRA_TEST_HOST], protocol_version=protocol_version, default_keyspace='cqlengine_test') class BaseCassEngTestCase(TestCase): diff --git a/docs/index.rst b/docs/index.rst index 82e5cb3743..e9663bc85d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -56,6 +56,9 @@ Getting Started # the list of hosts will be passed to create a Cluster() instance >>> connection.setup(['127.0.0.1']) + # if you're connecting to a 1.2 cluster + >>> connection.setup(['127.0.0.1'], protocol_version=1) + #...and create your CQL table >>> from cqlengine.management import sync_table >>> sync_table(ExampleModel) From e2e144053083770ad67422f6d7b3f35f2c1ede35 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 15:26:32 -0700 Subject: [PATCH 0856/3961] fixed sync issue with 1.2 --- cqlengine/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 2eeaa310ef..54128ceb23 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -232,7 +232,7 @@ def get_fields(model): try: return [Field(x['column_name'], x['validator']) for x in tmp if x['type'] == 'regular'] - except ValueError: + except KeyError: return [Field(x['column_name'], x['validator']) for x in tmp] # convert to Field named tuples From e38d02bbb31c0bb8a018740c4b9fa89979a4aff2 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 23 Jun 2014 16:24:27 -0700 Subject: [PATCH 0857/3961] fixed readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b99b182834..7965610d56 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,10 @@ class ExampleModel(Model): #next, setup the connection to your cassandra server(s)... >>> from cqlengine import connection ->>> connection.setup(['127.0.0.1:9160']) +>>> connection.setup(['127.0.0.1']) + +# or if you're still on cassandra 1.2 +>>> connection.setup(['127.0.0.1'], protocol_version=1) #...and create your CQL table >>> from cqlengine.management import sync_table From 71b1613d24f92f12a961d62362abe32fac550f5d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 12:14:10 -0500 Subject: [PATCH 0858/3961] Fix v1 protocol authentication example Relates to #128 --- docs/security.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/security.rst b/docs/security.rst index 04ecda017c..c87c5de8d1 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -42,15 +42,18 @@ Protocol v1 Authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^ When working with Cassandra 1.2 (or a higher version with :attr:`~.Cluster.protocol_version` set to ``1``), you will not pass in -an :class:`~.AuthProvider` instance. Instead, you should pass a dict -of credentials with a ``username`` and ``password`` key: +an :class:`~.AuthProvider` instance. Instead, you should pass in a +function that takes one argument, the IP address of a host, and returns +a dict of credentials with a ``username`` and ``password`` key: .. code-block:: python from cassandra.cluster import Cluster - credentials = {'username': 'joe', 'password': '1234'} - cluster = Cluster(auth_provider=credentials, protocol_version=1) + def get_credentials(host_address): + return {'username': 'joe', 'password': '1234'} + + cluster = Cluster(auth_provider=get_credentials, protocol_version=1) SSL --- From 0c9ef2c15dbf28625429740ce35489ff49804d0e Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 13:52:04 -0500 Subject: [PATCH 0859/3961] Fix CompositeType column spec in CREATE TABLE stmts --- CHANGELOG.rst | 2 ++ cassandra/cqltypes.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 17feba9a78..1e3a7d9437 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,8 @@ Bug Fixes process has connected a Cluster before forking (github #141) * Don't share prepared statement lock across Cluster instances +* Format CompositeType and DynamicCompositeType columns correctly + in CREATE TABLE statements. 2.0.2 ===== diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 7298287c0e..7c1dd7a102 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -742,10 +742,17 @@ class CompositeType(_ParameterizedType): typename = "'org.apache.cassandra.db.marshal.CompositeType'" num_subtypes = 'UNKNOWN' + @classmethod + def cql_parameterized_type(cls): + """ + There is no CQL notation for Composites, so we override this. + """ + typestring = cls.cass_parameterized_type(full=True) + return "'%s'" % (typestring,) + -class DynamicCompositeType(_ParameterizedType): +class DynamicCompositeType(CompositeType): typename = "'org.apache.cassandra.db.marshal.DynamicCompositeType'" - num_subtypes = 'UNKNOWN' class ColumnToCollectionType(_ParameterizedType): From 4567f2d610fc47b0b0ce584443705c93ee06d932 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 14:46:21 -0500 Subject: [PATCH 0860/3961] Add support for tuple type --- cassandra/cluster.py | 6 +-- cassandra/cqltypes.py | 60 +++++++++++++++--------- cassandra/encoder.py | 1 + tests/integration/standard/test_types.py | 35 ++++++++++++++ 4 files changed, 78 insertions(+), 24 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index d49d1c08eb..7b48395db1 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1086,7 +1086,7 @@ def __init__(self, cluster, hosts): self._metrics = cluster.metrics self._protocol_version = self.cluster.protocol_version - self._encoders = cql_encoders.copy() + self.encoders = cql_encoders.copy() # create connection pools in parallel futures = [] @@ -1210,7 +1210,7 @@ def _create_response_future(self, query, parameters, trace): if isinstance(query, SimpleStatement): query_string = query.query_string if parameters: - query_string = bind_params(query.query_string, parameters, self._encoders) + query_string = bind_params(query.query_string, parameters, self.encoders) message = QueryMessage( query_string, cl, query.serial_consistency_level, fetch_size, timestamp=timestamp) @@ -1460,7 +1460,7 @@ def encode(val): cql_encode_all_types(getattr(val, field_name)) ) for field_name in type_meta.field_names) - self._encoders[klass] = encode + self.encoders[klass] = encode def submit(self, fn, *args, **kwargs): """ Internal """ diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 8a623e4ed1..ad8629946a 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -774,10 +774,45 @@ def serialize_safe(cls, themap, protocol_version): return buf.getvalue() -class UserDefinedType(_ParameterizedType): - typename = "'org.apache.cassandra.db.marshal.UserType'" +class TupleType(_ParameterizedType): + typename = 'tuple' + num_subtypes = 'UNKNOWN' + + @classmethod + def deserialize_safe(cls, byts, protocol_version): + proto_version = max(3, protocol_version) + p = 0 + values = [] + for col_type in cls.subtypes: + if p == len(byts): + break + itemlen = int32_unpack(byts[p:p + 4]) + p += 4 + item = byts[p:p + itemlen] + p += itemlen + # collections inside UDTs are always encoded with at least the + # version 3 format + values.append(col_type.from_binary(item, proto_version)) + + if len(values) < len(cls.subtypes): + nones = [None] * (len(cls.subtypes) - len(values)) + values = values + nones - FIELD_LENGTH = 4 + return tuple(values) + + @classmethod + def serialize_safe(cls, val, protocol_version): + proto_version = max(3, protocol_version) + buf = io.BytesIO() + for item, subtype in zip(val, cls.subtypes): + packed_item = subtype.to_binary(item, proto_version) + buf.write(int32_pack(len(packed_item))) + buf.write(packed_item) + return buf.getvalue() + + +class UserDefinedType(TupleType): + typename = "'org.apache.cassandra.db.marshal.UserType'" _cache = {} @@ -804,24 +839,7 @@ def cql_parameterized_type(cls): @classmethod def deserialize_safe(cls, byts, protocol_version): - proto_version = max(3, protocol_version) - p = 0 - values = [] - for col_type in cls.subtypes: - if p == len(byts): - break - itemlen = int32_unpack(byts[p:p + cls.FIELD_LENGTH]) - p += cls.FIELD_LENGTH - item = byts[p:p + itemlen] - p += itemlen - # collections inside UDTs are always encoded with at least the - # version 3 format - values.append(col_type.from_binary(item, proto_version)) - - if len(values) < len(cls.subtypes): - nones = [None] * (len(cls.subtypes) - len(values)) - values = values + nones - + values = TupleType.deserialize_safe(byts, protocol_version) if cls.mapped_class: return cls.mapped_class(**dict(zip(cls.fieldnames, values))) else: diff --git a/cassandra/encoder.py b/cassandra/encoder.py index 36e887de5c..7c0afd66c5 100644 --- a/cassandra/encoder.py +++ b/cassandra/encoder.py @@ -84,6 +84,7 @@ def cql_encode_date(val): def cql_encode_sequence(val): return '( %s )' % ' , '.join(cql_encoders.get(type(v), cql_encode_object)(v) for v in val) +cql_encode_tuple = cql_encode_sequence def cql_encode_map_collection(val): diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 9ec1bc07d4..d82fc46ff8 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -33,6 +33,7 @@ from cassandra import InvalidRequest from cassandra.cluster import Cluster from cassandra.cqltypes import Int32Type, EMPTY +from cassandra.encoder import cql_encode_tuple from cassandra.query import dict_factory from cassandra.util import OrderedDict @@ -398,3 +399,37 @@ def test_timezone_aware_datetimes(self): s.execute(prepared, parameters=(dt,)) result = s.execute("SELECT b FROM mytable WHERE a='key2'")[0].b self.assertEqual(dt.utctimetuple(), result.utctimetuple()) + + def test_tuple_type(self): + if self._cass_version < (2, 1, 0): + raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") + + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + s.encoders[tuple] = cql_encode_tuple + + s.execute("""CREATE KEYSPACE test_tuple_type + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'}""") + s.set_keyspace("test_tuple_type") + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b tuple)") + + # test non-prepared statement + complete = ('foo', 123, True) + s.execute("INSERT INTO mytable (a, b) VALUES (0, %s)", parameters=(complete,)) + result = s.execute("SELECT b FROM mytable WHERE a=0")[0] + self.assertEqual(complete, result.b) + + partial = ('bar', 456) + partial_result = partial + (None,) + s.execute("INSERT INTO mytable (a, b) VALUES (1, %s)", parameters=(partial,)) + result = s.execute("SELECT b FROM mytable WHERE a=1")[0] + self.assertEqual(partial_result, result.b) + + # test prepared statement + prepared = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") + s.execute(prepared, parameters=(2, complete)) + s.execute(prepared, parameters=(3, partial)) + + prepared = s.prepare("SELECT b FROM mytable WHERE a=?") + self.assertEqual(complete, s.execute(prepared, (2,))[0].b) + self.assertEqual(partial_result, s.execute(prepared, (3,))[0].b) From 6bb41fca2eb296007cf016824dfc616ee013eab9 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 24 Jun 2014 13:36:56 -0700 Subject: [PATCH 0861/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index a803cc227f..a551051694 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.14.0 +0.15.0 From b57a91fa29bd96ef6496c93c115f06f876dd1264 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 16:21:51 -0500 Subject: [PATCH 0862/3961] Fix UDT deserialization --- cassandra/cqltypes.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index ad8629946a..e6eafd4a3a 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -839,7 +839,24 @@ def cql_parameterized_type(cls): @classmethod def deserialize_safe(cls, byts, protocol_version): - values = TupleType.deserialize_safe(byts, protocol_version) + proto_version = max(3, protocol_version) + p = 0 + values = [] + for col_type in cls.subtypes: + if p == len(byts): + break + itemlen = int32_unpack(byts[p:p + 4]) + p += 4 + item = byts[p:p + itemlen] + p += itemlen + # collections inside UDTs are always encoded with at least the + # version 3 format + values.append(col_type.from_binary(item, proto_version)) + + if len(values) < len(cls.subtypes): + nones = [None] * (len(cls.subtypes) - len(values)) + values = values + nones + if cls.mapped_class: return cls.mapped_class(**dict(zip(cls.fieldnames, values))) else: From 2949a36554490840c600d77b3f9dca012cbc8cad Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 16:23:57 -0500 Subject: [PATCH 0863/3961] Register type code for tuple type --- cassandra/protocol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 60d08ed15b..52733fa35f 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -33,7 +33,7 @@ InetAddressType, IntegerType, ListType, LongType, MapType, SetType, TimeUUIDType, UTF8Type, UUIDType, UserDefinedType, - lookup_casstype) + TupleType, lookup_casstype) from cassandra.policies import WriteType log = logging.getLogger(__name__) @@ -536,6 +536,7 @@ class ResultMessage(_MessageType): 0x0021: MapType, 0x0022: SetType, 0x0030: UserDefinedType, + 0x0031: TupleType, } _FLAGS_GLOBAL_TABLES_SPEC = 0x0001 From db3d4b0e421809234f92c8011d93731a99d30f58 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 16:36:25 -0500 Subject: [PATCH 0864/3961] Simpler host connections for v3 protocol --- cassandra/cluster.py | 53 +++++++++++++++++++-- cassandra/pool.py | 109 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 5 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 7b48395db1..94302e5326 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -61,7 +61,8 @@ ExponentialReconnectionPolicy, HostDistance, RetryPolicy) from cassandra.pool import (_ReconnectionHandler, _HostReconnectionHandler, - HostConnectionPool, NoConnectionsAvailable) + HostConnectionPool, HostConnection, + NoConnectionsAvailable) from cassandra.query import (SimpleStatement, PreparedStatement, BoundStatement, BatchStatement, bind_params, QueryTrace, Statement, named_tuple_factory, dict_factory) @@ -262,6 +263,15 @@ def auth_provider(self, value): :class:`.policies.SimpleConvictionPolicy`. """ + connect_to_remote_hosts = True + """ + If left as :const:`True`, hosts that are considered :attr:`~.HostDistance.REMOTE` + by the :attr:`~.Cluster.load_balancing_policy` will have a connection + opened to them. Otherwise, they will not have a connection opened to them. + + .. versionadded:: 2.1.0 + """ + metrics_enabled = False """ Whether or not metric collection is enabled. If enabled, :attr:`.metrics` @@ -465,12 +475,20 @@ def get_min_requests_per_connection(self, host_distance): return self._min_requests_per_connection[host_distance] def set_min_requests_per_connection(self, host_distance, min_requests): + if self.protocol_version >= 3: + raise UnsupportedOperation( + "Cluster.set_min_requests_per_connection() only has an effect " + "when using protocol_version 1 or 2.") self._min_requests_per_connection[host_distance] = min_requests def get_max_requests_per_connection(self, host_distance): return self._max_requests_per_connection[host_distance] def set_max_requests_per_connection(self, host_distance, max_requests): + if self.protocol_version >= 3: + raise UnsupportedOperation( + "Cluster.set_max_requests_per_connection() only has an effect " + "when using protocol_version 1 or 2.") self._max_requests_per_connection[host_distance] = max_requests def get_core_connections_per_host(self, host_distance): @@ -479,6 +497,9 @@ def get_core_connections_per_host(self, host_distance): for each host with :class:`~.HostDistance` equal to `host_distance`. The default is 2 for :attr:`~HostDistance.LOCAL` and 1 for :attr:`~HostDistance.REMOTE`. + + This property is ignored if :attr:`~.Cluster.protocol_version` is + 3 or higher. """ return self._core_connections_per_host[host_distance] @@ -488,7 +509,16 @@ def set_core_connections_per_host(self, host_distance, core_connections): for each host with :class:`~.HostDistance` equal to `host_distance`. The default is 2 for :attr:`~HostDistance.LOCAL` and 1 for :attr:`~HostDistance.REMOTE`. + + If :attr:`~.Cluster.protocol_version` is set to 3 or higher, this + is not supported (there is always one connection per host, unless + the host is remote and :attr:`connect_to_remote_hosts` is :const:`False`) + and using this will result in an :exc:`~.UnsupporteOperation`. """ + if self.protocol_version >= 3: + raise UnsupportedOperation( + "Cluster.set_core_connections_per_host() only has an effect " + "when using protocol_version 1 or 2.") old = self._core_connections_per_host[host_distance] self._core_connections_per_host[host_distance] = core_connections if old < core_connections: @@ -500,6 +530,9 @@ def get_max_connections_per_host(self, host_distance): for each host with :class:`~.HostDistance` equal to `host_distance`. The default is 8 for :attr:`~HostDistance.LOCAL` and 2 for :attr:`~HostDistance.REMOTE`. + + This property is ignored if :attr:`~.Cluster.protocol_version` is + 3 or higher. """ return self._max_connections_per_host[host_distance] @@ -509,7 +542,16 @@ def set_max_connections_per_host(self, host_distance, max_connections): for each host with :class:`~.HostDistance` equal to `host_distance`. The default is 2 for :attr:`~HostDistance.LOCAL` and 1 for :attr:`~HostDistance.REMOTE`. + + If :attr:`~.Cluster.protocol_version` is set to 3 or higher, this + is not supported (there is always one connection per host, unless + the host is remote and :attr:`connect_to_remote_hosts` is :const:`False`) + and using this will result in an :exc:`~.UnsupporteOperation`. """ + if self.protocol_version >= 3: + raise UnsupportedOperation( + "Cluster.set_max_connections_per_host() only has an effect " + "when using protocol_version 1 or 2.") self._max_connections_per_host[host_distance] = max_connections def connection_factory(self, address, *args, **kwargs): @@ -1341,14 +1383,17 @@ def add_or_renew_pool(self, host, is_host_addition): def run_add_or_renew_pool(): try: - new_pool = HostConnectionPool(host, distance, self) + if self._protocol_version >= 3: + new_pool = HostConnection(host, distance, self) + else: + new_pool = HostConnectionPool(host, distance, self) except AuthenticationFailed as auth_exc: conn_exc = ConnectionException(str(auth_exc), host=host) self.cluster.signal_connection_failure(host, conn_exc, is_host_addition) return False except Exception as conn_exc: - log.warning("Failed to create connection pool for new host %s: %s", - host, conn_exc) + log.warning("Failed to create connection pool for new host %s:", + host, exc_info=conn_exc) # the host itself will still be marked down, so we need to pass # a special flag to make sure the reconnector is created self.cluster.signal_connection_failure( diff --git a/cassandra/pool.py b/cassandra/pool.py index f99121d737..0f29c77f06 100644 --- a/cassandra/pool.py +++ b/cassandra/pool.py @@ -20,7 +20,7 @@ import re import socket import time -from threading import RLock, Condition +from threading import Lock, RLock, Condition import weakref try: from weakref import WeakSet @@ -29,6 +29,7 @@ from cassandra import AuthenticationFailed from cassandra.connection import ConnectionException +from cassandra.policies import HostDistance log = logging.getLogger(__name__) @@ -283,11 +284,117 @@ def on_exception(self, exc, next_delay): return True +class HostConnection(object): + """ + When using v3 of the native protocol, this is used instead of a connection + pool per host (HostConnectionPool) due to the increased in-flight capacity + of individual connections. + """ + + host = None + host_distance = None + is_shutdown = False + + _session = None + _connection = None + _lock = None + + def __init__(self, host, host_distance, session): + self.host = host + self.host_distance = host_distance + self._session = weakref.proxy(session) + self._lock = Lock() + + if host_distance == HostDistance.IGNORED: + log.debug("Not opening connection to ignored host %s", self.host) + return + elif host_distance == HostDistance.REMOTE and not session.cluster.connect_to_remote_hosts: + log.debug("Not opening connection to remote host %s", self.host) + return + + log.debug("Initializing connection for host %s", self.host) + self._connection = session.cluster.connection_factory(host.address) + if session.keyspace: + self._connection.set_keyspace_blocking(session.keyspace) + log.debug("Finished initializing connection for host %s", self.host) + + def borrow_connection(self, timeout): + if self.is_shutdown: + raise ConnectionException( + "Pool for %s is shutdown" % (self.host,), self.host) + + conn = self._connection + if not conn: + raise NoConnectionsAvailable() + + with conn.lock: + if conn.in_flight > conn.max_request_id: + raise NoConnectionsAvailable("All request IDs are currently in use") + conn.in_flight += 1 + return conn, conn.get_request_id() + + def return_connection(self, connection): + with connection.lock: + connection.in_flight -= 1 + + if connection.is_defunct or connection.is_closed: + log.debug("Defunct or closed connection (%s) returned to pool, potentially " + "marking host %s as down", id(connection), self.host) + is_down = self._session.cluster.signal_connection_failure( + self.host, connection.last_error, is_host_addition=False) + if is_down: + self.shutdown() + else: + self._connection = None + with self._lock: + if self._is_replacing: + return + self._is_replacing = True + self._session.submit(self._replace, connection) + + def _replace(self, connection): + log.debug("Replacing connection (%s) to %s", id(connection), self.host) + conn = self._session.cluster.connection_factory(self.host.address) + if self._session.keyspace: + conn.set_keyspace_blocking(self._session.keyspace) + self._connection = conn + with self._lock: + self._is_replacing = False + + def shutdown(self): + with self._lock: + if self.is_shutdown: + return + else: + self.is_shutdown = True + + if self._connection: + self._connection.close() + + def _set_keyspace_for_all_conns(self, keyspace, callback): + if self.is_shutdown or not self._connection: + return + + def connection_finished_setting_keyspace(conn, error): + errors = [] if not error else [error] + callback(self, errors) + + self._connection.set_keyspace_async(keyspace, connection_finished_setting_keyspace) + + def get_state(self): + have_conn = self._connection is not None + in_flight = self._connection.in_flight if have_conn else 0 + return "shutdown: %s, open: %s, in_flights: %s" % (self.is_shutdown, have_conn, in_flight) + + _MAX_SIMULTANEOUS_CREATION = 1 _MIN_TRASH_INTERVAL = 10 class HostConnectionPool(object): + """ + Used to pool connections to a host for v1 and v2 native protocol. + """ host = None host_distance = None From 8f43c80cfce918852bba93d1295adc833852fd91 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 16:40:31 -0500 Subject: [PATCH 0865/3961] Add protocol version option to benchmarks --- benchmarks/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/benchmarks/base.py b/benchmarks/base.py index 8355fcf231..77a168b0b0 100644 --- a/benchmarks/base.py +++ b/benchmarks/base.py @@ -102,8 +102,11 @@ def benchmark(thread_class): setup(options.hosts) log.info("==== %s ====" % (conn_class.__name__,)) - cluster = Cluster(options.hosts, metrics_enabled=options.enable_metrics) - cluster.connection_class = conn_class + kwargs = {'metrics_enabled': options.enable_metrics, + 'connection_class': conn_class} + if options.protocol_version: + kwargs['protocol_version'] = options.protocol_version + cluster = Cluster(options.hosts, **kwargs) session = cluster.connect(KEYSPACE) log.debug("Sleeping for two seconds...") @@ -183,6 +186,8 @@ def parse_options(): help='logging level: debug, info, warning, or error') parser.add_option('-p', '--profile', action='store_true', dest='profile', help='Profile the run') + parser.add_option('--protocol-version', type='int', dest='protocol_version', + help='Native protocol version to use') options, args = parser.parse_args() From dd245bf5795b7bf1fff00c6d81eabf3732160c4d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 16:44:53 -0500 Subject: [PATCH 0866/3961] Adjust concurrency based on protocol version --- benchmarks/base.py | 7 +++++-- benchmarks/callback_full_pipeline.py | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/benchmarks/base.py b/benchmarks/base.py index 77a168b0b0..66ca724dc3 100644 --- a/benchmarks/base.py +++ b/benchmarks/base.py @@ -124,7 +124,9 @@ def benchmark(thread_class): start = time.time() try: for i in range(options.threads): - thread = thread_class(i, session, query, values, per_thread, options.profile) + thread = thread_class( + i, session, query, values, per_thread, + cluster.protocol_version, options.profile) thread.daemon = True threads.append(thread) @@ -217,13 +219,14 @@ def parse_options(): class BenchmarkThread(Thread): - def __init__(self, thread_num, session, query, values, num_queries, profile): + def __init__(self, thread_num, session, query, values, num_queries, protocol_version, profile): Thread.__init__(self) self.thread_num = thread_num self.session = session self.query = query self.values = values self.num_queries = num_queries + self.protocol_version = protocol_version self.profiler = Profile() if profile else None def start_profile(self): diff --git a/benchmarks/callback_full_pipeline.py b/benchmarks/callback_full_pipeline.py index bf84077b59..c79a6a3465 100644 --- a/benchmarks/callback_full_pipeline.py +++ b/benchmarks/callback_full_pipeline.py @@ -48,7 +48,12 @@ def insert_next(self, previous_result=sentinel): def run(self): self.start_profile() - for _ in range(min(120, self.num_queries)): + if self.protocol_version >= 3: + concurrency = 1000 + else: + concurrency = 100 + + for _ in range(min(concurrency, self.num_queries)): self.insert_next() self.event.wait() From 496674d04c3c2ea140956caf7a4cd370524f7dfe Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 17:00:41 -0500 Subject: [PATCH 0867/3961] Document setting Session.default_fetch_size to None --- cassandra/cluster.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 94302e5326..839aeded90 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1091,8 +1091,10 @@ class Session(object): default_fetch_size = 5000 """ - By default, this many rows will be fetched at a time. This can be - specified per-query through :attr:`.Statement.fetch_size`. + By default, this many rows will be fetched at a time. Setting + this to :const:`None` will disable automatic paging for large query + results. The fetch size can be also specified per-query through + :attr:`.Statement.fetch_size`. This only takes effect when protocol version 2 or higher is used. See :attr:`.Cluster.protocol_version` for details. From 82477e20ac90f28b835e9ddac9b04f067dfae27c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 17:03:46 -0500 Subject: [PATCH 0868/3961] Don't set min/max/core requests/conns if using v3 protocol --- tests/integration/standard/test_cluster.py | 2 ++ tests/integration/standard/test_concurrent.py | 3 ++- tests/integration/standard/test_query.py | 6 ++++-- tests/integration/standard/test_query_paging.py | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/integration/standard/test_cluster.py b/tests/integration/standard/test_cluster.py index 28ef0009d5..1c485f4180 100644 --- a/tests/integration/standard/test_cluster.py +++ b/tests/integration/standard/test_cluster.py @@ -151,6 +151,8 @@ def test_cluster_settings(self): """ Test connection setting getters and setters """ + if PROTOCOL_VERSION >= 3: + raise unittest.SkipTest("min/max requests and core/max conns aren't used with v3 protocol") cluster = Cluster(protocol_version=PROTOCOL_VERSION) diff --git a/tests/integration/standard/test_concurrent.py b/tests/integration/standard/test_concurrent.py index ab533b1385..35804db1b8 100644 --- a/tests/integration/standard/test_concurrent.py +++ b/tests/integration/standard/test_concurrent.py @@ -33,7 +33,8 @@ class ClusterTests(unittest.TestCase): def setUp(self): self.cluster = Cluster(protocol_version=PROTOCOL_VERSION) - self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) + if PROTOCOL_VERSION < 3: + self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) self.session = self.cluster.connect() self.session.row_factory = tuple_factory diff --git a/tests/integration/standard/test_query.py b/tests/integration/standard/test_query.py index dac0ef3aa4..b8dfa7bd3a 100644 --- a/tests/integration/standard/test_query.py +++ b/tests/integration/standard/test_query.py @@ -223,7 +223,8 @@ def setUp(self): % (PROTOCOL_VERSION,)) self.cluster = Cluster(protocol_version=PROTOCOL_VERSION) - self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) + if PROTOCOL_VERSION < 3: + self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) self.session = self.cluster.connect() self.session.execute("TRUNCATE test3rf.test") @@ -318,7 +319,8 @@ def setUp(self): % (PROTOCOL_VERSION,)) self.cluster = Cluster(protocol_version=PROTOCOL_VERSION) - self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) + if PROTOCOL_VERSION < 3: + self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) self.session = self.cluster.connect() def test_conditional_update(self): diff --git a/tests/integration/standard/test_query_paging.py b/tests/integration/standard/test_query_paging.py index 0e087e8a37..3dce069c88 100644 --- a/tests/integration/standard/test_query_paging.py +++ b/tests/integration/standard/test_query_paging.py @@ -41,7 +41,8 @@ def setUp(self): % (PROTOCOL_VERSION,)) self.cluster = Cluster(protocol_version=PROTOCOL_VERSION) - self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) + if PROTOCOL_VERSION < 3: + self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) self.session = self.cluster.connect() self.session.execute("TRUNCATE test3rf.test") From fce517d87fd18e39d3d0b17c45ccebbc48f876b9 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 18:24:11 -0500 Subject: [PATCH 0869/3961] Correct collection packing for v3 protocol --- cassandra/cqltypes.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index e6eafd4a3a..2de44b00f8 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -699,11 +699,12 @@ def serialize_safe(cls, items, protocol_version): raise TypeError("Received a string for a type that expects a sequence") subtype, = cls.subtypes + pack = int32_pack if protocol_version >= 3 else uint16_pack buf = io.BytesIO() - buf.write(uint16_pack(len(items))) + buf.write(pack(len(items))) for item in items: itembytes = subtype.to_binary(item, protocol_version) - buf.write(uint16_pack(len(itembytes))) + buf.write(pack(len(itembytes))) buf.write(itembytes) return buf.getvalue() @@ -758,8 +759,9 @@ def deserialize_safe(cls, byts, protocol_version): @classmethod def serialize_safe(cls, themap, protocol_version): subkeytype, subvaltype = cls.subtypes + pack = int32_pack if protocol_version >= 3 else uint16_pack buf = io.BytesIO() - buf.write(uint16_pack(len(themap))) + buf.write(pack(len(themap))) try: items = six.iteritems(themap) except AttributeError: @@ -767,9 +769,9 @@ def serialize_safe(cls, themap, protocol_version): for key, val in items: keybytes = subkeytype.to_binary(key, protocol_version) valbytes = subvaltype.to_binary(val, protocol_version) - buf.write(uint16_pack(len(keybytes))) + buf.write(pack(len(keybytes))) buf.write(keybytes) - buf.write(uint16_pack(len(valbytes))) + buf.write(pack(len(valbytes))) buf.write(valbytes) return buf.getvalue() From 9bb087415b435b8e9f049fa64d30b86caa1c3807 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 18:25:01 -0500 Subject: [PATCH 0870/3961] Add missing TupleType handling --- cassandra/protocol.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 52733fa35f..fc65034e2e 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -629,6 +629,10 @@ def read_type(cls, f, user_type_map): keysubtype = cls.read_type(f, user_type_map) valsubtype = cls.read_type(f, user_type_map) typeclass = typeclass.apply_parameters((keysubtype, valsubtype)) + elif typeclass == TupleType: + num_items = read_short(f) + types = tuple(cls.read_type(f, user_type_map) for _ in xrange(num_items)) + typeclass = typeclass.apply_parameters(types) elif typeclass == UserDefinedType: ks = read_string(f) udt_name = read_string(f) From 720df07c88cf6a3d8bbe14058effe79db1b40433 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 18:25:35 -0500 Subject: [PATCH 0871/3961] BatchMessage flags were in the wrong place for v3 --- cassandra/protocol.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cassandra/protocol.py b/cassandra/protocol.py index fc65034e2e..f6c9f656fa 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -739,13 +739,6 @@ def __init__(self, batch_type, queries, consistency_level, def send_body(self, f, protocol_version): write_byte(f, self.batch_type.value) - if protocol_version >= 3: - flags = 0 - if self.serial_consistency_level: - flags |= _WITH_SERIAL_CONSISTENCY_FLAG - if self.timestamp is not None: - flags |= _PROTOCOL_TIMESTAMP - write_byte(f, flags) write_short(f, len(self.queries)) for prepared, string_or_query_id, params in self.queries: if not prepared: @@ -760,10 +753,18 @@ def send_body(self, f, protocol_version): write_value(f, param) write_consistency_level(f, self.consistency_level) - if protocol_version >= 3 and self.serial_consistency_level: - write_consistency_level(f, self.serial_consistency_level) - if self.timestamp is not None: - write_long(f, self.timestamp) + if protocol_version >= 3: + flags = 0 + if self.serial_consistency_level: + flags |= _WITH_SERIAL_CONSISTENCY_FLAG + if self.timestamp is not None: + flags |= _PROTOCOL_TIMESTAMP + write_byte(f, flags) + + if self.serial_consistency_level: + write_consistency_level(f, self.serial_consistency_level) + if self.timestamp is not None: + write_long(f, self.timestamp) known_event_types = frozenset(( From 7dd45c0a64a35e848a99644de784f61acf09c355 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 24 Jun 2014 18:29:07 -0500 Subject: [PATCH 0872/3961] Handle different error message in 2.1 --- tests/integration/standard/test_types.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index d82fc46ff8..08e48383c0 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -76,7 +76,10 @@ def test_blob_type_as_string(self): if six.PY2 and self._cql_version >= (3, 1, 0): # Blob values can't be specified using string notation in CQL 3.1.0 and # above which is used by default in Cassandra 2.0. - msg = r'.*Invalid STRING constant \(.*?\) for b of type blob.*' + if self._cass_version >= (2, 1, 0): + msg = r'.*Invalid STRING constant \(.*?\) for "b" of type blob.*' + else: + msg = r'.*Invalid STRING constant \(.*?\) for b of type blob.*' self.assertRaisesRegexp(InvalidRequest, msg, s.execute, query, params) return elif six.PY2: From 19b8e84caea8c47bfd1a70f4fd9377afe5b75184 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 07:37:32 -0700 Subject: [PATCH 0873/3961] fixed default keyspace --- cqlengine/connection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index b080792c93..bd000bd0cc 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -49,6 +49,10 @@ def setup( if 'username' in kwargs or 'password' in kwargs: raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") + if default_keyspace: + from cqlengine import models + models.DEFAULT_KEYSPACE = default_keyspace + default_consistency_level = consistency cluster = Cluster(hosts, **kwargs) session = cluster.connect() From 130f554e49a068d66822c9f1dd93e6a06b82d09e Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 07:37:52 -0700 Subject: [PATCH 0874/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index a551051694..e815b861f0 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.15.0 +0.15.1 From af9abb4cb69277979efc34e1295104e4ab49661d Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 10:01:39 -0700 Subject: [PATCH 0875/3961] added old cql package back in, will need to be removed in next release --- cqlengine/VERSION | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index e815b861f0..4312e0d0ca 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.15.1 +0.15.2 diff --git a/setup.py b/setup.py index a427f36bee..2d6740db31 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "Topic :: Software Development :: Libraries :: Python Modules", ], keywords='cassandra,cql,orm', - install_requires = ['cassandra-driver >= 2.0.0'], + install_requires = ['cassandra-driver >= 2.0.0', "cql"], author='Blake Eggleston, Jon Haddad', author_email='bdeggleston@gmail.com', url='https://github.com/cqlengine/cqlengine', From 704824ea83829a73ce504a7468e607e1d32e926a Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 25 Jun 2014 12:30:29 -0500 Subject: [PATCH 0876/3961] Update changelog --- CHANGELOG.rst | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1e3a7d9437..1a9aa1de12 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,8 +2,19 @@ ===== In Progress +This release adds support for Cassandra 2.1 features, including version +3 of the native protocol. + Features -------- +* When using the v3 protocol, only one connection is opened per-host, and + throughput is improved due to reduced pooling overhead and lock contention. +* Support for user-defined types (Cassandra 2.1+) +* Support for tuple type in (limited usage Cassandra 2.0.9, full usage + in Cassandra 2.1) +* Protocol-level client-side timestamps (see Session.use_client_timestamp) +* Overridable type encoding for non-prepared statements (see Session.encoders) +* Configurable serial consistency levels for batch statements * Use io.BytesIO for reduced CPU consumption (github #143) * Support Twisted as a reactor. Note that a Twisted-compatible API is not exposed (so no Deferreds), this is just a reactor @@ -11,17 +22,16 @@ Features Bug Fixes --------- -* Fix references to xrange that do not go through "six" in - libevreactor and geventreactor (github #138) +* Fix references to xrange that do not go through "six" in libevreactor and + geventreactor (github #138) * Make BoundStatements inherit fetch_size from their parent PreparedStatement (PYTHON-80) -* Clear reactor state in child process after forking - to prevent errors with multiprocessing when the parent - process has connected a Cluster before forking (github #141) -* Don't share prepared statement lock across Cluster - instances -* Format CompositeType and DynamicCompositeType columns correctly - in CREATE TABLE statements. +* Clear reactor state in child process after forking to prevent errors with + multiprocessing when the parent process has connected a Cluster before + forking (github #141) +* Don't share prepared statement lock across Cluster instances +* Format CompositeType and DynamicCompositeType columns correctly in + CREATE TABLE statements. 2.0.2 ===== @@ -36,12 +46,10 @@ Bug Fixes asyncore event loop is restarted multiple times * Delay initialization of reactors in order to avoid problems with shared state when using multiprocessing (PYTHON-60) -* Add python-six to debian dependencies, move python-blist to - recommends +* Add python-six to debian dependencies, move python-blist to recommends * Fix memory leak when libev connections are created and destroyed (github #93) -* Ensure token map is rebuilt when hosts are removed from - the cluster +* Ensure token map is rebuilt when hosts are removed from the cluster 2.0.1 ===== From c74434370b563b5de1558bb0fd437851b0cea1ad Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 10:50:20 -0700 Subject: [PATCH 0877/3961] removed dead code and tests --- cqlengine/columns.py | 182 ------------------ .../tests/columns/test_container_columns.py | 80 -------- 2 files changed, 262 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index c33304bace..e3a32b70e8 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -315,17 +315,6 @@ def __init__(self, required=required, ) - def get_update_statement(self, val, prev, ctx): - val = self.to_database(val) - prev = self.to_database(prev or 0) - field_id = uuid4().hex - - delta = val - prev - sign = '-' if delta < 0 else '+' - delta = abs(delta) - ctx[field_id] = delta - return ['"{0}" = "{0}" {1} {2}'.format(self.db_field_name, sign, delta)] - class DateTime(Column): db_type = 'timestamp' @@ -544,11 +533,6 @@ def get_column_def(self): db_type = self.db_type.format(self.value_type.db_type) return '{} {}'.format(self.cql, db_type) - def get_update_statement(self, val, prev, ctx): - """ - Used to add partial update statements - """ - raise NotImplementedError def _val_is_null(self, val): return not val @@ -609,48 +593,7 @@ def to_database(self, value): if isinstance(value, self.Quoter): return value return self.Quoter({self.value_col.to_database(v) for v in value}) - def get_update_statement(self, val, prev, ctx): - """ - Returns statements that will be added to an object's update statement - also updates the query context - :param val: the current column value - :param prev: the previous column value - :param ctx: the values that will be passed to the query - :rtype: list - """ - - # remove from Quoter containers, if applicable - val = self.to_database(val) - prev = self.to_database(prev) - if isinstance(val, self.Quoter): val = val.value - if isinstance(prev, self.Quoter): prev = prev.value - - if val is None or val == prev: - # don't return anything if the new value is the same as - # the old one, or if the new value is none - return [] - elif prev is None or not any({v in prev for v in val}): - field = uuid1().hex - ctx[field] = self.Quoter(val) - return ['"{}" = :{}'.format(self.db_field_name, field)] - else: - # partial update time - to_create = val - prev - to_delete = prev - val - statements = [] - - if to_create: - field_id = uuid1().hex - ctx[field_id] = self.Quoter(to_create) - statements += ['"{0}" = "{0}" + :{1}'.format(self.db_field_name, field_id)] - - if to_delete: - field_id = uuid1().hex - ctx[field_id] = self.Quoter(to_delete) - statements += ['"{0}" = "{0}" - :{1}'.format(self.db_field_name, field_id)] - - return statements class List(BaseContainerColumn): @@ -691,82 +634,6 @@ def to_database(self, value): if isinstance(value, self.Quoter): return value return self.Quoter([self.value_col.to_database(v) for v in value]) - def get_update_statement(self, val, prev, values): - """ - Returns statements that will be added to an object's update statement - also updates the query context - """ - # remove from Quoter containers, if applicable - val = self.to_database(val) - prev = self.to_database(prev) - if isinstance(val, self.Quoter): val = val.value - if isinstance(prev, self.Quoter): prev = prev.value - - def _insert(): - field_id = uuid1().hex - values[field_id] = self.Quoter(val) - return ['"{}" = :{}'.format(self.db_field_name, field_id)] - - if val is None or val == prev: - return [] - - elif prev is None: - return _insert() - - elif len(val) < len(prev): - # if elements have been removed, - # rewrite the whole list - return _insert() - - elif len(prev) == 0: - # if we're updating from an empty - # list, do a complete insert - return _insert() - - else: - # the prepend and append lists, - # if both of these are still None after looking - # at both lists, an insert statement will be returned - prepend = None - append = None - - # the max start idx we want to compare - search_space = len(val) - max(0, len(prev)-1) - - # the size of the sub lists we want to look at - search_size = len(prev) - - for i in range(search_space): - #slice boundary - j = i + search_size - sub = val[i:j] - idx_cmp = lambda idx: prev[idx] == sub[idx] - if idx_cmp(0) and idx_cmp(-1) and prev == sub: - prepend = val[:i] - append = val[j:] - break - - # create update statements - if prepend is append is None: - return _insert() - - statements = [] - if prepend: - field_id = uuid1().hex - # CQL seems to prepend element at a time, starting - # with the element at idx 0, we can either reverse - # it here, or have it inserted in reverse - prepend.reverse() - values[field_id] = self.Quoter(prepend) - statements += ['"{0}" = :{1} + "{0}"'.format(self.db_field_name, field_id)] - - if append: - field_id = uuid1().hex - values[field_id] = self.Quoter(append) - statements += ['"{0}" = "{0}" + :{1}'.format(self.db_field_name, field_id)] - - return statements - class Map(BaseContainerColumn): """ @@ -841,55 +708,6 @@ def to_database(self, value): if isinstance(value, self.Quoter): return value return self.Quoter({self.key_col.to_database(k):self.value_col.to_database(v) for k,v in value.items()}) - def get_update_statement(self, val, prev, ctx): - """ - http://www.datastax.com/docs/1.2/cql_cli/using/collections_map#deletion - """ - # remove from Quoter containers, if applicable - val = self.to_database(val) - prev = self.to_database(prev) - if isinstance(val, self.Quoter): val = val.value - if isinstance(prev, self.Quoter): prev = prev.value - val = val or {} - prev = prev or {} - - #get the updated map - update = {k:v for k,v in val.items() if v != prev.get(k)} - - statements = [] - for k,v in update.items(): - key_id = uuid1().hex - val_id = uuid1().hex - ctx[key_id] = k - ctx[val_id] = v - statements += ['"{}"[:{}] = :{}'.format(self.db_field_name, key_id, val_id)] - - return statements - - def get_delete_statement(self, val, prev, ctx): - """ - Returns statements that will be added to an object's delete statement - also updates the query context, used for removing keys from a map - """ - if val is prev is None: - return [] - - val = self.to_database(val) - prev = self.to_database(prev) - if isinstance(val, self.Quoter): val = val.value - if isinstance(prev, self.Quoter): prev = prev.value - - old_keys = set(prev.keys()) if prev else set() - new_keys = set(val.keys()) if val else set() - del_keys = old_keys - new_keys - - del_statements = [] - for key in del_keys: - field_id = uuid1().hex - ctx[field_id] = key - del_statements += ['"{}"[:{}]'.format(self.db_field_name, field_id)] - - return del_statements class _PartitionKeysToken(Column): diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 838f2eb298..86a96e7edb 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -124,46 +124,6 @@ def test_partial_updates(self): m2 = TestSetModel.get(partition=m1.partition) assert m2.int_set == {2, 3, 4, 5} - def test_partial_update_creation(self): - """ - Tests that proper update statements are created for a partial set update - :return: - """ - ctx = {} - col = columns.Set(columns.Integer, db_field="TEST") - statements = col.get_update_statement({1, 2, 3, 4}, {2, 3, 4, 5}, ctx) - - assert len([v for v in ctx.values() if {1} == v.value]) == 1 - assert len([v for v in ctx.values() if {5} == v.value]) == 1 - assert len([s for s in statements if '"TEST" = "TEST" -' in s]) == 1 - assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 - - def test_update_from_none(self): - """ Tests that updating a 'None' list creates a straight insert statement """ - ctx = {} - col = columns.Set(columns.Integer, db_field="TEST") - statements = col.get_update_statement({1, 2, 3, 4}, None, ctx) - - #only one variable /statement should be generated - assert len(ctx) == 1 - assert len(statements) == 1 - - assert ctx.values()[0].value == {1, 2, 3, 4} - assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) - - def test_update_from_empty(self): - """ Tests that updating an empty list creates a straight insert statement """ - ctx = {} - col = columns.Set(columns.Integer, db_field="TEST") - statements = col.get_update_statement({1, 2, 3, 4}, set(), ctx) - - #only one variable /statement should be generated - assert len(ctx) == 1 - assert len(statements) == 1 - - assert ctx.values()[0].value == {1, 2, 3, 4} - assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) - def test_instantiation_with_column_class(self): """ Tests that columns instantiated with a column class work properly @@ -273,46 +233,6 @@ def test_partial_updates(self): m2 = TestListModel.get(partition=m1.partition) assert list(m2.int_list) == final - def test_partial_update_creation(self): - """ Tests that proper update statements are created for a partial list update """ - final = range(10) - initial = final[3:7] - - ctx = {} - col = columns.List(columns.Integer, db_field="TEST") - statements = col.get_update_statement(final, initial, ctx) - - assert len([v for v in ctx.values() if [2, 1, 0] == v.value]) == 1 - assert len([v for v in ctx.values() if [7, 8, 9] == v.value]) == 1 - assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1 - assert len([s for s in statements if '+ "TEST"' in s]) == 1 - - def test_update_from_none(self): - """ Tests that updating an 'None' list creates a straight insert statement """ - ctx = {} - col = columns.List(columns.Integer, db_field="TEST") - statements = col.get_update_statement([1, 2, 3], None, ctx) - - #only one variable /statement should be generated - assert len(ctx) == 1 - assert len(statements) == 1 - - assert ctx.values()[0].value == [1, 2, 3] - assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) - - def test_update_from_empty(self): - """ Tests that updating an empty list creates a straight insert statement """ - ctx = {} - col = columns.List(columns.Integer, db_field="TEST") - statements = col.get_update_statement([1, 2, 3], [], ctx) - - #only one variable /statement should be generated - assert len(ctx) == 1 - assert len(statements) == 1 - - assert ctx.values()[0].value == [1, 2, 3] - assert statements[0] == '"TEST" = :{}'.format(ctx.keys()[0]) - def test_instantiation_with_column_class(self): """ Tests that columns instantiated with a column class work properly From a8a45c0fa6471e506c4fa842cbeaa4535fb64b44 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 11:11:33 -0700 Subject: [PATCH 0878/3961] fixed boolean defaults --- cqlengine/columns.py | 1 + cqlengine/tests/columns/test_validation.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index e3a32b70e8..e211a4478a 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -442,6 +442,7 @@ class Boolean(Column): def validate(self, value): """ Always returns a Python boolean. """ + value = super(Boolean, self).validate(value) return bool(value) def to_python(self, value): diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index ceaa9371aa..878186d22e 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -78,6 +78,24 @@ def test_datetime_none(self): assert dts[0][0] is None +class TestBoolDefault(BaseCassEngTestCase): + class BoolDefaultValueTest(Model): + test_id = Integer(primary_key=True) + stuff = Boolean(default=True) + + @classmethod + def setUpClass(cls): + super(TestBoolDefault, cls).setUpClass() + sync_table(cls.BoolDefaultValueTest) + + def test_default_is_set(self): + tmp = self.BoolDefaultValueTest.create(test_id=1) + self.assertEqual(True, tmp.stuff) + tmp2 = self.BoolDefaultValueTest.get(test_id=1) + self.assertEqual(True, tmp2.stuff) + + + class TestVarInt(BaseCassEngTestCase): class VarIntTest(Model): test_id = Integer(primary_key=True) @@ -86,12 +104,12 @@ class VarIntTest(Model): @classmethod def setUpClass(cls): super(TestVarInt, cls).setUpClass() - create_table(cls.VarIntTest) + sync_table(cls.VarIntTest) @classmethod def tearDownClass(cls): super(TestVarInt, cls).tearDownClass() - delete_table(cls.VarIntTest) + sync_table(cls.VarIntTest) def test_varint_io(self): long_int = sys.maxint + 1 From cb335d9be01f3ce3a4f99bfcf031d1da9cbfac3d Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 11:11:47 -0700 Subject: [PATCH 0879/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 4312e0d0ca..1985d91413 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.15.2 +0.15.3 From 6055df24d010c243e222a515039f5777aff26b1c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 25 Jun 2014 13:57:13 -0500 Subject: [PATCH 0880/3961] Fix cassandra.concurrent behavior with automatic paging Fixes PYTHON-81 --- CHANGELOG.rst | 2 ++ cassandra/cluster.py | 9 ++++++- cassandra/concurrent.py | 26 ++++++++++++++----- .../integration/standard/test_query_paging.py | 17 +++++++++++- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1a9aa1de12..0280c0430c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -32,6 +32,8 @@ Bug Fixes * Don't share prepared statement lock across Cluster instances * Format CompositeType and DynamicCompositeType columns correctly in CREATE TABLE statements. +* Fix cassandra.concurrent behavior when dealing with automatic paging + (PYTHON-81) 2.0.2 ===== diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 839aeded90..847b58ca3d 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -2704,6 +2704,11 @@ def add_callbacks(self, callback, errback, self.add_callback(callback, *callback_args, **(callback_kwargs or {})) self.add_errback(errback, *errback_args, **(errback_kwargs or {})) + def clear_callbacks(self): + with self._callback_lock: + self._callback = None + self._errback = None + def __str__(self): result = "(no result yet)" if self._final_result is _NOT_SET else self._final_result return "" \ @@ -2744,6 +2749,8 @@ class will be returned. .. versionadded: 2.0.0 """ + response_future = None + def __init__(self, response_future, initial_response): self.response_future = response_future self.current_response = iter(initial_response) @@ -2755,7 +2762,7 @@ def next(self): try: return next(self.current_response) except StopIteration: - if self.response_future._paging_state is None: + if not self.response_future.has_more_pages: raise self.response_future.start_fetching_next_page() diff --git a/cassandra/concurrent.py b/cassandra/concurrent.py index 3c3a520831..0f646da844 100644 --- a/cassandra/concurrent.py +++ b/cassandra/concurrent.py @@ -16,9 +16,14 @@ import sys from itertools import count, cycle +import logging from six.moves import xrange from threading import Event +from cassandra.cluster import PagedResult + +log = logging.getLogger(__name__) + def execute_concurrent(session, statements_and_parameters, concurrency=100, raise_on_first_error=True): """ @@ -81,7 +86,7 @@ def execute_concurrent(session, statements_and_parameters, concurrency=100, rais num_finished = count(start=1) statements = enumerate(iter(statements_and_parameters)) for i in xrange(min(concurrency, len(statements_and_parameters))): - _execute_next(_sentinel, i, event, session, statements, results, num_finished, to_execute, first_error) + _execute_next(_sentinel, i, event, session, statements, results, None, num_finished, to_execute, first_error) event.wait() if first_error: @@ -113,7 +118,8 @@ def execute_concurrent_with_args(session, statement, parameters, *args, **kwargs _sentinel = object() -def _handle_error(error, result_index, event, session, statements, results, num_finished, to_execute, first_error): +def _handle_error(error, result_index, event, session, statements, results, + future, num_finished, to_execute, first_error): if first_error is not None: first_error.append(error) event.set() @@ -129,9 +135,10 @@ def _handle_error(error, result_index, event, session, statements, results, num_ except StopIteration: return - args = (next_index, event, session, statements, results, num_finished, to_execute, first_error) try: - session.execute_async(statement, params).add_callbacks( + future = session.execute_async(statement, params) + args = (next_index, event, session, statements, results, future, num_finished, to_execute, first_error) + future.add_callbacks( callback=_execute_next, callback_args=args, errback=_handle_error, errback_args=args) except Exception as exc: @@ -149,8 +156,12 @@ def _handle_error(error, result_index, event, session, statements, results, num_ return -def _execute_next(result, result_index, event, session, statements, results, num_finished, to_execute, first_error): +def _execute_next(result, result_index, event, session, statements, results, + future, num_finished, to_execute, first_error): if result is not _sentinel: + if future.has_more_pages: + result = PagedResult(future, result) + future.clear_callbacks() results[result_index] = (True, result) finished = next(num_finished) if finished >= to_execute: @@ -162,9 +173,10 @@ def _execute_next(result, result_index, event, session, statements, results, num except StopIteration: return - args = (next_index, event, session, statements, results, num_finished, to_execute, first_error) try: - session.execute_async(statement, params).add_callbacks( + future = session.execute_async(statement, params) + args = (next_index, event, session, statements, results, future, num_finished, to_execute, first_error) + future.add_callbacks( callback=_execute_next, callback_args=args, errback=_handle_error, errback_args=args) except Exception as exc: diff --git a/tests/integration/standard/test_query_paging.py b/tests/integration/standard/test_query_paging.py index 3dce069c88..09438abcf4 100644 --- a/tests/integration/standard/test_query_paging.py +++ b/tests/integration/standard/test_query_paging.py @@ -27,7 +27,7 @@ from threading import Event from cassandra.cluster import Cluster -from cassandra.concurrent import execute_concurrent +from cassandra.concurrent import execute_concurrent, execute_concurrent_with_args from cassandra.policies import HostDistance from cassandra.query import SimpleStatement @@ -266,3 +266,18 @@ def handle_error(err): future.add_callbacks(callback=handle_page, callback_args=(future, counter), errback=handle_error) event.wait() self.assertEquals(next(counter), 100) + + def test_concurrent_with_paging(self): + statements_and_params = zip(cycle(["INSERT INTO test3rf.test (k, v) VALUES (%s, 0)"]), + [(i, ) for i in range(100)]) + execute_concurrent(self.session, list(statements_and_params)) + + prepared = self.session.prepare("SELECT * FROM test3rf.test") + + for fetch_size in (2, 3, 7, 10, 99, 100, 101, 10000): + self.session.default_fetch_size = fetch_size + results = execute_concurrent_with_args(self.session, prepared, [None] * 10) + self.assertEquals(10, len(results)) + for (success, result) in results: + self.assertTrue(success) + self.assertEquals(100, len(list(result))) From 5d75a339a96a324e09e84299fe869c5e8f1c3aaa Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 12:08:37 -0700 Subject: [PATCH 0881/3961] using native driver libs for decoding --- cqlengine/columns.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cqlengine/columns.py b/cqlengine/columns.py index e211a4478a..66286d38e8 100644 --- a/cqlengine/columns.py +++ b/cqlengine/columns.py @@ -3,11 +3,10 @@ from datetime import datetime from datetime import date import re -from uuid import uuid1, uuid4 -from cql.query import cql_quote -from cql.cqltypes import DateType +from cassandra.cqltypes import DateType from cqlengine.exceptions import ValidationError +from cassandra.encoder import cql_quote class BaseValueManager(object): From 469799385e3fa8f820506feac651697b09f21590 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 12:10:52 -0700 Subject: [PATCH 0882/3961] used quoter in native driver --- cqlengine/statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index ac4f281d6f..dd053d7c0f 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -36,7 +36,7 @@ def __str__(self): class InQuoter(ValueQuoter): def __unicode__(self): - from cql.query import cql_quote + from cassandra.encoder import cql_quote return '(' + ', '.join([cql_quote(v) for v in self.value]) + ')' From 65975ccf77abdf5ce0e6b2f07362080caf724bd4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 12:11:30 -0700 Subject: [PATCH 0883/3961] version bump, removed old cql --- cqlengine/VERSION | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 1985d91413..7ffdfa1cad 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.15.3 +0.15.4 diff --git a/setup.py b/setup.py index 2d6740db31..a427f36bee 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "Topic :: Software Development :: Libraries :: Python Modules", ], keywords='cassandra,cql,orm', - install_requires = ['cassandra-driver >= 2.0.0', "cql"], + install_requires = ['cassandra-driver >= 2.0.0'], author='Blake Eggleston, Jon Haddad', author_email='bdeggleston@gmail.com', url='https://github.com/cqlengine/cqlengine', From 380ddc5a03b9e086a2edabf6b8f5399c809c71e2 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 12:12:44 -0700 Subject: [PATCH 0884/3961] removed last cql.query reference --- cqlengine/statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index dd053d7c0f..1619da7f7d 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -13,7 +13,7 @@ def __init__(self, value): self.value = value def __unicode__(self): - from cql.query import cql_quote + from cassandra.encoder import cql_quote if isinstance(self.value, bool): return 'true' if self.value else 'false' elif isinstance(self.value, (list, tuple)): From 3fa7148dea870adeee511feabd9de88ace715262 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 25 Jun 2014 12:13:03 -0700 Subject: [PATCH 0885/3961] removed last instance of cql.quote --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 7ffdfa1cad..1282fff53b 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.15.4 +0.15.5 From 392288aa2b1940882c102665bd4ce2b2e20341b7 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 26 Jun 2014 14:05:36 -0500 Subject: [PATCH 0886/3961] Properly defunct connections after protocol errors --- CHANGELOG.rst | 1 + cassandra/connection.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0280c0430c..4380d2edf0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,6 +34,7 @@ Bug Fixes CREATE TABLE statements. * Fix cassandra.concurrent behavior when dealing with automatic paging (PYTHON-81) +* Properly defunct connections after protocol errors 2.0.2 ===== diff --git a/cassandra/connection.py b/cassandra/connection.py index c94cf9724d..9796bf44d0 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -35,7 +35,7 @@ QueryMessage, ResultMessage, decode_response, InvalidRequestException, SupportedMessage, AuthResponseMessage, AuthChallengeMessage, - AuthSuccessMessage) + AuthSuccessMessage, ProtocolException) from cassandra.util import OrderedDict @@ -361,10 +361,14 @@ def process_msg(self, msg, body_len): return try: - if stream_id < 0: + if stream_id >= 0: + if isinstance(response, ProtocolException): + log.error("Closing connection %s due to protocol error: %s", self, response.summary_msg()) + self.defunct(response) + if callback is not None: + callback(response) + else: self.handle_pushed(response) - elif callback is not None: - callback(response) except Exception: log.exception("Callback handler errored, ignoring:") From 9468f6bd8fcff4630901d7179c5855bbd3119cc1 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 26 Jun 2014 16:47:02 -0500 Subject: [PATCH 0887/3961] Avoid UnicodeDecodeError when query string is unicode Fixes PYTHON-76 --- CHANGELOG.rst | 1 + cassandra/cluster.py | 4 +++- tests/integration/standard/test_types.py | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4380d2edf0..b1f898163d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,7 @@ Bug Fixes * Fix cassandra.concurrent behavior when dealing with automatic paging (PYTHON-81) * Properly defunct connections after protocol errors +* Avoid UnicodeDecodeError when query string is unicode (PYTHON-76) 2.0.2 ===== diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 847b58ca3d..fd63f4361d 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1253,8 +1253,10 @@ def _create_response_future(self, query, parameters, trace): if isinstance(query, SimpleStatement): query_string = query.query_string + if six.PY2 and isinstance(query_string, six.text_type): + query_string = query_string.encode('utf-8') if parameters: - query_string = bind_params(query.query_string, parameters, self.encoders) + query_string = bind_params(query_string, parameters, self.encoders) message = QueryMessage( query_string, cl, query.serial_consistency_level, fetch_size, timestamp=timestamp) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 08e48383c0..8d11ba1008 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -436,3 +436,10 @@ def test_tuple_type(self): prepared = s.prepare("SELECT b FROM mytable WHERE a=?") self.assertEqual(complete, s.execute(prepared, (2,))[0].b) self.assertEqual(partial_result, s.execute(prepared, (3,))[0].b) + + def test_unicode_query_string(self): + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + + query = u"SELECT * FROM system.schema_columnfamilies WHERE keyspace_name = 'ef\u2052ef' AND columnfamily_name = %s" + s.execute(query, (u"fe\u2051fe",)) From 2c58106e87e0aa325e456f1bb8eefbca4666d677 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 16:18:00 -0700 Subject: [PATCH 0888/3961] LIMIT can be used in count queries --- cqlengine/statements.py | 2 +- cqlengine/tests/query/test_queryset.py | 3 ++- cqlengine/tests/statements/test_select_statement.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 1619da7f7d..18434d230e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -549,7 +549,7 @@ def __unicode__(self): if self.order_by and not self.count: qs += ['ORDER BY {}'.format(', '.join(unicode(o) for o in self.order_by))] - if self.limit and not self.count: + if self.limit: qs += ['LIMIT {}'.format(self.limit)] if self.allow_filtering: diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 97c5c4e8b0..3058d375f8 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -239,7 +239,8 @@ def test_query_limit_count(self): assert TestModel.objects.count() == 12 q = TestModel.objects(TestModel.test_id == 0).limit(2) - assert q.count() == 2 + result = q.count() + self.assertEqual(2, result) def test_iteration(self): """ Tests that iterating over a query set pulls back all of the expected results """ diff --git a/cqlengine/tests/statements/test_select_statement.py b/cqlengine/tests/statements/test_select_statement.py index 3afe34e2d9..9c466485a5 100644 --- a/cqlengine/tests/statements/test_select_statement.py +++ b/cqlengine/tests/statements/test_select_statement.py @@ -35,8 +35,8 @@ def test_where_clause_rendering(self): def test_count(self): ss = SelectStatement('table', count=True, limit=10, order_by='d') ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b')) - self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = %(0)s', unicode(ss)) - self.assertNotIn('LIMIT', unicode(ss)) + self.assertEqual(unicode(ss), 'SELECT COUNT(*) FROM table WHERE "a" = %(0)s LIMIT 10', unicode(ss)) + self.assertIn('LIMIT', unicode(ss)) self.assertNotIn('ORDER', unicode(ss)) def test_context(self): From d6ae786bc56e5669356e0a180462a92a6d569241 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 16:30:21 -0700 Subject: [PATCH 0889/3961] removed default cqlengine keyspace #217 --- cqlengine/connection.py | 2 +- cqlengine/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index bd000bd0cc..1b4f6b0a2b 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -31,7 +31,7 @@ class CQLConnectionError(CQLEngineException): pass def setup( hosts, - default_keyspace=None, + default_keyspace, consistency=ConsistencyLevel.ONE, **kwargs): """ diff --git a/cqlengine/models.py b/cqlengine/models.py index 028ab658e6..4697cd8fa5 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -11,7 +11,7 @@ class ModelDefinitionException(ModelException): pass class PolyMorphicModelException(ModelException): pass -DEFAULT_KEYSPACE = 'cqlengine' +DEFAULT_KEYSPACE = None class hybrid_classmethod(object): From 36c98d3f9502f42a7060140b5a22685356a9b240 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 16:35:44 -0700 Subject: [PATCH 0890/3961] make sure setup before querying --- cqlengine/connection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 1b4f6b0a2b..988d5fd271 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -60,6 +60,9 @@ def setup( def execute(query, params=None, consistency_level=None): + if not session: + raise CQLEngineException("It is required to setup() cqlengine before executing queries") + if consistency_level is None: consistency_level = default_consistency_level @@ -75,6 +78,7 @@ def execute(query, params=None, consistency_level=None): query = SimpleStatement(query, consistency_level=consistency_level) + params = params or {} result = session.execute(query, params) From 96d91793eb8a77505b7608ee0b1253e28196c74e Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 17:42:14 -0700 Subject: [PATCH 0891/3961] not allowing non-models to be passed to sync_table --- cqlengine/connection.py | 2 +- cqlengine/management.py | 6 +++++- cqlengine/tests/management/test_management.py | 10 +++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 988d5fd271..da86e60053 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -62,7 +62,7 @@ def execute(query, params=None, consistency_level=None): if not session: raise CQLEngineException("It is required to setup() cqlengine before executing queries") - + if consistency_level is None: consistency_level = default_consistency_level diff --git a/cqlengine/management.py b/cqlengine/management.py index 54128ceb23..a47e9e9275 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -12,7 +12,7 @@ Field = namedtuple('Field', ['name', 'type']) logger = logging.getLogger(__name__) - +from cqlengine.models import Model # system keyspaces schema_columnfamilies = NamedTable('system', 'schema_columnfamilies') @@ -74,9 +74,13 @@ def sync_table(model, create_missing_keyspace=True): :type create_missing_keyspace: bool """ + if not issubclass(model, Model): + raise CQLEngineException("Models must be derived from base Model.") + if model.__abstract__: raise CQLEngineException("cannot create table from abstract model") + #construct query string cf_name = model.column_family_name() raw_cf_name = model.column_family_name(include_keyspace=False) diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 67e77a356f..b7c492db89 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -1,4 +1,4 @@ - +from cqlengine.exceptions import CQLEngineException from cqlengine.management import get_fields, sync_table, drop_table from cqlengine.tests.base import BaseCassEngTestCase from cqlengine import management @@ -140,3 +140,11 @@ def test_sync_table_works_with_primary_keys_only_tables(self): table_settings = management.get_table_settings(PrimaryKeysOnlyModel) assert SizeTieredCompactionStrategy in table_settings.options['compaction_strategy_class'] + +class NonModelFailureTest(BaseCassEngTestCase): + class FakeModel(object): + pass + + def test_failure(self): + with self.assertRaises(CQLEngineException): + sync_table(self.FakeModel) From e824a239362f861636055ff85e16407054922e29 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 19:37:14 -0700 Subject: [PATCH 0892/3961] added faq, needs formatting --- cqlengine/statements.py | 4 ++++ docs/topics/faq.rst | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 docs/topics/faq.rst diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 18434d230e..549b529df7 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -523,6 +523,10 @@ def __init__(self, limit=None, allow_filtering=False): + """ + :param where + :type where list of cqlengine.statements.WhereClause + """ super(SelectStatement, self).__init__( table, consistency=consistency, diff --git a/docs/topics/faq.rst b/docs/topics/faq.rst new file mode 100644 index 0000000000..f0f98e4526 --- /dev/null +++ b/docs/topics/faq.rst @@ -0,0 +1,7 @@ +========================== +Frequently Asked Questions +========================== + +Q: Why does calling my Model(field=blah, field2=blah2) not work? + +A: The __init__() of a model is used by cqlengine internally. If you want to create a new row in the database, use create(). From 3d036f2e0ca562fdef07df1cceedbec595c71fa8 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 19:40:04 -0700 Subject: [PATCH 0893/3961] formatting for faq --- docs/index.rst | 1 + docs/topics/faq.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index e9663bc85d..99a08efc3a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,7 @@ Contents: topics/columns topics/connection topics/manage_schemas + topics/faq .. _getting-started: diff --git a/docs/topics/faq.rst b/docs/topics/faq.rst index f0f98e4526..0b58b6358e 100644 --- a/docs/topics/faq.rst +++ b/docs/topics/faq.rst @@ -3,5 +3,6 @@ Frequently Asked Questions ========================== Q: Why does calling my Model(field=blah, field2=blah2) not work? +------------------------------------------------------------------- A: The __init__() of a model is used by cqlengine internally. If you want to create a new row in the database, use create(). From 0d3dbbed6c93b18d7e655cd1fdb51ac3f726d267 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 19:57:08 -0700 Subject: [PATCH 0894/3961] document named table --- docs/topics/queryset.rst | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/topics/queryset.rst b/docs/topics/queryset.rst index a8dca85356..e1e172645a 100644 --- a/docs/topics/queryset.rst +++ b/docs/topics/queryset.rst @@ -227,7 +227,7 @@ Token Function last = first_page[-1] next_page = list(query.filter(pk__token__gt=cqlengine.Token(last.pk))) -QuerySets are imutable +QuerySets are immutable ====================== When calling any method that changes a queryset, the method does not actually change the queryset object it's called on, but returns a new queryset object with the attributes of the original queryset, plus the attributes added in the method call. @@ -505,3 +505,23 @@ QuerySet method reference # add items to a map Row.objects(row_id=5).update(map_column__update={1: 2, 3: 4}) + + +Named Tables +=================== + +Named tables are a way of querying a table without creating an class. They're useful for querying system tables or exploring an unfamiliar database. + + + .. code-block:: python + + from cqlengine.connection import setup + setup("127.0.0.1", "cqlengine_test") + + from cqlengine.named import NamedTable + user = NamedTable("cqlengine_test", "user") + user.objects() + user.objects()[0] + + # {u'pk': 1, u't': datetime.datetime(2014, 6, 26, 17, 10, 31, 774000)} + From 1ccc7e64fb1676cf9ec2e76ec96473f77879b3b0 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Thu, 26 Jun 2014 21:23:05 -0700 Subject: [PATCH 0895/3961] load test --- cqlengine/tests/test_load.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 cqlengine/tests/test_load.py diff --git a/cqlengine/tests/test_load.py b/cqlengine/tests/test_load.py new file mode 100644 index 0000000000..16a42eac88 --- /dev/null +++ b/cqlengine/tests/test_load.py @@ -0,0 +1,33 @@ +import os +from unittest import TestCase, skipUnless + +from cqlengine import Model, Integer +from cqlengine.management import sync_table +from cqlengine.tests import base +import resource +import gc + +class LoadTest(Model): + k = Integer(primary_key=True) + v = Integer() + + +@skipUnless("LOADTEST" in os.environ, "LOADTEST not on") +def test_lots_of_queries(): + sync_table(LoadTest) + import objgraph + gc.collect() + objgraph.show_most_common_types() + + print "Starting..." + + for i in range(1000000): + if i % 25000 == 0: + # print memory statistic + print "Memory usage: %s" % (resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) + + LoadTest.create(k=i, v=i) + + objgraph.show_most_common_types() + + raise Exception("you shouldn't be here") From 5a6f9c89ad69f70ce27e1331fc3e8976d83e1323 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 11:04:01 -0700 Subject: [PATCH 0896/3961] fixed issue #162, allows non equality filtering with allow_filtering --- cqlengine/query.py | 2 +- cqlengine/tests/query/test_queryset.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/cqlengine/query.py b/cqlengine/query.py index 320084ef00..ee776cd227 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -631,7 +631,7 @@ def _validate_select_where(self): #check that there's either a = or IN relationship with a primary key or indexed field equal_ops = [self.model._columns.get(w.field) for w in self._where if isinstance(w.operator, EqualsOperator)] token_comparison = any([w for w in self._where if isinstance(w.value, Token)]) - if not any([w.primary_key or w.index for w in equal_ops]) and not token_comparison: + if not any([w.primary_key or w.index for w in equal_ops]) and not token_comparison and not self._allow_filtering: raise QueryException('Where clauses require either a "=" or "IN" comparison with either a primary key or indexed field') if not self._allow_filtering: diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 3058d375f8..700eae3d46 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -1,12 +1,14 @@ from datetime import datetime import time +from unittest import TestCase from uuid import uuid1, uuid4 +import uuid from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException from cqlengine import functions -from cqlengine.management import create_table, drop_table +from cqlengine.management import create_table, drop_table, sync_table from cqlengine.management import delete_table from cqlengine.models import Model from cqlengine import columns @@ -369,6 +371,28 @@ def test_allow_filtering_flag(self): """ +def test_non_quality_filtering(): + class NonEqualityFilteringModel(Model): + example_id = columns.UUID(primary_key=True, default=uuid.uuid4) + sequence_id = columns.Integer(primary_key=True) # sequence_id is a clustering key + example_type = columns.Integer(index=True) + created_at = columns.DateTime() + + drop_table(NonEqualityFilteringModel) + sync_table(NonEqualityFilteringModel) + + # setup table, etc. + + NonEqualityFilteringModel.create(sequence_id=1, example_type=0, created_at=datetime.now()) + NonEqualityFilteringModel.create(sequence_id=3, example_type=0, created_at=datetime.now()) + NonEqualityFilteringModel.create(sequence_id=5, example_type=1, created_at=datetime.now()) + + qA = NonEqualityFilteringModel.objects(NonEqualityFilteringModel.sequence_id > 3).allow_filtering() + num = qA.count() + assert num == 1, num + + + class TestQuerySetOrdering(BaseQuerySetUsage): def test_order_by_success_case(self): From 8e18ab025029f20afb8549d6a7f2a96a4e1a0da0 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 12:50:38 -0700 Subject: [PATCH 0897/3961] fixed changelog --- changelog | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/changelog b/changelog index f967fec06a..e30733c62b 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,14 @@ CHANGELOG +0.16.0 + +* Check number of elements in collections. +* Add support for table properties. + + +0.15.0 +* native driver integration + 0.14.0 * fix for setting map to empty (Lifto) @@ -7,11 +16,6 @@ CHANGELOG * use stable version of sure package (maedhroz) * performance improvements -<<<<<<< HEAD -======= -* Check number of elements in collections. -* Add support for table properties. ->>>>>>> 3c56fec1d8d79d6037d3b83703608d4889994012 0.13.0 * adding support for post batch callbacks (thanks Daniel Dotsenko github.com/dvdotsenko) From 36c3ae905b3f5c79e2a458d6593c782344797c1a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 13:36:25 -0700 Subject: [PATCH 0898/3961] disallow None as a value in a filter #172 --- cqlengine/models.py | 3 ++ cqlengine/query.py | 3 ++ cqlengine/tests/model/test_model_io.py | 58 ++++++++++++++++---------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index 871fe401c1..b7f80f60eb 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -500,6 +500,9 @@ def all(cls): @classmethod def filter(cls, *args, **kwargs): + # if kwargs.values().count(None): + # raise CQLEngineException("Cannot pass None as a filter") + return cls.objects.filter(*args, **kwargs) @classmethod diff --git a/cqlengine/query.py b/cqlengine/query.py index ee776cd227..0a64a5c78a 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -405,6 +405,9 @@ def filter(self, *args, **kwargs): :rtype: AbstractQuerySet """ #add arguments to the where clause filters + if kwargs.values().count(None): + raise CQLEngineException("None values on filter are not allowed") + clone = copy.deepcopy(self) for operator in args: if not isinstance(operator, WhereClause): diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index 7c26bae4a3..e114d75a86 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -2,10 +2,11 @@ import random from datetime import date from operator import itemgetter +from cqlengine.exceptions import CQLEngineException from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.management import create_table -from cqlengine.management import delete_table +from cqlengine.management import sync_table +from cqlengine.management import drop_table from cqlengine.models import Model from cqlengine import columns @@ -27,12 +28,12 @@ class TestModelIO(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestModelIO, cls).setUpClass() - create_table(TestModel) + sync_table(TestModel) @classmethod def tearDownClass(cls): super(TestModelIO, cls).tearDownClass() - delete_table(TestModel) + drop_table(TestModel) def test_model_save_and_load(self): """ @@ -109,8 +110,8 @@ def test_column_deleting_works_properly(self): def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): """ """ - create_table(TestModel) - create_table(TestModel) + sync_table(TestModel) + sync_table(TestModel) class TestMultiKeyModel(Model): @@ -125,13 +126,13 @@ class TestDeleting(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestDeleting, cls).setUpClass() - delete_table(TestMultiKeyModel) - create_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) + sync_table(TestMultiKeyModel) @classmethod def tearDownClass(cls): super(TestDeleting, cls).tearDownClass() - delete_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) def test_deleting_only_deletes_one_object(self): partition = random.randint(0,1000) @@ -152,13 +153,13 @@ class TestUpdating(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestUpdating, cls).setUpClass() - delete_table(TestMultiKeyModel) - create_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) + sync_table(TestMultiKeyModel) @classmethod def tearDownClass(cls): super(TestUpdating, cls).tearDownClass() - delete_table(TestMultiKeyModel) + drop_table(TestMultiKeyModel) def setUp(self): super(TestUpdating, self).setUp() @@ -201,13 +202,13 @@ class TestCanUpdate(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestCanUpdate, cls).setUpClass() - delete_table(TestModel) - create_table(TestModel) + drop_table(TestModel) + sync_table(TestModel) @classmethod def tearDownClass(cls): super(TestCanUpdate, cls).tearDownClass() - delete_table(TestModel) + drop_table(TestModel) def test_success_case(self): tm = TestModel(count=8, text='123456789') @@ -245,8 +246,8 @@ class IndexDefinitionModel(Model): class TestIndexedColumnDefinition(BaseCassEngTestCase): def test_exception_isnt_raised_if_an_index_is_defined_more_than_once(self): - create_table(IndexDefinitionModel) - create_table(IndexDefinitionModel) + sync_table(IndexDefinitionModel) + sync_table(IndexDefinitionModel) class ReservedWordModel(Model): token = columns.Text(primary_key=True) @@ -257,7 +258,7 @@ class TestQueryQuoting(BaseCassEngTestCase): def test_reserved_cql_words_can_be_used_as_column_names(self): """ """ - create_table(ReservedWordModel) + sync_table(ReservedWordModel) model1 = ReservedWordModel.create(token='1', insert=5) @@ -279,13 +280,13 @@ class TestQuerying(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestQuerying, cls).setUpClass() - delete_table(TestQueryModel) - create_table(TestQueryModel) + drop_table(TestQueryModel) + sync_table(TestQueryModel) @classmethod def tearDownClass(cls): super(TestQuerying, cls).tearDownClass() - delete_table(TestQueryModel) + drop_table(TestQueryModel) def test_query_with_date(self): uid = uuid4() @@ -300,3 +301,18 @@ def test_query_with_date(self): assert inst.test_id == uid assert inst.date == day + +def test_none_filter_fails(): + class NoneFilterModel(Model): + pk = columns.Integer(primary_key=True) + v = columns.Integer() + sync_table(NoneFilterModel) + + try: + NoneFilterModel.objects(pk=None) + raise Exception("fail") + except CQLEngineException as e: + pass + + + From 393e2029a69eee48d5f21a53bb06a266adf0c478 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 13:53:47 -0700 Subject: [PATCH 0899/3961] create_table > sync_table --- cqlengine/tests/model/test_class_construction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 517798f2f4..4f7ee53d5e 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -262,9 +262,9 @@ def test_attempting_to_save_abstract_model_fails(self): def test_attempting_to_create_abstract_table_fails(self): """ Attempting to create a table from an abstract model should fail """ - from cqlengine.management import create_table + from cqlengine.management import sync_table with self.assertRaises(CQLEngineException): - create_table(AbstractModelWithFullCols) + sync_table(AbstractModelWithFullCols) def test_attempting_query_on_abstract_model_fails(self): """ Tests attempting to execute query with an abstract model fails """ @@ -279,8 +279,8 @@ def test_abstract_columns_are_inherited(self): def test_concrete_class_table_creation_cycle(self): """ Tests that models with inherited abstract classes can be created, and have io performed """ - from cqlengine.management import create_table, delete_table - create_table(ConcreteModelWithCol) + from cqlengine.management import sync_table, delete_table + sync_table(ConcreteModelWithCol) w1 = ConcreteModelWithCol.create(pkey=5, data=6) w2 = ConcreteModelWithCol.create(pkey=6, data=7) From 17811251f6fc22aa95fe8398c77b989cef9e3d36 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 13:54:13 -0700 Subject: [PATCH 0900/3961] delete_table no more --- cqlengine/tests/model/test_class_construction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 4f7ee53d5e..c8a54eb1c2 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -279,7 +279,7 @@ def test_abstract_columns_are_inherited(self): def test_concrete_class_table_creation_cycle(self): """ Tests that models with inherited abstract classes can be created, and have io performed """ - from cqlengine.management import sync_table, delete_table + from cqlengine.management import sync_table, drop_table sync_table(ConcreteModelWithCol) w1 = ConcreteModelWithCol.create(pkey=5, data=6) @@ -293,7 +293,7 @@ def test_concrete_class_table_creation_cycle(self): assert w2.pkey == r2.pkey assert w2.data == r2.data - delete_table(ConcreteModelWithCol) + drop_table(ConcreteModelWithCol) class TestCustomQuerySet(BaseCassEngTestCase): From dc80fb9912eda98c2e152eae2f2e6dd8833eb5e1 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 13:57:48 -0700 Subject: [PATCH 0901/3961] removed deprecated calls --- cqlengine/tests/columns/test_validation.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 878186d22e..749066f32d 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -24,7 +24,7 @@ from cqlengine.columns import Float from cqlengine.columns import Decimal -from cqlengine.management import create_table, delete_table, sync_table, drop_table +from cqlengine.management import sync_table, drop_table from cqlengine.models import Model import sys @@ -38,12 +38,12 @@ class DatetimeTest(Model): @classmethod def setUpClass(cls): super(TestDatetime, cls).setUpClass() - create_table(cls.DatetimeTest) + sync_table(cls.DatetimeTest) @classmethod def tearDownClass(cls): super(TestDatetime, cls).tearDownClass() - delete_table(cls.DatetimeTest) + drop_table(cls.DatetimeTest) def test_datetime_io(self): now = datetime.now() @@ -126,12 +126,12 @@ class DateTest(Model): @classmethod def setUpClass(cls): super(TestDate, cls).setUpClass() - create_table(cls.DateTest) + sync_table(cls.DateTest) @classmethod def tearDownClass(cls): super(TestDate, cls).tearDownClass() - delete_table(cls.DateTest) + drop_table(cls.DateTest) def test_date_io(self): today = date.today() @@ -164,12 +164,12 @@ class DecimalTest(Model): @classmethod def setUpClass(cls): super(TestDecimal, cls).setUpClass() - create_table(cls.DecimalTest) + sync_table(cls.DecimalTest) @classmethod def tearDownClass(cls): super(TestDecimal, cls).tearDownClass() - delete_table(cls.DecimalTest) + drop_table(cls.DecimalTest) def test_decimal_io(self): dt = self.DecimalTest.objects.create(test_id=0, dec_val=D('0.00')) @@ -188,12 +188,12 @@ class UUIDTest(Model): @classmethod def setUpClass(cls): super(TestUUID, cls).setUpClass() - create_table(cls.UUIDTest) + sync_table(cls.UUIDTest) @classmethod def tearDownClass(cls): super(TestUUID, cls).tearDownClass() - delete_table(cls.UUIDTest) + drop_table(cls.UUIDTest) def test_uuid_str_with_dashes(self): a_uuid = uuid4() @@ -215,12 +215,12 @@ class TimeUUIDTest(Model): @classmethod def setUpClass(cls): super(TestTimeUUID, cls).setUpClass() - create_table(cls.TimeUUIDTest) + sync_table(cls.TimeUUIDTest) @classmethod def tearDownClass(cls): super(TestTimeUUID, cls).tearDownClass() - delete_table(cls.TimeUUIDTest) + drop_table(cls.TimeUUIDTest) def test_timeuuid_io(self): """ From 2c9b49ab805a086eefcb8130c312b315d85edfff Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 13:58:40 -0700 Subject: [PATCH 0902/3961] removing deprecated calls --- cqlengine/tests/columns/test_counter_column.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/columns/test_counter_column.py b/cqlengine/tests/columns/test_counter_column.py index c260f38ed0..725b83905d 100644 --- a/cqlengine/tests/columns/test_counter_column.py +++ b/cqlengine/tests/columns/test_counter_column.py @@ -2,7 +2,7 @@ from cqlengine import Model from cqlengine import columns -from cqlengine.management import create_table, delete_table +from cqlengine.management import sync_table, drop_table from cqlengine.models import ModelDefinitionException from cqlengine.tests.base import BaseCassEngTestCase @@ -46,13 +46,13 @@ class TestCounterColumn(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestCounterColumn, cls).setUpClass() - delete_table(TestCounterModel) - create_table(TestCounterModel) + drop_table(TestCounterModel) + sync_table(TestCounterModel) @classmethod def tearDownClass(cls): super(TestCounterColumn, cls).tearDownClass() - delete_table(TestCounterModel) + drop_table(TestCounterModel) def test_updates(self): """ Tests that counter updates work as intended """ From bc466895f02a47d82d84bef2cd19b11a756ea593 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 14:00:15 -0700 Subject: [PATCH 0903/3961] removed deprecated calls --- cqlengine/tests/query/test_queryset.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index 700eae3d46..ced3891261 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -8,8 +8,8 @@ from cqlengine.exceptions import ModelException from cqlengine import functions -from cqlengine.management import create_table, drop_table, sync_table -from cqlengine.management import delete_table +from cqlengine.management import sync_table, drop_table, sync_table +from cqlengine.management import drop_table from cqlengine.models import Model from cqlengine import columns from cqlengine import query @@ -174,11 +174,11 @@ class BaseQuerySetUsage(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(BaseQuerySetUsage, cls).setUpClass() - delete_table(TestModel) - delete_table(IndexedTestModel) - create_table(TestModel) - create_table(IndexedTestModel) - create_table(TestMultiClusteringModel) + drop_table(TestModel) + drop_table(IndexedTestModel) + sync_table(TestModel) + sync_table(IndexedTestModel) + sync_table(TestMultiClusteringModel) TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30) TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30) @@ -551,12 +551,12 @@ class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestMinMaxTimeUUIDFunctions, cls).setUpClass() - create_table(TimeUUIDQueryModel) + sync_table(TimeUUIDQueryModel) @classmethod def tearDownClass(cls): super(TestMinMaxTimeUUIDFunctions, cls).tearDownClass() - delete_table(TimeUUIDQueryModel) + drop_table(TimeUUIDQueryModel) def test_tzaware_datetime_support(self): """Test that using timezone aware datetime instances works with the From 5e856f8bc9b8ba01c68332ba031083fa5bd9eb9f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 14:20:56 -0700 Subject: [PATCH 0904/3961] more deprecation removal --- cqlengine/tests/columns/test_value_io.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 121eb2580c..4b4ac7d200 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -4,8 +4,8 @@ from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.management import create_table -from cqlengine.management import delete_table +from cqlengine.management import sync_table +from cqlengine.management import drop_table from cqlengine.models import Model from cqlengine.columns import ValueQuoter from cqlengine import columns @@ -44,7 +44,7 @@ class IOTestModel(Model): pkey = cls.column(primary_key=True) data = cls.column() cls._generated_model = IOTestModel - create_table(cls._generated_model) + sync_table(cls._generated_model) #tupleify the tested values if not isinstance(cls.pkey_val, tuple): @@ -56,7 +56,7 @@ class IOTestModel(Model): def tearDownClass(cls): super(BaseColumnIOTest, cls).tearDownClass() if not cls.column: return - delete_table(cls._generated_model) + drop_table(cls._generated_model) def comparator_converter(self, val): """ If you want to convert the original value used to compare the model vales """ From 578764a3fa5e00223539fd5a72623e8d4629e65a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 14:22:38 -0700 Subject: [PATCH 0905/3961] deprecation cleanup --- cqlengine/tests/model/test_equality_operations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cqlengine/tests/model/test_equality_operations.py b/cqlengine/tests/model/test_equality_operations.py index a3e592b3dd..c851e6089b 100644 --- a/cqlengine/tests/model/test_equality_operations.py +++ b/cqlengine/tests/model/test_equality_operations.py @@ -2,8 +2,8 @@ from uuid import uuid4 from cqlengine.tests.base import BaseCassEngTestCase -from cqlengine.management import create_table -from cqlengine.management import delete_table +from cqlengine.management import sync_table +from cqlengine.management import drop_table from cqlengine.models import Model from cqlengine import columns @@ -17,7 +17,7 @@ class TestEqualityOperators(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestEqualityOperators, cls).setUpClass() - create_table(TestModel) + sync_table(TestModel) def setUp(self): super(TestEqualityOperators, self).setUp() @@ -27,7 +27,7 @@ def setUp(self): @classmethod def tearDownClass(cls): super(TestEqualityOperators, cls).tearDownClass() - delete_table(TestModel) + drop_table(TestModel) def test_an_instance_evaluates_as_equal_to_itself(self): """ From a7c908db7b45fc7f86da291452e4b49c37ddad15 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 14:23:21 -0700 Subject: [PATCH 0906/3961] removed cql requirement --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 62eba662b1..5973311b23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -cql==1.4.0 ipython==0.13.1 ipdb==0.7 Sphinx==1.1.3 From 6d8f77c740b497c07c53c27b10455c4b4b55848a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 14:24:36 -0700 Subject: [PATCH 0907/3961] updated docs --- docs/topics/manage_schemas.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/topics/manage_schemas.rst b/docs/topics/manage_schemas.rst index 7f9c63c1d5..aafaed7596 100644 --- a/docs/topics/manage_schemas.rst +++ b/docs/topics/manage_schemas.rst @@ -26,14 +26,14 @@ Once a connection has been made to Cassandra, you can use the functions in ``cql deletes the keyspace with the given name -.. function:: create_table(model [, create_missing_keyspace=True]) - +.. function:: sync_table(model [, create_missing_keyspace=True]) + :param model: the :class:`~cqlengine.model.Model` class to make a table with :type model: :class:`~cqlengine.model.Model` :param create_missing_keyspace: *Optional* If True, the model's keyspace will be created if it does not already exist. Defaults to ``True`` :type create_missing_keyspace: bool - creates a CQL table for the given model + syncs a python model to cassandra (creates & alters) .. function:: delete_table(model) @@ -42,7 +42,7 @@ Once a connection has been made to Cassandra, you can use the functions in ``cql deletes the CQL table for the given model - + See the example at :ref:`getting-started` From bbae559c3288e9f3a80eb0e4b50d3a6364789146 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 14:26:16 -0700 Subject: [PATCH 0908/3961] removed last of deprecated code --- cqlengine/tests/query/test_datetime_queries.py | 8 ++++---- docs/topics/manage_schemas.rst | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cqlengine/tests/query/test_datetime_queries.py b/cqlengine/tests/query/test_datetime_queries.py index 39cc0e332f..c6cb0627b7 100644 --- a/cqlengine/tests/query/test_datetime_queries.py +++ b/cqlengine/tests/query/test_datetime_queries.py @@ -4,8 +4,8 @@ from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException -from cqlengine.management import create_table -from cqlengine.management import delete_table +from cqlengine.management import sync_table +from cqlengine.management import drop_table from cqlengine.models import Model from cqlengine import columns from cqlengine import query @@ -20,7 +20,7 @@ class TestDateTimeQueries(BaseCassEngTestCase): @classmethod def setUpClass(cls): super(TestDateTimeQueries, cls).setUpClass() - create_table(DateTimeQueryTestModel) + sync_table(DateTimeQueryTestModel) cls.base_date = datetime.now() - timedelta(days=10) for x in range(7): @@ -35,7 +35,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): super(TestDateTimeQueries, cls).tearDownClass() - delete_table(DateTimeQueryTestModel) + drop_table(DateTimeQueryTestModel) def test_range_query(self): """ Tests that loading from a range of dates works properly """ diff --git a/docs/topics/manage_schemas.rst b/docs/topics/manage_schemas.rst index aafaed7596..1da5b1c9e9 100644 --- a/docs/topics/manage_schemas.rst +++ b/docs/topics/manage_schemas.rst @@ -35,7 +35,7 @@ Once a connection has been made to Cassandra, you can use the functions in ``cql syncs a python model to cassandra (creates & alters) -.. function:: delete_table(model) +.. function:: drop_table(model) :param model: the :class:`~cqlengine.model.Model` class to delete a column family for :type model: :class:`~cqlengine.model.Model` From 39e752cb25709eb7d4a1195e4963d8e9a7ced3bc Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Fri, 27 Jun 2014 14:30:47 -0700 Subject: [PATCH 0909/3961] #218 create_table and delete_table are fully deprecated --- cqlengine/management.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cqlengine/management.py b/cqlengine/management.py index 8285c2cb85..095e41b4c5 100644 --- a/cqlengine/management.py +++ b/cqlengine/management.py @@ -59,8 +59,7 @@ def delete_keyspace(name): execute("DROP KEYSPACE {}".format(name)) def create_table(model, create_missing_keyspace=True): - warnings.warn("create_table has been deprecated in favor of sync_table and will be removed in a future release", DeprecationWarning) - sync_table(model, create_missing_keyspace) + raise CQLEngineException("create_table is deprecated, please use sync_table") def sync_table(model, create_missing_keyspace=True): """ @@ -76,7 +75,7 @@ def sync_table(model, create_missing_keyspace=True): if not issubclass(model, Model): raise CQLEngineException("Models must be derived from base Model.") - + if model.__abstract__: raise CQLEngineException("cannot create table from abstract model") @@ -308,8 +307,7 @@ def update_compaction(model): def delete_table(model): - warnings.warn("delete_table has been deprecated in favor of drop_table()", DeprecationWarning) - return drop_table(model) + raise CQLEngineException("delete_table has been deprecated in favor of drop_table()") def drop_table(model): From 8834ec6392884cc020101f30f36b911a9850eea7 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Sat, 28 Jun 2014 20:42:47 -0400 Subject: [PATCH 0910/3961] delayed connect. use setup(delayed_connect=True) --- cqlengine/connection.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index bd000bd0cc..89e18c1d07 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -27,12 +27,14 @@ class CQLConnectionError(CQLEngineException): pass cluster = None session = None +delayed_connect_args = None default_consistency_level = None def setup( hosts, default_keyspace=None, consistency=ConsistencyLevel.ONE, + delayed_connect=False, **kwargs): """ Records the hosts and connects to one of them @@ -43,8 +45,10 @@ def setup( :type default_keyspace: str :param consistency: The global consistency level :type consistency: int + :param delayed_connect: True if should not connect until first use + :type delayed_connect: bool """ - global cluster, session, default_consistency_level + global cluster, session, default_consistency_level, delayed_connect_args if 'username' in kwargs or 'password' in kwargs: raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") @@ -54,12 +58,18 @@ def setup( models.DEFAULT_KEYSPACE = default_keyspace default_consistency_level = consistency + if delayed_connect: + delayed_connect_args = (hosts, default_keyspace, consistency, kwargs) + return + cluster = Cluster(hosts, **kwargs) session = cluster.connect() session.row_factory = dict_factory def execute(query, params=None, consistency_level=None): + handle_delayed_connect() + if consistency_level is None: consistency_level = default_consistency_level @@ -82,7 +92,16 @@ def execute(query, params=None, consistency_level=None): def get_session(): + handle_delayed_connect() return session def get_cluster(): + handle_delayed_connect() return cluster + +def handle_delayed_connect(): + global delayed_connect_args + if delayed_connect_args: + hosts, default_keyspace, consistency, kwargs = delayed_connect_args + delayed_connect_args = None + setup(hosts, default_keyspace, consistency, **kwargs) From 533ab80060030701a8ab0e2108fe1632272f502f Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 1 Jul 2014 18:48:58 +0800 Subject: [PATCH 0911/3961] 'IF NOT EXISTS' for insert statement The usage is just like 'ttl'. --- cqlengine/models.py | 24 +++++- cqlengine/query.py | 15 +++- cqlengine/statements.py | 21 +++++ cqlengine/tests/test_checkexist.py | 129 +++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 cqlengine/tests/test_checkexist.py diff --git a/cqlengine/models.py b/cqlengine/models.py index 028ab658e6..17d403df9c 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -111,6 +111,23 @@ def timestamp_setter(ts): def __call__(self, *args, **kwargs): raise NotImplementedError +class CheckExistDescriptor(object): + """ + return a query set descriptor with a check-exist flag specified + """ + def __get__(self, instance, model): + if instance: + # instance method + def checkexist_setter(ce): + instance._check_exist = ce + return instance + return checkexist_setter + + return model.objects.check_exist + + def __call__(self, *args, **kwargs): + raise NotImplementedError + class ConsistencyDescriptor(object): """ returns a query set descriptor if called on Class, instance if it was an instance call @@ -220,6 +237,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass # custom timestamps, see USING TIMESTAMP X timestamp = TimestampDescriptor() + + check_exist = CheckExistDescriptor() # _len is lazily created by __len__ @@ -261,6 +280,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass _timestamp = None # optional timestamp to include with the operation (USING TIMESTAMP) + _check_exist = False # optional check_exist flag to check existence before insertion + def __init__(self, **values): self._values = {} self._ttl = None @@ -510,7 +531,8 @@ def save(self): batch=self._batch, ttl=self._ttl, timestamp=self._timestamp, - consistency=self.__consistency__).save() + consistency=self.__consistency__, + check_exist=self._check_exist).save() #reset the value managers for v in self._values.values(): diff --git a/cqlengine/query.py b/cqlengine/query.py index 320084ef00..c95124a64c 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -222,6 +222,7 @@ def __init__(self, model): self._ttl = None self._consistency = None self._timestamp = None + self._check_exist = False @property def column_family_name(self): @@ -568,7 +569,7 @@ def defer(self, fields): def create(self, **kwargs): return self.model(**kwargs).batch(self._batch).ttl(self._ttl).\ - consistency(self._consistency).\ + consistency(self._consistency).check_exist(self._check_exist).\ timestamp(self._timestamp).save() def delete(self): @@ -707,6 +708,11 @@ def timestamp(self, timestamp): clone._timestamp = timestamp return clone + def check_exist(self, check_exist): + clone = copy.deepcopy(self) + clone._check_exist = check_exist + return clone + def update(self, **values): """ Updates the rows in this queryset """ if not values: @@ -766,8 +772,9 @@ class DMLQuery(object): _ttl = None _consistency = None _timestamp = None + _check_exist = False - def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None): + def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None, check_exist=False): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance @@ -775,6 +782,7 @@ def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, self._ttl = ttl self._consistency = consistency self._timestamp = timestamp + self._check_exist = check_exist def _execute(self, q): if self._batch: @@ -886,7 +894,7 @@ def save(self): if self.instance._has_counter or self.instance._can_update(): return self.update() else: - insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp) + insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp, check_exist=self._check_exist) for name, col in self.instance._columns.items(): val = getattr(self.instance, name, None) if col._val_is_null(val): @@ -902,7 +910,6 @@ def save(self): # caused by pointless update queries if not insert.is_empty: self._execute(insert) - # delete any nulled columns self._delete_null_columns() diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 1619da7f7d..849a7940d5 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -613,6 +613,24 @@ def get_context(self): class InsertStatement(AssignmentStatement): """ an cql insert select statement """ + def __init__(self, + table, + assignments=None, + consistency=None, + where=None, + ttl=None, + timestamp=None, + check_exist=False): + super(InsertStatement, self).__init__( + table, + assignments=assignments, + consistency=consistency, + where=where, + ttl=ttl, + timestamp=timestamp) + + self.check_exist = check_exist + def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") @@ -627,6 +645,9 @@ def __unicode__(self): qs += ['VALUES'] qs += ["({})".format(', '.join(['%({})s'.format(v) for v in values]))] + if self.check_exist: + qs += ["IF NOT EXISTS"] + if self.ttl: qs += ["USING TTL {}".format(self.ttl)] diff --git a/cqlengine/tests/test_checkexist.py b/cqlengine/tests/test_checkexist.py new file mode 100644 index 0000000000..7797459e62 --- /dev/null +++ b/cqlengine/tests/test_checkexist.py @@ -0,0 +1,129 @@ +from cqlengine.management import sync_table, drop_table, create_keyspace, delete_keyspace +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.models import Model +from cqlengine import columns +from uuid import uuid4 +import mock +from cqlengine.connection import get_session + + +class TestChechExistModel(Model): + + __keyspace__ = 'cqlengine_test_checkexist' + + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + count = columns.Integer() + text = columns.Text(required=False) + + +class BaseCheckExistTest(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(BaseCheckExistTest, cls).setUpClass() + """ + when receiving an insert statement with 'if not exist', cassandra would + perform a read with QUORUM level. Unittest would be failed if replica_factor + is 3 and one node only. Therefore I have create a new keyspace with + replica_factor:1. + """ + create_keyspace(TestChechExistModel.__keyspace__, replication_factor=1) + sync_table(TestChechExistModel) + + @classmethod + def tearDownClass(cls): + super(BaseCassEngTestCase, cls).tearDownClass() + drop_table(TestChechExistModel) + delete_keyspace(TestChechExistModel.__keyspace__) + + +class CheckExistInsertTests(BaseCheckExistTest): + + def test_insert_check_exist_success(self): + """ tests that insertion with check_exist work as expected """ + id = uuid4() + + TestChechExistModel.create(id=id, count=8, text='123456789') + TestChechExistModel.check_exist(True).create(id=id, count=9, text='111111111111') + + q = TestChechExistModel.objects(id=id) + self.assertEqual(len(q), 1) + + tm = q.first() + self.assertEquals(tm.count, 8) + self.assertEquals(tm.text, '123456789') + + def test_insert_check_exist_failure(self): + """ tests that insertion with check_exist failure """ + id = uuid4() + + TestChechExistModel.create(id=id, count=8, text='123456789') + TestChechExistModel.check_exist(False).create(id=id, count=9, text='111111111111') + + q = TestChechExistModel.objects(id=id) + self.assertEquals(len(q), 1) + + tm = q.first() + self.assertEquals(tm.count, 9) + self.assertEquals(tm.text, '111111111111') + + +class CheckExistModelTest(BaseCheckExistTest): + + def test_check_exist_included_on_create(self): + """ tests that check_exist on models works as expected """ + + session = get_session() + + with mock.patch.object(session, 'execute') as m: + TestChechExistModel.check_exist(True).create(count=8) + + query = m.call_args[0][0].query_string + self.assertIn("IF NOT EXISTS", query) + + def test_check_exist_included_on_save(self): + + session = get_session() + + with mock.patch.object(session, 'execute') as m: + tm = TestChechExistModel(count=8) + tm.check_exist(True).save() + + query = m.call_args[0][0].query_string + self.assertIn("IF NOT EXISTS", query) + + def test_queryset_is_returned_on_class(self): + """ ensure we get a queryset description back """ + qs = TestChechExistModel.check_exist(True) + self.assertTrue(isinstance(qs, TestChechExistModel.__queryset__), type(qs)) + + +class CheckExistInstanceTest(BaseCheckExistTest): + + def test_instance_is_returned(self): + """ + ensures that we properly handle the instance.check_exist(True).save() + scenario + """ + o = TestChechExistModel.create(text="whatever") + o.text = "new stuff" + o = o.check_exist(True) + self.assertEqual(True, o._check_exist) + + def test_check_exist_is_not_include_with_query_on_update(self): + """ + make sure we don't put 'IF NOT EXIST' in update statements + """ + session = get_session() + + o = TestChechExistModel.create(text="whatever") + o.text = "new stuff" + o = o.check_exist(True) + + with mock.patch.object(session, 'execute') as m: + o.save() + + query = m.call_args[0][0].query_string + self.assertNotIn("IF NOT EXIST", query) + + From fe28679e7bf826f1e058e76ff24fd49749ec0b9d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 1 Jul 2014 12:05:54 -0500 Subject: [PATCH 0912/3961] Fix dclocal_read_repair_chance table metadata Fixes PYTHON-84 --- CHANGELOG.rst | 2 ++ cassandra/metadata.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b1f898163d..2b98671539 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,8 @@ Bug Fixes (PYTHON-81) * Properly defunct connections after protocol errors * Avoid UnicodeDecodeError when query string is unicode (PYTHON-76) +* Correctly capture dclocal_read_repair_chance for tables and + use it when generating CREATE TABLE statements (PYTHON-84) 2.0.2 ===== diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 76de53d7f4..9b15fc27ad 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -301,6 +301,16 @@ def _build_table_metadata(self, keyspace_metadata, row, col_rows): def _build_table_options(self, row): """ Setup the mostly-non-schema table options, like caching settings """ options = dict((o, row.get(o)) for o in TableMetadata.recognized_options if o in row) + + # the option name when creating tables is "dclocal_read_repair_chance", + # but the column name in system.schema_columnfamilies is + # "local_read_repair_chance". We'll store this as dclocal_read_repair_chance, + # since that's probably what users are expecting (and we need it for the + # CREATE TABLE statement anyway). + if "local_read_repair_chance" in options: + val = options.pop("local_read_repair_chance") + options["dc_local_read_repair_chance"] = val + return options def _build_column_metadata(self, table_metadata, row): @@ -662,7 +672,8 @@ def primary_key(self): recognized_options = ( "comment", "read_repair_chance", - "dclocal_read_repair_chance", + "dclocal_read_repair_chance", # kept to be safe, but see _build_table_options() + "local_read_repair_chance", "replicate_on_write", "gc_grace_seconds", "bloom_filter_fp_chance", From f5d1f8432b7936bb0db6b2d1cddb3e376b968fef Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Tue, 1 Jul 2014 16:09:31 -0400 Subject: [PATCH 0913/3961] delayed_connect is now lazy_connect --- cqlengine/connection.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 89e18c1d07..0f9d220c52 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -27,14 +27,14 @@ class CQLConnectionError(CQLEngineException): pass cluster = None session = None -delayed_connect_args = None +lazy_connect_args = None default_consistency_level = None def setup( hosts, default_keyspace=None, consistency=ConsistencyLevel.ONE, - delayed_connect=False, + lazy_connect=False, **kwargs): """ Records the hosts and connects to one of them @@ -45,10 +45,10 @@ def setup( :type default_keyspace: str :param consistency: The global consistency level :type consistency: int - :param delayed_connect: True if should not connect until first use - :type delayed_connect: bool + :param lazy_connect: True if should not connect until first use + :type lazy_connect: bool """ - global cluster, session, default_consistency_level, delayed_connect_args + global cluster, session, default_consistency_level, lazy_connect_args if 'username' in kwargs or 'password' in kwargs: raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") @@ -58,8 +58,8 @@ def setup( models.DEFAULT_KEYSPACE = default_keyspace default_consistency_level = consistency - if delayed_connect: - delayed_connect_args = (hosts, default_keyspace, consistency, kwargs) + if lazy_connect: + lazy_connect_args = (hosts, default_keyspace, consistency, kwargs) return cluster = Cluster(hosts, **kwargs) @@ -68,7 +68,7 @@ def setup( def execute(query, params=None, consistency_level=None): - handle_delayed_connect() + handle_lazy_connect() if consistency_level is None: consistency_level = default_consistency_level @@ -92,16 +92,16 @@ def execute(query, params=None, consistency_level=None): def get_session(): - handle_delayed_connect() + handle_lazy_connect() return session def get_cluster(): - handle_delayed_connect() + handle_lazy_connect() return cluster -def handle_delayed_connect(): - global delayed_connect_args - if delayed_connect_args: - hosts, default_keyspace, consistency, kwargs = delayed_connect_args - delayed_connect_args = None +def handle_lazy_connect(): + global lazy_connect_args + if lazy_connect_args: + hosts, default_keyspace, consistency, kwargs = lazy_connect_args + lazy_connect_args = None setup(hosts, default_keyspace, consistency, **kwargs) From 88069cab8f6e2c0071b885f2a2a3ad4f42f08ac0 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Wed, 2 Jul 2014 12:53:22 +0800 Subject: [PATCH 0914/3961] rename all 'check_exist' related things to 'if_not_exists' --- cqlengine/models.py | 18 ++--- cqlengine/query.py | 16 ++--- cqlengine/statements.py | 6 +- ...test_checkexist.py => test_ifnotexists.py} | 72 +++++++++---------- 4 files changed, 56 insertions(+), 56 deletions(-) rename cqlengine/tests/{test_checkexist.py => test_ifnotexists.py} (50%) diff --git a/cqlengine/models.py b/cqlengine/models.py index 17d403df9c..ddb7980cfd 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -111,19 +111,19 @@ def timestamp_setter(ts): def __call__(self, *args, **kwargs): raise NotImplementedError -class CheckExistDescriptor(object): +class IfNotExistsDescriptor(object): """ - return a query set descriptor with a check-exist flag specified + return a query set descriptor with a if_not_exists flag specified """ def __get__(self, instance, model): if instance: # instance method - def checkexist_setter(ce): - instance._check_exist = ce + def ifnotexists_setter(ife): + instance._if_not_exists = ife return instance - return checkexist_setter + return ifnotexists_setter - return model.objects.check_exist + return model.objects.if_not_exists def __call__(self, *args, **kwargs): raise NotImplementedError @@ -238,7 +238,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass # custom timestamps, see USING TIMESTAMP X timestamp = TimestampDescriptor() - check_exist = CheckExistDescriptor() + if_not_exists = IfNotExistsDescriptor() # _len is lazily created by __len__ @@ -280,7 +280,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass _timestamp = None # optional timestamp to include with the operation (USING TIMESTAMP) - _check_exist = False # optional check_exist flag to check existence before insertion + _if_not_exists = False # optional if_not_exists flag to check existence before insertion def __init__(self, **values): self._values = {} @@ -532,7 +532,7 @@ def save(self): ttl=self._ttl, timestamp=self._timestamp, consistency=self.__consistency__, - check_exist=self._check_exist).save() + if_not_exists=self._if_not_exists).save() #reset the value managers for v in self._values.values(): diff --git a/cqlengine/query.py b/cqlengine/query.py index c95124a64c..430c137270 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -222,7 +222,7 @@ def __init__(self, model): self._ttl = None self._consistency = None self._timestamp = None - self._check_exist = False + self._if_not_exists = False @property def column_family_name(self): @@ -569,7 +569,7 @@ def defer(self, fields): def create(self, **kwargs): return self.model(**kwargs).batch(self._batch).ttl(self._ttl).\ - consistency(self._consistency).check_exist(self._check_exist).\ + consistency(self._consistency).if_not_exists(self._if_not_exists).\ timestamp(self._timestamp).save() def delete(self): @@ -708,9 +708,9 @@ def timestamp(self, timestamp): clone._timestamp = timestamp return clone - def check_exist(self, check_exist): + def if_not_exists(self, if_not_exists): clone = copy.deepcopy(self) - clone._check_exist = check_exist + clone._if_not_exists = if_not_exists return clone def update(self, **values): @@ -772,9 +772,9 @@ class DMLQuery(object): _ttl = None _consistency = None _timestamp = None - _check_exist = False + _if_not_exists = False - def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None, check_exist=False): + def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None, if_not_exists=False): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance @@ -782,7 +782,7 @@ def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, self._ttl = ttl self._consistency = consistency self._timestamp = timestamp - self._check_exist = check_exist + self._if_not_exists = if_not_exists def _execute(self, q): if self._batch: @@ -894,7 +894,7 @@ def save(self): if self.instance._has_counter or self.instance._can_update(): return self.update() else: - insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp, check_exist=self._check_exist) + insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp, if_not_exists=self._if_not_exists) for name, col in self.instance._columns.items(): val = getattr(self.instance, name, None) if col._val_is_null(val): diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 849a7940d5..62c8dc7a3e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -620,7 +620,7 @@ def __init__(self, where=None, ttl=None, timestamp=None, - check_exist=False): + if_not_exists=False): super(InsertStatement, self).__init__( table, assignments=assignments, @@ -629,7 +629,7 @@ def __init__(self, ttl=ttl, timestamp=timestamp) - self.check_exist = check_exist + self.if_not_exists = if_not_exists def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") @@ -645,7 +645,7 @@ def __unicode__(self): qs += ['VALUES'] qs += ["({})".format(', '.join(['%({})s'.format(v) for v in values]))] - if self.check_exist: + if self.if_not_exists: qs += ["IF NOT EXISTS"] if self.ttl: diff --git a/cqlengine/tests/test_checkexist.py b/cqlengine/tests/test_ifnotexists.py similarity index 50% rename from cqlengine/tests/test_checkexist.py rename to cqlengine/tests/test_ifnotexists.py index 7797459e62..bca737be6e 100644 --- a/cqlengine/tests/test_checkexist.py +++ b/cqlengine/tests/test_ifnotexists.py @@ -7,60 +7,60 @@ from cqlengine.connection import get_session -class TestChechExistModel(Model): +class TestIfNotExistsModel(Model): - __keyspace__ = 'cqlengine_test_checkexist' + __keyspace__ = 'cqlengine_test_ifnotexists' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) -class BaseCheckExistTest(BaseCassEngTestCase): +class BaseIfNotExistsTest(BaseCassEngTestCase): @classmethod def setUpClass(cls): - super(BaseCheckExistTest, cls).setUpClass() + super(BaseIfNotExistsTest, cls).setUpClass() """ when receiving an insert statement with 'if not exist', cassandra would perform a read with QUORUM level. Unittest would be failed if replica_factor is 3 and one node only. Therefore I have create a new keyspace with replica_factor:1. """ - create_keyspace(TestChechExistModel.__keyspace__, replication_factor=1) - sync_table(TestChechExistModel) + create_keyspace(TestIfNotExistsModel.__keyspace__, replication_factor=1) + sync_table(TestIfNotExistsModel) @classmethod def tearDownClass(cls): super(BaseCassEngTestCase, cls).tearDownClass() - drop_table(TestChechExistModel) - delete_keyspace(TestChechExistModel.__keyspace__) + drop_table(TestIfNotExistsModel) + delete_keyspace(TestIfNotExistsModel.__keyspace__) -class CheckExistInsertTests(BaseCheckExistTest): +class IfNotExistsInsertTests(BaseIfNotExistsTest): - def test_insert_check_exist_success(self): - """ tests that insertion with check_exist work as expected """ + def test_insert_if_not_exists_success(self): + """ tests that insertion with if_not_exists work as expected """ id = uuid4() - TestChechExistModel.create(id=id, count=8, text='123456789') - TestChechExistModel.check_exist(True).create(id=id, count=9, text='111111111111') + TestIfNotExistsModel.create(id=id, count=8, text='123456789') + TestIfNotExistsModel.if_not_exists(True).create(id=id, count=9, text='111111111111') - q = TestChechExistModel.objects(id=id) + q = TestIfNotExistsModel.objects(id=id) self.assertEqual(len(q), 1) tm = q.first() self.assertEquals(tm.count, 8) self.assertEquals(tm.text, '123456789') - def test_insert_check_exist_failure(self): - """ tests that insertion with check_exist failure """ + def test_insert_if_not_exists_failure(self): + """ tests that insertion with if_not_exists failure """ id = uuid4() - TestChechExistModel.create(id=id, count=8, text='123456789') - TestChechExistModel.check_exist(False).create(id=id, count=9, text='111111111111') + TestIfNotExistsModel.create(id=id, count=8, text='123456789') + TestIfNotExistsModel.if_not_exists(False).create(id=id, count=9, text='111111111111') - q = TestChechExistModel.objects(id=id) + q = TestIfNotExistsModel.objects(id=id) self.assertEquals(len(q), 1) tm = q.first() @@ -68,57 +68,57 @@ def test_insert_check_exist_failure(self): self.assertEquals(tm.text, '111111111111') -class CheckExistModelTest(BaseCheckExistTest): +class IfNotExistsModelTest(BaseIfNotExistsTest): - def test_check_exist_included_on_create(self): - """ tests that check_exist on models works as expected """ + def test_if_not_exists_included_on_create(self): + """ tests that if_not_exists on models works as expected """ session = get_session() with mock.patch.object(session, 'execute') as m: - TestChechExistModel.check_exist(True).create(count=8) + TestIfNotExistsModel.if_not_exists(True).create(count=8) query = m.call_args[0][0].query_string self.assertIn("IF NOT EXISTS", query) - def test_check_exist_included_on_save(self): + def test_if_not_exists_included_on_save(self): session = get_session() with mock.patch.object(session, 'execute') as m: - tm = TestChechExistModel(count=8) - tm.check_exist(True).save() + tm = TestIfNotExistsModel(count=8) + tm.if_not_exists(True).save() query = m.call_args[0][0].query_string self.assertIn("IF NOT EXISTS", query) def test_queryset_is_returned_on_class(self): """ ensure we get a queryset description back """ - qs = TestChechExistModel.check_exist(True) - self.assertTrue(isinstance(qs, TestChechExistModel.__queryset__), type(qs)) + qs = TestIfNotExistsModel.if_not_exists(True) + self.assertTrue(isinstance(qs, TestIfNotExistsModel.__queryset__), type(qs)) -class CheckExistInstanceTest(BaseCheckExistTest): +class IfNotExistsInstanceTest(BaseIfNotExistsTest): def test_instance_is_returned(self): """ - ensures that we properly handle the instance.check_exist(True).save() + ensures that we properly handle the instance.if_not_exists(True).save() scenario """ - o = TestChechExistModel.create(text="whatever") + o = TestIfNotExistsModel.create(text="whatever") o.text = "new stuff" - o = o.check_exist(True) - self.assertEqual(True, o._check_exist) + o = o.if_not_exists(True) + self.assertEqual(True, o._if_not_exists) - def test_check_exist_is_not_include_with_query_on_update(self): + def test_if_not_exists_is_not_include_with_query_on_update(self): """ make sure we don't put 'IF NOT EXIST' in update statements """ session = get_session() - o = TestChechExistModel.create(text="whatever") + o = TestIfNotExistsModel.create(text="whatever") o.text = "new stuff" - o = o.check_exist(True) + o = o.if_not_exists(True) with mock.patch.object(session, 'execute') as m: o.save() From 39efc92fa24a47fbc5f9289d3956224a0411050c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 2 Jul 2014 10:26:37 -0500 Subject: [PATCH 0915/3961] Fix typo in dclocal_read_repair_chance Relates to PYTHON-84 --- cassandra/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 9b15fc27ad..6aac4aef5e 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -309,7 +309,7 @@ def _build_table_options(self, row): # CREATE TABLE statement anyway). if "local_read_repair_chance" in options: val = options.pop("local_read_repair_chance") - options["dc_local_read_repair_chance"] = val + options["dclocal_read_repair_chance"] = val return options From 1893dadc452e1aba1964d8f6101c362841d70cd3 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Sat, 28 Jun 2014 20:42:47 -0400 Subject: [PATCH 0916/3961] delayed connect. use setup(delayed_connect=True) --- cqlengine/connection.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index da86e60053..82c333bbdb 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -27,12 +27,14 @@ class CQLConnectionError(CQLEngineException): pass cluster = None session = None +delayed_connect_args = None default_consistency_level = None def setup( hosts, default_keyspace, consistency=ConsistencyLevel.ONE, + delayed_connect=False, **kwargs): """ Records the hosts and connects to one of them @@ -43,8 +45,10 @@ def setup( :type default_keyspace: str :param consistency: The global consistency level :type consistency: int + :param delayed_connect: True if should not connect until first use + :type delayed_connect: bool """ - global cluster, session, default_consistency_level + global cluster, session, default_consistency_level, delayed_connect_args if 'username' in kwargs or 'password' in kwargs: raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") @@ -54,6 +58,10 @@ def setup( models.DEFAULT_KEYSPACE = default_keyspace default_consistency_level = consistency + if delayed_connect: + delayed_connect_args = (hosts, default_keyspace, consistency, kwargs) + return + cluster = Cluster(hosts, **kwargs) session = cluster.connect() session.row_factory = dict_factory @@ -63,6 +71,8 @@ def execute(query, params=None, consistency_level=None): if not session: raise CQLEngineException("It is required to setup() cqlengine before executing queries") + handle_delayed_connect() + if consistency_level is None: consistency_level = default_consistency_level @@ -86,7 +96,16 @@ def execute(query, params=None, consistency_level=None): def get_session(): + handle_delayed_connect() return session def get_cluster(): + handle_delayed_connect() return cluster + +def handle_delayed_connect(): + global delayed_connect_args + if delayed_connect_args: + hosts, default_keyspace, consistency, kwargs = delayed_connect_args + delayed_connect_args = None + setup(hosts, default_keyspace, consistency, **kwargs) From 4a8d19f01628048e0be7cb900a54b679e49f47df Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Tue, 1 Jul 2014 16:09:31 -0400 Subject: [PATCH 0917/3961] delayed_connect is now lazy_connect --- cqlengine/connection.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 82c333bbdb..e22f7556dd 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -27,14 +27,14 @@ class CQLConnectionError(CQLEngineException): pass cluster = None session = None -delayed_connect_args = None +lazy_connect_args = None default_consistency_level = None def setup( hosts, default_keyspace, consistency=ConsistencyLevel.ONE, - delayed_connect=False, + lazy_connect=False, **kwargs): """ Records the hosts and connects to one of them @@ -45,10 +45,10 @@ def setup( :type default_keyspace: str :param consistency: The global consistency level :type consistency: int - :param delayed_connect: True if should not connect until first use - :type delayed_connect: bool + :param lazy_connect: True if should not connect until first use + :type lazy_connect: bool """ - global cluster, session, default_consistency_level, delayed_connect_args + global cluster, session, default_consistency_level, lazy_connect_args if 'username' in kwargs or 'password' in kwargs: raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") @@ -58,8 +58,8 @@ def setup( models.DEFAULT_KEYSPACE = default_keyspace default_consistency_level = consistency - if delayed_connect: - delayed_connect_args = (hosts, default_keyspace, consistency, kwargs) + if lazy_connect: + lazy_connect_args = (hosts, default_keyspace, consistency, kwargs) return cluster = Cluster(hosts, **kwargs) @@ -71,7 +71,7 @@ def execute(query, params=None, consistency_level=None): if not session: raise CQLEngineException("It is required to setup() cqlengine before executing queries") - handle_delayed_connect() + handle_lazy_connect() if consistency_level is None: consistency_level = default_consistency_level @@ -96,16 +96,16 @@ def execute(query, params=None, consistency_level=None): def get_session(): - handle_delayed_connect() + handle_lazy_connect() return session def get_cluster(): - handle_delayed_connect() + handle_lazy_connect() return cluster -def handle_delayed_connect(): - global delayed_connect_args - if delayed_connect_args: - hosts, default_keyspace, consistency, kwargs = delayed_connect_args - delayed_connect_args = None +def handle_lazy_connect(): + global lazy_connect_args + if lazy_connect_args: + hosts, default_keyspace, consistency, kwargs = lazy_connect_args + lazy_connect_args = None setup(hosts, default_keyspace, consistency, **kwargs) From 8f847245fa244a2e23de34a6a29ba0e6e121eb0c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 2 Jul 2014 16:02:03 -0500 Subject: [PATCH 0918/3961] Fix bad request ID management from 2.1 support changes --- cassandra/connection.py | 52 ++++++++++++++++++++------- cassandra/pool.py | 22 ++++++------ tests/unit/io/test_asyncorereactor.py | 8 ++--- tests/unit/io/test_libevreactor.py | 8 ++--- 4 files changed, 58 insertions(+), 32 deletions(-) diff --git a/cassandra/connection.py b/cassandra/connection.py index 9796bf44d0..ead994300f 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import defaultdict +from collections import defaultdict, deque import errno from functools import wraps, partial import logging @@ -150,8 +150,20 @@ class Connection(object): ssl_options = None last_error = None + + # The current number of operations that are in flight. More precisely, + # the number of request IDs that are currently in use. in_flight = 0 - current_request_id = 0 + + # A set of available request IDs. When using the v3 protocol or higher, + # this will no initially include all request IDs in order to save memory, + # but the set will grow if it is exhausted. + request_ids = None + + # Tracks the highest used request ID in order to help with growing the + # request_ids set + highest_request_id = 0 + is_defunct = False is_closed = False lock = None @@ -178,10 +190,16 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None, self._header_unpack = v3_header_unpack self._header_length = 5 self.max_request_id = (2 ** 15) - 1 + # Don't fill the deque with 2**15 items right away. Start with 300 and add + # more if needed. + self.request_ids = deque(range(300)) + self.highest_request_id = 299 else: self._header_unpack = header_unpack self._header_length = 4 self.max_request_id = (2 ** 7) - 1 + self.request_ids = deque(range(self.max_request_id + 1)) + self.highest_request_id = self.max_request_id + 1 # 0 8 16 24 32 40 # +---------+---------+---------+---------+---------+ @@ -246,9 +264,16 @@ def error_all_callbacks(self, exc): id(self), self.host, exc_info=True) def get_request_id(self): - current = self.current_request_id - self.current_request_id = (current + 1) % self.max_request_id - return current + """ + This must be called while self.lock is held. + """ + try: + return self.request_ids.popleft() + except IndexError: + self.highest_request_id += 1 + # in_flight checks should guarantee this + assert self.highest_request_id <= self.max_request_id + return self.highest_request_id def handle_pushed(self, response): log.debug("Message pushed from server: %r", response) @@ -283,13 +308,12 @@ def wait_for_responses(self, *msgs, **kwargs): needed = len(msgs) - messages_sent with self.lock: available = min(needed, self.max_request_id - self.in_flight) - start_request_id = self.current_request_id - self.current_request_id = (self.current_request_id + available) % self.max_request_id + request_ids = [self.get_request_id() for _ in range(available)] self.in_flight += available - for i in range(available): + for i, request_id in enumerate(request_ids): self.send_msg(msgs[messages_sent + i], - (start_request_id + i) % self.max_request_id, + request_id, partial(waiter.got_response, index=messages_sent + i)) messages_sent += available @@ -327,6 +351,8 @@ def process_msg(self, msg, body_len): callback = None else: callback = self._callbacks.pop(stream_id, None) + with self.lock: + self.request_ids.append(stream_id) body = None try: @@ -383,7 +409,7 @@ def _send_options_message(self): self._send_startup_message() else: log.debug("Sending initial options message for new connection (%s) to %s", id(self), self.host) - self.send_msg(OptionsMessage(), 0, self._handle_options_response) + self.send_msg(OptionsMessage(), self.get_request_id(), self._handle_options_response) @defunct_on_error def _handle_options_response(self, options_response): @@ -455,7 +481,7 @@ def _send_startup_message(self, compression=None): if compression: opts['COMPRESSION'] = compression sm = StartupMessage(cqlversion=self.cql_version, options=opts) - self.send_msg(sm, 0, cb=self._handle_startup_response) + self.send_msg(sm, self.get_request_id(), cb=self._handle_startup_response) log.debug("Sent StartupMessage on %s", self) @defunct_on_error @@ -480,7 +506,7 @@ def _handle_startup_response(self, startup_response, did_authenticate=False): log.debug("Sending credentials-based auth response on %s", self) cm = CredentialsMessage(creds=self.authenticator) callback = partial(self._handle_startup_response, did_authenticate=True) - self.send_msg(cm, 0, cb=callback) + self.send_msg(cm, self.get_request_id(), cb=callback) else: log.debug("Sending SASL-based auth response on %s", self) initial_response = self.authenticator.initial_response() @@ -520,7 +546,7 @@ def _handle_auth_response(self, auth_response): response = self.authenticator.evaluate_challenge(auth_response.challenge) msg = AuthResponseMessage("" if response is None else response) log.debug("Responding to auth challenge on %s", self) - self.send_msg(msg, 0, self._handle_auth_response) + self.send_msg(msg, self.get_request_id(), self._handle_auth_response) elif isinstance(auth_response, ErrorMessage): log.debug("Received ErrorMessage on new connection (%s) from %s: %s", id(self), self.host, auth_response.summary_msg()) diff --git a/cassandra/pool.py b/cassandra/pool.py index 0f29c77f06..dd462998c8 100644 --- a/cassandra/pool.py +++ b/cassandra/pool.py @@ -328,10 +328,11 @@ def borrow_connection(self, timeout): raise NoConnectionsAvailable() with conn.lock: - if conn.in_flight > conn.max_request_id: - raise NoConnectionsAvailable("All request IDs are currently in use") - conn.in_flight += 1 - return conn, conn.get_request_id() + if conn.in_flight < conn.max_request_id: + conn.in_flight += 1 + return conn, conn.get_request_id() + + raise NoConnectionsAvailable("All request IDs are currently in use") def return_connection(self, connection): with connection.lock: @@ -341,7 +342,7 @@ def return_connection(self, connection): log.debug("Defunct or closed connection (%s) returned to pool, potentially " "marking host %s as down", id(connection), self.host) is_down = self._session.cluster.signal_connection_failure( - self.host, connection.last_error, is_host_addition=False) + self.host, connection.last_error, is_host_addition=False) if is_down: self.shutdown() else: @@ -463,13 +464,12 @@ def borrow_connection(self, timeout): # its in_flight count need_to_wait = False with least_busy.lock: - - if least_busy.in_flight >= least_busy.max_request_id: - # once we release the lock, wait for another connection - need_to_wait = True - else: + if least_busy.in_flight < least_busy.max_request_id: least_busy.in_flight += 1 request_id = least_busy.get_request_id() + else: + # once we release the lock, wait for another connection + need_to_wait = True if need_to_wait: # wait_for_conn will increment in_flight on the conn @@ -587,7 +587,7 @@ def return_connection(self, connection): log.debug("Defunct or closed connection (%s) returned to pool, potentially " "marking host %s as down", id(connection), self.host) is_down = self._session.cluster.signal_connection_failure( - self.host, connection.last_error, is_host_addition=False) + self.host, connection.last_error, is_host_addition=False) if is_down: self.shutdown() else: diff --git a/tests/unit/io/test_asyncorereactor.py b/tests/unit/io/test_asyncorereactor.py index ecb59bd0a4..5ab59670a6 100644 --- a/tests/unit/io/test_asyncorereactor.py +++ b/tests/unit/io/test_asyncorereactor.py @@ -101,7 +101,7 @@ def test_successful_connection(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ReadyMessage, stream_id=0) + header = self.make_header_prefix(ReadyMessage, stream_id=1) c.socket.recv.return_value = self.make_msg(header) c.handle_read() @@ -173,7 +173,7 @@ def test_error_message_on_startup(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ServerError, stream_id=0) + header = self.make_header_prefix(ServerError, stream_id=1) body = self.make_error_body(ServerError.error_code, ServerError.summary) c.socket.recv.return_value = self.make_msg(header, body) c.handle_read() @@ -255,7 +255,7 @@ def test_partial_header_read(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ReadyMessage, stream_id=0) + header = self.make_header_prefix(ReadyMessage, stream_id=1) c.socket.recv.return_value = self.make_msg(header) c.handle_read() @@ -282,7 +282,7 @@ def test_partial_message_read(self, *args): # let it write out a StartupMessage c.handle_write() - header = self.make_header_prefix(ReadyMessage, stream_id=0) + header = self.make_header_prefix(ReadyMessage, stream_id=1) c.socket.recv.return_value = self.make_msg(header) c.handle_read() diff --git a/tests/unit/io/test_libevreactor.py b/tests/unit/io/test_libevreactor.py index bef4ecbf4d..3b677ab547 100644 --- a/tests/unit/io/test_libevreactor.py +++ b/tests/unit/io/test_libevreactor.py @@ -98,7 +98,7 @@ def test_successful_connection(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ReadyMessage, stream_id=0) + header = self.make_header_prefix(ReadyMessage, stream_id=1) c._socket.recv.return_value = self.make_msg(header) c.handle_read(None, 0) @@ -170,7 +170,7 @@ def test_error_message_on_startup(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ServerError, stream_id=0) + header = self.make_header_prefix(ServerError, stream_id=1) body = self.make_error_body(ServerError.error_code, ServerError.summary) c._socket.recv.return_value = self.make_msg(header, body) c.handle_read(None, 0) @@ -253,7 +253,7 @@ def test_partial_header_read(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ReadyMessage, stream_id=0) + header = self.make_header_prefix(ReadyMessage, stream_id=1) c._socket.recv.return_value = self.make_msg(header) c.handle_read(None, 0) @@ -280,7 +280,7 @@ def test_partial_message_read(self, *args): # let it write out a StartupMessage c.handle_write(None, 0) - header = self.make_header_prefix(ReadyMessage, stream_id=0) + header = self.make_header_prefix(ReadyMessage, stream_id=1) c._socket.recv.return_value = self.make_msg(header) c.handle_read(None, 0) From b5924f38a44c9ebb2e6936206188fe83d6e4e06a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 2 Jul 2014 14:06:29 -0700 Subject: [PATCH 0919/3961] trying to get travis to use nose instead of py.test --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 86b92c0ac8..172874a6f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,9 @@ before_install: - sudo service cassandra start install: - "pip install -r requirements.txt --use-mirrors" - - "pip install pytest --use-mirrors" + #- "pip install pytest --use-mirrors" + script: - while [ ! -f /var/run/cassandra.pid ] ; do sleep 1 ; done # wait until cassandra is ready - - "py.test cqlengine/tests/" + - "nosetests --no-skip" + # - "py.test cqlengine/tests/" From 8372825687f5bb62adf5f5efed9489b60e21c24a Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 2 Jul 2014 16:47:45 -0700 Subject: [PATCH 0920/3961] setting up tox for python 2 + 3 testing --- tox.ini | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..eee198488f --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist=py27,py34 + +[testenv] +deps= -rrequirements.txt +commands=nosetests --no-skip From 0ced4896fa2313db399d2387aa2edd0b536f2ef4 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 2 Jul 2014 17:16:12 -0700 Subject: [PATCH 0921/3961] use current version of the dev libraries --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5973311b23..e648425ad6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -ipython==0.13.1 -ipdb==0.7 -Sphinx==1.1.3 -mock==1.0.1 +ipython +ipdb +Sphinx +mock sure==1.2.5 cassandra-driver>=2.0.0 From 2bfdfcc495a0bc2ba3626d345388afd9abf98677 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 2 Jul 2014 17:31:48 -0700 Subject: [PATCH 0922/3961] changelog --- changelog | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/changelog b/changelog index e30733c62b..f6bd732333 100644 --- a/changelog +++ b/changelog @@ -2,11 +2,27 @@ CHANGELOG 0.16.0 -* Check number of elements in collections. -* Add support for table properties. - - -0.15.0 + 222: figure out how to make travis not totally fail when a test is skipped + 220: delayed connect. use setup(delayed_connect=True) + 218: throw exception on create_table and delete_table + 212: Unchanged primary key trigger error on update + 206: FAQ - why we dont' do #189 + 191: Add support for simple table properties. + 172: raise exception when None is passed in as query param + 170: trying to perform queries before connection is established should raise useful exception + 162: Not Possible to Make Non-Equality Filtering Queries + 161: Filtering on DateTime column + 154: Blob(bytes) column type issue + 128: remove default read_repair_chance & ensure changes are saved when using sync_table + 106: specify caching on model + 99: specify caching options table management + 94: type checking on sync_table (currently allows anything then fails miserably) + 73: remove default 'cqlengine' keyspace table management + 71: add named table and query expression usage to docs + + + +0.15.0 * native driver integration 0.14.0 @@ -14,7 +30,7 @@ CHANGELOG * fix for setting map to empty (Lifto) * report when creating models with attributes that conflict with cqlengine (maedhroz) * use stable version of sure package (maedhroz) -* performance improvements +* performance improvements 0.13.0 From 752960ef5a0dcc7caa29c069a897bfdbc64b1930 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 1 Jul 2014 18:48:58 +0800 Subject: [PATCH 0923/3961] 'IF NOT EXISTS' for insert statement The usage is just like 'ttl'. --- cqlengine/models.py | 24 +++++- cqlengine/query.py | 15 +++- cqlengine/statements.py | 21 +++++ cqlengine/tests/test_checkexist.py | 129 +++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 cqlengine/tests/test_checkexist.py diff --git a/cqlengine/models.py b/cqlengine/models.py index b7f80f60eb..b1bc8569ae 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -111,6 +111,23 @@ def timestamp_setter(ts): def __call__(self, *args, **kwargs): raise NotImplementedError +class CheckExistDescriptor(object): + """ + return a query set descriptor with a check-exist flag specified + """ + def __get__(self, instance, model): + if instance: + # instance method + def checkexist_setter(ce): + instance._check_exist = ce + return instance + return checkexist_setter + + return model.objects.check_exist + + def __call__(self, *args, **kwargs): + raise NotImplementedError + class ConsistencyDescriptor(object): """ returns a query set descriptor if called on Class, instance if it was an instance call @@ -220,6 +237,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass # custom timestamps, see USING TIMESTAMP X timestamp = TimestampDescriptor() + + check_exist = CheckExistDescriptor() # _len is lazily created by __len__ @@ -271,6 +290,8 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass _timestamp = None # optional timestamp to include with the operation (USING TIMESTAMP) + _check_exist = False # optional check_exist flag to check existence before insertion + def __init__(self, **values): self._values = {} self._ttl = None @@ -523,7 +544,8 @@ def save(self): batch=self._batch, ttl=self._ttl, timestamp=self._timestamp, - consistency=self.__consistency__).save() + consistency=self.__consistency__, + check_exist=self._check_exist).save() #reset the value managers for v in self._values.values(): diff --git a/cqlengine/query.py b/cqlengine/query.py index 0a64a5c78a..f6305d3c40 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -222,6 +222,7 @@ def __init__(self, model): self._ttl = None self._consistency = None self._timestamp = None + self._check_exist = False @property def column_family_name(self): @@ -571,7 +572,7 @@ def defer(self, fields): def create(self, **kwargs): return self.model(**kwargs).batch(self._batch).ttl(self._ttl).\ - consistency(self._consistency).\ + consistency(self._consistency).check_exist(self._check_exist).\ timestamp(self._timestamp).save() def delete(self): @@ -710,6 +711,11 @@ def timestamp(self, timestamp): clone._timestamp = timestamp return clone + def check_exist(self, check_exist): + clone = copy.deepcopy(self) + clone._check_exist = check_exist + return clone + def update(self, **values): """ Updates the rows in this queryset """ if not values: @@ -769,8 +775,9 @@ class DMLQuery(object): _ttl = None _consistency = None _timestamp = None + _check_exist = False - def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None): + def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None, check_exist=False): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance @@ -778,6 +785,7 @@ def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, self._ttl = ttl self._consistency = consistency self._timestamp = timestamp + self._check_exist = check_exist def _execute(self, q): if self._batch: @@ -889,7 +897,7 @@ def save(self): if self.instance._has_counter or self.instance._can_update(): return self.update() else: - insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp) + insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp, check_exist=self._check_exist) for name, col in self.instance._columns.items(): val = getattr(self.instance, name, None) if col._val_is_null(val): @@ -905,7 +913,6 @@ def save(self): # caused by pointless update queries if not insert.is_empty: self._execute(insert) - # delete any nulled columns self._delete_null_columns() diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 549b529df7..9373f70faf 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -617,6 +617,24 @@ def get_context(self): class InsertStatement(AssignmentStatement): """ an cql insert select statement """ + def __init__(self, + table, + assignments=None, + consistency=None, + where=None, + ttl=None, + timestamp=None, + check_exist=False): + super(InsertStatement, self).__init__( + table, + assignments=assignments, + consistency=consistency, + where=where, + ttl=ttl, + timestamp=timestamp) + + self.check_exist = check_exist + def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") @@ -631,6 +649,9 @@ def __unicode__(self): qs += ['VALUES'] qs += ["({})".format(', '.join(['%({})s'.format(v) for v in values]))] + if self.check_exist: + qs += ["IF NOT EXISTS"] + if self.ttl: qs += ["USING TTL {}".format(self.ttl)] diff --git a/cqlengine/tests/test_checkexist.py b/cqlengine/tests/test_checkexist.py new file mode 100644 index 0000000000..7797459e62 --- /dev/null +++ b/cqlengine/tests/test_checkexist.py @@ -0,0 +1,129 @@ +from cqlengine.management import sync_table, drop_table, create_keyspace, delete_keyspace +from cqlengine.tests.base import BaseCassEngTestCase +from cqlengine.models import Model +from cqlengine import columns +from uuid import uuid4 +import mock +from cqlengine.connection import get_session + + +class TestChechExistModel(Model): + + __keyspace__ = 'cqlengine_test_checkexist' + + id = columns.UUID(primary_key=True, default=lambda:uuid4()) + count = columns.Integer() + text = columns.Text(required=False) + + +class BaseCheckExistTest(BaseCassEngTestCase): + + @classmethod + def setUpClass(cls): + super(BaseCheckExistTest, cls).setUpClass() + """ + when receiving an insert statement with 'if not exist', cassandra would + perform a read with QUORUM level. Unittest would be failed if replica_factor + is 3 and one node only. Therefore I have create a new keyspace with + replica_factor:1. + """ + create_keyspace(TestChechExistModel.__keyspace__, replication_factor=1) + sync_table(TestChechExistModel) + + @classmethod + def tearDownClass(cls): + super(BaseCassEngTestCase, cls).tearDownClass() + drop_table(TestChechExistModel) + delete_keyspace(TestChechExistModel.__keyspace__) + + +class CheckExistInsertTests(BaseCheckExistTest): + + def test_insert_check_exist_success(self): + """ tests that insertion with check_exist work as expected """ + id = uuid4() + + TestChechExistModel.create(id=id, count=8, text='123456789') + TestChechExistModel.check_exist(True).create(id=id, count=9, text='111111111111') + + q = TestChechExistModel.objects(id=id) + self.assertEqual(len(q), 1) + + tm = q.first() + self.assertEquals(tm.count, 8) + self.assertEquals(tm.text, '123456789') + + def test_insert_check_exist_failure(self): + """ tests that insertion with check_exist failure """ + id = uuid4() + + TestChechExistModel.create(id=id, count=8, text='123456789') + TestChechExistModel.check_exist(False).create(id=id, count=9, text='111111111111') + + q = TestChechExistModel.objects(id=id) + self.assertEquals(len(q), 1) + + tm = q.first() + self.assertEquals(tm.count, 9) + self.assertEquals(tm.text, '111111111111') + + +class CheckExistModelTest(BaseCheckExistTest): + + def test_check_exist_included_on_create(self): + """ tests that check_exist on models works as expected """ + + session = get_session() + + with mock.patch.object(session, 'execute') as m: + TestChechExistModel.check_exist(True).create(count=8) + + query = m.call_args[0][0].query_string + self.assertIn("IF NOT EXISTS", query) + + def test_check_exist_included_on_save(self): + + session = get_session() + + with mock.patch.object(session, 'execute') as m: + tm = TestChechExistModel(count=8) + tm.check_exist(True).save() + + query = m.call_args[0][0].query_string + self.assertIn("IF NOT EXISTS", query) + + def test_queryset_is_returned_on_class(self): + """ ensure we get a queryset description back """ + qs = TestChechExistModel.check_exist(True) + self.assertTrue(isinstance(qs, TestChechExistModel.__queryset__), type(qs)) + + +class CheckExistInstanceTest(BaseCheckExistTest): + + def test_instance_is_returned(self): + """ + ensures that we properly handle the instance.check_exist(True).save() + scenario + """ + o = TestChechExistModel.create(text="whatever") + o.text = "new stuff" + o = o.check_exist(True) + self.assertEqual(True, o._check_exist) + + def test_check_exist_is_not_include_with_query_on_update(self): + """ + make sure we don't put 'IF NOT EXIST' in update statements + """ + session = get_session() + + o = TestChechExistModel.create(text="whatever") + o.text = "new stuff" + o = o.check_exist(True) + + with mock.patch.object(session, 'execute') as m: + o.save() + + query = m.call_args[0][0].query_string + self.assertNotIn("IF NOT EXIST", query) + + From 708f09a4cd9900c80e3fd5dd1a783fbda1ddc6c5 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Wed, 2 Jul 2014 12:53:22 +0800 Subject: [PATCH 0924/3961] rename all 'check_exist' related things to 'if_not_exists' --- cqlengine/models.py | 18 ++--- cqlengine/query.py | 16 ++--- cqlengine/statements.py | 6 +- ...test_checkexist.py => test_ifnotexists.py} | 72 +++++++++---------- 4 files changed, 56 insertions(+), 56 deletions(-) rename cqlengine/tests/{test_checkexist.py => test_ifnotexists.py} (50%) diff --git a/cqlengine/models.py b/cqlengine/models.py index b1bc8569ae..19798d4bee 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -111,19 +111,19 @@ def timestamp_setter(ts): def __call__(self, *args, **kwargs): raise NotImplementedError -class CheckExistDescriptor(object): +class IfNotExistsDescriptor(object): """ - return a query set descriptor with a check-exist flag specified + return a query set descriptor with a if_not_exists flag specified """ def __get__(self, instance, model): if instance: # instance method - def checkexist_setter(ce): - instance._check_exist = ce + def ifnotexists_setter(ife): + instance._if_not_exists = ife return instance - return checkexist_setter + return ifnotexists_setter - return model.objects.check_exist + return model.objects.if_not_exists def __call__(self, *args, **kwargs): raise NotImplementedError @@ -238,7 +238,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass # custom timestamps, see USING TIMESTAMP X timestamp = TimestampDescriptor() - check_exist = CheckExistDescriptor() + if_not_exists = IfNotExistsDescriptor() # _len is lazily created by __len__ @@ -290,7 +290,7 @@ class MultipleObjectsReturned(_MultipleObjectsReturned): pass _timestamp = None # optional timestamp to include with the operation (USING TIMESTAMP) - _check_exist = False # optional check_exist flag to check existence before insertion + _if_not_exists = False # optional if_not_exists flag to check existence before insertion def __init__(self, **values): self._values = {} @@ -545,7 +545,7 @@ def save(self): ttl=self._ttl, timestamp=self._timestamp, consistency=self.__consistency__, - check_exist=self._check_exist).save() + if_not_exists=self._if_not_exists).save() #reset the value managers for v in self._values.values(): diff --git a/cqlengine/query.py b/cqlengine/query.py index f6305d3c40..b4dcecb2b8 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -222,7 +222,7 @@ def __init__(self, model): self._ttl = None self._consistency = None self._timestamp = None - self._check_exist = False + self._if_not_exists = False @property def column_family_name(self): @@ -572,7 +572,7 @@ def defer(self, fields): def create(self, **kwargs): return self.model(**kwargs).batch(self._batch).ttl(self._ttl).\ - consistency(self._consistency).check_exist(self._check_exist).\ + consistency(self._consistency).if_not_exists(self._if_not_exists).\ timestamp(self._timestamp).save() def delete(self): @@ -711,9 +711,9 @@ def timestamp(self, timestamp): clone._timestamp = timestamp return clone - def check_exist(self, check_exist): + def if_not_exists(self, if_not_exists): clone = copy.deepcopy(self) - clone._check_exist = check_exist + clone._if_not_exists = if_not_exists return clone def update(self, **values): @@ -775,9 +775,9 @@ class DMLQuery(object): _ttl = None _consistency = None _timestamp = None - _check_exist = False + _if_not_exists = False - def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None, check_exist=False): + def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, timestamp=None, if_not_exists=False): self.model = model self.column_family_name = self.model.column_family_name() self.instance = instance @@ -785,7 +785,7 @@ def __init__(self, model, instance=None, batch=None, ttl=None, consistency=None, self._ttl = ttl self._consistency = consistency self._timestamp = timestamp - self._check_exist = check_exist + self._if_not_exists = if_not_exists def _execute(self, q): if self._batch: @@ -897,7 +897,7 @@ def save(self): if self.instance._has_counter or self.instance._can_update(): return self.update() else: - insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp, check_exist=self._check_exist) + insert = InsertStatement(self.column_family_name, ttl=self._ttl, timestamp=self._timestamp, if_not_exists=self._if_not_exists) for name, col in self.instance._columns.items(): val = getattr(self.instance, name, None) if col._val_is_null(val): diff --git a/cqlengine/statements.py b/cqlengine/statements.py index 9373f70faf..faf38e521e 100644 --- a/cqlengine/statements.py +++ b/cqlengine/statements.py @@ -624,7 +624,7 @@ def __init__(self, where=None, ttl=None, timestamp=None, - check_exist=False): + if_not_exists=False): super(InsertStatement, self).__init__( table, assignments=assignments, @@ -633,7 +633,7 @@ def __init__(self, ttl=ttl, timestamp=timestamp) - self.check_exist = check_exist + self.if_not_exists = if_not_exists def add_where_clause(self, clause): raise StatementException("Cannot add where clauses to insert statements") @@ -649,7 +649,7 @@ def __unicode__(self): qs += ['VALUES'] qs += ["({})".format(', '.join(['%({})s'.format(v) for v in values]))] - if self.check_exist: + if self.if_not_exists: qs += ["IF NOT EXISTS"] if self.ttl: diff --git a/cqlengine/tests/test_checkexist.py b/cqlengine/tests/test_ifnotexists.py similarity index 50% rename from cqlengine/tests/test_checkexist.py rename to cqlengine/tests/test_ifnotexists.py index 7797459e62..bca737be6e 100644 --- a/cqlengine/tests/test_checkexist.py +++ b/cqlengine/tests/test_ifnotexists.py @@ -7,60 +7,60 @@ from cqlengine.connection import get_session -class TestChechExistModel(Model): +class TestIfNotExistsModel(Model): - __keyspace__ = 'cqlengine_test_checkexist' + __keyspace__ = 'cqlengine_test_ifnotexists' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) -class BaseCheckExistTest(BaseCassEngTestCase): +class BaseIfNotExistsTest(BaseCassEngTestCase): @classmethod def setUpClass(cls): - super(BaseCheckExistTest, cls).setUpClass() + super(BaseIfNotExistsTest, cls).setUpClass() """ when receiving an insert statement with 'if not exist', cassandra would perform a read with QUORUM level. Unittest would be failed if replica_factor is 3 and one node only. Therefore I have create a new keyspace with replica_factor:1. """ - create_keyspace(TestChechExistModel.__keyspace__, replication_factor=1) - sync_table(TestChechExistModel) + create_keyspace(TestIfNotExistsModel.__keyspace__, replication_factor=1) + sync_table(TestIfNotExistsModel) @classmethod def tearDownClass(cls): super(BaseCassEngTestCase, cls).tearDownClass() - drop_table(TestChechExistModel) - delete_keyspace(TestChechExistModel.__keyspace__) + drop_table(TestIfNotExistsModel) + delete_keyspace(TestIfNotExistsModel.__keyspace__) -class CheckExistInsertTests(BaseCheckExistTest): +class IfNotExistsInsertTests(BaseIfNotExistsTest): - def test_insert_check_exist_success(self): - """ tests that insertion with check_exist work as expected """ + def test_insert_if_not_exists_success(self): + """ tests that insertion with if_not_exists work as expected """ id = uuid4() - TestChechExistModel.create(id=id, count=8, text='123456789') - TestChechExistModel.check_exist(True).create(id=id, count=9, text='111111111111') + TestIfNotExistsModel.create(id=id, count=8, text='123456789') + TestIfNotExistsModel.if_not_exists(True).create(id=id, count=9, text='111111111111') - q = TestChechExistModel.objects(id=id) + q = TestIfNotExistsModel.objects(id=id) self.assertEqual(len(q), 1) tm = q.first() self.assertEquals(tm.count, 8) self.assertEquals(tm.text, '123456789') - def test_insert_check_exist_failure(self): - """ tests that insertion with check_exist failure """ + def test_insert_if_not_exists_failure(self): + """ tests that insertion with if_not_exists failure """ id = uuid4() - TestChechExistModel.create(id=id, count=8, text='123456789') - TestChechExistModel.check_exist(False).create(id=id, count=9, text='111111111111') + TestIfNotExistsModel.create(id=id, count=8, text='123456789') + TestIfNotExistsModel.if_not_exists(False).create(id=id, count=9, text='111111111111') - q = TestChechExistModel.objects(id=id) + q = TestIfNotExistsModel.objects(id=id) self.assertEquals(len(q), 1) tm = q.first() @@ -68,57 +68,57 @@ def test_insert_check_exist_failure(self): self.assertEquals(tm.text, '111111111111') -class CheckExistModelTest(BaseCheckExistTest): +class IfNotExistsModelTest(BaseIfNotExistsTest): - def test_check_exist_included_on_create(self): - """ tests that check_exist on models works as expected """ + def test_if_not_exists_included_on_create(self): + """ tests that if_not_exists on models works as expected """ session = get_session() with mock.patch.object(session, 'execute') as m: - TestChechExistModel.check_exist(True).create(count=8) + TestIfNotExistsModel.if_not_exists(True).create(count=8) query = m.call_args[0][0].query_string self.assertIn("IF NOT EXISTS", query) - def test_check_exist_included_on_save(self): + def test_if_not_exists_included_on_save(self): session = get_session() with mock.patch.object(session, 'execute') as m: - tm = TestChechExistModel(count=8) - tm.check_exist(True).save() + tm = TestIfNotExistsModel(count=8) + tm.if_not_exists(True).save() query = m.call_args[0][0].query_string self.assertIn("IF NOT EXISTS", query) def test_queryset_is_returned_on_class(self): """ ensure we get a queryset description back """ - qs = TestChechExistModel.check_exist(True) - self.assertTrue(isinstance(qs, TestChechExistModel.__queryset__), type(qs)) + qs = TestIfNotExistsModel.if_not_exists(True) + self.assertTrue(isinstance(qs, TestIfNotExistsModel.__queryset__), type(qs)) -class CheckExistInstanceTest(BaseCheckExistTest): +class IfNotExistsInstanceTest(BaseIfNotExistsTest): def test_instance_is_returned(self): """ - ensures that we properly handle the instance.check_exist(True).save() + ensures that we properly handle the instance.if_not_exists(True).save() scenario """ - o = TestChechExistModel.create(text="whatever") + o = TestIfNotExistsModel.create(text="whatever") o.text = "new stuff" - o = o.check_exist(True) - self.assertEqual(True, o._check_exist) + o = o.if_not_exists(True) + self.assertEqual(True, o._if_not_exists) - def test_check_exist_is_not_include_with_query_on_update(self): + def test_if_not_exists_is_not_include_with_query_on_update(self): """ make sure we don't put 'IF NOT EXIST' in update statements """ session = get_session() - o = TestChechExistModel.create(text="whatever") + o = TestIfNotExistsModel.create(text="whatever") o.text = "new stuff" - o = o.check_exist(True) + o = o.if_not_exists(True) with mock.patch.object(session, 'execute') as m: o.save() From dd878240dc84c79885fd1de6afaf7c0b3ae41d68 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 3 Jul 2014 14:27:48 +0800 Subject: [PATCH 0925/3961] rename keyspace for testing --- cqlengine/tests/test_ifnotexists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/tests/test_ifnotexists.py b/cqlengine/tests/test_ifnotexists.py index bca737be6e..ca23c49c60 100644 --- a/cqlengine/tests/test_ifnotexists.py +++ b/cqlengine/tests/test_ifnotexists.py @@ -9,7 +9,7 @@ class TestIfNotExistsModel(Model): - __keyspace__ = 'cqlengine_test_ifnotexists' + __keyspace__ = 'cqlengine_test_lwt' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() From 7a381327219f582aae6802e407a1094153c805cb Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 3 Jul 2014 14:41:37 +0800 Subject: [PATCH 0926/3961] update docs --- docs/topics/models.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 14f905b40d..786c0642a1 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -147,6 +147,12 @@ Model Methods Sets the ttl values to run instance updates and inserts queries with. + .. method:: if_not_exists(enable_or_not) + + Check the existence of an object before insertion. The existence of an + object is determined by its primary key(s). And please note using this flag + would incur performance cost. + .. method:: update(**values) Performs an update on the model instance. You can pass in values to set on the model From 82195877b4e1871b10677944a4831c1c9aedd9ae Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 3 Jul 2014 11:40:32 -0700 Subject: [PATCH 0927/3961] rewording FAQ item --- docs/topics/faq.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/faq.rst b/docs/topics/faq.rst index 0b58b6358e..b250180bb2 100644 --- a/docs/topics/faq.rst +++ b/docs/topics/faq.rst @@ -2,7 +2,7 @@ Frequently Asked Questions ========================== -Q: Why does calling my Model(field=blah, field2=blah2) not work? +Q: Why don't updates work correctly on models instantiated as Model(field=blah, field2=blah2)? ------------------------------------------------------------------- -A: The __init__() of a model is used by cqlengine internally. If you want to create a new row in the database, use create(). +A: The recommended way to create new rows is with the models .create method. The values passed into a model's init method are interpreted by the model as the values as they were read from a row. This allows the model to "know" which rows have changed since the row was read out of cassandra, and create suitable update statements. \ No newline at end of file From cae022209fada866c2943b71f13a5a78a65fb705 Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 3 Jul 2014 13:25:01 -0700 Subject: [PATCH 0928/3961] adding warning when keyspaces are undefined. Since a default keyspace can be set on the setup function, we can't throw an exception, because it may not have been called by the time the model class is created --- cqlengine/models.py | 16 ++++++++++++++ .../tests/model/test_class_construction.py | 21 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/cqlengine/models.py b/cqlengine/models.py index b7f80f60eb..b800394994 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -1,5 +1,7 @@ from collections import OrderedDict import re +import warnings + from cqlengine import columns from cqlengine.exceptions import ModelException, CQLEngineException, ValidationError from cqlengine.query import ModelQuerySet, DMLQuery, AbstractQueryableColumn @@ -13,6 +15,9 @@ class PolyMorphicModelException(ModelException): pass DEFAULT_KEYSPACE = None +class UndefinedKeyspaceWarning(Warning): + pass + class hybrid_classmethod(object): """ @@ -756,6 +761,17 @@ def _get_polymorphic_base(bases): #create the class and add a QuerySet to it klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) + + if not klass.__abstract__ and klass.__keyspace__ is None: + warnings.warn( + "No keyspace defined on {}.{}\n" + " The default keyspace 'cqlengine' was removed in 0.16.\n" + " Please define a keyspace on the '__keyspace__' model class attribute\n" + " or set a default keyspace with management.setup function\n" + .format(klass.__module__, klass.__name__), + UndefinedKeyspaceWarning + ) + return klass diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index c8a54eb1c2..58b6112995 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -1,9 +1,10 @@ from uuid import uuid4 +import warnings from cqlengine.query import QueryException, ModelQuerySet, DMLQuery from cqlengine.tests.base import BaseCassEngTestCase from cqlengine.exceptions import ModelException, CQLEngineException -from cqlengine.models import Model, ModelDefinitionException, ColumnQueryEvaluator +from cqlengine.models import Model, ModelDefinitionException, ColumnQueryEvaluator, UndefinedKeyspaceWarning from cqlengine import columns import cqlengine @@ -208,6 +209,23 @@ class Model2(Model1): except Exception: assert False, "Model2 exception should not be caught by Model1" + def test_no_keyspace_warning(self): + with warnings.catch_warnings(record=True) as warn: + class NoKeyspace(Model): + key = columns.UUID(primary_key=True) + + self.assertEqual(len(warn), 1) + warn_message = warn[0] + self.assertEqual(warn_message.category, UndefinedKeyspaceWarning) + + def test_abstract_model_keyspace_warning_is_skipped(self): + with warnings.catch_warnings(record=True) as warn: + class NoKeyspace(Model): + __abstract__ = True + key = columns.UUID(primary_key=True) + + self.assertEqual(len(warn), 0) + class TestManualTableNaming(BaseCassEngTestCase): class RenamedTest(cqlengine.Model): @@ -223,6 +241,7 @@ def test_proper_table_naming(self): class AbstractModel(Model): __abstract__ = True + __keyspace__ = 'test' class ConcreteModel(AbstractModel): pkey = columns.Integer(primary_key=True) From d3ce856f91343dc2cbea6cf9e879c2dd43038ddb Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 3 Jul 2014 15:10:01 -0700 Subject: [PATCH 0929/3961] adding explicit keyspaces to ALL of the test models --- .../tests/columns/test_container_columns.py | 4 ++++ cqlengine/tests/columns/test_counter_column.py | 1 + cqlengine/tests/columns/test_validation.py | 11 +++++++++++ cqlengine/tests/columns/test_value_io.py | 1 + .../tests/management/test_compaction_settings.py | 10 ++++++++++ cqlengine/tests/management/test_management.py | 8 ++++++++ cqlengine/tests/model/test_class_construction.py | 16 ++++++++++++++++ .../tests/model/test_equality_operations.py | 1 + cqlengine/tests/model/test_model.py | 5 +++++ cqlengine/tests/model/test_model_io.py | 7 +++++++ cqlengine/tests/model/test_polymorphism.py | 8 ++++++++ cqlengine/tests/model/test_updates.py | 1 + cqlengine/tests/model/test_value_lists.py | 2 ++ cqlengine/tests/query/test_batch_query.py | 2 ++ cqlengine/tests/query/test_datetime_queries.py | 1 + cqlengine/tests/query/test_queryoperators.py | 2 ++ cqlengine/tests/query/test_queryset.py | 5 +++++ cqlengine/tests/query/test_updates.py | 1 + cqlengine/tests/test_batch_query.py | 1 + cqlengine/tests/test_consistency.py | 1 + cqlengine/tests/test_load.py | 1 + cqlengine/tests/test_timestamp.py | 1 + cqlengine/tests/test_ttl.py | 1 + 23 files changed, 91 insertions(+) diff --git a/cqlengine/tests/columns/test_container_columns.py b/cqlengine/tests/columns/test_container_columns.py index 86a96e7edb..0b756f3316 100644 --- a/cqlengine/tests/columns/test_container_columns.py +++ b/cqlengine/tests/columns/test_container_columns.py @@ -9,6 +9,7 @@ class TestSetModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid4) int_set = columns.Set(columns.Integer, required=False) @@ -161,6 +162,7 @@ def test_default_empty_container_saving(self): class TestListModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid4) int_list = columns.List(columns.Integer, required=False) @@ -306,6 +308,7 @@ def test_blind_list_updates_from_none(self): assert m3.int_list == [] class TestMapModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid4) int_map = columns.Map(columns.Integer, columns.UUID, required=False) @@ -505,6 +508,7 @@ def test_default_empty_container_saving(self): class TestCamelMapModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid4) camelMap = columns.Map(columns.Text, columns.Integer, required=False) diff --git a/cqlengine/tests/columns/test_counter_column.py b/cqlengine/tests/columns/test_counter_column.py index 725b83905d..8e8b2361e4 100644 --- a/cqlengine/tests/columns/test_counter_column.py +++ b/cqlengine/tests/columns/test_counter_column.py @@ -8,6 +8,7 @@ class TestCounterModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid4) cluster = columns.UUID(primary_key=True, default=uuid4) counter = columns.Counter() diff --git a/cqlengine/tests/columns/test_validation.py b/cqlengine/tests/columns/test_validation.py index 749066f32d..0df006e8e9 100644 --- a/cqlengine/tests/columns/test_validation.py +++ b/cqlengine/tests/columns/test_validation.py @@ -32,6 +32,7 @@ class TestDatetime(BaseCassEngTestCase): class DatetimeTest(Model): + __keyspace__ = 'test' test_id = Integer(primary_key=True) created_at = DateTime() @@ -80,6 +81,7 @@ def test_datetime_none(self): class TestBoolDefault(BaseCassEngTestCase): class BoolDefaultValueTest(Model): + __keyspace__ = 'test' test_id = Integer(primary_key=True) stuff = Boolean(default=True) @@ -98,6 +100,7 @@ def test_default_is_set(self): class TestVarInt(BaseCassEngTestCase): class VarIntTest(Model): + __keyspace__ = 'test' test_id = Integer(primary_key=True) bignum = VarInt(primary_key=True) @@ -120,6 +123,7 @@ def test_varint_io(self): class TestDate(BaseCassEngTestCase): class DateTest(Model): + __keyspace__ = 'test' test_id = Integer(primary_key=True) created_at = Date() @@ -158,6 +162,7 @@ def test_date_none(self): class TestDecimal(BaseCassEngTestCase): class DecimalTest(Model): + __keyspace__ = 'test' test_id = Integer(primary_key=True) dec_val = Decimal() @@ -182,6 +187,7 @@ def test_decimal_io(self): class TestUUID(BaseCassEngTestCase): class UUIDTest(Model): + __keyspace__ = 'test' test_id = Integer(primary_key=True) a_uuid = UUID(default=uuid4()) @@ -209,6 +215,7 @@ def test_uuid_str_no_dashes(self): class TestTimeUUID(BaseCassEngTestCase): class TimeUUIDTest(Model): + __keyspace__ = 'test' test_id = Integer(primary_key=True) timeuuid = TimeUUID(default=uuid1()) @@ -234,6 +241,7 @@ def test_timeuuid_io(self): class TestInteger(BaseCassEngTestCase): class IntegerTest(Model): + __keyspace__ = 'test' test_id = UUID(primary_key=True, default=lambda:uuid4()) value = Integer(default=0, required=True) @@ -244,6 +252,7 @@ def test_default_zero_fields_validate(self): class TestBigInt(BaseCassEngTestCase): class BigIntTest(Model): + __keyspace__ = 'test' test_id = UUID(primary_key=True, default=lambda:uuid4()) value = BigInt(default=0, required=True) @@ -301,6 +310,7 @@ def test_non_required_validation(self): class TestExtraFieldsRaiseException(BaseCassEngTestCase): class TestModel(Model): + __keyspace__ = 'test' id = UUID(primary_key=True, default=uuid4) def test_extra_field(self): @@ -309,6 +319,7 @@ def test_extra_field(self): class TestPythonDoesntDieWhenExtraFieldIsInCassandra(BaseCassEngTestCase): class TestModel(Model): + __keyspace__ = 'test' __table_name__ = 'alter_doesnt_break_running_app' id = UUID(primary_key=True, default=uuid4) diff --git a/cqlengine/tests/columns/test_value_io.py b/cqlengine/tests/columns/test_value_io.py index 4b4ac7d200..1515c25a26 100644 --- a/cqlengine/tests/columns/test_value_io.py +++ b/cqlengine/tests/columns/test_value_io.py @@ -40,6 +40,7 @@ def setUpClass(cls): # create a table with the given column class IOTestModel(Model): + __keyspace__ = 'test' table_name = cls.column.db_type + "_io_test_model_{}".format(uuid4().hex[:8]) pkey = cls.column(primary_key=True) data = cls.column() diff --git a/cqlengine/tests/management/test_compaction_settings.py b/cqlengine/tests/management/test_compaction_settings.py index b34a5cafe0..2cad5d554a 100644 --- a/cqlengine/tests/management/test_compaction_settings.py +++ b/cqlengine/tests/management/test_compaction_settings.py @@ -9,6 +9,7 @@ class CompactionModel(Model): + __keyspace__ = 'test' __compaction__ = None cid = columns.UUID(primary_key=True) name = columns.Text() @@ -73,6 +74,7 @@ def test_sstable_size_in_mb(self): class LeveledcompactionTestTable(Model): + __keyspace__ = 'test' __compaction__ = LeveledCompactionStrategy __compaction_sstable_size_in_mb__ = 64 @@ -94,6 +96,7 @@ def test_compaction_not_altered_without_changes_leveled(self): from cqlengine.management import update_compaction class LeveledCompactionChangesDetectionTest(Model): + __keyspace__ = 'test' __compaction__ = LeveledCompactionStrategy __compaction_sstable_size_in_mb__ = 160 __compaction_tombstone_threshold__ = 0.125 @@ -110,6 +113,7 @@ def test_compaction_not_altered_without_changes_sizetiered(self): from cqlengine.management import update_compaction class SizeTieredCompactionChangesDetectionTest(Model): + __keyspace__ = 'test' __compaction__ = SizeTieredCompactionStrategy __compaction_bucket_high__ = 20 __compaction_bucket_low__ = 10 @@ -142,6 +146,7 @@ def test_alter_actually_alters(self): def test_alter_options(self): class AlterTable(Model): + __keyspace__ = 'test' __compaction__ = LeveledCompactionStrategy __compaction_sstable_size_in_mb__ = 64 @@ -158,6 +163,7 @@ class AlterTable(Model): class EmptyCompactionTest(BaseCassEngTestCase): def test_empty_compaction(self): class EmptyCompactionModel(Model): + __keyspace__ = 'test' __compaction__ = None cid = columns.UUID(primary_key=True) name = columns.Text() @@ -167,12 +173,14 @@ class EmptyCompactionModel(Model): class CompactionLeveledStrategyModel(Model): + __keyspace__ = 'test' __compaction__ = LeveledCompactionStrategy cid = columns.UUID(primary_key=True) name = columns.Text() class CompactionSizeTieredModel(Model): + __keyspace__ = 'test' __compaction__ = SizeTieredCompactionStrategy cid = columns.UUID(primary_key=True) name = columns.Text() @@ -183,6 +191,7 @@ class OptionsTest(BaseCassEngTestCase): def test_all_size_tiered_options(self): class AllSizeTieredOptionsModel(Model): + __keyspace__ = 'test' __compaction__ = SizeTieredCompactionStrategy __compaction_bucket_low__ = .3 __compaction_bucket_high__ = 2 @@ -211,6 +220,7 @@ class AllSizeTieredOptionsModel(Model): def test_all_leveled_options(self): class AllLeveledOptionsModel(Model): + __keyspace__ = 'test' __compaction__ = LeveledCompactionStrategy __compaction_sstable_size_in_mb__ = 64 diff --git a/cqlengine/tests/management/test_management.py b/cqlengine/tests/management/test_management.py index 44d0c33d06..1ed3137f92 100644 --- a/cqlengine/tests/management/test_management.py +++ b/cqlengine/tests/management/test_management.py @@ -27,16 +27,19 @@ def test_multiple_deletes_dont_fail(self): drop_table(TestModel) class LowercaseKeyModel(Model): + __keyspace__ = 'test' first_key = columns.Integer(primary_key=True) second_key = columns.Integer(primary_key=True) some_data = columns.Text() class CapitalizedKeyModel(Model): + __keyspace__ = 'test' firstKey = columns.Integer(primary_key=True) secondKey = columns.Integer(primary_key=True) someData = columns.Text() class PrimaryKeysOnlyModel(Model): + __keyspace__ = 'test' __compaction__ = LeveledCompactionStrategy first_ey = columns.Integer(primary_key=True) @@ -55,12 +58,14 @@ def test_table_definition(self): class FirstModel(Model): + __keyspace__ = 'test' __table_name__ = 'first_model' first_key = columns.UUID(primary_key=True) second_key = columns.UUID() third_key = columns.Text() class SecondModel(Model): + __keyspace__ = 'test' __table_name__ = 'first_model' first_key = columns.UUID(primary_key=True) second_key = columns.UUID() @@ -68,6 +73,7 @@ class SecondModel(Model): fourth_key = columns.Text() class ThirdModel(Model): + __keyspace__ = 'test' __table_name__ = 'first_model' first_key = columns.UUID(primary_key=True) second_key = columns.UUID() @@ -76,6 +82,7 @@ class ThirdModel(Model): blah = columns.Map(columns.Text, columns.Text) class FourthModel(Model): + __keyspace__ = 'test' __table_name__ = 'first_model' first_key = columns.UUID(primary_key=True) second_key = columns.UUID() @@ -109,6 +116,7 @@ def test_add_column(self): class ModelWithTableProperties(Model): + __keyspace__ = 'test' # Set random table properties __bloom_filter_fp_chance__ = 0.76328 __caching__ = CACHING_ALL diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 58b6112995..06195ed66c 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -20,6 +20,7 @@ def test_column_attributes_handled_correctly(self): """ class TestModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) text = columns.Text() @@ -41,6 +42,7 @@ def test_db_map(self): -the db_map allows columns """ class WildDBNames(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) content = columns.Text(db_field='words_and_whatnot') numbers = columns.Integer(db_field='integers_etc') @@ -65,6 +67,7 @@ def test_column_ordering_is_preserved(self): """ class Stuff(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) words = columns.Text() content = columns.Text() @@ -75,6 +78,7 @@ class Stuff(Model): def test_exception_raised_when_creating_class_without_pk(self): with self.assertRaises(ModelDefinitionException): class TestModel(Model): + __keyspace__ = 'test' count = columns.Integer() text = columns.Text(required=False) @@ -84,6 +88,7 @@ def test_value_managers_are_keeping_model_instances_isolated(self): Tests that instance value managers are isolated from other instances """ class Stuff(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) num = columns.Integer() @@ -99,6 +104,7 @@ def test_superclass_fields_are_inherited(self): Tests that fields defined on the super class are inherited properly """ class TestModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) text = columns.Text() @@ -111,6 +117,7 @@ class InheritedModel(TestModel): def test_column_family_name_generation(self): """ Tests that auto column family name generation works as expected """ class TestModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) text = columns.Text() @@ -146,6 +153,7 @@ def test_partition_keys(self): Test compound partition key definition """ class ModelWithPartitionKeys(cqlengine.Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) c1 = cqlengine.Text(primary_key=True) p1 = cqlengine.Text(partition_key=True) @@ -167,6 +175,7 @@ class ModelWithPartitionKeys(cqlengine.Model): def test_del_attribute_is_assigned_properly(self): """ Tests that columns that can be deleted have the del attribute """ class DelModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) key = columns.Integer(primary_key=True) data = columns.Integer(required=False) @@ -180,9 +189,11 @@ def test_does_not_exist_exceptions_are_not_shared_between_model(self): """ Tests that DoesNotExist exceptions are not the same exception between models """ class Model1(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) class Model2(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) try: @@ -196,6 +207,7 @@ class Model2(Model): def test_does_not_exist_inherits_from_superclass(self): """ Tests that a DoesNotExist exception can be caught by it's parent class DoesNotExist """ class Model1(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) class Model2(Model1): @@ -248,6 +260,7 @@ class ConcreteModel(AbstractModel): data = columns.Integer() class AbstractModelWithCol(Model): + __keyspace__ = 'test' __abstract__ = True pkey = columns.Integer(primary_key=True) @@ -256,6 +269,7 @@ class ConcreteModelWithCol(AbstractModelWithCol): class AbstractModelWithFullCols(Model): __abstract__ = True + __keyspace__ = 'test' pkey = columns.Integer(primary_key=True) data = columns.Integer() @@ -328,6 +342,7 @@ def create(iself, **kwargs): class CQModel(Model): __queryset__ = QSet + __keyspace__ = 'test' part = columns.UUID(primary_key=True) data = columns.Text() @@ -341,6 +356,7 @@ def save(iself): raise self.TestException class CDQModel(Model): + __keyspace__ = 'test' __dmlquery__ = DMLQ part = columns.UUID(primary_key=True) data = columns.Text() diff --git a/cqlengine/tests/model/test_equality_operations.py b/cqlengine/tests/model/test_equality_operations.py index c851e6089b..eeb8992b98 100644 --- a/cqlengine/tests/model/test_equality_operations.py +++ b/cqlengine/tests/model/test_equality_operations.py @@ -8,6 +8,7 @@ from cqlengine import columns class TestModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) diff --git a/cqlengine/tests/model/test_model.py b/cqlengine/tests/model/test_model.py index 1e18359ec0..1bd143d01c 100644 --- a/cqlengine/tests/model/test_model.py +++ b/cqlengine/tests/model/test_model.py @@ -10,6 +10,7 @@ class TestModel(TestCase): def test_instance_equality(self): """ tests the model equality functionality """ class EqualityModel(Model): + __keyspace__ = 'test' pk = columns.Integer(primary_key=True) m0 = EqualityModel(pk=0) @@ -21,9 +22,11 @@ class EqualityModel(Model): def test_model_equality(self): """ tests the model equality functionality """ class EqualityModel0(Model): + __keyspace__ = 'test' pk = columns.Integer(primary_key=True) class EqualityModel1(Model): + __keyspace__ = 'test' kk = columns.Integer(primary_key=True) m0 = EqualityModel0(pk=0) @@ -40,6 +43,7 @@ def test_model_with_attribute_name_conflict(self): """should raise exception when model defines column that conflicts with built-in attribute""" with self.assertRaises(ModelDefinitionException): class IllegalTimestampColumnModel(Model): + __keyspace__ = 'test' my_primary_key = columns.Integer(primary_key=True) timestamp = columns.BigInt() @@ -47,5 +51,6 @@ def test_model_with_method_name_conflict(self): """should raise exception when model defines column that conflicts with built-in method""" with self.assertRaises(ModelDefinitionException): class IllegalFilterColumnModel(Model): + __keyspace__ = 'test' my_primary_key = columns.Integer(primary_key=True) filter = columns.Text() \ No newline at end of file diff --git a/cqlengine/tests/model/test_model_io.py b/cqlengine/tests/model/test_model_io.py index e114d75a86..1faaaf029a 100644 --- a/cqlengine/tests/model/test_model_io.py +++ b/cqlengine/tests/model/test_model_io.py @@ -11,12 +11,14 @@ from cqlengine import columns class TestModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) a_bool = columns.Boolean(default=False) class TestModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) @@ -115,6 +117,7 @@ def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): class TestMultiKeyModel(Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) cluster = columns.Integer(primary_key=True) count = columns.Integer(required=False) @@ -240,6 +243,7 @@ def test_success_case(self): class IndexDefinitionModel(Model): + __keyspace__ = 'test' key = columns.UUID(primary_key=True) val = columns.Text(index=True) @@ -250,6 +254,7 @@ def test_exception_isnt_raised_if_an_index_is_defined_more_than_once(self): sync_table(IndexDefinitionModel) class ReservedWordModel(Model): + __keyspace__ = 'test' token = columns.Text(primary_key=True) insert = columns.Integer(index=True) @@ -270,6 +275,7 @@ def test_reserved_cql_words_can_be_used_as_column_names(self): class TestQueryModel(Model): + __keyspace__ = 'test' test_id = columns.UUID(primary_key=True, default=uuid4) date = columns.Date(primary_key=True) description = columns.Text() @@ -304,6 +310,7 @@ def test_query_with_date(self): def test_none_filter_fails(): class NoneFilterModel(Model): + __keyspace__ = 'test' pk = columns.Integer(primary_key=True) v = columns.Integer() sync_table(NoneFilterModel) diff --git a/cqlengine/tests/model/test_polymorphism.py b/cqlengine/tests/model/test_polymorphism.py index 28c3dccb82..450a6ed4cc 100644 --- a/cqlengine/tests/model/test_polymorphism.py +++ b/cqlengine/tests/model/test_polymorphism.py @@ -14,6 +14,7 @@ def test_multiple_polymorphic_key_failure(self): """ Tests that defining a model with more than one polymorphic key fails """ with self.assertRaises(models.ModelDefinitionException): class M(models.Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) type1 = columns.Integer(polymorphic_key=True) type2 = columns.Integer(polymorphic_key=True) @@ -21,6 +22,7 @@ class M(models.Model): def test_polymorphic_key_inheritance(self): """ Tests that polymorphic_key attribute is not inherited """ class Base(models.Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) type1 = columns.Integer(polymorphic_key=True) @@ -35,6 +37,7 @@ class M2(M1): def test_polymorphic_metaclass(self): """ Tests that the model meta class configures polymorphic models properly """ class Base(models.Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) type1 = columns.Integer(polymorphic_key=True) @@ -55,6 +58,7 @@ class M1(Base): def test_table_names_are_inherited_from_poly_base(self): class Base(models.Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) type1 = columns.Integer(polymorphic_key=True) @@ -66,11 +70,13 @@ class M1(Base): def test_collection_columns_cant_be_polymorphic_keys(self): with self.assertRaises(models.ModelDefinitionException): class Base(models.Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) type1 = columns.Set(columns.Integer, polymorphic_key=True) class PolyBase(models.Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid.uuid4) row_type = columns.Integer(polymorphic_key=True) @@ -137,6 +143,7 @@ def test_delete_on_polymorphic_subclass_does_not_include_polymorphic_key(self): class UnindexedPolyBase(models.Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid.uuid4) cluster = columns.UUID(primary_key=True, default=uuid.uuid4) row_type = columns.Integer(polymorphic_key=True) @@ -194,6 +201,7 @@ def test_conflicting_type_results(self): class IndexedPolyBase(models.Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid.uuid4) cluster = columns.UUID(primary_key=True, default=uuid.uuid4) row_type = columns.Integer(polymorphic_key=True, index=True) diff --git a/cqlengine/tests/model/test_updates.py b/cqlengine/tests/model/test_updates.py index 6141436c8f..4ffc817efd 100644 --- a/cqlengine/tests/model/test_updates.py +++ b/cqlengine/tests/model/test_updates.py @@ -10,6 +10,7 @@ class TestUpdateModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid4) cluster = columns.UUID(primary_key=True, default=uuid4) count = columns.Integer(required=False) diff --git a/cqlengine/tests/model/test_value_lists.py b/cqlengine/tests/model/test_value_lists.py index 8d4c76de5c..306fa28d89 100644 --- a/cqlengine/tests/model/test_value_lists.py +++ b/cqlengine/tests/model/test_value_lists.py @@ -8,10 +8,12 @@ class TestModel(Model): + __keyspace__ = 'test' id = columns.Integer(primary_key=True) clustering_key = columns.Integer(primary_key=True, clustering_order='desc') class TestClusteringComplexModel(Model): + __keyspace__ = 'test' id = columns.Integer(primary_key=True) clustering_key = columns.Integer(primary_key=True, clustering_order='desc') some_value = columns.Integer() diff --git a/cqlengine/tests/query/test_batch_query.py b/cqlengine/tests/query/test_batch_query.py index 1d7e1804db..9f3ecb5d6a 100644 --- a/cqlengine/tests/query/test_batch_query.py +++ b/cqlengine/tests/query/test_batch_query.py @@ -8,12 +8,14 @@ from cqlengine.tests.base import BaseCassEngTestCase class TestMultiKeyModel(Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) cluster = columns.Integer(primary_key=True) count = columns.Integer(required=False) text = columns.Text(required=False) class BatchQueryLogModel(Model): + __keyspace__ = 'test' # simple k/v table k = columns.Integer(primary_key=True) v = columns.Integer() diff --git a/cqlengine/tests/query/test_datetime_queries.py b/cqlengine/tests/query/test_datetime_queries.py index c6cb0627b7..8179b0f290 100644 --- a/cqlengine/tests/query/test_datetime_queries.py +++ b/cqlengine/tests/query/test_datetime_queries.py @@ -11,6 +11,7 @@ from cqlengine import query class DateTimeQueryTestModel(Model): + __keyspace__ = 'test' user = columns.Integer(primary_key=True) day = columns.DateTime(primary_key=True) data = columns.Text() diff --git a/cqlengine/tests/query/test_queryoperators.py b/cqlengine/tests/query/test_queryoperators.py index 01cce27fc8..5572d00c56 100644 --- a/cqlengine/tests/query/test_queryoperators.py +++ b/cqlengine/tests/query/test_queryoperators.py @@ -39,6 +39,7 @@ def test_mintimeuuid_function(self): class TokenTestModel(Model): + __keyspace__ = 'test' key = columns.Integer(primary_key=True) val = columns.Integer() @@ -74,6 +75,7 @@ def test_token_function(self): def test_compound_pk_token_function(self): class TestModel(Model): + __keyspace__ = 'test' p1 = columns.Text(partition_key=True) p2 = columns.Text(partition_key=True) diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index ced3891261..feb8905bbc 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -39,6 +39,7 @@ def dst(self, dt): class TestModel(Model): + __keyspace__ = 'test' test_id = columns.Integer(primary_key=True) attempt_id = columns.Integer(primary_key=True) description = columns.Text() @@ -47,6 +48,7 @@ class TestModel(Model): class IndexedTestModel(Model): + __keyspace__ = 'test' test_id = columns.Integer(primary_key=True) attempt_id = columns.Integer(index=True) description = columns.Text() @@ -55,6 +57,7 @@ class IndexedTestModel(Model): class TestMultiClusteringModel(Model): + __keyspace__ = 'test' one = columns.Integer(primary_key=True) two = columns.Integer(primary_key=True) three = columns.Integer(primary_key=True) @@ -373,6 +376,7 @@ def test_allow_filtering_flag(self): def test_non_quality_filtering(): class NonEqualityFilteringModel(Model): + __keyspace__ = 'test' example_id = columns.UUID(primary_key=True, default=uuid.uuid4) sequence_id = columns.Integer(primary_key=True) # sequence_id is a clustering key example_type = columns.Integer(index=True) @@ -542,6 +546,7 @@ def test_conn_is_returned_after_filling_cache(self): class TimeUUIDQueryModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True) time = columns.TimeUUID(primary_key=True) data = columns.Text(required=False) diff --git a/cqlengine/tests/query/test_updates.py b/cqlengine/tests/query/test_updates.py index d32755af8f..1037cf5a2a 100644 --- a/cqlengine/tests/query/test_updates.py +++ b/cqlengine/tests/query/test_updates.py @@ -9,6 +9,7 @@ class TestQueryUpdateModel(Model): + __keyspace__ = 'test' partition = columns.UUID(primary_key=True, default=uuid4) cluster = columns.Integer(primary_key=True) count = columns.Integer(required=False) diff --git a/cqlengine/tests/test_batch_query.py b/cqlengine/tests/test_batch_query.py index 9c2d4140c8..39204a594f 100644 --- a/cqlengine/tests/test_batch_query.py +++ b/cqlengine/tests/test_batch_query.py @@ -11,6 +11,7 @@ from cqlengine.tests.base import BaseCassEngTestCase class TestMultiKeyModel(Model): + __keyspace__ = 'test' partition = columns.Integer(primary_key=True) cluster = columns.Integer(primary_key=True) count = columns.Integer(required=False) diff --git a/cqlengine/tests/test_consistency.py b/cqlengine/tests/test_consistency.py index 9f0f3f86b3..a45117b00f 100644 --- a/cqlengine/tests/test_consistency.py +++ b/cqlengine/tests/test_consistency.py @@ -7,6 +7,7 @@ from cqlengine import ALL, BatchQuery class TestConsistencyModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) diff --git a/cqlengine/tests/test_load.py b/cqlengine/tests/test_load.py index 16a42eac88..6e64d96d87 100644 --- a/cqlengine/tests/test_load.py +++ b/cqlengine/tests/test_load.py @@ -8,6 +8,7 @@ import gc class LoadTest(Model): + __keyspace__ = 'test' k = Integer(primary_key=True) v = Integer() diff --git a/cqlengine/tests/test_timestamp.py b/cqlengine/tests/test_timestamp.py index b1a6a2860d..cd298cf457 100644 --- a/cqlengine/tests/test_timestamp.py +++ b/cqlengine/tests/test_timestamp.py @@ -12,6 +12,7 @@ class TestTimestampModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() diff --git a/cqlengine/tests/test_ttl.py b/cqlengine/tests/test_ttl.py index 45f865e0e6..bb5e1bb440 100644 --- a/cqlengine/tests/test_ttl.py +++ b/cqlengine/tests/test_ttl.py @@ -8,6 +8,7 @@ class TestTTLModel(Model): + __keyspace__ = 'test' id = columns.UUID(primary_key=True, default=lambda:uuid4()) count = columns.Integer() text = columns.Text(required=False) From f03bd5b2e534de0b394d095a606d17091b24535d Mon Sep 17 00:00:00 2001 From: Blake Eggleston Date: Thu, 3 Jul 2014 15:38:04 -0700 Subject: [PATCH 0930/3961] documenting the removal of the cqlengine keyspace --- README.md | 2 +- docs/index.rst | 4 +--- docs/topics/models.rst | 6 +++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7965610d56..361fff385f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ cqlengine cqlengine is a Cassandra CQL 3 Object Mapper for Python -**Users of versions < 0.4, please read this post: [Breaking Changes](https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU)** +**Users of versions < 0.16, the default keyspace 'cqlengine' has been removed. Please read this before upgrading:** [Breaking Changes](https://cqlengine.readthedocs.org/en/latest/topics/models.html#keyspace-change) [Documentation](https://cqlengine.readthedocs.org/en/latest/) diff --git a/docs/index.rst b/docs/index.rst index 99a08efc3a..7bb0e5e979 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,9 +5,7 @@ cqlengine documentation ======================= -**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_ - -.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU +**Users of versions < 0.16, the default keyspace 'cqlengine' has been removed. Please read this before upgrading:** :ref:`Breaking Changes ` cqlengine is a Cassandra CQL 3 Object Mapper for Python diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 14f905b40d..c7f28f3ee8 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -169,9 +169,13 @@ Model Attributes *Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix. Manually defined table names are not inherited. + .. _keyspace-change: .. attribute:: Model.__keyspace__ - *Optional.* Sets the name of the keyspace used by this model. Defaults to cqlengine + Sets the name of the keyspace used by this model. + + **Prior to cqlengine 0.16, this setting defaulted + to 'cqlengine'. As of 0.16, this field needs to be set on all non-abstract models, or their base classes.** Table Polymorphism From 912ecb1131d3142549b0655909abae8d80281e3c Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Fri, 4 Jul 2014 03:50:50 -0400 Subject: [PATCH 0931/3961] setup has retry_connect, which will retry connect on next execute if Cassandra was down during setup. --- cqlengine/connection.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index e22f7556dd..9f17fe8388 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -3,7 +3,7 @@ #http://cassandra.apache.org/doc/cql/CQL.html from collections import namedtuple -from cassandra.cluster import Cluster +from cassandra.cluster import Cluster, NoHostAvailable from cassandra.query import SimpleStatement, Statement try: @@ -35,6 +35,7 @@ def setup( default_keyspace, consistency=ConsistencyLevel.ONE, lazy_connect=False, + retry_connect=False, **kwargs): """ Records the hosts and connects to one of them @@ -59,11 +60,24 @@ def setup( default_consistency_level = consistency if lazy_connect: - lazy_connect_args = (hosts, default_keyspace, consistency, kwargs) + kwargs['default_keyspace'] = default_keyspace + kwargs['consistency'] = consistency + kwargs['lazy_connect'] = False + kwargs['retry_connect'] = retry_connect + lazy_connect_args = (hosts, kwargs) return cluster = Cluster(hosts, **kwargs) - session = cluster.connect() + try: + session = cluster.connect() + except NoHostAvailable: + if retry_connect: + kwargs['default_keyspace'] = default_keyspace + kwargs['consistency'] = consistency + kwargs['lazy_connect'] = False + kwargs['retry_connect'] = retry_connect + lazy_connect_args = (hosts, kwargs) + raise session.row_factory = dict_factory def execute(query, params=None, consistency_level=None): @@ -106,6 +120,6 @@ def get_cluster(): def handle_lazy_connect(): global lazy_connect_args if lazy_connect_args: - hosts, default_keyspace, consistency, kwargs = lazy_connect_args + hosts, kwargs = lazy_connect_args lazy_connect_args = None - setup(hosts, default_keyspace, consistency, **kwargs) + setup(hosts, **kwargs) From 8edb27bdc87611a9b39b762b20823c8b56d7896e Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 7 Jul 2014 14:19:28 -0700 Subject: [PATCH 0932/3961] fixed pagedresult issue --- cqlengine/connection.py | 10 ++++--- cqlengine/exceptions.py | 1 + cqlengine/models.py | 10 ------- cqlengine/query.py | 2 +- .../tests/model/test_class_construction.py | 9 ------- cqlengine/tests/query/test_queryset.py | 27 +++++++++++++++++-- 6 files changed, 33 insertions(+), 26 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index e22f7556dd..d5f6ed5c5e 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -14,7 +14,7 @@ import logging -from cqlengine.exceptions import CQLEngineException +from cqlengine.exceptions import CQLEngineException, UndefinedKeyspaceException from cassandra import ConsistencyLevel from cqlengine.statements import BaseCQLStatement from cassandra.query import dict_factory @@ -53,9 +53,11 @@ def setup( if 'username' in kwargs or 'password' in kwargs: raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider") - if default_keyspace: - from cqlengine import models - models.DEFAULT_KEYSPACE = default_keyspace + if not default_keyspace: + raise UndefinedKeyspaceException() + + from cqlengine import models + models.DEFAULT_KEYSPACE = default_keyspace default_consistency_level = consistency if lazy_connect: diff --git a/cqlengine/exceptions.py b/cqlengine/exceptions.py index 87b5cdc4c5..94a51b92ac 100644 --- a/cqlengine/exceptions.py +++ b/cqlengine/exceptions.py @@ -3,3 +3,4 @@ class CQLEngineException(Exception): pass class ModelException(CQLEngineException): pass class ValidationError(CQLEngineException): pass +class UndefinedKeyspaceException(CQLEngineException): pass diff --git a/cqlengine/models.py b/cqlengine/models.py index b800394994..d4ce1e7708 100644 --- a/cqlengine/models.py +++ b/cqlengine/models.py @@ -762,16 +762,6 @@ def _get_polymorphic_base(bases): #create the class and add a QuerySet to it klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs) - if not klass.__abstract__ and klass.__keyspace__ is None: - warnings.warn( - "No keyspace defined on {}.{}\n" - " The default keyspace 'cqlengine' was removed in 0.16.\n" - " Please define a keyspace on the '__keyspace__' model class attribute\n" - " or set a default keyspace with management.setup function\n" - .format(klass.__module__, klass.__name__), - UndefinedKeyspaceWarning - ) - return klass diff --git a/cqlengine/query.py b/cqlengine/query.py index 0a64a5c78a..738822f3d5 100644 --- a/cqlengine/query.py +++ b/cqlengine/query.py @@ -293,7 +293,7 @@ def _execute_query(self): if self._batch: raise CQLEngineException("Only inserts, updates, and deletes are available in batch mode") if self._result_cache is None: - self._result_cache = self._execute(self._select_query()) + self._result_cache = list(self._execute(self._select_query())) self._construct_result = self._get_result_constructor() def _fill_result_cache_to_idx(self, idx): diff --git a/cqlengine/tests/model/test_class_construction.py b/cqlengine/tests/model/test_class_construction.py index 06195ed66c..37f0f81965 100644 --- a/cqlengine/tests/model/test_class_construction.py +++ b/cqlengine/tests/model/test_class_construction.py @@ -221,15 +221,6 @@ class Model2(Model1): except Exception: assert False, "Model2 exception should not be caught by Model1" - def test_no_keyspace_warning(self): - with warnings.catch_warnings(record=True) as warn: - class NoKeyspace(Model): - key = columns.UUID(primary_key=True) - - self.assertEqual(len(warn), 1) - warn_message = warn[0] - self.assertEqual(warn_message.category, UndefinedKeyspaceWarning) - def test_abstract_model_keyspace_warning_is_skipped(self): with warnings.catch_warnings(record=True) as warn: class NoKeyspace(Model): diff --git a/cqlengine/tests/query/test_queryset.py b/cqlengine/tests/query/test_queryset.py index feb8905bbc..1c9a3ed5f9 100644 --- a/cqlengine/tests/query/test_queryset.py +++ b/cqlengine/tests/query/test_queryset.py @@ -1,11 +1,11 @@ from datetime import datetime import time -from unittest import TestCase +from unittest import TestCase, skipUnless from uuid import uuid1, uuid4 import uuid from cqlengine.tests.base import BaseCassEngTestCase - +import mock from cqlengine.exceptions import ModelException from cqlengine import functions from cqlengine.management import sync_table, drop_table, sync_table @@ -20,6 +20,11 @@ from cqlengine import operators +from cqlengine.connection import get_cluster, get_session + +cluster = get_cluster() + + class TzOffset(tzinfo): """Minimal implementation of a timezone offset to help testing with timezone aware datetimes. @@ -684,3 +689,21 @@ def test_objects_property_returns_fresh_queryset(self): assert TestModel.objects._result_cache is None +@skipUnless(cluster.protocol_version >= 2, "only runs against the cql3 protocol v2.0") +def test_paged_result_handling(): + # addresses #225 + class PagingTest(Model): + id = columns.Integer(primary_key=True) + val = columns.Integer() + sync_table(PagingTest) + + PagingTest.create(id=1, val=1) + PagingTest.create(id=2, val=2) + + session = get_session() + with mock.patch.object(session, 'default_fetch_size', 1): + results = PagingTest.objects()[:] + + assert len(results) == 2 + + From 73ea42d932bd76d0104ee780ef4dacc408478dde Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 7 Jul 2014 14:27:05 -0700 Subject: [PATCH 0933/3961] updated changelog --- changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog b/changelog index f6bd732333..2aa66d4186 100644 --- a/changelog +++ b/changelog @@ -2,6 +2,7 @@ CHANGELOG 0.16.0 + 225: No handling of PagedResult from execute 222: figure out how to make travis not totally fail when a test is skipped 220: delayed connect. use setup(delayed_connect=True) 218: throw exception on create_table and delete_table From 2610fd782be8da812fe5f92e57b9db72d403936f Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Mon, 7 Jul 2014 14:27:22 -0700 Subject: [PATCH 0934/3961] version bump --- cqlengine/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 1282fff53b..04a373efe6 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.15.5 +0.16.0 From bf58fdf30e4ea7f1de4e1f45bd8429ede86d1820 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 8 Jul 2014 09:18:26 -0500 Subject: [PATCH 0935/3961] Update copyright on murmur3.c --- cassandra/murmur3.c | 1 + 1 file changed, 1 insertion(+) diff --git a/cassandra/murmur3.c b/cassandra/murmur3.c index b89d11b887..f5ef0e6575 100644 --- a/cassandra/murmur3.c +++ b/cassandra/murmur3.c @@ -6,6 +6,7 @@ * * Copyright (c) 2011 Austin Appleby (Murmur3 routine) * Copyright (c) 2011 Patrick Hensley (Python wrapper, packaging) + * Copyright 2013 DataStax (Minor modifications to match Cassandra's MM3 hashes) * */ From 7c5a608418d43f15ebd2089e0f71509595fd0ef6 Mon Sep 17 00:00:00 2001 From: Michael Cyrulnik Date: Tue, 8 Jul 2014 11:20:43 -0400 Subject: [PATCH 0936/3961] reorder session check in execute to allow lazy connect --- cqlengine/connection.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index 0ad7e05ad2..1dd35fb4cb 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -84,11 +84,11 @@ def setup( def execute(query, params=None, consistency_level=None): + handle_lazy_connect() + if not session: raise CQLEngineException("It is required to setup() cqlengine before executing queries") - handle_lazy_connect() - if consistency_level is None: consistency_level = default_consistency_level @@ -103,14 +103,11 @@ def execute(query, params=None, consistency_level=None): elif isinstance(query, basestring): query = SimpleStatement(query, consistency_level=consistency_level) - - params = params or {} result = session.execute(query, params) return result - def get_session(): handle_lazy_connect() return session From 831b4a2ab9ae4ffb7d8605e62cae433426e17e7c Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 8 Jul 2014 09:14:43 -0700 Subject: [PATCH 0937/3961] version bump, makefile for releasing --- Makefile | 14 ++++++++++++++ cqlengine/VERSION | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..061fc27426 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +clean: + find . -name *.pyc -delete + rm -rf cqlengine/__pycache__ + + +build: clean + python setup.py build + +release: clean + python setup.py sdist upload + + +.PHONY: build + diff --git a/cqlengine/VERSION b/cqlengine/VERSION index 04a373efe6..2a0970ca75 100644 --- a/cqlengine/VERSION +++ b/cqlengine/VERSION @@ -1 +1 @@ -0.16.0 +0.16.1 From d0988232edd47e72d061edca07e1d35c7f9125bb Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 8 Jul 2014 11:34:26 -0500 Subject: [PATCH 0938/3961] Fix typo in Getting Started import Fixes #148 --- docs/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 2c6cda32a1..8b33b3e807 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -40,7 +40,7 @@ behavior in some other way, this is the place to do it: .. code-block:: python from cassandra.cluster import Cluster - from cassandra.polices import DCAwareRoundRobinPolicy + from cassandra.policies import DCAwareRoundRobinPolicy cluster = Cluster( ['10.1.1.3', '10.1.1.4', '10.1.1.5'], From 1fca5e0a027bf23e760864517033d6e02b3f0432 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 8 Jul 2014 12:12:33 -0500 Subject: [PATCH 0939/3961] Fix unit tests that may break under gevent --- setup.py | 7 ++++--- tests/unit/io/test_asyncorereactor.py | 3 +++ tests/unit/io/test_libevreactor.py | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index f7cb93b172..8539ec2cba 100644 --- a/setup.py +++ b/setup.py @@ -15,13 +15,14 @@ from __future__ import print_function import sys -import ez_setup -ez_setup.use_setuptools() - if __name__ == '__main__' and sys.argv[1] == "gevent_nosetests": + print("Running gevent tests") from gevent.monkey import patch_all patch_all() +import ez_setup +ez_setup.use_setuptools() + from setuptools import setup from distutils.command.build_ext import build_ext from distutils.core import Extension diff --git a/tests/unit/io/test_asyncorereactor.py b/tests/unit/io/test_asyncorereactor.py index 5ab59670a6..6acb1ef839 100644 --- a/tests/unit/io/test_asyncorereactor.py +++ b/tests/unit/io/test_asyncorereactor.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import sys import six try: @@ -42,6 +43,8 @@ class AsyncoreConnectionTest(unittest.TestCase): @classmethod def setUpClass(cls): + if 'gevent.monkey' in sys.modules: + raise unittest.SkipTest("gevent monkey-patching detected") AsyncoreConnection.initialize_reactor() cls.socket_patcher = patch('socket.socket', spec=socket.socket) cls.mock_socket = cls.socket_patcher.start() diff --git a/tests/unit/io/test_libevreactor.py b/tests/unit/io/test_libevreactor.py index 3b677ab547..78e08f27a7 100644 --- a/tests/unit/io/test_libevreactor.py +++ b/tests/unit/io/test_libevreactor.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - try: import unittest2 as unittest except ImportError: @@ -19,6 +18,7 @@ import errno import os +import sys import six from six import BytesIO @@ -48,6 +48,8 @@ class LibevConnectionTest(unittest.TestCase): def setUp(self): + if 'gevent.monkey' in sys.modules: + raise unittest.SkipTest("gevent monkey-patching detected") if LibevConnection is None: raise unittest.SkipTest('libev does not appear to be installed correctly') LibevConnection.initialize_reactor() From 60ded354a9d0dbf39eeb8a549f1f05adcf97be7a Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 8 Jul 2014 14:03:11 -0500 Subject: [PATCH 0940/3961] Fix v3 proto header reading w/ gevent, twisted reactors Fixes PYTHON-87 --- cassandra/connection.py | 40 ++++++++++++++++++++++++++++++++- cassandra/io/asyncorereactor.py | 40 ++------------------------------- cassandra/io/geventreactor.py | 39 ++------------------------------ cassandra/io/libevreactor.py | 37 +----------------------------- cassandra/io/twistedreactor.py | 38 +------------------------------ 5 files changed, 45 insertions(+), 149 deletions(-) diff --git a/cassandra/connection.py b/cassandra/connection.py index ead994300f..6aa47481b4 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import # to enable import io from stdlib from collections import defaultdict, deque import errno from functools import wraps, partial +import io import logging +import os import sys from threading import Event, RLock import time @@ -29,7 +32,7 @@ from six.moves import range from cassandra import ConsistencyLevel, AuthenticationFailed, OperationTimedOut -from cassandra.marshal import int32_pack, header_unpack, v3_header_unpack +from cassandra.marshal import int32_pack, header_unpack, v3_header_unpack, int32_unpack from cassandra.protocol import (ReadyMessage, AuthenticateMessage, OptionsMessage, StartupMessage, ErrorMessage, CredentialsMessage, QueryMessage, ResultMessage, decode_response, @@ -170,6 +173,7 @@ class Connection(object): user_type_map = None is_control_connection = False + _iobuf = None def __init__(self, host='127.0.0.1', port=9042, authenticator=None, ssl_options=None, sockopts=None, compression=True, @@ -186,6 +190,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None, self.is_control_connection = is_control_connection self.user_type_map = user_type_map self._push_watchers = defaultdict(set) + self._iobuf = io.BytesIO() if protocol_version >= 3: self._header_unpack = v3_header_unpack self._header_length = 5 @@ -344,6 +349,39 @@ def control_conn_disposed(self): self.is_control_connection = False self._push_watchers = {} + def process_io_buffer(self): + while True: + pos = self._iobuf.tell() + if pos < self._full_header_length or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): + # we don't have a complete header yet or we + # already saw a header, but we don't have a + # complete message yet + return + else: + # have enough for header, read body len from header + self._iobuf.seek(self._header_length) + body_len = int32_unpack(self._iobuf.read(4)) + + # seek to end to get length of current buffer + self._iobuf.seek(0, os.SEEK_END) + pos = self._iobuf.tell() + + if pos >= body_len + self._full_header_length: + # read message header and body + self._iobuf.seek(0) + msg = self._iobuf.read(self._full_header_length + body_len) + + # leave leftover in current buffer + leftover = self._iobuf.read() + self._iobuf = io.BytesIO() + self._iobuf.write(leftover) + + self._total_reqd_bytes = 0 + self.process_msg(msg, body_len) + else: + self._total_reqd_bytes = body_len + self._full_header_length + return + @defunct_on_error def process_msg(self, msg, body_len): version, flags, stream_id, opcode = self._header_unpack(msg[:self._header_length]) diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index 7f7e522663..c5dd44fe2a 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -11,12 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from __future__ import absolute_import # to enable import io from stdlib import atexit from collections import deque from functools import partial -import io import logging import os import socket @@ -43,7 +40,6 @@ from cassandra.connection import (Connection, ConnectionShutdown, ConnectionException, NONBLOCKING) from cassandra.protocol import RegisterMessage -from cassandra.marshal import int32_unpack log = logging.getLogger(__name__) @@ -178,7 +174,6 @@ def __init__(self, *args, **kwargs): asyncore.dispatcher.__init__(self) self.connected_event = Event() - self._iobuf = io.BytesIO() self._callbacks = {} self.deque = deque() @@ -226,7 +221,7 @@ def connect(self, address): self.socket.settimeout(1.0) err = self.socket.connect_ex(address) if err in (EINPROGRESS, EALREADY, EWOULDBLOCK) \ - or err == EINVAL and os.name in ('nt', 'ce'): + or err == EINVAL and os.name in ('nt', 'ce'): raise ConnectionException("Timed out connecting to %s" % (address[0])) if err in (0, EISCONN): self.addr = address @@ -308,38 +303,7 @@ def handle_read(self): return if self._iobuf.tell(): - while True: - pos = self._iobuf.tell() - if pos < self._full_header_length or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): - # we don't have a complete header yet or we - # already saw a header, but we don't have a - # complete message yet - break - else: - # have enough for header, read body len from header - self._iobuf.seek(self._header_length) - body_len = int32_unpack(self._iobuf.read(4)) - - # seek to end to get length of current buffer - self._iobuf.seek(0, os.SEEK_END) - pos = self._iobuf.tell() - - if pos >= body_len + self._full_header_length: - # read message header and body - self._iobuf.seek(0) - msg = self._iobuf.read(self._full_header_length + body_len) - - # leave leftover in current buffer - leftover = self._iobuf.read() - self._iobuf = io.BytesIO() - self._iobuf.write(leftover) - - self._total_reqd_bytes = 0 - self.process_msg(msg, body_len) - else: - self._total_reqd_bytes = body_len + self._full_header_length - break - + self.process_io_buffer() if not self._callbacks and not self.is_control_connection: self._readable = False diff --git a/cassandra/io/geventreactor.py b/cassandra/io/geventreactor.py index 93ebab8243..0caaf014b0 100644 --- a/cassandra/io/geventreactor.py +++ b/cassandra/io/geventreactor.py @@ -11,8 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from __future__ import absolute_import # to enable import io from stdlib import gevent from gevent import select, socket from gevent.event import Event @@ -20,7 +18,6 @@ from collections import defaultdict from functools import partial -import io import logging import os @@ -31,7 +28,6 @@ from cassandra import OperationTimedOut from cassandra.connection import Connection, ConnectionShutdown from cassandra.protocol import RegisterMessage -from cassandra.marshal import int32_unpack log = logging.getLogger(__name__) @@ -71,7 +67,6 @@ def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) self.connected_event = Event() - self._iobuf = io.BytesIO() self._write_queue = Queue() self._callbacks = {} @@ -154,39 +149,9 @@ def handle_read(self): return # leave the read loop if self._iobuf.tell(): - while True: - pos = self._iobuf.tell() - if pos < 8 or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): - # we don't have a complete header yet or we - # already saw a header, but we don't have a - # complete message yet - break - else: - # have enough for header, read body len from header - self._iobuf.seek(4) - body_len = int32_unpack(self._iobuf.read(4)) - - # seek to end to get length of current buffer - self._iobuf.seek(0, os.SEEK_END) - pos = self._iobuf.tell() - - if pos >= body_len + 8: - # read message header and body - self._iobuf.seek(0) - msg = self._iobuf.read(8 + body_len) - - # leave leftover in current buffer - leftover = self._iobuf.read() - self._iobuf = io.BytesIO() - self._iobuf.write(leftover) - - self._total_reqd_bytes = 0 - self.process_msg(msg, body_len) - else: - self._total_reqd_bytes = body_len + 8 - break + self.process_io_buffer() else: - log.debug("connection closed by server") + log.debug("Connection %s closed by server", self) self.close() return diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index eb95f5bc57..5adf008f02 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -11,12 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from __future__ import absolute_import # to enable import io from stdlib import atexit from collections import deque from functools import partial -import io import logging import os import socket @@ -28,7 +25,6 @@ from cassandra import OperationTimedOut from cassandra.connection import Connection, ConnectionShutdown, NONBLOCKING from cassandra.protocol import RegisterMessage -from cassandra.marshal import int32_unpack try: import cassandra.io.libevwrapper as libev except ImportError: @@ -257,7 +253,6 @@ def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) self.connected_event = Event() - self._iobuf = io.BytesIO() self._callbacks = {} self.deque = deque() @@ -359,37 +354,7 @@ def handle_read(self, watcher, revents, errno=None): return if self._iobuf.tell(): - while True: - pos = self._iobuf.tell() - if pos < self._full_header_length or (self._total_reqd_bytes > 0 and pos < self._total_reqd_bytes): - # we don't have a complete header yet or we - # already saw a header, but we don't have a - # complete message yet - break - else: - # have enough for header, read body len from header - self._iobuf.seek(self._header_length) - body_len = int32_unpack(self._iobuf.read(4)) - - # seek to end to get length of current buffer - self._iobuf.seek(0, os.SEEK_END) - pos = self._iobuf.tell() - - if pos >= body_len + self._full_header_length: - # read message header and body - self._iobuf.seek(0) - msg = self._iobuf.read(self._full_header_length + body_len) - - # leave leftover in current buffer - leftover = self._iobuf.read() - self._iobuf = io.BytesIO() - self._iobuf.write(leftover) - - self._total_reqd_bytes = 0 - self.process_msg(msg, body_len) - else: - self._total_reqd_bytes = body_len + self._full_header_length - break + self.process_io_buffer() else: log.debug("Connection %s closed by server", self) self.close() diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py index 9ca401dd6d..ac7f97e4c4 100644 --- a/cassandra/io/twistedreactor.py +++ b/cassandra/io/twistedreactor.py @@ -21,14 +21,10 @@ import logging import weakref import atexit -import os - -from io import BytesIO from cassandra import OperationTimedOut from cassandra.connection import Connection, ConnectionShutdown from cassandra.protocol import RegisterMessage -from cassandra.marshal import int32_unpack log = logging.getLogger(__name__) @@ -182,7 +178,6 @@ def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) self.connected_event = Event() - self._iobuf = BytesIO() self.is_closed = True self.connector = None @@ -231,38 +226,7 @@ def handle_read(self): """ Process the incoming data buffer. """ - while True: - pos = self._iobuf.tell() - if pos < 8 or (self._total_reqd_bytes > 0 and - pos < self._total_reqd_bytes): - # we don't have a complete header yet or we - # already saw a header, but we don't have a - # complete message yet - return - else: - # have enough for header, read body len from header - self._iobuf.seek(4) - body_len = int32_unpack(self._iobuf.read(4)) - - # seek to end to get length of current buffer - self._iobuf.seek(0, os.SEEK_END) - pos = self._iobuf.tell() - - if pos >= body_len + 8: - # read message header and body - self._iobuf.seek(0) - msg = self._iobuf.read(8 + body_len) - - # leave leftover in current buffer - leftover = self._iobuf.read() - self._iobuf = BytesIO() - self._iobuf.write(leftover) - - self._total_reqd_bytes = 0 - self.process_msg(msg, body_len) - else: - self._total_reqd_bytes = body_len + 8 - return + self.process_io_buffer() def push(self, data): """ From 9dc31d00e87e23bde952e3df8981a1fade779c39 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 8 Jul 2014 17:13:27 -0500 Subject: [PATCH 0941/3961] Avoid asyncore race condition on write availability --- CHANGELOG.rst | 2 ++ cassandra/io/asyncorereactor.py | 13 ++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b98671539..ad5b3e00b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,6 +38,8 @@ Bug Fixes * Avoid UnicodeDecodeError when query string is unicode (PYTHON-76) * Correctly capture dclocal_read_repair_chance for tables and use it when generating CREATE TABLE statements (PYTHON-84) +* Avoid race condition with AsyncoreConnection that may cause messages + to fail to be written until a new message is pushed 2.0.2 ===== diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index c5dd44fe2a..624fc4a315 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -262,12 +262,12 @@ def handle_close(self): def handle_write(self): while True: - try: - with self.deque_lock: + with self.deque_lock: + try: next_msg = self.deque.popleft() - except IndexError: - self._writable = False - return + except IndexError: + self._writable = False + return try: sent = self.send(next_msg) @@ -318,8 +318,7 @@ def push(self, data): with self.deque_lock: self.deque.extend(chunks) - - self._writable = True + self._writable = True def writable(self): return self._writable From 8ea861a60061ec3d3d5074cdcf4519c3b0209668 Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Wed, 9 Jul 2014 10:59:45 -0500 Subject: [PATCH 0942/3961] Bump ccm version to 2.0.9 --- tests/integration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index a6c9e95d46..c7e8aadb3b 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -40,7 +40,7 @@ CCM_CLUSTER = None CASSANDRA_DIR = os.getenv('CASSANDRA_DIR', None) -CASSANDRA_VERSION = os.getenv('CASSANDRA_VERSION', '2.0.6') +CASSANDRA_VERSION = os.getenv('CASSANDRA_VERSION', '2.0.9') if CASSANDRA_VERSION.startswith('1'): default_protocol_version = 1 From 02e92d6f34bcc08dac1951ef554859e8e633a4de Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 9 Jul 2014 11:38:58 -0500 Subject: [PATCH 0943/3961] Build partitioner, token map when contact_points == cluster Fixes PYTHON-90 --- CHANGELOG.rst | 3 +++ cassandra/cluster.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ad5b3e00b5..4e89c2b422 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,6 +40,9 @@ Bug Fixes use it when generating CREATE TABLE statements (PYTHON-84) * Avoid race condition with AsyncoreConnection that may cause messages to fail to be written until a new message is pushed +* Make sure cluster.metadata.partitioner and cluster.metadata.token_map + are populated when all nodes in the cluster are included in the + contact points (PYTHON-90) 2.0.2 ===== diff --git a/cassandra/cluster.py b/cassandra/cluster.py index fd63f4361d..1453237e39 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1867,7 +1867,10 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, if partitioner and tokens: token_map[host] = tokens - should_rebuild_token_map = force_token_rebuild + # Check metadata.partitioner to see if we haven't built anything yet. If + # every node in the cluster was in the contact points, we won't discover + # any new nodes, so we need this additional check. (See PYTHON-90) + should_rebuild_token_map = force_token_rebuild or self._cluster.metadata.partitioner is None found_hosts = set() for row in peers_result: addr = row.get("rpc_address") From d8e802d6971e610f6d2327efa4e632ed80ed489d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 9 Jul 2014 14:55:14 -0500 Subject: [PATCH 0944/3961] Make MM3 hash match C*'s hash for all values Fixes #147 Fixes PYTHON-89 --- CHANGELOG.rst | 2 + cassandra/murmur3.c | 86 ++++++++++++++++++++----------------- tests/unit/test_metadata.py | 3 ++ 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e89c2b422..dd62713939 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -43,6 +43,8 @@ Bug Fixes * Make sure cluster.metadata.partitioner and cluster.metadata.token_map are populated when all nodes in the cluster are included in the contact points (PYTHON-90) +* Make Murmur3 hash match Cassandra's hash for all values (PYTHON-89, + github #147) 2.0.2 ===== diff --git a/cassandra/murmur3.c b/cassandra/murmur3.c index f5ef0e6575..bdcb97289f 100644 --- a/cassandra/murmur3.c +++ b/cassandra/murmur3.c @@ -31,6 +31,10 @@ typedef unsigned char uint8_t; typedef unsigned long uint32_t; typedef unsigned __int64 uint64_t; +typedef char int8_t; +typedef long int32_t; +typedef __int64 int64_t; + #define FORCE_INLINE __forceinline #include @@ -48,20 +52,22 @@ typedef unsigned __int64 uint64_t; #define FORCE_INLINE inline __attribute__((always_inline)) -inline uint32_t rotl32 ( uint32_t x, int8_t r ) +inline uint32_t rotl32 ( int32_t x, int8_t r ) { - return (x << r) | (x >> (32 - r)); + // cast to unsigned for logical right bitshift (to match C* MM3 implementation) + return (x << r) | ((int32_t) (((uint32_t) x) >> (32 - r))); } -inline uint64_t rotl64 ( uint64_t x, int8_t r ) +inline int64_t rotl64 ( int64_t x, int8_t r ) { - return (x << r) | (x >> (64 - r)); + // cast to unsigned for logical right bitshift (to match C* MM3 implementation) + return (x << r) | ((int64_t) (((uint64_t) x) >> (64 - r))); } -#define ROTL32(x,y) rotl32(x,y) +#define ROTL32(x,y) rotl32(x,y) #define ROTL64(x,y) rotl64(x,y) -#define BIG_CONSTANT(x) (x##LLU) +#define BIG_CONSTANT(x) (x##LL) #endif // !defined(_MSC_VER) @@ -71,7 +77,7 @@ inline uint64_t rotl64 ( uint64_t x, int8_t r ) // TODO 32bit? -FORCE_INLINE uint64_t getblock ( const uint64_t * p, int i ) +FORCE_INLINE int64_t getblock ( const int64_t * p, int i ) { return p[i]; } @@ -79,33 +85,34 @@ FORCE_INLINE uint64_t getblock ( const uint64_t * p, int i ) //----------------------------------------------------------------------------- // Finalization mix - force all bits of a hash block to avalanche -FORCE_INLINE uint64_t fmix ( uint64_t k ) +FORCE_INLINE int64_t fmix ( int64_t k ) { - k ^= k >> 33; + // cast to unsigned for logical right bitshift (to match C* MM3 implementation) + k ^= ((uint64_t) k) >> 33; k *= BIG_CONSTANT(0xff51afd7ed558ccd); - k ^= k >> 33; + k ^= ((uint64_t) k) >> 33; k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); - k ^= k >> 33; + k ^= ((uint64_t) k) >> 33; return k; } -uint64_t MurmurHash3_x64_128 (const void * key, const int len, +int64_t MurmurHash3_x64_128 (const void * key, const int len, const uint32_t seed) { - const uint8_t * data = (const uint8_t*)key; + const int8_t * data = (const int8_t*)key; const int nblocks = len / 16; - uint64_t h1 = seed; - uint64_t h2 = seed; + int64_t h1 = seed; + int64_t h2 = seed; - uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); - uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); - uint64_t k1 = 0; - uint64_t k2 = 0; + int64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); + int64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); + int64_t k1 = 0; + int64_t k2 = 0; - const uint64_t * blocks = (const uint64_t *)(data); - const uint8_t * tail = (const uint8_t*)(data + nblocks*16); + const int64_t * blocks = (const int64_t *)(data); + const int8_t * tail = (const int8_t*)(data + nblocks*16); //---------- // body @@ -113,8 +120,8 @@ uint64_t MurmurHash3_x64_128 (const void * key, const int len, int i; for(i = 0; i < nblocks; i++) { - uint64_t k1 = getblock(blocks,i*2+0); - uint64_t k2 = getblock(blocks,i*2+1); + int64_t k1 = getblock(blocks,i*2+0); + int64_t k2 = getblock(blocks,i*2+1); k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1; @@ -128,26 +135,25 @@ uint64_t MurmurHash3_x64_128 (const void * key, const int len, //---------- // tail - switch(len & 15) { - case 15: k2 ^= (uint64_t)(tail[14]) << 48; - case 14: k2 ^= (uint64_t)(tail[13]) << 40; - case 13: k2 ^= (uint64_t)(tail[12]) << 32; - case 12: k2 ^= (uint64_t)(tail[11]) << 24; - case 11: k2 ^= (uint64_t)(tail[10]) << 16; - case 10: k2 ^= (uint64_t)(tail[ 9]) << 8; - case 9: k2 ^= (uint64_t)(tail[ 8]) << 0; + case 15: k2 ^= ((int64_t) (tail[14])) << 48; + case 14: k2 ^= ((int64_t) (tail[13])) << 40; + case 13: k2 ^= ((int64_t) (tail[12])) << 32; + case 12: k2 ^= ((int64_t) (tail[11])) << 24; + case 11: k2 ^= ((int64_t) (tail[10])) << 16; + case 10: k2 ^= ((int64_t) (tail[ 9])) << 8; + case 9: k2 ^= ((int64_t) (tail[ 8])) << 0; k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2; - case 8: k1 ^= (uint64_t)(tail[ 7]) << 56; - case 7: k1 ^= (uint64_t)(tail[ 6]) << 48; - case 6: k1 ^= (uint64_t)(tail[ 5]) << 40; - case 5: k1 ^= (uint64_t)(tail[ 4]) << 32; - case 4: k1 ^= (uint64_t)(tail[ 3]) << 24; - case 3: k1 ^= (uint64_t)(tail[ 2]) << 16; - case 2: k1 ^= (uint64_t)(tail[ 1]) << 8; - case 1: k1 ^= (uint64_t)(tail[ 0]) << 0; + case 8: k1 ^= ((int64_t) (tail[ 7])) << 56; + case 7: k1 ^= ((int64_t) (tail[ 6])) << 48; + case 6: k1 ^= ((int64_t) (tail[ 5])) << 40; + case 5: k1 ^= ((int64_t) (tail[ 4])) << 32; + case 4: k1 ^= ((int64_t) (tail[ 3])) << 24; + case 3: k1 ^= ((int64_t) (tail[ 2])) << 16; + case 2: k1 ^= ((int64_t) (tail[ 1])) << 8; + case 1: k1 ^= ((int64_t) (tail[ 0])) << 0; k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1; }; @@ -186,7 +192,7 @@ murmur3(PyObject *self, PyObject *args) const char *key; Py_ssize_t len; uint32_t seed = 0; - uint64_t result = 0; + int64_t result = 0; if (!PyArg_ParseTuple(args, "s#|I", &key, &len, &seed)) { diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index 98688aea41..37cc0bf9a0 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -222,6 +222,9 @@ def test_murmur3_tokens(self): try: murmur3_token = Murmur3Token(cassandra.metadata.MIN_LONG - 1) self.assertEqual(murmur3_token.hash_fn('123'), -7468325962851647638) + self.assertEqual(murmur3_token.hash_fn('\x00\xff\x10\xfa\x99' * 10), 5837342703291459765) + self.assertEqual(murmur3_token.hash_fn('\xfe' * 8), -8927430733708461935) + self.assertEqual(murmur3_token.hash_fn('\x10' * 8), 1446172840243228796) self.assertEqual(murmur3_token.hash_fn(str(cassandra.metadata.MAX_LONG)), 7162290910810015547) self.assertEqual(str(murmur3_token), '') except NoMurmur3: From 9be7b48e23944a445965fef6db28c07a9e63277f Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 9 Jul 2014 15:35:30 -0500 Subject: [PATCH 0945/3961] Fix mm3 hash tests under python3 --- tests/unit/test_metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index 37cc0bf9a0..5e1adadae4 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -222,9 +222,9 @@ def test_murmur3_tokens(self): try: murmur3_token = Murmur3Token(cassandra.metadata.MIN_LONG - 1) self.assertEqual(murmur3_token.hash_fn('123'), -7468325962851647638) - self.assertEqual(murmur3_token.hash_fn('\x00\xff\x10\xfa\x99' * 10), 5837342703291459765) - self.assertEqual(murmur3_token.hash_fn('\xfe' * 8), -8927430733708461935) - self.assertEqual(murmur3_token.hash_fn('\x10' * 8), 1446172840243228796) + self.assertEqual(murmur3_token.hash_fn(b'\x00\xff\x10\xfa\x99' * 10), 5837342703291459765) + self.assertEqual(murmur3_token.hash_fn(b'\xfe' * 8), -8927430733708461935) + self.assertEqual(murmur3_token.hash_fn(b'\x10' * 8), 1446172840243228796) self.assertEqual(murmur3_token.hash_fn(str(cassandra.metadata.MAX_LONG)), 7162290910810015547) self.assertEqual(str(murmur3_token), '') except NoMurmur3: From 1ae4b82fac6c7136696f1740d5282a577d5dfc36 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 9 Jul 2014 17:02:59 -0500 Subject: [PATCH 0946/3961] Avoid unnecessary ccm cluster switches in tests --- tests/integration/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index c7e8aadb3b..78a2fac55d 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -37,6 +37,7 @@ CLUSTER_NAME = 'test_cluster' MULTIDC_CLUSTER_NAME = 'multidc_test_cluster' + CCM_CLUSTER = None CASSANDRA_DIR = os.getenv('CASSANDRA_DIR', None) @@ -128,6 +129,11 @@ def setup_package(): def use_multidc(dc_list): + global CCM_CLUSTER + if CCM_CLUSTER.name == MULTIDC_CLUSTER_NAME: + log.debug("Cluster is alread multi-dc, not replacing") + return + teardown_package() try: try: @@ -150,13 +156,17 @@ def use_multidc(dc_list): log.exception("Failed to start ccm cluster:") raise - global CCM_CLUSTER CCM_CLUSTER = cluster setup_test_keyspace() log.debug("Switched to multidc cluster") def use_singledc(): + global CCM_CLUSTER + if CCM_CLUSTER.name == CLUSTER_NAME: + log.debug("Cluster is alread single-dc, not replacing") + return + teardown_package() setup_package() From 25a7acf5f930f2675ae50b31b45914ed6ae7c30d Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 9 Jul 2014 18:29:49 -0500 Subject: [PATCH 0947/3961] Don't reconnect to IGNORED hosts when marked down --- CHANGELOG.rst | 3 +++ cassandra/cluster.py | 3 +++ tests/integration/long/test_loadbalancingpolicies.py | 10 ++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dd62713939..f22563bcf5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,6 +45,9 @@ Bug Fixes contact points (PYTHON-90) * Make Murmur3 hash match Cassandra's hash for all values (PYTHON-89, github #147) +* Don't attempt to reconnect to hosts that should be ignored (according + to the load balancing policy) when a notification is received that the + host is down. 2.0.2 ===== diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 1453237e39..5539b3e637 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -772,6 +772,9 @@ def on_up(self, host): return futures def _start_reconnector(self, host, is_host_addition): + if self.load_balancing_policy.distance(host) == HostDistance.IGNORED: + return + schedule = self.reconnection_policy.new_schedule() # in order to not hold references to this Cluster open and prevent diff --git a/tests/integration/long/test_loadbalancingpolicies.py b/tests/integration/long/test_loadbalancingpolicies.py index 9ccce6de83..93b5818a32 100644 --- a/tests/integration/long/test_loadbalancingpolicies.py +++ b/tests/integration/long/test_loadbalancingpolicies.py @@ -13,6 +13,7 @@ # limitations under the License. import struct +import time from cassandra import ConsistencyLevel, Unavailable from cassandra.cluster import Cluster, NoHostAvailable from cassandra.concurrent import execute_concurrent_with_args @@ -469,9 +470,14 @@ def test_white_list(self): self.coordinator_stats.assert_query_count_equals(self, 2, 12) self.coordinator_stats.assert_query_count_equals(self, 3, 0) + # white list policy should not allow reconnecting to ignored hosts + force_stop(3) + wait_for_down(cluster, 3) + self.assertFalse(cluster.metadata._hosts[IP_FORMAT % 3].is_currently_reconnecting()) + self.coordinator_stats.reset_counts() - decommission(2) - wait_for_down(cluster, 2, wait=True) + force_stop(2) + time.sleep(10) try: self._query(session, keyspace) From d8aa5ff4881f630c857366be753b616ffe5aba2c Mon Sep 17 00:00:00 2001 From: lenolib Date: Thu, 10 Jul 2014 11:29:41 +0200 Subject: [PATCH 0948/3961] Fix lazy_connect, call handle_lazy_connect before checking session existence --- cqlengine/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cqlengine/connection.py b/cqlengine/connection.py index d5f6ed5c5e..5b483e40aa 100644 --- a/cqlengine/connection.py +++ b/cqlengine/connection.py @@ -70,10 +70,11 @@ def setup( def execute(query, params=None, consistency_level=None): + handle_lazy_connect() + if not session: raise CQLEngineException("It is required to setup() cqlengine before executing queries") - handle_lazy_connect() if consistency_level is None: consistency_level = default_consistency_level From 9bd8146f40211ae896e2f0e3c5bc9c78c55d3563 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 11 Jul 2014 14:31:22 -0500 Subject: [PATCH 0949/3961] Add CAS WriteType, avoiding KeyError on CAS write timeout Fixes PYTHON-91 --- cassandra/policies.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cassandra/policies.py b/cassandra/policies.py index 1580eebfaf..d09b2b41e0 100644 --- a/cassandra/policies.py +++ b/cassandra/policies.py @@ -572,12 +572,18 @@ class WriteType(object): internally before a BATCH write. """ + CAS = 5 + """ + A lighweight-transaction write, such as "DELETE ... IF EXISTS". + """ + WriteType.name_to_value = { 'SIMPLE': WriteType.SIMPLE, 'BATCH': WriteType.BATCH, 'UNLOGGED_BATCH': WriteType.UNLOGGED_BATCH, 'COUNTER': WriteType.COUNTER, 'BATCH_LOG': WriteType.BATCH_LOG, + 'CAS': WriteType.CAS } From 72259aab4bb294a8036b911a2b62802d593db766 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 11 Jul 2014 15:35:50 -0500 Subject: [PATCH 0950/3961] Release version 2.1.0b1 --- CHANGELOG.rst | 6 +++--- cassandra/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f22563bcf5..f9cdadf3b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,6 @@ -2.1.0 -===== -In Progress +2.1.0b1 +======= +July 11, 2014 This release adds support for Cassandra 2.1 features, including version 3 of the native protocol. diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 1462df37fe..74e4e81647 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 0, 2, 'post') +__version_info__ = (2, 1, 0, 'b1') __version__ = '.'.join(map(str, __version_info__)) From ec36406dbd7fccf75a3085dc5f78f54fdda082eb Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 11 Jul 2014 15:40:24 -0500 Subject: [PATCH 0951/3961] Correct version for 2.1.0b2 --- cassandra/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 74e4e81647..dbc5535ee0 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 1, 0, 'b1') +__version_info__ = (2, 1, '0b1') __version__ = '.'.join(map(str, __version_info__)) From 4fd8498ca9bda4ee0d6bc481695a23426a6764be Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 11 Jul 2014 15:46:18 -0500 Subject: [PATCH 0952/3961] Make version 2.1.0b1.post --- cassandra/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index dbc5535ee0..c6de6d57cf 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (2, 1, '0b1') +__version_info__ = (2, 1, '0b1', 'post') __version__ = '.'.join(map(str, __version_info__)) From 168d52b5521be4d58ed9c8d5828b5935d9a249bb Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 15 Jul 2014 09:12:14 -0700 Subject: [PATCH 0953/3961] fixed docs --- docs/conf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1c01444817..82a8833b95 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ # General information about the project. project = u'cqlengine' -copyright = u'2012, Blake Eggleston' +copyright = u'2012, Blake Eggleston, Jon Haddad' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -185,7 +185,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'cqlengine.tex', u'cqlengine Documentation', u'Blake Eggleston', 'manual'), + ('index', 'cqlengine.tex', u'cqlengine Documentation', u'Blake Eggleston, Jon Haddad', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -215,7 +215,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'cqlengine', u'cqlengine Documentation', - [u'Blake Eggleston'], 1) + [u'Blake Eggleston, Jon Haddad'], 1) ] # If true, show URL addresses after external links. @@ -229,7 +229,7 @@ # dir menu entry, description, category) texinfo_documents = [ ('index', 'cqlengine', u'cqlengine Documentation', - u'Blake Eggleston', 'cqlengine', 'One line description of project.', + u'Blake Eggleston, Jon Haddad', 'cqlengine', 'One line description of project.', 'Miscellaneous'), ] From 5b3f11b8cebb379bdb5e1b0082e55cf7a807874c Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 15 Jul 2014 14:29:58 -0500 Subject: [PATCH 0954/3961] Add more test and benchmark instructions to README-dev.rst --- README-dev.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README-dev.rst b/README-dev.rst index 5242e5d3d3..1dcd608428 100644 --- a/README-dev.rst +++ b/README-dev.rst @@ -58,6 +58,39 @@ You can run a specific test module or package like so:: python setup.py nosetests -w tests/unit/ +You can run a specific test method like so:: + + python setup.py nosetests -w tests/unit/test_connection.py:ConnectionTest.test_bad_protocol_version + +Seeing Test Logs in Real Time +----------------------------- +Sometimes it's useful to output logs for the tests as they run:: + + python setup.py nosetests -w tests/unit/ --nocapture --nologcapture + +Use tee to capture logs and see them on your terminal:: + + python setup.py nosetests -w tests/unit/ --nocapture --nologcapture 2>&1 | tee test.log + +Specifying a Cassandra Version for Integration Tests +---------------------------------------------------- +You can specify a cassandra version with the ``CASSANDRA_VERSION`` environment variable:: + + CASSANDRA_VERSION=2.0.9 python setup.py nosetests -w tests/integration/standard + +You can also specify a cassandra directory (to test unreleased versions):: + + CASSANDRA_DIR=/home/thobbs/cassandra python setup.py nosetests -w tests/integration/standard + +Specify a Protocol Version for Tests +------------------------------------ +The protocol version defaults to 1 for cassandra 1.2 and 2 otherwise. You can explicitly set +it with the ``PROTOCOL_VERSION`` environment variable:: + + PROTOCOL_VERSION=3 python setup.py nosetests -w tests/integration/standard + +Testing Multiple Python Versions +-------------------------------- If you want to test all of python 2.6, 2.7, and pypy, use tox (this is what TravisCI runs):: @@ -70,3 +103,13 @@ tests should work locally. To run them, edit the following line in tox.ini:: commands = {envpython} setup.py build_ext --inplace nosetests --verbosity=2 tests/unit/ and change ``tests/unit/`` to ``tests/``. + +Running the Benchmarks +====================== +To run the benchmarks, pick one of the files under the ``benchmarks/`` dir and run it:: + + python benchmarks/future_batches.py + +There are a few options. Use ``--help`` to see them all:: + + python benchmarks/future_batches.py --help From ec097f113142358cb928339af81859c39f508592 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 15 Jul 2014 17:10:56 -0500 Subject: [PATCH 0955/3961] Fix UDT column types in CREATE TABLE statements --- CHANGELOG.rst | 8 ++++++++ cassandra/cqltypes.py | 19 +++++++++++++++++-- cassandra/protocol.py | 8 ++++---- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f9cdadf3b6..6ec090abe4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,11 @@ +2.1.0c1 +======= +In Progress + +Bug Fixes +--------- +* Properly specify UDTs for columns in CREATE TABLE statements + 2.1.0b1 ======= July 11, 2014 diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 2de44b00f8..c6b0066d23 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -28,6 +28,7 @@ # .from_cql_literal() and .as_cql_literal() classmethods (or whatever). from __future__ import absolute_import # to enable import io from stdlib +from binascii import unhexlify import calendar from collections import namedtuple from decimal import Decimal @@ -136,6 +137,7 @@ def parse_casstype_args(typestring): elif tok == ')': types, names = args.pop() prev_types, prev_names = args[-1] + prev_types[-1] = prev_types[-1].apply_parameters(types, names) else: types, names = args[-1] @@ -813,13 +815,13 @@ def serialize_safe(cls, val, protocol_version): return buf.getvalue() -class UserDefinedType(TupleType): +class UserType(TupleType): typename = "'org.apache.cassandra.db.marshal.UserType'" _cache = {} @classmethod - def apply_parameters(cls, keyspace, udt_name, names_and_types, mapped_class): + def make_udt_class(cls, keyspace, udt_name, names_and_types, mapped_class): if six.PY2 and isinstance(udt_name, unicode): udt_name = udt_name.encode('utf-8') @@ -831,10 +833,23 @@ def apply_parameters(cls, keyspace, udt_name, names_and_types, mapped_class): 'cassname': cls.cassname, 'typename': udt_name, 'fieldnames': fieldnames, + 'keyspace': keyspace, 'mapped_class': mapped_class}) cls._cache[(keyspace, udt_name)] = instance return instance + @classmethod + def apply_parameters(cls, subtypes, names): + keyspace = subtypes[0] + udt_name = unhexlify(subtypes[1].cassname) + field_names = [unhexlify(encoded_name) for encoded_name in names[2:]] + assert len(field_names) == len(subtypes[2:]) + return type(udt_name, (cls,), {'subtypes': subtypes[2:], + 'cassname': cls.cassname, + 'typename': udt_name, + 'fieldnames': field_names, + 'keyspace': keyspace}) + @classmethod def cql_parameterized_type(cls): return cls.typename diff --git a/cassandra/protocol.py b/cassandra/protocol.py index f6c9f656fa..e995dee76e 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -32,7 +32,7 @@ DoubleType, FloatType, Int32Type, InetAddressType, IntegerType, ListType, LongType, MapType, SetType, TimeUUIDType, - UTF8Type, UUIDType, UserDefinedType, + UTF8Type, UUIDType, UserType, TupleType, lookup_casstype) from cassandra.policies import WriteType @@ -535,7 +535,7 @@ class ResultMessage(_MessageType): 0x0020: ListType, 0x0021: MapType, 0x0022: SetType, - 0x0030: UserDefinedType, + 0x0030: UserType, 0x0031: TupleType, } @@ -633,14 +633,14 @@ def read_type(cls, f, user_type_map): num_items = read_short(f) types = tuple(cls.read_type(f, user_type_map) for _ in xrange(num_items)) typeclass = typeclass.apply_parameters(types) - elif typeclass == UserDefinedType: + elif typeclass == UserType: ks = read_string(f) udt_name = read_string(f) num_fields = read_short(f) names_and_types = tuple((read_string(f), cls.read_type(f, user_type_map)) for _ in xrange(num_fields)) mapped_class = user_type_map.get(ks, {}).get(udt_name) - typeclass = typeclass.apply_parameters( + typeclass = typeclass.make_udt_class( ks, udt_name, names_and_types, mapped_class) elif typeclass == CUSTOM_TYPE: classname = read_string(f) From 1ef8f3331fb5d33d6e35fbbca947ae2109a736cd Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 15 Jul 2014 17:57:16 -0500 Subject: [PATCH 0956/3961] Return list columns as lists instead of tuples --- CHANGELOG.rst | 5 +++++ cassandra/cqltypes.py | 5 +++-- tests/integration/standard/test_types.py | 6 +++--- tests/unit/test_marshalling.py | 4 ++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6ec090abe4..0cc92eaad2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,11 @@ Bug Fixes --------- * Properly specify UDTs for columns in CREATE TABLE statements +Other +----- +* Return list collection columns as python lists instead of tuples + now that tuples are a specific Cassandra type + 2.1.0b1 ======= July 11, 2014 diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index c6b0066d23..b70ada9994 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -714,7 +714,7 @@ def serialize_safe(cls, items, protocol_version): class ListType(_SimpleParameterizedType): typename = 'list' num_subtypes = 1 - adapter = tuple + adapter = list class SetType(_SimpleParameterizedType): @@ -848,7 +848,8 @@ def apply_parameters(cls, subtypes, names): 'cassname': cls.cassname, 'typename': udt_name, 'fieldnames': field_names, - 'keyspace': keyspace}) + 'keyspace': keyspace, + 'mapped_class': None}) @classmethod def cql_parameterized_type(cls): diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 8d11ba1008..0b19c5d143 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -203,7 +203,7 @@ def test_basic_types(self): 1.25, # float "1.2.3.4", # inet 12345, # int - ('a', 'b', 'c'), # list collection + ['a', 'b', 'c'], # list collection sortedset((1, 2, 3)), # set collection {'a': 1, 'b': 2}, # map collection "text", # text @@ -280,11 +280,11 @@ def test_empty_strings_and_nones(self): s.execute("INSERT INTO mytable (a, b, c, o, s, l, n) VALUES ('a', 'b', %s, %s, %s, %s, %s)", ('', '', '', [''], {'': 3})) self.assertEqual( - {'c': '', 'o': '', 's': '', 'l': ('', ), 'n': OrderedDict({'': 3})}, + {'c': '', 'o': '', 's': '', 'l': [''], 'n': OrderedDict({'': 3})}, s.execute("SELECT c, o, s, l, n FROM mytable WHERE a='a' AND b='b'")[0]) self.assertEqual( - {'c': '', 'o': '', 's': '', 'l': ('', ), 'n': OrderedDict({'': 3})}, + {'c': '', 'o': '', 's': '', 'l': [''], 'n': OrderedDict({'': 3})}, s.execute(s.prepare("SELECT c, o, s, l, n FROM mytable WHERE a='a' AND b='b'"), [])[0]) # non-string types shouldn't accept empty strings diff --git a/tests/unit/test_marshalling.py b/tests/unit/test_marshalling.py index 685eaf5632..16299fc289 100644 --- a/tests/unit/test_marshalling.py +++ b/tests/unit/test_marshalling.py @@ -81,9 +81,9 @@ (b'', 'ListType(FloatType)', None), (b'', 'SetType(LongType)', None), (b'\x00\x00', 'MapType(DecimalType, BooleanType)', OrderedDict()), - (b'\x00\x00', 'ListType(FloatType)', ()), + (b'\x00\x00', 'ListType(FloatType)', []), (b'\x00\x00', 'SetType(IntegerType)', sortedset()), - (b'\x00\x01\x00\x10\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0', 'ListType(TimeUUIDType)', (UUID(bytes=b'\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0'),)), + (b'\x00\x01\x00\x10\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0', 'ListType(TimeUUIDType)', [UUID(bytes=b'\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0')]), ) ordered_dict_value = OrderedDict() From 4bf5a41595a90ea1b3cd0673b7f6366ae9753dff Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Tue, 15 Jul 2014 18:04:43 -0500 Subject: [PATCH 0957/3961] Log when using CASSANDRA_DIR for integration tests --- tests/integration/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 78a2fac55d..8e1cb59751 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -24,7 +24,6 @@ log = logging.getLogger(__name__) import os -from six import print_ from threading import Event from cassandra.cluster import Cluster @@ -96,7 +95,10 @@ def get_node(node_id): def setup_package(): - print_('Using Cassandra version: %s' % CASSANDRA_VERSION) + if CASSANDRA_DIR: + log.info("Using Cassandra dir: %s", CASSANDRA_DIR) + else: + log.info('Using Cassandra version: %s', CASSANDRA_VERSION) try: try: cluster = CCMCluster.load(path, CLUSTER_NAME) From e95f74b825841dfe89a61c0b165939b25321e0ef Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Tue, 15 Jul 2014 19:55:50 -0500 Subject: [PATCH 0958/3961] Start building test for PYTHON-96 --- tests/integration/standard/test_metadata.py | 41 ++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/integration/standard/test_metadata.py b/tests/integration/standard/test_metadata.py index 41eef61ba8..2485739943 100644 --- a/tests/integration/standard/test_metadata.py +++ b/tests/integration/standard/test_metadata.py @@ -28,7 +28,7 @@ from cassandra.policies import SimpleConvictionPolicy from cassandra.pool import Host -from tests.integration import get_cluster, PROTOCOL_VERSION +from tests.integration import get_cluster, PROTOCOL_VERSION, CASSANDRA_VERSION class SchemaMetadataTest(unittest.TestCase): @@ -327,6 +327,45 @@ def test_export_keyspace_schema(self): self.assertIsInstance(keyspace_metadata.export_as_string(), six.string_types) self.assertIsInstance(keyspace_metadata.as_cql_query(), six.string_types) + def test_export_keyspace_schema_udts(self): + """ + Test udt exports + """ + + if not CASSANDRA_VERSION.startswith('2.1'): + raise unittest.SkipTest('UDTs only supported in 2.1+') + + cluster = Cluster(protocol_version=PROTOCOL_VERSION) + session = cluster.connect() + + session.execute(""" + CREATE KEYSPACE export_udts + WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} + AND durable_writes = true; + """) + session.execute(""" + CREATE TYPE export_udts.street ( + street_number int, + street_name text) + """) + session.execute(""" + CREATE TYPE export_udts.zip ( + zipcode int, + zip_plus_4 int) + """) + session.execute(""" + CREATE TYPE export_udts.address ( + street_address street, + zip_code zip) + """) + session.execute(""" + CREATE TABLE export_udts.users ( + user text PRIMARY KEY, + addresses map) + """) + + self.assertEqual(cluster.metadata.keyspaces['export_udts'].export_as_string(), 'TODO') + def test_case_sensitivity(self): """ Test that names that need to be escaped in CREATE statements are From 3929b72aecfb1982da0c9748821c0c39fb7169fa Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 15 Jul 2014 20:01:06 -0700 Subject: [PATCH 0959/3961] added clustering_order --- docs/topics/models.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index c7f28f3ee8..c8683159f9 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -81,6 +81,9 @@ Column Options If True, this column is created as partition primary key. There may be many partition keys defined, forming a *composite partition key* + :attr:`~cqlengine.columns.BaseColumn.clustering_order` + ``ASC`` or ``DESC``, determines the clustering order of a clustering key. + :attr:`~cqlengine.columns.BaseColumn.index` If True, an index will be created for this column. Defaults to False. From 93586e48f2e12614dd767ebcd61efb37806eced0 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 15 Jul 2014 20:05:39 -0700 Subject: [PATCH 0960/3961] clustering keys example --- docs/topics/models.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index c8683159f9..fcaf0631c5 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -38,6 +38,18 @@ The Person model would create this CQL table: PRIMARY KEY (id) ) +Here's an example of a comment table created with clustering keys, in descending order: + +.. code-block:: python + + from cqlengine import columns + from cqlengine.models import Model + + class Comment(Model): + photo_id = columns.UUID(primary_key=True) + comment_id = columns.TimeUUID(primary_key=True, clustering_order="DESC") + comment = columns.Text() + Columns ======= From cbc5f205500e1c8353907732164e80fc53e90655 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 15 Jul 2014 20:08:11 -0700 Subject: [PATCH 0961/3961] clustering example --- docs/topics/models.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index fcaf0631c5..a4c9d88be1 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -50,6 +50,17 @@ Here's an example of a comment table created with clustering keys, in descending comment_id = columns.TimeUUID(primary_key=True, clustering_order="DESC") comment = columns.Text() +The Comment model's ``create table`` would look like the following: + +.. code-block:: sql + + CREATE TABLE comment ( + photo_id uuid, + comment_id timeuuid, + comment text, + PRIMARY KEY (photo_id, comment_id) + ) WITH CLUSTERING ORDER BY (comment_id DESC) + Columns ======= From 011dc2508d14b817176ebe856dc0b1f35f0dd54b Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 15 Jul 2014 20:08:34 -0700 Subject: [PATCH 0962/3961] examples, plural --- docs/topics/models.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index a4c9d88be1..aea3be7207 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -12,8 +12,8 @@ Models A model is a python class representing a CQL table. -Example -======= +Examples +======== This example defines a Person table, with the columns ``first_name`` and ``last_name`` From 2d92a702805dd755fb24476413e8b70b12696634 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 15 Jul 2014 20:10:41 -0700 Subject: [PATCH 0963/3961] syncing examples --- docs/topics/models.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index aea3be7207..08d4cfb0be 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -61,6 +61,15 @@ The Comment model's ``create table`` would look like the following: PRIMARY KEY (photo_id, comment_id) ) WITH CLUSTERING ORDER BY (comment_id DESC) +To sync the models to the database, you may do the following: + +`` code-block:: python + + from cqlengine.management import sync_table + sync_table(Person) + sync_table(Comment) + + Columns ======= From b92bf2021bb0c6ae80544d041576487b7bcaa925 Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Tue, 15 Jul 2014 20:11:19 -0700 Subject: [PATCH 0964/3961] fixed code block --- docs/topics/models.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/models.rst b/docs/topics/models.rst index 08d4cfb0be..1602590795 100644 --- a/docs/topics/models.rst +++ b/docs/topics/models.rst @@ -63,12 +63,12 @@ The Comment model's ``create table`` would look like the following: To sync the models to the database, you may do the following: -`` code-block:: python +.. code-block:: python from cqlengine.management import sync_table sync_table(Person) sync_table(Comment) - + Columns ======= From efa680c2f3da62e3c5f19d41de48c0a6b4c2b021 Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Wed, 16 Jul 2014 11:55:33 -0500 Subject: [PATCH 0965/3961] Add table_meta.export_as_string() test inf --- tests/integration/standard/test_metadata.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/standard/test_metadata.py b/tests/integration/standard/test_metadata.py index 2485739943..b46708ca51 100644 --- a/tests/integration/standard/test_metadata.py +++ b/tests/integration/standard/test_metadata.py @@ -366,6 +366,9 @@ def test_export_keyspace_schema_udts(self): self.assertEqual(cluster.metadata.keyspaces['export_udts'].export_as_string(), 'TODO') + table_meta = cluster.metadata.keyspaces['export_udts'].tables['users'] + self.assertEqual(table_meta.export_as_string(), 'TODO') + def test_case_sensitivity(self): """ Test that names that need to be escaped in CREATE statements are From c662a8864fd95146b09924575ccb4e676f17f9dd Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Wed, 16 Jul 2014 12:03:40 -0500 Subject: [PATCH 0966/3961] Use get_server_versions() for version checks --- tests/integration/standard/test_metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/standard/test_metadata.py b/tests/integration/standard/test_metadata.py index b46708ca51..af56497fe5 100644 --- a/tests/integration/standard/test_metadata.py +++ b/tests/integration/standard/test_metadata.py @@ -28,7 +28,7 @@ from cassandra.policies import SimpleConvictionPolicy from cassandra.pool import Host -from tests.integration import get_cluster, PROTOCOL_VERSION, CASSANDRA_VERSION +from tests.integration import get_cluster, PROTOCOL_VERSION, get_server_versions class SchemaMetadataTest(unittest.TestCase): @@ -332,8 +332,8 @@ def test_export_keyspace_schema_udts(self): Test udt exports """ - if not CASSANDRA_VERSION.startswith('2.1'): - raise unittest.SkipTest('UDTs only supported in 2.1+') + if get_server_versions()[0] < (2, 1, 0): + raise unittest.SkipTest('UDTs were introduced in Cassandra 2.1') cluster = Cluster(protocol_version=PROTOCOL_VERSION) session = cluster.connect() From d67699db26d39b81584a2837073fd91dbf10034c Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Wed, 16 Jul 2014 13:06:29 -0500 Subject: [PATCH 0967/3961] Check CASSANDRA_HOME as well during testing --- tests/integration/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 8e1cb59751..061aaa30cb 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -40,6 +40,7 @@ CCM_CLUSTER = None CASSANDRA_DIR = os.getenv('CASSANDRA_DIR', None) +CASSANDRA_HOME = os.getenv('CASSANDRA_HOME', None) CASSANDRA_VERSION = os.getenv('CASSANDRA_VERSION', '2.0.9') if CASSANDRA_VERSION.startswith('1'): @@ -97,6 +98,8 @@ def get_node(node_id): def setup_package(): if CASSANDRA_DIR: log.info("Using Cassandra dir: %s", CASSANDRA_DIR) + elif CASSANDRA_HOME: + log.info("Using Cassandra home: %s", CASSANDRA_HOME) else: log.info('Using Cassandra version: %s', CASSANDRA_VERSION) try: From 3cdde4840052c81fece9984c7eaad6661499d5e8 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 16 Jul 2014 17:59:12 -0500 Subject: [PATCH 0968/3961] Avoid moving retry to next host when request ID == 0 Fixes PYTHON-88 --- CHANGELOG.rst | 1 + cassandra/cluster.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0cc92eaad2..c8a0b24a38 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ In Progress Bug Fixes --------- * Properly specify UDTs for columns in CREATE TABLE statements +* Avoid moving retries to a new host when using request ID zero (PYTHON-88) Other ----- diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 5539b3e637..08e61debfc 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -2558,7 +2558,7 @@ def _retry_task(self, reuse_connection): # to retry the operation return - if reuse_connection and self._query(self._current_host): + if reuse_connection and self._query(self._current_host) is not None: return # otherwise, move onto another host From 12d11fe8e938288d6bf8e954d6110206c3ae3d2e Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Wed, 16 Jul 2014 18:20:45 -0500 Subject: [PATCH 0969/3961] Add UserType.as_cql_query, unit test coverage --- cassandra/metadata.py | 48 +++++++++++++++++++++++++++++++++++++ tests/unit/test_metadata.py | 22 ++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 6aac4aef5e..46ee3a8242 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -607,11 +607,33 @@ def as_cql_query(self): class UserType(object): + """ + A user defined type, as created by ``CREATE TYPE`` statements. + + User-defined types were introduced in Cassandra 2.1. + + .. versionadded:: 2.1.0 + """ keyspace = None + """ + The string name of the keyspace in which this type is defined. + """ + name = None + """ + The name of this type. + """ + field_names = None + """ + An ordered list of the names for each field in this user-defined type. + """ + field_types = None + """ + An ordered list of the types for each field in this user-defined type. + """ def __init__(self, keyspace, name, field_names, field_types): self.keyspace = keyspace @@ -619,6 +641,32 @@ def __init__(self, keyspace, name, field_names, field_types): self.field_names = field_names self.field_types = field_types + def as_cql_query(self, formatted=False): + """ + Returns a CQL query that can be used to recreate this type. + If `formatted` is set to :const:`True`, extra whitespace will + be added to make the query more readable. + """ + ret = "CREATE TYPE %s.%s (%s" % ( + protect_name(self.keyspace), + protect_name(self.name), + "\n" if formatted else "") + + if formatted: + field_join = ",\n" + padding = " " + else: + field_join = ", " + padding = "" + + fields = [] + for field_name, field_type in zip(self.field_names, self.field_types): + fields.append("%s %s" % (protect_name(field_name), field_type.cql_parameterized_type())) + + ret += field_join.join("%s%s" % (padding, field) for field in fields) + ret += "\n)" if formatted else ")" + return ret + class TableMetadata(object): """ diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index 5e1adadae4..dd42040aa2 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -18,11 +18,13 @@ import unittest # noqa import cassandra +from cassandra.cqltypes import IntegerType, AsciiType, TupleType from cassandra.metadata import (Murmur3Token, MD5Token, BytesToken, ReplicationStrategy, NetworkTopologyStrategy, SimpleStrategy, LocalStrategy, NoMurmur3, protect_name, - protect_names, protect_value, is_valid_name) + protect_names, protect_value, is_valid_name, + UserType) from cassandra.policies import SimpleConvictionPolicy from cassandra.pool import Host @@ -248,3 +250,21 @@ def test_bytes_tokens(self): self.fail('Tokens for ByteOrderedPartitioner should be only strings') except TypeError: pass + + +class TestUserTypes(unittest.TestCase): + + def test_as_cql_query(self): + field_types = [IntegerType, AsciiType, TupleType.apply_parameters([IntegerType, AsciiType])] + udt = UserType("ks1", "mytype", ["a", "b", "c"], field_types) + self.assertEqual("CREATE TYPE ks1.mytype (a varint, b ascii, c tuple)", udt.as_cql_query(formatted=False)) + + self.assertEqual("""CREATE TYPE ks1.mytype ( + a varint, + b ascii, + c tuple +)""", udt.as_cql_query(formatted=True)) + + def test_as_cql_query_name_escaping(self): + udt = UserType("MyKeyspace", "MyType", ["AbA", "keyspace"], [AsciiType, AsciiType]) + self.assertEqual('CREATE TYPE "MyKeyspace"."MyType" ("AbA" ascii, "keyspace" ascii)', udt.as_cql_query(formatted=False)) From da5109a20bea2b67f8284ecb84e01304be07d54f Mon Sep 17 00:00:00 2001 From: Usman Masood Date: Thu, 17 Jul 2014 00:04:47 +0000 Subject: [PATCH 0970/3961] Set fetch_size correctly in Statement.__init__ --- cassandra/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/query.py b/cassandra/query.py index 659ce6ecd2..ecdad38844 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -182,7 +182,7 @@ def __init__(self, retry_policy=None, consistency_level=None, routing_key=None, if serial_consistency_level is not None: self.serial_consistency_level = serial_consistency_level if fetch_size is not None: - self.fetch_size = None + self.fetch_size = fetch_size self._routing_key = routing_key def _get_routing_key(self): From d8973e7eca8331b3a28e3f3e3f337db1ab013d74 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 17 Jul 2014 15:57:52 -0500 Subject: [PATCH 0971/3961] Fix disabling paging on a per-statement basis Relates to #151 Fixes PYTHON-93 --- CHANGELOG.rst | 3 + cassandra/cluster.py | 4 +- cassandra/query.py | 16 ++-- .../integration/standard/test_query_paging.py | 78 ++++++++++++++++++- 4 files changed, 93 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c8a0b24a38..3208304193 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,9 @@ Bug Fixes --------- * Properly specify UDTs for columns in CREATE TABLE statements * Avoid moving retries to a new host when using request ID zero (PYTHON-88) +* Don't ignore fetch_size arguments to Statement constructors (github-151) +* Allow disabling automatic paging on a per-statement basis when it's + enabled by default for the session (PYTHON-93) Other ----- diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 08e61debfc..5747fa2fc2 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -65,7 +65,7 @@ NoConnectionsAvailable) from cassandra.query import (SimpleStatement, PreparedStatement, BoundStatement, BatchStatement, bind_params, QueryTrace, Statement, - named_tuple_factory, dict_factory) + named_tuple_factory, dict_factory, FETCH_SIZE_UNSET) # default to gevent when we are monkey patched, otherwise if libev is available, use that as the # default because it's fastest. Otherwise, use asyncore. @@ -1246,7 +1246,7 @@ def _create_response_future(self, query, parameters, trace): cl = query.consistency_level if query.consistency_level is not None else self.default_consistency_level fetch_size = query.fetch_size - if not fetch_size and self._protocol_version >= 2: + if fetch_size is FETCH_SIZE_UNSET and self._protocol_version >= 2: fetch_size = self.default_fetch_size if self._protocol_version >= 3 and self.use_client_timestamp: diff --git a/cassandra/query.py b/cassandra/query.py index ecdad38844..a79a2c263b 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -133,6 +133,9 @@ def ordered_dict_factory(colnames, rows): return [OrderedDict(zip(colnames, row)) for row in rows] +FETCH_SIZE_UNSET = object() + + class Statement(object): """ An abstract class representing a single query. There are three subclasses: @@ -160,7 +163,7 @@ class Statement(object): the Session this is executed in will be used. """ - fetch_size = None + fetch_size = FETCH_SIZE_UNSET """ How many rows will be fetched at a time. This overrides the default of :attr:`.Session.default_fetch_size` @@ -175,13 +178,13 @@ class Statement(object): _routing_key = None def __init__(self, retry_policy=None, consistency_level=None, routing_key=None, - serial_consistency_level=None, fetch_size=None): + serial_consistency_level=None, fetch_size=FETCH_SIZE_UNSET): self.retry_policy = retry_policy if consistency_level is not None: self.consistency_level = consistency_level if serial_consistency_level is not None: self.serial_consistency_level = serial_consistency_level - if fetch_size is not None: + if fetch_size is not FETCH_SIZE_UNSET: self.fetch_size = fetch_size self._routing_key = routing_key @@ -315,9 +318,11 @@ class PreparedStatement(object): _protocol_version = None + fetch_size = FETCH_SIZE_UNSET + def __init__(self, column_metadata, query_id, routing_key_indexes, query, keyspace, protocol_version, consistency_level=None, serial_consistency_level=None, - fetch_size=None): + fetch_size=FETCH_SIZE_UNSET): self.column_metadata = column_metadata self.query_id = query_id self.routing_key_indexes = routing_key_indexes @@ -326,7 +331,8 @@ def __init__(self, column_metadata, query_id, routing_key_indexes, query, keyspa self._protocol_version = protocol_version self.consistency_level = consistency_level self.serial_consistency_level = serial_consistency_level - self.fetch_size = fetch_size + if fetch_size is not FETCH_SIZE_UNSET: + self.fetch_size = fetch_size @classmethod def from_message(cls, query_id, column_metadata, cluster_metadata, query, keyspace, protocol_version): diff --git a/tests/integration/standard/test_query_paging.py b/tests/integration/standard/test_query_paging.py index 09438abcf4..448daf0c02 100644 --- a/tests/integration/standard/test_query_paging.py +++ b/tests/integration/standard/test_query_paging.py @@ -26,7 +26,7 @@ from six.moves import range from threading import Event -from cassandra.cluster import Cluster +from cassandra.cluster import Cluster, PagedResult from cassandra.concurrent import execute_concurrent, execute_concurrent_with_args from cassandra.policies import HostDistance from cassandra.query import SimpleStatement @@ -281,3 +281,79 @@ def test_concurrent_with_paging(self): for (success, result) in results: self.assertTrue(success) self.assertEquals(100, len(list(result))) + + def test_fetch_size(self): + """ + Ensure per-statement fetch_sizes override the default fetch size. + """ + statements_and_params = zip(cycle(["INSERT INTO test3rf.test (k, v) VALUES (%s, 0)"]), + [(i, ) for i in range(100)]) + execute_concurrent(self.session, list(statements_and_params)) + + prepared = self.session.prepare("SELECT * FROM test3rf.test") + + self.session.default_fetch_size = 10 + result = self.session.execute(prepared, []) + self.assertIsInstance(result, PagedResult) + + self.session.default_fetch_size = 2000 + result = self.session.execute(prepared, []) + self.assertIsInstance(result, list) + + self.session.default_fetch_size = None + result = self.session.execute(prepared, []) + self.assertIsInstance(result, list) + + self.session.default_fetch_size = 10 + + prepared.fetch_size = 2000 + result = self.session.execute(prepared, []) + self.assertIsInstance(result, list) + + prepared.fetch_size = None + result = self.session.execute(prepared, []) + self.assertIsInstance(result, list) + + prepared.fetch_size = 10 + result = self.session.execute(prepared, []) + self.assertIsInstance(result, PagedResult) + + prepared.fetch_size = 2000 + bound = prepared.bind([]) + result = self.session.execute(bound, []) + self.assertIsInstance(result, list) + + prepared.fetch_size = None + bound = prepared.bind([]) + result = self.session.execute(bound, []) + self.assertIsInstance(result, list) + + prepared.fetch_size = 10 + bound = prepared.bind([]) + result = self.session.execute(bound, []) + self.assertIsInstance(result, PagedResult) + + bound.fetch_size = 2000 + result = self.session.execute(bound, []) + self.assertIsInstance(result, list) + + bound.fetch_size = None + result = self.session.execute(bound, []) + self.assertIsInstance(result, list) + + bound.fetch_size = 10 + result = self.session.execute(bound, []) + self.assertIsInstance(result, PagedResult) + + s = SimpleStatement("SELECT * FROM test3rf.test", fetch_size=None) + result = self.session.execute(s, []) + self.assertIsInstance(result, list) + + s = SimpleStatement("SELECT * FROM test3rf.test") + result = self.session.execute(s, []) + self.assertIsInstance(result, PagedResult) + + s = SimpleStatement("SELECT * FROM test3rf.test") + s.fetch_size = None + result = self.session.execute(s, []) + self.assertIsInstance(result, list) From 1e99ac643802e029a504abd5793c2e0b5b18182b Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Thu, 17 Jul 2014 18:24:24 -0500 Subject: [PATCH 0972/3961] Add a few extra tests for tuples --- tests/integration/standard/test_types.py | 71 ++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 0b19c5d143..78919086bb 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from tests.integration.datatype_utils import get_sample, DATA_TYPE_PRIMITIVES try: import unittest2 as unittest @@ -428,14 +429,76 @@ def test_tuple_type(self): result = s.execute("SELECT b FROM mytable WHERE a=1")[0] self.assertEqual(partial_result, result.b) + subpartial = ('zoo',) + subpartial_result = subpartial + (None, None) + s.execute("INSERT INTO mytable (a, b) VALUES (2, %s)", parameters=(subpartial,)) + result = s.execute("SELECT b FROM mytable WHERE a=2")[0] + self.assertEqual(subpartial_result, result.b) + # test prepared statement prepared = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") - s.execute(prepared, parameters=(2, complete)) - s.execute(prepared, parameters=(3, partial)) + s.execute(prepared, parameters=(3, complete)) + s.execute(prepared, parameters=(4, partial)) + s.execute(prepared, parameters=(5, subpartial)) prepared = s.prepare("SELECT b FROM mytable WHERE a=?") - self.assertEqual(complete, s.execute(prepared, (2,))[0].b) - self.assertEqual(partial_result, s.execute(prepared, (3,))[0].b) + self.assertEqual(complete, s.execute(prepared, (3,))[0].b) + self.assertEqual(partial_result, s.execute(prepared, (4,))[0].b) + self.assertEqual(subpartial_result, s.execute(prepared, (5,))[0].b) + + def test_tuple_type_varying_lengths(self): + if self._cass_version < (2, 1, 0): + raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") + + MAX_LENGTH = 384 + + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + s.row_factory = dict_factory + s.encoders[tuple] = cql_encode_tuple + + s.execute("""CREATE KEYSPACE test_tuple_type_varying_lengths + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'}""") + s.set_keyspace("test_tuple_type_varying_lengths") + + value_schema = [] + for i in range(1, MAX_LENGTH): + value_schema += [' v_%s tuple<%s>' % (i, ','.join(['int'] * i))] + s.execute("CREATE TABLE mytable (k int PRIMARY KEY, %s)", (','.join(value_schema),)) + + for i in range(1, MAX_LENGTH): + created_tuple = tuple(range(0, i)) + + s.execute("INSERT INTO mytable (k, v_%s) VALUES (0, %s)", (i, created_tuple)) + + result = s.execute("SELECT v_%s FROM mytable WHERE k=0", (i,))[0] + self.assertEqual(tuple(created_tuple), result['v_%s' % i]) + + def test_tuple_types(self): + if self._cass_version < (2, 1, 0): + raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") + + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + s.encoders[tuple] = cql_encode_tuple + + s.execute("""CREATE KEYSPACE test_tuple_types + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'}""") + s.set_keyspace("test_tuple_types") + + s.execute("CREATE TABLE mytable (" + "k int PRIMARY KEY, " + "v tuple<%s>)" % ','.join(DATA_TYPE_PRIMITIVES)) + + for i in range(len(DATA_TYPE_PRIMITIVES)): + created_tuple = [get_sample(DATA_TYPE_PRIMITIVES[j]) for j in range(i + 1)] + response_tuple = tuple(created_tuple + [None for j in range(len(DATA_TYPE_PRIMITIVES) - i - 1)]) + written_tuple = tuple(created_tuple) + + s.execute("INSERT INTO mytable (k, v) VALUES (%s, %s)", (i, written_tuple)) + + result = s.execute("SELECT v FROM mytable WHERE k=%s", (i,))[0] + self.assertEqual(response_tuple, result.v) def test_unicode_query_string(self): c = Cluster(protocol_version=PROTOCOL_VERSION) From 2532e4ffb9644ec687db647155eac463ee058a4e Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Thu, 17 Jul 2014 18:26:42 -0500 Subject: [PATCH 0973/3961] Added datatype_utils file --- tests/integration/datatype_utils.py | 106 ++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/integration/datatype_utils.py diff --git a/tests/integration/datatype_utils.py b/tests/integration/datatype_utils.py new file mode 100644 index 0000000000..66e6f9de28 --- /dev/null +++ b/tests/integration/datatype_utils.py @@ -0,0 +1,106 @@ +# Copyright 2013-2014 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from decimal import Decimal +import datetime +from uuid import UUID +import pytz + +DATA_TYPE_PRIMITIVES = [ + 'ascii', + 'bigint', + 'blob', + 'boolean', + # 'counter', counters are not allowed inside tuples + 'decimal', + 'double', + 'float', + 'inet', + 'int', + 'text', + 'timestamp', + 'timeuuid', + 'uuid', + 'varchar', + 'varint', +] + +DATA_TYPE_NON_PRIMITIVE_NAMES = [ + 'list', + 'set', + 'map', +] + + +def get_sample_data(): + sample_data = {} + + for datatype in DATA_TYPE_PRIMITIVES: + if datatype == 'ascii': + sample_data[datatype] = 'ascii' + + elif datatype == 'bigint': + sample_data[datatype] = 2 ** 63 - 1 + + elif datatype == 'blob': + sample_data[datatype] = bytearray(b'hello world') + + elif datatype == 'boolean': + sample_data[datatype] = True + + elif datatype == 'counter': + # Not supported in an insert statement + pass + + elif datatype == 'decimal': + sample_data[datatype] = Decimal('12.3E+7') + + elif datatype == 'double': + sample_data[datatype] = 1.23E+8 + + elif datatype == 'float': + sample_data[datatype] = 3.4028234663852886e+38 + + elif datatype == 'inet': + sample_data[datatype] = '123.123.123.123' + + elif datatype == 'int': + sample_data[datatype] = 2147483647 + + elif datatype == 'text': + sample_data[datatype] = 'text' + + elif datatype == 'timestamp': + sample_data[datatype] = datetime.datetime.fromtimestamp(872835240, tz=pytz.timezone('America/New_York')).astimezone(pytz.UTC).replace(tzinfo=None) + + elif datatype == 'timeuuid': + sample_data[datatype] = UUID('FE2B4360-28C6-11E2-81C1-0800200C9A66') + + elif datatype == 'uuid': + sample_data[datatype] = UUID('067e6162-3b6f-4ae2-a171-2470b63dff00') + + elif datatype == 'varchar': + sample_data[datatype] = 'varchar' + + elif datatype == 'varint': + sample_data[datatype] = int(str(2147483647) + '000') + + else: + raise Exception('Missing handling of %s.' % datatype) + + return sample_data + +SAMPLE_DATA = get_sample_data() + +def get_sample(datatype): + return SAMPLE_DATA[datatype] From 944c35de4bcd11108b0271854b326bb216713639 Mon Sep 17 00:00:00 2001 From: Kishan Karunaratne Date: Thu, 17 Jul 2014 18:20:11 -0700 Subject: [PATCH 0974/3961] work in progress with UDT tests --- tests/integration/standard/test_udts.py | 100 +++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/tests/integration/standard/test_udts.py b/tests/integration/standard/test_udts.py index 4bf7f9bac0..56ea1970d5 100644 --- a/tests/integration/standard/test_udts.py +++ b/tests/integration/standard/test_udts.py @@ -25,11 +25,12 @@ from cassandra.cluster import Cluster from tests.integration import get_server_versions, PROTOCOL_VERSION - +from tests.integration.long.datatype_utils import get_sample class TypeTests(unittest.TestCase): def setUp(self): + PROTOCOL_VERSION = 3 if PROTOCOL_VERSION < 3: raise unittest.SkipTest("v3 protocol is required for UDT tests") @@ -227,3 +228,100 @@ def test_prepared_registered_udts(self): self.assertTrue(type(row.b) is User) c.shutdown() + + create_datatype_types = """ + CREATE TYPE alldatatypes ( + a ascii, + b bigint, + c blob, + d boolean, + e decimal, + f double, + g float, + h inet, + i int, + j text, + k timestamp, + l timeuuid, + m uuid, + n varchar, + o varint, + ) + """ + + def test_datatypes(self): + c = Cluster(protocol_version=PROTOCOL_VERSION) + s = c.connect() + + #create keyspace + s.execute(""" + CREATE KEYSPACE datatypetests + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'} + """) + s.set_keyspace("datatypetests") + + #create UDT's + s.execute(self.create_datatype_types) + AllDataTypes = namedtuple('alldatatypes', ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o')) + + s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b alldatatypes)") + + params = ( + get_sample('ascii'), + get_sample('bigint'), + get_sample('blob'), + get_sample('boolean'), + get_sample('decimal'), + get_sample('double'), + get_sample('float'), + get_sample('inet'), + get_sample('int'), + get_sample('text'), + get_sample('timestamp'), + get_sample('timeuuid'), + get_sample('uuid'), + get_sample('varchar'), + get_sample('varint') + ) + + #try with regular statement + s.execute(""" + INSERT INTO mytable (a, b) + VALUES (%s, %s) + """, (0, AllDataTypes(params))) + + results = s.execute("SELECT * FROM mytable") + self.assertEqual(1, len(result)) + + row = result[0] + for expected, actual in zip(params, results[0]): + self.assertEqual(expected, actual) + + #retry with prepared statement + prepared = s.prepare(""" + INSERT INTO mytable (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """) + + s.execute(prepared, (0, AllDataTypes(get_sample('ascii'), get_sample('bigint'), get_sample('blob'), get_sample('boolean'), + get_sample('decimal'), get_sample('double'), get_sample('float'), get_sample('inet'), + get_sample('int'), get_sample('text'), get_sample('timestamp'), get_sample('timeuuid'), + get_sample('uuid'), get_sample('varchar'), get_sample('varint')))) + + + + + + + + + + + + + + + + + + c.shutdown() \ No newline at end of file From 66d67a9758e80ab1a80b3cf49af495093400a58a Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 17 Jul 2014 21:45:55 -0500 Subject: [PATCH 0975/3961] Minor tuple test adjustments --- tests/integration/standard/test_types.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 78919086bb..14c00a46bc 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -88,8 +88,8 @@ def test_blob_type_as_string(self): s.execute(query, params) expected_vals = [ - 'key1', - bytearray(b'blobyblob') + 'key1', + bytearray(b'blobyblob') ] results = s.execute("SELECT * FROM mytable") @@ -450,8 +450,6 @@ def test_tuple_type_varying_lengths(self): if self._cass_version < (2, 1, 0): raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") - MAX_LENGTH = 384 - c = Cluster(protocol_version=PROTOCOL_VERSION) s = c.connect() s.row_factory = dict_factory @@ -461,12 +459,13 @@ def test_tuple_type_varying_lengths(self): WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'}""") s.set_keyspace("test_tuple_type_varying_lengths") + lengths = (1, 2, 3, 384) value_schema = [] - for i in range(1, MAX_LENGTH): - value_schema += [' v_%s tuple<%s>' % (i, ','.join(['int'] * i))] - s.execute("CREATE TABLE mytable (k int PRIMARY KEY, %s)", (','.join(value_schema),)) + for i in lengths: + value_schema += [' v_%s tuple<%s>' % (i, ', '.join(['int'] * i))] + s.execute("CREATE TABLE mytable (k int PRIMARY KEY, %s)" % (', '.join(value_schema),)) - for i in range(1, MAX_LENGTH): + for i in lengths: created_tuple = tuple(range(0, i)) s.execute("INSERT INTO mytable (k, v_%s) VALUES (0, %s)", (i, created_tuple)) @@ -474,7 +473,7 @@ def test_tuple_type_varying_lengths(self): result = s.execute("SELECT v_%s FROM mytable WHERE k=0", (i,))[0] self.assertEqual(tuple(created_tuple), result['v_%s' % i]) - def test_tuple_types(self): + def test_tuple_subtypes(self): if self._cass_version < (2, 1, 0): raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") From 4b946a7074e28d95202202f2327b7c8f7dbd14bb Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 18 Jul 2014 11:02:48 -0500 Subject: [PATCH 0976/3961] Fix typo in getting started guide Fixes #154 --- docs/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 8b33b3e807..d82098faa5 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -137,7 +137,7 @@ when you execute: """ INSERT INTO users (name, credits, user_id) VALUES (%s, %s, %s) - """ + """, ("John O'Reilly", 42, uuid.uuid1()) ) From 09f737b2048f4a150f05ebfe3bc88cbdb9d3642e Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 18 Jul 2014 11:50:25 -0500 Subject: [PATCH 0977/3961] Docs and example for Cluster.register_user_type --- cassandra/cluster.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 5747fa2fc2..c8996c967a 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -467,6 +467,53 @@ def __init__(self, self, self.control_connection_timeout) def register_user_type(self, keyspace, user_type, klass): + """ + Registers a class to use to represent a particular user-defined type. + Query parameters for this user-defined type will be assumed to be + instances of `klass`. Result sets for this user-defined type will + be instances of `klass`. If no class is registered for a user-defined + type, a namedtuple will be used for result sets, and non-prepared + statements may not encode parameters for this type correctly. + + `keyspace` is the name of the keyspace that the UDT is defined in. + + `user_type` is the string name of the UDT to register the mapping + for. + + `klass` should be a class with attributes whose names match the + fields of the user-defined type. The constructor must accepts kwargs + for each of the fields in the UDT. + + This method should only be called after the type has been created + within Cassandra. + + Example:: + + cluster = Cluster(protocol_version=3) + session = cluster.connect() + session.set_keyspace('mykeyspace') + session.execute("CREATE TYPE address (street text, zipcode int)") + session.execute("CREATE TABLE users (id int PRIMARY KEY, location address)") + + # create a class to map to the "address" UDT + class Address(object): + + def __init__(self, street, zipcode): + self.street = street + self.zipcode = zipcode + + cluster.register_user_type('mykeyspace', 'address', Address) + + # insert a row using an instance of Address + session.execute("INSERT INTO users (id, location) VALUES (%s, %s)", + (0, Address("123 Main St.", 78723))) + + # results will include Address instances + results = session.execute("SELECT * FROM users") + row = results[0] + print row.id, row.location.street, row.location.zipcode + + """ self._user_types[keyspace][user_type] = klass for session in self.sessions: session.user_type_registered(keyspace, user_type, klass) From 1dd1bce344a1c043a7708d311903de413120b88f Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 18 Jul 2014 11:57:38 -0500 Subject: [PATCH 0978/3961] Update API doc methods --- docs/api/cassandra/cluster.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api/cassandra/cluster.rst b/docs/api/cassandra/cluster.rst index 0ddf705f7e..50fc36eabe 100644 --- a/docs/api/cassandra/cluster.rst +++ b/docs/api/cassandra/cluster.rst @@ -43,6 +43,8 @@ .. automethod:: shutdown + .. automethod:: register_user_type + .. automethod:: register_listener .. automethod:: unregister_listener @@ -63,6 +65,10 @@ .. autoattribute:: default_fetch_size + .. autoattribute:: use_client_timestamp + + .. autoattribute:: encoders + .. automethod:: execute(statement[, parameters][, timeout][, trace]) .. automethod:: execute_async(statement[, parameters][, trace]) From e38f8580d1b691ddd0c561d54c209abc221e58f3 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 18 Jul 2014 12:36:54 -0500 Subject: [PATCH 0979/3961] Friendlier exception when user type does not exist --- cassandra/cluster.py | 19 ++++++++++++++++++- docs/api/cassandra/cluster.rst | 2 ++ tests/integration/standard/test_udts.py | 9 ++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index c8996c967a..b0609061bb 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1551,7 +1551,17 @@ def user_type_registered(self, keyspace, user_type, klass): mapping from a user-defined type to a class. Intended for internal use only. """ - type_meta = self.cluster.metadata.keyspaces[keyspace].user_types[user_type] + try: + ks_meta = self.cluster.metadata.keyspaces[keyspace] + except KeyError: + raise UserTypeDoesNotExist( + 'Keyspace %s does not exist or has not been discovered by the driver' % (keyspace,)) + + try: + type_meta = ks_meta.user_types[user_type] + except KeyError: + raise UserTypeDoesNotExist( + 'User type %s does not exist in keyspace %s' % (user_type, keyspace)) def encode(val): return '{ %s }' % ' , '.join('%s : %s' % ( @@ -1570,6 +1580,13 @@ def get_pool_state(self): return dict((host, pool.get_state()) for host, pool in self._pools.items()) +class UserTypeDoesNotExist(Exception): + """ + An attempt was made to use a user-defined type that does not exist. + """ + pass + + class _ControlReconnectionHandler(_ReconnectionHandler): """ Internal diff --git a/docs/api/cassandra/cluster.rst b/docs/api/cassandra/cluster.rst index 50fc36eabe..da28eef013 100644 --- a/docs/api/cassandra/cluster.rst +++ b/docs/api/cassandra/cluster.rst @@ -104,3 +104,5 @@ .. autoexception:: NoHostAvailable () :members: + +.. autoexception:: UserTypeDoesNotExist () diff --git a/tests/integration/standard/test_udts.py b/tests/integration/standard/test_udts.py index 4bf7f9bac0..012a7fc7df 100644 --- a/tests/integration/standard/test_udts.py +++ b/tests/integration/standard/test_udts.py @@ -22,7 +22,7 @@ from collections import namedtuple -from cassandra.cluster import Cluster +from cassandra.cluster import Cluster, UserTypeDoesNotExist from tests.integration import get_server_versions, PROTOCOL_VERSION @@ -227,3 +227,10 @@ def test_prepared_registered_udts(self): self.assertTrue(type(row.b) is User) c.shutdown() + + def test_non_existing_types(self): + c = Cluster(protocol_version=PROTOCOL_VERSION) + c.connect() + User = namedtuple('user', ('age', 'name')) + self.assertRaises(UserTypeDoesNotExist, c.register_user_type, "some_bad_keyspace", "user", User) + self.assertRaises(UserTypeDoesNotExist, c.register_user_type, "system", "user", User) From 45a3a04458c5aa08ebdfbd2467e0835d527294ef Mon Sep 17 00:00:00 2001 From: Kishan Karunaratne Date: Fri, 18 Jul 2014 13:20:53 -0700 Subject: [PATCH 0980/3961] implemented udt datatype tests --- tests/integration/standard/test_udts.py | 106 ++++++++++-------------- 1 file changed, 45 insertions(+), 61 deletions(-) diff --git a/tests/integration/standard/test_udts.py b/tests/integration/standard/test_udts.py index 56ea1970d5..22c4a81a16 100644 --- a/tests/integration/standard/test_udts.py +++ b/tests/integration/standard/test_udts.py @@ -229,43 +229,41 @@ def test_prepared_registered_udts(self): c.shutdown() - create_datatype_types = """ - CREATE TYPE alldatatypes ( - a ascii, - b bigint, - c blob, - d boolean, - e decimal, - f double, - g float, - h inet, - i int, - j text, - k timestamp, - l timeuuid, - m uuid, - n varchar, - o varint, - ) - """ - def test_datatypes(self): c = Cluster(protocol_version=PROTOCOL_VERSION) s = c.connect() - #create keyspace + # create keyspace s.execute(""" CREATE KEYSPACE datatypetests - WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'} + WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } """) s.set_keyspace("datatypetests") - #create UDT's - s.execute(self.create_datatype_types) - AllDataTypes = namedtuple('alldatatypes', ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o')) + # create UDT's + s.execute(""" + CREATE TYPE alldatatypes (a ascii, + b bigint, + c blob, + d boolean, + e decimal, + f double, + g float, + h inet, + i int, + j text, + k timestamp, + l timeuuid, + m uuid, + n varchar, + o varint, + ) + """) s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b alldatatypes)") + Alldatatypes = namedtuple('alldatatypes', ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o')) + # insert UDT data params = ( get_sample('ascii'), get_sample('bigint'), @@ -284,44 +282,30 @@ def test_datatypes(self): get_sample('varint') ) - #try with regular statement - s.execute(""" - INSERT INTO mytable (a, b) - VALUES (%s, %s) - """, (0, AllDataTypes(params))) - + insert = s.prepare("INSERT INTO mytable (a, b) VALUES (?, ?)") + s.execute(insert, (0, Alldatatypes(params[0], + params[1], + params[2], + params[3], + params[4], + params[5], + params[6], + params[7], + params[8], + params[9], + params[10], + params[11], + params[12], + params[13], + params[14]))) + + # retrieve and verify data results = s.execute("SELECT * FROM mytable") - self.assertEqual(1, len(result)) + self.assertEqual(1, len(results)) - row = result[0] - for expected, actual in zip(params, results[0]): + row = results[0][1] + print row + for expected, actual in zip(params, row): self.assertEqual(expected, actual) - #retry with prepared statement - prepared = s.prepare(""" - INSERT INTO mytable (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """) - - s.execute(prepared, (0, AllDataTypes(get_sample('ascii'), get_sample('bigint'), get_sample('blob'), get_sample('boolean'), - get_sample('decimal'), get_sample('double'), get_sample('float'), get_sample('inet'), - get_sample('int'), get_sample('text'), get_sample('timestamp'), get_sample('timeuuid'), - get_sample('uuid'), get_sample('varchar'), get_sample('varint')))) - - - - - - - - - - - - - - - - - c.shutdown() \ No newline at end of file From 1e17e70892dd0b4f88736b99156d4e165074cd54 Mon Sep 17 00:00:00 2001 From: Joaquin Casares Date: Fri, 18 Jul 2014 15:22:13 -0500 Subject: [PATCH 0981/3961] Add comments for recently submitted tests --- tests/integration/datatype_utils.py | 9 +++++++++ tests/integration/standard/test_types.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/integration/datatype_utils.py b/tests/integration/datatype_utils.py index 66e6f9de28..ce55efcdb7 100644 --- a/tests/integration/datatype_utils.py +++ b/tests/integration/datatype_utils.py @@ -39,10 +39,15 @@ 'list', 'set', 'map', + 'tuple' ] def get_sample_data(): + """ + Create a standard set of sample inputs for testing. + """ + sample_data = {} for datatype in DATA_TYPE_PRIMITIVES: @@ -103,4 +108,8 @@ def get_sample_data(): SAMPLE_DATA = get_sample_data() def get_sample(datatype): + """ + Helper method to access created sample data + """ + return SAMPLE_DATA[datatype] diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 14c00a46bc..78d2c1b5b2 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -405,11 +405,17 @@ def test_timezone_aware_datetimes(self): self.assertEqual(dt.utctimetuple(), result.utctimetuple()) def test_tuple_type(self): + """ + Basic test of tuple functionality + """ + if self._cass_version < (2, 1, 0): raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") c = Cluster(protocol_version=PROTOCOL_VERSION) s = c.connect() + + # use this encoder in order to insert tuples s.encoders[tuple] = cql_encode_tuple s.execute("""CREATE KEYSPACE test_tuple_type @@ -429,6 +435,7 @@ def test_tuple_type(self): result = s.execute("SELECT b FROM mytable WHERE a=1")[0] self.assertEqual(partial_result, result.b) + # test single value tuples subpartial = ('zoo',) subpartial_result = subpartial + (None, None) s.execute("INSERT INTO mytable (a, b) VALUES (2, %s)", parameters=(subpartial,)) @@ -447,11 +454,19 @@ def test_tuple_type(self): self.assertEqual(subpartial_result, s.execute(prepared, (5,))[0].b) def test_tuple_type_varying_lengths(self): + """ + Test tuple types of lengths of 1, 2, 3, and 384 to ensure edge cases work + as expected. + """ + if self._cass_version < (2, 1, 0): raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") c = Cluster(protocol_version=PROTOCOL_VERSION) s = c.connect() + + # set the row_factory to dict_factory for programmatic access + # set the encoder for tuples for the ability to write tuples s.row_factory = dict_factory s.encoders[tuple] = cql_encode_tuple @@ -459,12 +474,15 @@ def test_tuple_type_varying_lengths(self): WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'}""") s.set_keyspace("test_tuple_type_varying_lengths") + # programmatically create the table with tuples of said sizes lengths = (1, 2, 3, 384) value_schema = [] for i in lengths: value_schema += [' v_%s tuple<%s>' % (i, ', '.join(['int'] * i))] s.execute("CREATE TABLE mytable (k int PRIMARY KEY, %s)" % (', '.join(value_schema),)) + # insert tuples into same key using different columns + # and verify the results for i in lengths: created_tuple = tuple(range(0, i)) @@ -474,6 +492,10 @@ def test_tuple_type_varying_lengths(self): self.assertEqual(tuple(created_tuple), result['v_%s' % i]) def test_tuple_subtypes(self): + """ + Ensure tuple subtypes are appropriately handled. + """ + if self._cass_version < (2, 1, 0): raise unittest.SkipTest("The tuple type was introduced in Cassandra 2.1") @@ -490,6 +512,8 @@ def test_tuple_subtypes(self): "v tuple<%s>)" % ','.join(DATA_TYPE_PRIMITIVES)) for i in range(len(DATA_TYPE_PRIMITIVES)): + # create tuples to be written and ensure they match with the expected response + # responses have trailing None values for every element that has not been written created_tuple = [get_sample(DATA_TYPE_PRIMITIVES[j]) for j in range(i + 1)] response_tuple = tuple(created_tuple + [None for j in range(len(DATA_TYPE_PRIMITIVES) - i - 1)]) written_tuple = tuple(created_tuple) From b80387e32e06d1794f59150f30d80cad8f552eda Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 18 Jul 2014 15:27:31 -0500 Subject: [PATCH 0982/3961] Docs for cassandra.encoder --- cassandra/encoder.py | 55 ++++++++++++++++++++++++++++++++++ docs/api/cassandra/encoder.rst | 43 ++++++++++++++++++++++++++ docs/api/cassandra/metrics.rst | 1 - docs/api/index.rst | 1 + 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 docs/api/cassandra/encoder.rst diff --git a/cassandra/encoder.py b/cassandra/encoder.py index 7c0afd66c5..6058d5dadf 100644 --- a/cassandra/encoder.py +++ b/cassandra/encoder.py @@ -11,6 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +These functions are used to convert Python objects into CQL strings. +When non-prepared statements are executed, these encoder functions are +called on each query parameter. +""" import logging log = logging.getLogger(__name__) @@ -45,14 +50,23 @@ def cql_quote(term): def cql_encode_none(val): + """ + Converts :const:`None` to the string 'NULL'. + """ return 'NULL' def cql_encode_unicode(val): + """ + Converts :class:`unicode` objects to UTF-8 encoded strings with quote escaping. + """ return cql_quote(val.encode('utf-8')) def cql_encode_str(val): + """ + Escapes quotes in :class:`str` objects. + """ return cql_quote(val) @@ -69,25 +83,51 @@ def cql_encode_bytes(val): # noqa def cql_encode_object(val): + """ + Default encoder for all objects that do not have a specific encoder function + registered. This function simply calls :meth:`str()` on the object. + """ return str(val) def cql_encode_datetime(val): + """ + Converts a :class:`datetime.datetime` object to a (string) integer timestamp + with millisecond precision. + """ timestamp = calendar.timegm(val.utctimetuple()) return str(long(timestamp * 1e3 + getattr(val, 'microsecond', 0) / 1e3)) def cql_encode_date(val): + """ + Converts a :class:`datetime.date` object to a string with format + ``YYYY-MM-DD-0000``. + """ return "'%s'" % val.strftime('%Y-%m-%d-0000') def cql_encode_sequence(val): + """ + Converts a sequence to a string of the form ``(item1, item2, ...)``. This + is suitable for ``IN`` value lists. + """ return '( %s )' % ' , '.join(cql_encoders.get(type(v), cql_encode_object)(v) for v in val) + + cql_encode_tuple = cql_encode_sequence +""" +Converts a sequence to a string of the form ``(item1, item2, ...)``. This +is suitable for ``tuple`` type columns. +""" def cql_encode_map_collection(val): + """ + Converts a dict into a string of the form ``{key1: val1, key2: val2, ...}``. + This is suitable for ``map`` type columns. + """ return '{ %s }' % ' , '.join('%s : %s' % ( cql_encode_all_types(k), cql_encode_all_types(v) @@ -95,14 +135,26 @@ def cql_encode_map_collection(val): def cql_encode_list_collection(val): + """ + Converts a sequence to a string of the form ``[item1, item2, ...]``. This + is suitable for ``list`` type columns. + """ return '[ %s ]' % ' , '.join(map(cql_encode_all_types, val)) def cql_encode_set_collection(val): + """ + Converts a sequence to a string of the form ``{item1, item2, ...}``. This + is suitable for ``set`` type columns. + """ return '{ %s }' % ' , '.join(map(cql_encode_all_types, val)) def cql_encode_all_types(val): + """ + Converts any type into a CQL string, defaulting to ``cql_encode_object`` + if :attr:`~.cql_encoders` does not contain an entry for the type. + """ return cql_encoders.get(type(val), cql_encode_object)(val) @@ -122,6 +174,9 @@ def cql_encode_all_types(val): frozenset: cql_encode_set_collection, types.GeneratorType: cql_encode_list_collection } +""" +A map of python types to encoder functions. +""" if six.PY2: cql_encoders.update({ diff --git a/docs/api/cassandra/encoder.rst b/docs/api/cassandra/encoder.rst new file mode 100644 index 0000000000..9507f929a9 --- /dev/null +++ b/docs/api/cassandra/encoder.rst @@ -0,0 +1,43 @@ +``cassandra.encoder`` - Encoders for non-prepared Statements +============================================================ + +.. module:: cassandra.encoder + +.. data:: cql_encoders + + A map of python types to encoder functions. + +.. autofunction:: cql_encode_none () + +.. autofunction:: cql_encode_object () + +.. autofunction:: cql_encode_all_types () + +.. autofunction:: cql_encode_sequence () + +String Types +------------ + +.. autofunction:: cql_encode_str () + +.. autofunction:: cql_encode_unicode () + +.. autofunction:: cql_encode_bytes () + +Date Types +---------- + +.. autofunction:: cql_encode_datetime () + +.. autofunction:: cql_encode_date () + +Collection Types +---------------- + +.. autofunction:: cql_encode_map_collection () + +.. autofunction:: cql_encode_list_collection () + +.. autofunction:: cql_encode_set_collection () + +.. autofunction:: cql_encode_tuple () diff --git a/docs/api/cassandra/metrics.rst b/docs/api/cassandra/metrics.rst index 755ab42924..0df7f8b5b9 100644 --- a/docs/api/cassandra/metrics.rst +++ b/docs/api/cassandra/metrics.rst @@ -2,7 +2,6 @@ =========================================== .. module:: cassandra.metrics - :members: .. autoclass:: cassandra.metrics.Metrics () :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index 65545028e0..fc083c6a7d 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -12,6 +12,7 @@ API Documentation cassandra/metrics cassandra/query cassandra/pool + cassandra/encoder cassandra/decoder cassandra/concurrent cassandra/connection From f41d05d539d619a1ff8032c14328ac8c7aee1380 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 18 Jul 2014 15:35:32 -0500 Subject: [PATCH 0983/3961] Document Session.encoders --- cassandra/cluster.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index b0609061bb..cbac8f1aec 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1162,14 +1162,36 @@ class Session(object): .. versionadded:: 2.1.0 """ + encoders = None + """ + A map of python types to CQL encoder functions that will be used when + formatting query parameters for non-prepared statements. This mapping + is not used for prepared statements (because prepared statements + give the driver more information about what CQL types are expected, allowing + it to accept a wider range of python types). + + This mapping can be be modified by users as they see fit. Functions from + :mod:`cassandra.encoder` should be used, if possible, because they take + precautions to avoid injections and properly sanitize data. + + Example:: + + from cassandra.encoder import cql_encode_tuple + + cluster = Cluster() + session = cluster.connect("mykeyspace") + session.encoders[tuple] = cql_encode_tuple + + session.execute("CREATE TABLE mytable (k int PRIMARY KEY, col tuple)") + session.execute("INSERT INTO mytable (k, col) VALUES (%s, %s)", [0, (123, 'abc')]) + """ + _lock = None _pools = None _load_balancer = None _metrics = None _protocol_version = None - encoders = None - def __init__(self, cluster, hosts): self.cluster = cluster self.hosts = hosts From 871ae525fd3c59477f4b2bb64cc6d0f2210f1c25 Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Fri, 18 Jul 2014 15:44:31 -0500 Subject: [PATCH 0984/3961] Don't depend on v3 protocol for reading UDTs --- cassandra/cluster.py | 40 +++++++++++++++------- cassandra/connection.py | 44 ++++++++++++++++++++----- cassandra/pool.py | 22 ------------- tests/unit/test_host_connection_pool.py | 18 ---------- 4 files changed, 65 insertions(+), 59 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index cbac8f1aec..dc70db04f8 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -42,7 +42,7 @@ from itertools import groupby from cassandra import (ConsistencyLevel, AuthenticationFailed, - OperationTimedOut, UnsupportedOperation) + InvalidRequest, OperationTimedOut, UnsupportedOperation) from cassandra.connection import ConnectionException, ConnectionShutdown from cassandra.encoder import cql_encode_all_types, cql_encoders from cassandra.protocol import (QueryMessage, ResultMessage, @@ -1893,19 +1893,37 @@ def _refresh_schema(self, connection, keyspace=None, table=None, usertype=None, queries = [ QueryMessage(query=self._SELECT_KEYSPACES, consistency_level=cl), QueryMessage(query=self._SELECT_COLUMN_FAMILIES, consistency_level=cl), - QueryMessage(query=self._SELECT_COLUMNS, consistency_level=cl) + QueryMessage(query=self._SELECT_COLUMNS, consistency_level=cl), + QueryMessage(query=self._SELECT_USERTYPES, consistency_level=cl) ] - if self._protocol_version >= 3: - queries.append(QueryMessage(query=self._SELECT_USERTYPES, consistency_level=cl)) - ks_result, cf_result, col_result, types_result = connection.wait_for_responses(*queries) - types_result = dict_factory(*types_result.results) if types_result.results else {} + + responses = connection.wait_for_responses(*queries, fail_on_error=False) + (ks_success, ks_result), (cf_success, cf_result), (col_success, col_result), (types_success, types_result) = responses + + if ks_success: + ks_result = dict_factory(*ks_result.results) + else: + raise ks_result + + if cf_success: + cf_result = dict_factory(*cf_result.results) else: - ks_result, cf_result, col_result = connection.wait_for_responses(*queries) - types_result = {} + raise cf_result - ks_result = dict_factory(*ks_result.results) - cf_result = dict_factory(*cf_result.results) - col_result = dict_factory(*col_result.results) + if col_success: + col_result = dict_factory(*col_result.results) + else: + raise col_result + + # if we're connected to Cassandra < 2.1, the usertypes table will not exist + if types_success: + types_result = dict_factory(*types_result.results) if types_result.results else {} + else: + if isinstance(types_result, InvalidRequest): + log.debug("[control connection] user types table not found") + types_result = {} + else: + raise types_result log.debug("[control connection] Fetched schema, rebuilding metadata") self._cluster.metadata.rebuild_schema(ks_result, types_result, cf_result, col_result) diff --git a/cassandra/connection.py b/cassandra/connection.py index 6aa47481b4..64f0122d54 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -302,10 +302,19 @@ def wait_for_response(self, msg, timeout=None): return self.wait_for_responses(msg, timeout=timeout)[0] def wait_for_responses(self, *msgs, **kwargs): + """ + Returns a list of (success, response) tuples. If success + is False, response will be an Exception. Otherwise, response + will be the normal query response. + + If fail_on_error was left as True and one of the requests + failed, the corresponding Exception will be raised. + """ if self.is_closed or self.is_defunct: raise ConnectionShutdown("Connection %s is already closed" % (self, )) timeout = kwargs.get('timeout') - waiter = ResponseWaiter(self, len(msgs)) + fail_on_error = kwargs.get('fail_on_error', True) + waiter = ResponseWaiter(self, len(msgs), fail_on_error) # busy wait for sufficient space on the connection messages_sent = 0 @@ -677,9 +686,10 @@ def __str__(self): class ResponseWaiter(object): - def __init__(self, connection, num_responses): + def __init__(self, connection, num_responses, fail_on_error): self.connection = connection self.pending = num_responses + self.fail_on_error = fail_on_error self.error = None self.responses = [None] * num_responses self.event = Event() @@ -688,15 +698,33 @@ def got_response(self, response, index): with self.connection.lock: self.connection.in_flight -= 1 if isinstance(response, Exception): - self.error = response - self.event.set() - else: - self.responses[index] = response - self.pending -= 1 - if not self.pending: + if hasattr(response, 'to_exception'): + response = response.to_exception() + if self.fail_on_error: + self.error = response self.event.set() + else: + self.responses[index] = (False, response) + else: + if not self.fail_on_error: + self.responses[index] = (True, response) + else: + self.responses[index] = response + + self.pending -= 1 + if not self.pending: + self.event.set() def deliver(self, timeout=None): + """ + If fail_on_error was set to False, a list of (success, response) + tuples will be returned. If success is False, response will be + an Exception. Otherwise, response will be the normal query response. + + If fail_on_error was left as True and one of the requests + failed, the corresponding Exception will be raised. Otherwise, + the normal response will be returned. + """ self.event.wait(timeout) if self.error: raise self.error diff --git a/cassandra/pool.py b/cassandra/pool.py index dd462998c8..462f597bbd 100644 --- a/cassandra/pool.py +++ b/cassandra/pool.py @@ -17,7 +17,6 @@ """ import logging -import re import socket import time from threading import Lock, RLock, Condition @@ -42,13 +41,6 @@ class NoConnectionsAvailable(Exception): pass -# example matches: -# 1.0.0 -# 1.0.0-beta1 -# 2.0-SNAPSHOT -version_re = re.compile(r"(?P\d+)\.(?P\d+)(?:\.(?P\d+))?(?:-(?P