From 20b30a12f3cf9a1e8f7c55e7dce823f80ffb540b Mon Sep 17 00:00:00 2001 From: Alan Boudreault Date: Wed, 24 Feb 2016 17:58:28 -0500 Subject: [PATCH] PYTHON-337: case sensitive support for column family name --- cassandra/cqlengine/management.py | 2 +- cassandra/cqlengine/models.py | 18 +++++++- docs/api/cassandra/cqlengine/models.rst | 2 + .../cqlengine/management/test_management.py | 43 +++++++++++++++++++ .../model/test_class_construction.py | 26 +++++++++++ .../integration/cqlengine/model/test_model.py | 17 ++++++++ 6 files changed, 105 insertions(+), 3 deletions(-) diff --git a/cassandra/cqlengine/management.py b/cassandra/cqlengine/management.py index f1b66d95..9e7b0363 100644 --- a/cassandra/cqlengine/management.py +++ b/cassandra/cqlengine/management.py @@ -194,7 +194,7 @@ def sync_table(model): if table.indexes.get(index_name): continue - qs = ['CREATE INDEX {0}'.format(index_name)] + qs = ['CREATE INDEX {0}'.format(metadata.protect_name(index_name))] qs += ['ON {0}'.format(cf_name)] qs += ['("{0}")'.format(column.db_field_name)] qs = ' '.join(qs) diff --git a/cassandra/cqlengine/models.py b/cassandra/cqlengine/models.py index 6f9f61c6..b20dca99 100644 --- a/cassandra/cqlengine/models.py +++ b/cassandra/cqlengine/models.py @@ -15,7 +15,7 @@ import logging import re import six -import warnings +from warnings import warn from cassandra.cqlengine import CQLEngineException, ValidationError from cassandra.cqlengine import columns @@ -308,6 +308,8 @@ class BaseModel(object): __table_name__ = None + __table_name_case_sensitive__ = False + __keyspace__ = None __discriminator_value__ = None @@ -492,7 +494,14 @@ class BaseModel(object): def _raw_column_family_name(cls): if not cls._table_name: if cls.__table_name__: - cls._table_name = cls.__table_name__.lower() + if cls.__table_name_case_sensitive__: + cls._table_name = cls.__table_name__ + else: + table_name = cls.__table_name__.lower() + if cls.__table_name__ != table_name: + warn(("Model __table_name__ will be case sensitive by default in 4.0. " + "You should fix the __table_name__ value of the '{0}' model.").format(cls.__name__)) + cls._table_name = table_name else: if cls._is_polymorphic and not cls._is_polymorphic_base: cls._table_name = cls._polymorphic_base._raw_column_family_name() @@ -924,6 +933,11 @@ class Model(BaseModel): *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. """ + __table_name_case_sensitive__ = False + """ + *Optional.* By default, __table_name__ is case insensitive. Set this to True if you want to preserve the case sensitivity. + """ + __keyspace__ = None """ Sets the name of the keyspace used by this model. diff --git a/docs/api/cassandra/cqlengine/models.rst b/docs/api/cassandra/cqlengine/models.rst index ff198a49..efcd331b 100644 --- a/docs/api/cassandra/cqlengine/models.rst +++ b/docs/api/cassandra/cqlengine/models.rst @@ -28,6 +28,8 @@ Model .. autoattribute:: __table_name__ + .. autoattribute:: __table_name_case_sensitive__ + .. autoattribute:: __keyspace__ .. _ttl-change: diff --git a/tests/integration/cqlengine/management/test_management.py b/tests/integration/cqlengine/management/test_management.py index 93aa8dc1..e90d82d7 100644 --- a/tests/integration/cqlengine/management/test_management.py +++ b/tests/integration/cqlengine/management/test_management.py @@ -235,6 +235,49 @@ class SyncTableTests(BaseCassEngTestCase): self.assertIn('SizeTieredCompactionStrategy', table_meta.as_cql_query()) +class IndexModel(Model): + + __table_name__ = 'index_model' + first_key = columns.UUID(primary_key=True) + second_key = columns.Text(index=True) + + +class IndexCaseSensitiveModel(Model): + + __table_name__ = 'IndexModel' + __table_name_case_sensitive__ = True + first_key = columns.UUID(primary_key=True) + second_key = columns.Text(index=True) + + +class IndexTests(BaseCassEngTestCase): + + def setUp(self): + drop_table(IndexModel) + + def test_sync_index(self): + + sync_table(IndexModel) + table_meta = management._get_table_metadata(IndexModel) + self.assertIn("index_index_model_second_key", table_meta.indexes) + + # index already exists + sync_table(IndexModel) + table_meta = management._get_table_metadata(IndexModel) + self.assertIn("index_index_model_second_key", table_meta.indexes) + + def test_sync_index_case_sensitive(self): + + sync_table(IndexCaseSensitiveModel) + table_meta = management._get_table_metadata(IndexCaseSensitiveModel) + self.assertIn("index_IndexModel_second_key", table_meta.indexes) + + # index already exists + sync_table(IndexCaseSensitiveModel) + table_meta = management._get_table_metadata(IndexCaseSensitiveModel) + self.assertIn("index_IndexModel_second_key", table_meta.indexes) + + class NonModelFailureTest(BaseCassEngTestCase): class FakeModel(object): pass diff --git a/tests/integration/cqlengine/model/test_class_construction.py b/tests/integration/cqlengine/model/test_class_construction.py index 7b333a58..c47c1723 100644 --- a/tests/integration/cqlengine/model/test_class_construction.py +++ b/tests/integration/cqlengine/model/test_class_construction.py @@ -217,6 +217,7 @@ class TestModelClassFunction(BaseCassEngTestCase): self.assertEqual(len(warn), 0) + class TestManualTableNaming(BaseCassEngTestCase): class RenamedTest(Model): @@ -230,6 +231,31 @@ class TestManualTableNaming(BaseCassEngTestCase): assert self.RenamedTest.column_family_name(include_keyspace=False) == 'manual_name' assert self.RenamedTest.column_family_name(include_keyspace=True) == 'whatever.manual_name' + +class TestManualTableNamingCaseSensitive(BaseCassEngTestCase): + + class RenamedCaseInsensitiveTest(Model): + __keyspace__ = 'whatever' + __table_name__ = 'Manual_Name' + + id = columns.UUID(primary_key=True) + + class RenamedCaseSensitiveTest(Model): + __keyspace__ = 'whatever' + __table_name__ = 'Manual_Name' + __table_name_case_sensitive__ = True + + id = columns.UUID(primary_key=True) + + def test_proper_table_naming_case_insensitive(self): + self.assertEqual(self.RenamedCaseInsensitiveTest.column_family_name(include_keyspace=False), 'manual_name') + self.assertEqual(self.RenamedCaseInsensitiveTest.column_family_name(include_keyspace=True), 'whatever.manual_name') + + def test_proper_table_naming_case_sensitive(self): + self.assertEqual(self.RenamedCaseSensitiveTest.column_family_name(include_keyspace=False), '"Manual_Name"') + self.assertEqual(self.RenamedCaseSensitiveTest.column_family_name(include_keyspace=True), 'whatever."Manual_Name"') + + class AbstractModel(Model): __abstract__ = True diff --git a/tests/integration/cqlengine/model/test_model.py b/tests/integration/cqlengine/model/test_model.py index e9f795d8..cb6bdeb1 100644 --- a/tests/integration/cqlengine/model/test_model.py +++ b/tests/integration/cqlengine/model/test_model.py @@ -126,6 +126,23 @@ class TestModel(unittest.TestCase): # .. but we can still get the bare CF name self.assertEqual(TestModel.column_family_name(include_keyspace=False), "test_model") + def test_column_family_case_sensitive(self): + class TestModel(Model): + __table_name__ = 'TestModel' + __table_name_case_sensitive__ = True + + k = columns.Integer(primary_key=True) + + self.assertEqual(TestModel.column_family_name(), '%s."TestModel"' % (models.DEFAULT_KEYSPACE,)) + + TestModel.__keyspace__ = "my_test_keyspace" + self.assertEqual(TestModel.column_family_name(), '%s."TestModel"' % (TestModel.__keyspace__,)) + + del TestModel.__keyspace__ + with patch('cassandra.cqlengine.models.DEFAULT_KEYSPACE', None): + self.assertRaises(CQLEngineException, TestModel.column_family_name) + self.assertEqual(TestModel.column_family_name(include_keyspace=False), '"TestModel"') + class BuiltInAttributeConflictTest(unittest.TestCase): """tests Model definitions that conflict with built-in attributes/methods"""