diff --git a/README.rst b/README.rst index d161f2a..ac48d5c 100644 --- a/README.rst +++ b/README.rst @@ -32,9 +32,9 @@ Models are defined in a way reminiscent of Django's ORM: engine = engines.MergeTree('birthday', ('first_name', 'last_name', 'birthday')) -It is possible to provide a default value for a field, instead of it's "natural" default (empty string for string fields, zero for numeric fields etc.). +It is possible to provide a default value for a field, instead of its "natural" default (empty string for string fields, zero for numeric fields etc.). -See below for the supported model field types. +See below for the supported field types and table engines. Using Models ------------ @@ -48,8 +48,8 @@ Once you have a model, you can create model instances: >>> dan.first_name u'Dan' -When values are assigned to a model fields, they are immediately converted to their Pythonic data type. -In case the value is invalid, a ValueError is raised: +When values are assigned to model fields, they are immediately converted to their Pythonic data type. +In case the value is invalid, a ``ValueError`` is raised: .. code:: python @@ -61,7 +61,10 @@ In case the value is invalid, a ValueError is raised: >>> suzy.birthday = '1922-05-31' ValueError: DateField out of range - 1922-05-31 is not between 1970-01-01 and 2038-01-19 -To write your instances to ClickHouse, you need a Database instance: +Inserting to the Database +------------------------- + +To write your instances to ClickHouse, you need a ``Database`` instance: .. code:: python @@ -76,16 +79,53 @@ If necessary, you can specify a different database URL and optional credentials: db = Database('my_test_db', db_url='http://192.168.1.1:8050', username='scott', password='tiger') -Using the Database instance you can create a table for your model, and insert instances to it: +Using the ``Database`` instance you can create a table for your model, and insert instances to it: .. code:: python db.create_table(Person) db.insert([dan, suzy]) -The insert method can take any iterable of model instances, but they all must belong to the same model class. +The ``insert`` method can take any iterable of model instances, but they all must belong to the same model class. +Reading from the Database +------------------------- +Loading model instances from the database is simple: + +.. code:: python + + for person in db.select("SELECT * FROM my_test_db.person", model_class=Person): + print person.first_name, person.last_name + +Do not include a ``FORMAT`` clause in the query, since the ORM automatically sets the format to ``TabSeparatedWithNamesAndTypes``. + +It is possible to select only a subset of the columns, and the rest will receive their default values: + +.. code:: python + + for person in db.select("SELECT first_name FROM my_test_db.person WHERE last_name='Smith'", model_class=Person): + print person.first_name + +Specifying a model class is not required. In case you do not provide a model class, an ad-hoc class will +be defined based on the column names and types returned by the query: + +.. code:: python + + for row in db.select("SELECT max(height) as max_height FROM my_test_db.person"): + print row.max_height + +Counting +-------- + +The ``Database`` class also supports counting records easily: + +.. code:: python + + >>> db.count(Person) + 117 + >>> db.count(Person, conditions="height > 1.90") + 6 Field Types ----------- @@ -106,6 +146,10 @@ Currently the following field types are supported: - DateField - DateTimeField +Table Engines +------------- + +TBD Development @@ -119,4 +163,4 @@ After cloning the project, run the following commands:: To run the tests, ensure that the ClickHouse server is running on http://localhost:8123/ (this is the default), and run:: - bin/nosetests + bin/nosetests \ No newline at end of file diff --git a/src/infi/clickhouse_orm/database.py b/src/infi/clickhouse_orm/database.py index 2472a58..b8c2e9f 100644 --- a/src/infi/clickhouse_orm/database.py +++ b/src/infi/clickhouse_orm/database.py @@ -17,6 +17,7 @@ class Database(object): self._send('CREATE DATABASE IF NOT EXISTS ' + db_name) def create_table(self, model_class): + # TODO check that model has an engine self._send(model_class.create_table_sql(self.db_name)) def drop_table(self, model_class): diff --git a/src/infi/clickhouse_orm/fields.py b/src/infi/clickhouse_orm/fields.py index c90363e..feebc96 100644 --- a/src/infi/clickhouse_orm/fields.py +++ b/src/infi/clickhouse_orm/fields.py @@ -15,23 +15,30 @@ class Field(object): self.default = default or self.class_default def to_python(self, value): - """ + ''' Converts the input value into the expected Python data type, raising ValueError if the data can't be converted. Returns the converted value. Subclasses should override this. - """ + ''' return value def validate(self, value): + ''' + Called after to_python to validate that the value is suitable for the field's database type. + Subclasses should override this. + ''' pass def _range_check(self, value, min_value, max_value): + ''' + Utility method to check that the given value is between min_value and max_value. + ''' if value < min_value or value > max_value: raise ValueError('%s out of range - %s is not between %s and %s' % (self.__class__.__name__, value, min_value, max_value)) def get_db_prep_value(self, value): - """ + ''' Returns the field's value prepared for interacting with the database. - """ + ''' return value diff --git a/src/infi/clickhouse_orm/models.py b/src/infi/clickhouse_orm/models.py index e4a4209..13dffa4 100644 --- a/src/infi/clickhouse_orm/models.py +++ b/src/infi/clickhouse_orm/models.py @@ -58,6 +58,10 @@ class Model(object): setattr(self, name, field.default) def __setattr__(self, name, value): + ''' + When setting a field value, converts the value to its Pythonic type and validates it. + This may raise a ValueError. + ''' field = self.get_field(name) if field: value = field.to_python(value) @@ -65,15 +69,24 @@ class Model(object): super(Model, self).__setattr__(name, value) def get_field(self, name): + ''' + Get a Field instance given its name, or None if not found. + ''' field = getattr(self.__class__, name, None) return field if isinstance(field, Field) else None @classmethod def table_name(cls): + ''' + Returns the model's database table name. + ''' return cls.__name__.lower() @classmethod def create_table_sql(cls, db_name): + ''' + Returns the SQL command for creating a table for this model. + ''' parts = ['CREATE TABLE IF NOT EXISTS %s.%s (' % (db_name, cls.table_name())] cols = [] for name, field in cls._fields: @@ -86,6 +99,9 @@ class Model(object): @classmethod def drop_table_sql(cls, db_name): + ''' + Returns the SQL command for deleting this model's table. + ''' return 'DROP TABLE IF EXISTS %s.%s' % (db_name, cls.table_name()) @classmethod