diff --git a/.gitignore b/.gitignore index ed48ca4..107afc3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ Mongothon.egg-info build dist env +#*# +*# +.#* +*~ diff --git a/mongothon/__init__.py b/mongothon/__init__.py index 606ee7b..47b5600 100644 --- a/mongothon/__init__.py +++ b/mongothon/__init__.py @@ -2,7 +2,7 @@ from inflection import camelize from document import Document from model import Model, NotFoundException -from schema import Schema +from schema import Schema, IndexSpec from schemer import Mixed, ValidationException, Array diff --git a/mongothon/model.py b/mongothon/model.py index 97fc4e7..69330bb 100644 --- a/mongothon/model.py +++ b/mongothon/model.py @@ -7,7 +7,7 @@ from .exceptions import NotFoundException from .events import EventHandlerRegistrar from .scopes import STANDARD_SCOPES - +from .schema import IndexSpec OBJECTIDEXPR = re.compile(r"^[a-fA-F0-9]{24}$") @@ -109,6 +109,46 @@ def apply_defaults(self): self.schema.apply_defaults(self) self.emit('did_apply_defaults') + @classmethod + def apply_index(cls, index): + index.apply_to(cls.get_collection()) + + @classmethod + def apply_indexes(cls): + for index in cls.schema.indexes: + index.apply_to(cls.get_collection()) + + @classmethod + def _existing_indexes(cls): + """ + >>> db..index_information() + {u'_id_': {u'key': [(u'_id', 1)]}, + u'x_1': {u'unique': True, u'key': [(u'x', 1)]}} + """ + info = cls.get_collection().index_information() + indexes = [] + for k, v in info.iteritems(): + if k == '_id_': # this is the primary key index, not interesting + continue + index = IndexSpec(k, v['key']) + for arg, val in v.iteritems(): + if arg == 'key': + continue + index.kwargs[arg] == val + index.validate() + indexes.append(index) + return indexes + + @classmethod + def applied_indexes(cls): + return [i.name for i in cls._existing_indexes()] + + @classmethod + def unapplied_indexes(cls): + existing_names = set([i.name for i in cls._existing_indexes()]) + expected_names = [i.name for i in cls.schema.indexes] + return [name for name in expected_names if name not in existing_names] + @classmethod def get_collection(cls): if not hasattr(cls, '_collection'): diff --git a/mongothon/schema.py b/mongothon/schema.py index 0ca984e..6efd701 100644 --- a/mongothon/schema.py +++ b/mongothon/schema.py @@ -1,13 +1,39 @@ from bson.objectid import ObjectId +import pymongo import schemer +class IndexSpec(object): + def __init__(self, name, key_spec, **kwargs): + self.name = name + self.key_spec = key_spec + self.kwargs = kwargs + + def apply_to(self, collection): + collection.create_index(self.key_spec, name=self.name, **self.kwargs) + + def validate(self): + if not self.name: + raise ValueError("Must specify a non-nil name for every index") + if not self.key_spec: + raise ValueError("Must specify the actual index for {}".format(self.name)) + for name, index_type in self.key_spec: + if index_type not in {pymongo.ASCENDING, pymongo.DESCENDING, pymongo.HASHED}: + raise ValueError('Unsupported Index Type {} for {}'.format(index_type, self.name)) + return self + + + class Schema(schemer.Schema): """A Schema encapsulates the structure and constraints of a Mongo document.""" - def __init__(self, doc_spec, **kwargs): + indexes = [] + + def __init__(self, doc_spec, indexes=[], **kwargs): super(Schema, self).__init__(doc_spec, **kwargs) # Every mongothon schema should expect an ID field. if '_id' not in self._doc_spec: self._doc_spec['_id'] = {"type": ObjectId} + + self.indexes = [i.validate() for i in indexes] diff --git a/tests/mongothon/model_test.py b/tests/mongothon/model_test.py index 45ed3d2..a54daa9 100644 --- a/tests/mongothon/model_test.py +++ b/tests/mongothon/model_test.py @@ -2,7 +2,7 @@ from pickle import dumps, loads from unittest import TestCase from mock import Mock, ANY, call, NonCallableMock -from mongothon import Document, Schema, NotFoundException, Array +from mongothon import Document, Schema, NotFoundException, Array, IndexSpec from mongothon.validators import one_of from mongothon.scopes import STANDARD_SCOPES from bson import ObjectId @@ -22,7 +22,8 @@ "diameter": {"type": int} }))}, "options": {"type": Array(basestring)} -}) +}, + indexes=[IndexSpec('make_1', [('make', 1)])]) doc = { @@ -68,6 +69,7 @@ class TestModel(TestCase): def setUp(self): self.mock_collection = Mock() self.mock_collection.name = "car" + self.mock_collection.index_information = Mock(return_value={}) self.Car = create_model(car_schema, self.mock_collection) self.CarOffline = create_model_offline(car_schema, lambda: self.mock_collection, 'Car') self.car = self.Car(doc) @@ -126,6 +128,14 @@ def test_constructor_with_kwargs_and_initial_state(self): def test_instantiate(self): self.assert_predicates(self.car, is_new=True) + def test_indexes(self): + self.assertEqual(['make_1'], self.Car.unapplied_indexes()) + self.Car.apply_indexes() + self.mock_collection.create_index.assert_called_once_with([('make', 1)], name='make_1') + self.mock_collection.index_information.return_value = {'make_1': {'key': [('make', 1)]}} + self.assertEqual([], self.Car.unapplied_indexes()) + self.assertEqual(['make_1'], self.Car.applied_indexes()) + def test_validation_of_valid_doc(self): self.car.validate() diff --git a/tests/mongothon/schema_test.py b/tests/mongothon/schema_test.py new file mode 100644 index 0000000..7cc5ab3 --- /dev/null +++ b/tests/mongothon/schema_test.py @@ -0,0 +1,7 @@ +from mongothon.schema import Schema, IndexSpec +from mock import Mock +import unittest + +class TestSchema(unittest.TestCase): + def test_indexes(self): + Schema({}, indexes=[IndexSpec('myindex', [('key', 1)])])