mirror of
https://github.com/Infinidat/infi.clickhouse_orm.git
synced 2024-11-22 17:16:34 +03:00
Finished Release v0.9.3
This commit is contained in:
commit
220616a151
|
@ -1,6 +1,12 @@
|
||||||
Change Log
|
Change Log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
v0.9.3
|
||||||
|
------
|
||||||
|
- Changed license from PSF to BSD
|
||||||
|
- Nullable fields support (yamiou)
|
||||||
|
- Support for queryset slicing
|
||||||
|
|
||||||
v0.9.2
|
v0.9.2
|
||||||
------
|
------
|
||||||
- Added `ne` and `not_in` queryset operators
|
- Added `ne` and `not_in` queryset operators
|
||||||
|
|
26
LICENSE
Normal file
26
LICENSE
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Copyright (c) 2017 INFINIDAT
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder 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.
|
|
@ -378,6 +378,13 @@ Extends Field
|
||||||
#### ArrayField(inner_field, default=None, alias=None, materialized=None)
|
#### ArrayField(inner_field, default=None, alias=None, materialized=None)
|
||||||
|
|
||||||
|
|
||||||
|
### NullableField
|
||||||
|
|
||||||
|
Extends Field
|
||||||
|
|
||||||
|
#### NullableField(inner_field, default=None, alias=None, materialized=None, extra_null_values=None)
|
||||||
|
|
||||||
|
|
||||||
### FixedStringField
|
### FixedStringField
|
||||||
|
|
||||||
Extends StringField
|
Extends StringField
|
||||||
|
|
|
@ -22,6 +22,7 @@ Currently the following field types are supported:
|
||||||
| Enum8Field | Enum8 | Enum | See below
|
| Enum8Field | Enum8 | Enum | See below
|
||||||
| Enum16Field | Enum16 | Enum | See below
|
| Enum16Field | Enum16 | Enum | See below
|
||||||
| ArrayField | Array | list | See below
|
| ArrayField | Array | list | See below
|
||||||
|
| NullableField | Nullable | See below | See below
|
||||||
|
|
||||||
DateTimeField and Time Zones
|
DateTimeField and Time Zones
|
||||||
----------------------------
|
----------------------------
|
||||||
|
@ -103,6 +104,32 @@ Usage:
|
||||||
# created_date and username will contain a default value
|
# created_date and username will contain a default value
|
||||||
db.select('SELECT * FROM $db.event', model_class=Event)
|
db.select('SELECT * FROM $db.event', model_class=Event)
|
||||||
|
|
||||||
|
Working with nullable fields
|
||||||
|
----------------------------
|
||||||
|
From [some time](https://github.com/yandex/ClickHouse/pull/70) ClickHouse provides a NULL value support.
|
||||||
|
Also see some information [here](https://github.com/yandex/ClickHouse/blob/master/dbms/tests/queries/0_stateless/00395_nullable.sql).
|
||||||
|
|
||||||
|
Wrapping another field in a `NullableField` makes it possible to assign `None` to that field. For example:
|
||||||
|
|
||||||
|
class EventData(models.Model):
|
||||||
|
|
||||||
|
date = fields.DateField()
|
||||||
|
comment = fields.NullableField(fields.StringField(), extra_null_values={''})
|
||||||
|
score = fields.NullableField(fields.UInt8Field())
|
||||||
|
serie = fields.NullableField(fields.ArrayField(fields.UInt8Field()))
|
||||||
|
|
||||||
|
engine = engines.MergeTree('date', ('date',))
|
||||||
|
|
||||||
|
|
||||||
|
score_event = EventData(date=date.today(), comment=None, score=5, serie=None)
|
||||||
|
comment_event = EventData(date=date.today(), comment='Excellent!', score=None, serie=None)
|
||||||
|
another_event = EventData(date=date.today(), comment='', score=None, serie=None)
|
||||||
|
action_event = EventData(date=date.today(), comment='', score=None, serie=[1, 2, 3])
|
||||||
|
|
||||||
|
The `extra_null_values` parameter is an iterable of additional values that should be converted
|
||||||
|
to `None`.
|
||||||
|
|
||||||
|
NOTE: `ArrayField` of `NullableField` is not supported. Also `EnumField` cannot be nullable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,25 @@ When some of the model fields aren't needed, it is more efficient to omit them f
|
||||||
qs = Person.objects_in(database).only('first_name', 'birthday')
|
qs = Person.objects_in(database).only('first_name', 'birthday')
|
||||||
|
|
||||||
|
|
||||||
|
Slicing
|
||||||
|
-------
|
||||||
|
|
||||||
|
It is possible to get a specific item from the queryset by index.
|
||||||
|
|
||||||
|
qs = Person.objects_in(database).order_by('last_name', 'first_name')
|
||||||
|
first = qs[0]
|
||||||
|
|
||||||
|
It is also possible to get a range a instances using a slice. This returns a queryset,
|
||||||
|
that you can either iterate over or convert to a list.
|
||||||
|
|
||||||
|
qs = Person.objects_in(database).order_by('last_name', 'first_name')
|
||||||
|
first_ten_people = list(qs[:10])
|
||||||
|
next_ten_people = list(qs[10:20])
|
||||||
|
|
||||||
|
You should use `order_by` to ensure a consistent ordering of the results.
|
||||||
|
|
||||||
|
Trying to use negative indexes or a slice with a step (e.g. [0:100:2]) is not supported and will raise an `AssertionError`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[<< Models and Databases](models_and_databases.md) | [Table of Contents](toc.md) | [Field Types >>](field_types.md)
|
[<< Models and Databases](models_and_databases.md) | [Table of Contents](toc.md) | [Field Types >>](field_types.md)
|
|
@ -20,12 +20,14 @@
|
||||||
* [Counting and Checking Existence](querysets.md#counting-and-checking-existence)
|
* [Counting and Checking Existence](querysets.md#counting-and-checking-existence)
|
||||||
* [Ordering](querysets.md#ordering)
|
* [Ordering](querysets.md#ordering)
|
||||||
* [Omitting Fields](querysets.md#omitting-fields)
|
* [Omitting Fields](querysets.md#omitting-fields)
|
||||||
|
* [Slicing](querysets.md#slicing)
|
||||||
|
|
||||||
* [Field Types](field_types.md#field-types)
|
* [Field Types](field_types.md#field-types)
|
||||||
* [DateTimeField and Time Zones](field_types.md#datetimefield-and-time-zones)
|
* [DateTimeField and Time Zones](field_types.md#datetimefield-and-time-zones)
|
||||||
* [Working with enum fields](field_types.md#working-with-enum-fields)
|
* [Working with enum fields](field_types.md#working-with-enum-fields)
|
||||||
* [Working with array fields](field_types.md#working-with-array-fields)
|
* [Working with array fields](field_types.md#working-with-array-fields)
|
||||||
* [Working with materialized and alias fields](field_types.md#working-with-materialized-and-alias-fields)
|
* [Working with materialized and alias fields](field_types.md#working-with-materialized-and-alias-fields)
|
||||||
|
* [Working with nullable fields](field_types.md#working-with-nullable-fields)
|
||||||
|
|
||||||
* [Table Engines](table_engines.md#table-engines)
|
* [Table Engines](table_engines.md#table-engines)
|
||||||
* [Simple Engines](table_engines.md#simple-engines)
|
* [Simple Engines](table_engines.md#simple-engines)
|
||||||
|
@ -60,6 +62,7 @@
|
||||||
* [BaseFloatField](class_reference.md#basefloatfield)
|
* [BaseFloatField](class_reference.md#basefloatfield)
|
||||||
* [BaseEnumField](class_reference.md#baseenumfield)
|
* [BaseEnumField](class_reference.md#baseenumfield)
|
||||||
* [ArrayField](class_reference.md#arrayfield)
|
* [ArrayField](class_reference.md#arrayfield)
|
||||||
|
* [NullableField](class_reference.md#nullablefield)
|
||||||
* [FixedStringField](class_reference.md#fixedstringfield)
|
* [FixedStringField](class_reference.md#fixedstringfield)
|
||||||
* [UInt8Field](class_reference.md#uint8field)
|
* [UInt8Field](class_reference.md#uint8field)
|
||||||
* [UInt16Field](class_reference.md#uint16field)
|
* [UInt16Field](class_reference.md#uint16field)
|
||||||
|
|
4
setup.in
4
setup.in
|
@ -6,14 +6,14 @@ SETUP_INFO = dict(
|
||||||
author_email = '${infi.recipe.template.version:author_email}',
|
author_email = '${infi.recipe.template.version:author_email}',
|
||||||
|
|
||||||
url = ${infi.recipe.template.version:homepage},
|
url = ${infi.recipe.template.version:homepage},
|
||||||
license = 'PSF',
|
license = 'BSD',
|
||||||
description = """${project:description}""",
|
description = """${project:description}""",
|
||||||
|
|
||||||
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Intended Audience :: System Administrators",
|
"Intended Audience :: System Administrators",
|
||||||
"License :: OSI Approved :: Python Software Foundation License",
|
"License :: OSI Approved :: BSD License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 2.7",
|
"Programming Language :: Python :: 2.7",
|
||||||
|
|
|
@ -362,3 +362,32 @@ class ArrayField(Field):
|
||||||
from .utils import escape
|
from .utils import escape
|
||||||
return 'Array(%s)' % self.inner_field.get_sql(with_default=False)
|
return 'Array(%s)' % self.inner_field.get_sql(with_default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class NullableField(Field):
|
||||||
|
|
||||||
|
class_default = None
|
||||||
|
|
||||||
|
def __init__(self, inner_field, default=None, alias=None, materialized=None,
|
||||||
|
extra_null_values=None):
|
||||||
|
self.inner_field = inner_field
|
||||||
|
self._null_values = [None]
|
||||||
|
if extra_null_values:
|
||||||
|
self._null_values.extend(extra_null_values)
|
||||||
|
super(NullableField, self).__init__(default, alias, materialized)
|
||||||
|
|
||||||
|
def to_python(self, value, timezone_in_use):
|
||||||
|
if value == '\\N' or value is None:
|
||||||
|
return None
|
||||||
|
return self.inner_field.to_python(value, timezone_in_use)
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
value is None or self.inner_field.validate(value)
|
||||||
|
|
||||||
|
def to_db_string(self, value, quote=True):
|
||||||
|
if value in self._null_values:
|
||||||
|
return '\\N'
|
||||||
|
return self.inner_field.to_db_string(value, quote=quote)
|
||||||
|
|
||||||
|
def get_sql(self, with_default=True):
|
||||||
|
from .utils import escape
|
||||||
|
return 'Nullable(%s)' % self.inner_field.get_sql(with_default=False)
|
||||||
|
|
|
@ -62,6 +62,10 @@ class ModelBase(type):
|
||||||
if db_type.startswith('FixedString'):
|
if db_type.startswith('FixedString'):
|
||||||
length = int(db_type[12 : -1])
|
length = int(db_type[12 : -1])
|
||||||
return orm_fields.FixedStringField(length)
|
return orm_fields.FixedStringField(length)
|
||||||
|
# Nullable
|
||||||
|
if db_type.startswith('Nullable'):
|
||||||
|
inner_field = cls.create_ad_hoc_field(db_type[9 : -1])
|
||||||
|
return orm_fields.NullableField(inner_field)
|
||||||
# Simple fields
|
# Simple fields
|
||||||
name = db_type + 'Field'
|
name = db_type + 'Field'
|
||||||
if not hasattr(orm_fields, name):
|
if not hasattr(orm_fields, name):
|
||||||
|
|
|
@ -183,6 +183,7 @@ class QuerySet(object):
|
||||||
self._order_by = []
|
self._order_by = []
|
||||||
self._q = []
|
self._q = []
|
||||||
self._fields = []
|
self._fields = []
|
||||||
|
self._limits = None
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -202,6 +203,24 @@ class QuerySet(object):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.as_sql()
|
return self.as_sql()
|
||||||
|
|
||||||
|
def __getitem__(self, s):
|
||||||
|
if isinstance(s, six.integer_types):
|
||||||
|
# Single index
|
||||||
|
assert s >= 0, 'negative indexes are not supported'
|
||||||
|
qs = copy(self)
|
||||||
|
qs._limits = (s, 1)
|
||||||
|
return iter(qs).next()
|
||||||
|
else:
|
||||||
|
# Slice
|
||||||
|
assert s.step in (None, 1), 'step is not supported in slices'
|
||||||
|
start = s.start or 0
|
||||||
|
stop = s.stop or 2**63 - 1
|
||||||
|
assert start >= 0 and stop >= 0, 'negative indexes are not supported'
|
||||||
|
assert start <= stop, 'start of slice cannot be smaller than its end'
|
||||||
|
qs = copy(self)
|
||||||
|
qs._limits = (start, stop - start)
|
||||||
|
return qs
|
||||||
|
|
||||||
def as_sql(self):
|
def as_sql(self):
|
||||||
"""
|
"""
|
||||||
Returns the whole query as a SQL string.
|
Returns the whole query as a SQL string.
|
||||||
|
@ -210,8 +229,10 @@ class QuerySet(object):
|
||||||
if self._fields:
|
if self._fields:
|
||||||
fields = ', '.join('`%s`' % field for field in self._fields)
|
fields = ', '.join('`%s`' % field for field in self._fields)
|
||||||
ordering = '\nORDER BY ' + self.order_by_as_sql() if self._order_by else ''
|
ordering = '\nORDER BY ' + self.order_by_as_sql() if self._order_by else ''
|
||||||
params = (fields, self._database.db_name, self._model_cls.table_name(), self.conditions_as_sql(), ordering)
|
limit = '\nLIMIT %d, %d' % self._limits if self._limits else ''
|
||||||
return u'SELECT %s\nFROM `%s`.`%s`\nWHERE %s%s' % params
|
params = (fields, self._model_cls.table_name(),
|
||||||
|
self.conditions_as_sql(), ordering, limit)
|
||||||
|
return u'SELECT %s\nFROM `%s`\nWHERE %s%s%s' % params
|
||||||
|
|
||||||
def order_by_as_sql(self):
|
def order_by_as_sql(self):
|
||||||
"""
|
"""
|
||||||
|
|
134
tests/test_nullable_fields.py
Normal file
134
tests/test_nullable_fields.py
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import unittest
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from infi.clickhouse_orm.database import Database
|
||||||
|
from infi.clickhouse_orm.models import Model
|
||||||
|
from infi.clickhouse_orm.fields import *
|
||||||
|
from infi.clickhouse_orm.engines import *
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
|
||||||
|
class NullableFieldsTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.database = Database('test-db')
|
||||||
|
self.database.create_table(ModelWithNullable)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.database.drop_database()
|
||||||
|
|
||||||
|
def test_nullable_datetime_field(self):
|
||||||
|
f = NullableField(DateTimeField())
|
||||||
|
epoch = datetime(1970, 1, 1, tzinfo=pytz.utc)
|
||||||
|
# Valid values
|
||||||
|
for value in (date(1970, 1, 1),
|
||||||
|
datetime(1970, 1, 1),
|
||||||
|
epoch,
|
||||||
|
epoch.astimezone(pytz.timezone('US/Eastern')),
|
||||||
|
epoch.astimezone(pytz.timezone('Asia/Jerusalem')),
|
||||||
|
'1970-01-01 00:00:00',
|
||||||
|
'1970-01-17 00:00:17',
|
||||||
|
'0000-00-00 00:00:00',
|
||||||
|
0,
|
||||||
|
'\\N'):
|
||||||
|
dt = f.to_python(value, pytz.utc)
|
||||||
|
if value == '\\N':
|
||||||
|
self.assertIsNone(dt)
|
||||||
|
else:
|
||||||
|
self.assertEquals(dt.tzinfo, pytz.utc)
|
||||||
|
# Verify that conversion to and from db string does not change value
|
||||||
|
dt2 = f.to_python(f.to_db_string(dt, quote=False), pytz.utc)
|
||||||
|
self.assertEquals(dt, dt2)
|
||||||
|
# Invalid values
|
||||||
|
for value in ('nope', '21/7/1999', 0.5):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
f.to_python(value, pytz.utc)
|
||||||
|
|
||||||
|
def test_nullable_uint8_field(self):
|
||||||
|
f = NullableField(UInt8Field())
|
||||||
|
# Valid values
|
||||||
|
for value in (17, '17', 17.0, '\\N'):
|
||||||
|
python_value = f.to_python(value, pytz.utc)
|
||||||
|
if value == '\\N':
|
||||||
|
self.assertIsNone(python_value)
|
||||||
|
self.assertEqual(value, f.to_db_string(python_value))
|
||||||
|
else:
|
||||||
|
self.assertEquals(python_value, 17)
|
||||||
|
|
||||||
|
# Invalid values
|
||||||
|
for value in ('nope', date.today()):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
f.to_python(value, pytz.utc)
|
||||||
|
|
||||||
|
def test_nullable_string_field(self):
|
||||||
|
f = NullableField(StringField())
|
||||||
|
# Valid values
|
||||||
|
for value in ('\\\\N', 'N', 'some text', '\\N'):
|
||||||
|
python_value = f.to_python(value, pytz.utc)
|
||||||
|
if value == '\\N':
|
||||||
|
self.assertIsNone(python_value)
|
||||||
|
self.assertEqual(value, f.to_db_string(python_value))
|
||||||
|
else:
|
||||||
|
self.assertEquals(python_value, value)
|
||||||
|
|
||||||
|
def _insert_sample_data(self):
|
||||||
|
dt = date(1970, 1, 1)
|
||||||
|
self.database.insert([
|
||||||
|
ModelWithNullable(date_field='2016-08-30',
|
||||||
|
null_str='', null_int=42, null_date=dt,
|
||||||
|
null_array=None),
|
||||||
|
ModelWithNullable(date_field='2016-08-30',
|
||||||
|
null_str='nothing', null_int=None, null_date=None,
|
||||||
|
null_array=[1, 2, 3]),
|
||||||
|
ModelWithNullable(date_field='2016-08-31',
|
||||||
|
null_str=None, null_int=42, null_date=dt,
|
||||||
|
null_array=[]),
|
||||||
|
ModelWithNullable(date_field='2016-08-31',
|
||||||
|
null_str=None, null_int=None, null_date=None,
|
||||||
|
null_array=[3, 2, 1])
|
||||||
|
])
|
||||||
|
|
||||||
|
def _assert_sample_data(self, results):
|
||||||
|
dt = date(1970, 1, 1)
|
||||||
|
self.assertEquals(len(results), 4)
|
||||||
|
self.assertIsNone(results[0].null_str)
|
||||||
|
self.assertEquals(results[0].null_int, 42)
|
||||||
|
self.assertEquals(results[0].null_date, dt)
|
||||||
|
self.assertIsNone(results[1].null_date)
|
||||||
|
self.assertEquals(results[1].null_str, 'nothing')
|
||||||
|
self.assertIsNone(results[1].null_date)
|
||||||
|
self.assertIsNone(results[2].null_str)
|
||||||
|
self.assertEquals(results[2].null_date, dt)
|
||||||
|
self.assertEquals(results[2].null_int, 42)
|
||||||
|
self.assertIsNone(results[3].null_int)
|
||||||
|
self.assertIsNone(results[3].null_str)
|
||||||
|
self.assertIsNone(results[3].null_date)
|
||||||
|
|
||||||
|
self.assertIsNone(results[0].null_array)
|
||||||
|
self.assertEquals(results[1].null_array, [1, 2, 3])
|
||||||
|
self.assertEquals(results[2].null_array, [])
|
||||||
|
self.assertEquals(results[3].null_array, [3, 2, 1])
|
||||||
|
|
||||||
|
def test_insert_and_select(self):
|
||||||
|
self._insert_sample_data()
|
||||||
|
query = 'SELECT * from $table ORDER BY date_field'
|
||||||
|
results = list(self.database.select(query, ModelWithNullable))
|
||||||
|
self._assert_sample_data(results)
|
||||||
|
|
||||||
|
def test_ad_hoc_model(self):
|
||||||
|
self._insert_sample_data()
|
||||||
|
query = 'SELECT * from $db.modelwithnullable ORDER BY date_field'
|
||||||
|
results = list(self.database.select(query))
|
||||||
|
self._assert_sample_data(results)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelWithNullable(Model):
|
||||||
|
|
||||||
|
date_field = DateField()
|
||||||
|
null_str = NullableField(StringField(), extra_null_values={''})
|
||||||
|
null_int = NullableField(Int32Field())
|
||||||
|
null_date = NullableField(DateField())
|
||||||
|
null_array = NullableField(ArrayField(Int32Field()))
|
||||||
|
|
||||||
|
engine = MergeTree('date_field', ('date_field',))
|
|
@ -138,6 +138,30 @@ class QuerySetTestCase(TestCaseWithData):
|
||||||
self._test_qs(qs.filter(num__in=(1, 2, 3)), 3)
|
self._test_qs(qs.filter(num__in=(1, 2, 3)), 3)
|
||||||
self._test_qs(qs.filter(num__in=range(1, 4)), 3)
|
self._test_qs(qs.filter(num__in=range(1, 4)), 3)
|
||||||
|
|
||||||
|
def test_slicing(self):
|
||||||
|
db = Database('system')
|
||||||
|
numbers = range(100)
|
||||||
|
qs = Numbers.objects_in(db)
|
||||||
|
self.assertEquals(qs[0].number, numbers[0])
|
||||||
|
self.assertEquals(qs[5].number, numbers[5])
|
||||||
|
self.assertEquals([row.number for row in qs[:1]], numbers[:1])
|
||||||
|
self.assertEquals([row.number for row in qs[:10]], numbers[:10])
|
||||||
|
self.assertEquals([row.number for row in qs[3:10]], numbers[3:10])
|
||||||
|
self.assertEquals([row.number for row in qs[9:10]], numbers[9:10])
|
||||||
|
self.assertEquals([row.number for row in qs[10:10]], numbers[10:10])
|
||||||
|
|
||||||
|
def test_invalid_slicing(self):
|
||||||
|
db = Database('system')
|
||||||
|
qs = Numbers.objects_in(db)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
qs[3:10:2]
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
qs[-5]
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
qs[:-5]
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
qs[50:1]
|
||||||
|
|
||||||
|
|
||||||
Color = Enum('Color', u'red blue green yellow brown white black')
|
Color = Enum('Color', u'red blue green yellow brown white black')
|
||||||
|
|
||||||
|
@ -150,3 +174,8 @@ class SampleModel(Model):
|
||||||
color = Enum8Field(Color)
|
color = Enum8Field(Color)
|
||||||
|
|
||||||
engine = MergeTree('materialized_date', ('materialized_date',))
|
engine = MergeTree('materialized_date', ('materialized_date',))
|
||||||
|
|
||||||
|
|
||||||
|
class Numbers(Model):
|
||||||
|
|
||||||
|
number = UInt64Field()
|
Loading…
Reference in New Issue
Block a user