Merge branch 'qs-prewhere' into qs-with-totals

This commit is contained in:
M1ha 2018-12-17 10:30:01 +05:00
commit d912bf56d7
9 changed files with 127 additions and 14 deletions

View File

@ -1,6 +1,12 @@
Change Log Change Log
========== ==========
Unreleased
----------
- Added `timeout` parameter to database initializer (SUHAR1K)
- Added `verify_ssl_cert` parameter to database initializer
- Added `final()` method to querysets (M1hacka)
v1.0.3 v1.0.3
------ ------
- Bug fix: `QuerySet.count()` ignores slicing - Bug fix: `QuerySet.count()` ignores slicing

View File

@ -10,7 +10,7 @@ infi.clickhouse_orm.database
Database instances connect to a specific ClickHouse database for running queries, Database instances connect to a specific ClickHouse database for running queries,
inserting data and other operations. inserting data and other operations.
#### Database(db_name, db_url="http://localhost:8123/", username=None, password=None, readonly=False, autocreate=True) #### Database(db_name, db_url="http://localhost:8123/", username=None, password=None, readonly=False, autocreate=True, timeout=60, verify_ssl_cert=True)
Initializes a database instance. Unless it's readonly, the database will be Initializes a database instance. Unless it's readonly, the database will be
@ -21,7 +21,9 @@ created on the ClickHouse server if it does not already exist.
- `username`: optional connection credentials. - `username`: optional connection credentials.
- `password`: optional connection credentials. - `password`: optional connection credentials.
- `readonly`: use a read-only connection. - `readonly`: use a read-only connection.
- `autocreate`: automatically create the database if does not exist (unless in readonly mode). - `autocreate`: automatically create the database if it does not exist (unless in readonly mode).
- `timeout`: the connection timeout in seconds.
- `verify_ssl_cert`: whether to verify the server's certificate when connecting via HTTPS.
#### add_setting(name, value) #### add_setting(name, value)
@ -865,6 +867,13 @@ Returns a copy of this queryset that includes only rows matching the conditions.
Add q object to query if it specified. Add q object to query if it specified.
#### final()
Adds a FINAL modifier to table, meaning data will be collapsed to final version.
Can be used with `CollapsingMergeTree` engine only.
#### only(*field_names) #### only(*field_names)
@ -966,6 +975,13 @@ Returns a copy of this queryset that includes only rows matching the conditions.
Add q object to query if it specified. Add q object to query if it specified.
#### final()
Adds a FINAL modifier to table, meaning data will be collapsed to final version.
Can be used with `CollapsingMergeTree` engine only.
#### group_by(*args) #### group_by(*args)

View File

@ -125,6 +125,17 @@ Adds a DISTINCT clause to the query, meaning that any duplicate rows in the resu
>>> Person.objects_in(database).only('first_name').distinct().count() >>> Person.objects_in(database).only('first_name').distinct().count()
94 94
Final
--------
This method can be used only with CollapsingMergeTree engine.
Adds a FINAL modifier to the query, meaning data is selected fully "collapsed" by sign field.
>>> Person.objects_in(database).count()
100
>>> Person.objects_in(database).final().count()
94
Slicing Slicing
------- -------

View File

@ -25,6 +25,7 @@
* [Ordering](querysets.md#ordering) * [Ordering](querysets.md#ordering)
* [Omitting Fields](querysets.md#omitting-fields) * [Omitting Fields](querysets.md#omitting-fields)
* [Distinct](querysets.md#distinct) * [Distinct](querysets.md#distinct)
* [Final](querysets.md#final)
* [Slicing](querysets.md#slicing) * [Slicing](querysets.md#slicing)
* [Pagination](querysets.md#pagination) * [Pagination](querysets.md#pagination)
* [Aggregation](querysets.md#aggregation) * [Aggregation](querysets.md#aggregation)

View File

@ -73,7 +73,8 @@ class Database(object):
''' '''
def __init__(self, db_name, db_url='http://localhost:8123/', def __init__(self, db_name, db_url='http://localhost:8123/',
username=None, password=None, readonly=False, autocreate=True): username=None, password=None, readonly=False, autocreate=True,
timeout=60, verify_ssl_cert=True):
''' '''
Initializes a database instance. Unless it's readonly, the database will be Initializes a database instance. Unless it's readonly, the database will be
created on the ClickHouse server if it does not already exist. created on the ClickHouse server if it does not already exist.
@ -83,15 +84,20 @@ class Database(object):
- `username`: optional connection credentials. - `username`: optional connection credentials.
- `password`: optional connection credentials. - `password`: optional connection credentials.
- `readonly`: use a read-only connection. - `readonly`: use a read-only connection.
- `autocreate`: automatically create the database if does not exist (unless in readonly mode). - `autocreate`: automatically create the database if it does not exist (unless in readonly mode).
- `timeout`: the connection timeout in seconds.
- `verify_ssl_cert`: whether to verify the server's certificate when connecting via HTTPS.
''' '''
self.db_name = db_name self.db_name = db_name
self.db_url = db_url self.db_url = db_url
self.username = username self.username = username
self.password = password self.password = password
self.readonly = False self.readonly = False
self.timeout = timeout
self.request_session = requests.Session()
self.request_session.verify = verify_ssl_cert
self.settings = {} self.settings = {}
self.db_exists = False self.db_exists = False # this is required before running _is_existing_database
self.db_exists = self._is_existing_database() self.db_exists = self._is_existing_database()
if readonly: if readonly:
if not self.db_exists: if not self.db_exists:
@ -116,6 +122,7 @@ class Database(object):
Deletes the database on the ClickHouse server. Deletes the database on the ClickHouse server.
''' '''
self._send('DROP DATABASE `%s`' % self.db_name) self._send('DROP DATABASE `%s`' % self.db_name)
self.db_exists = False
def create_table(self, model_class): def create_table(self, model_class):
''' '''
@ -319,7 +326,7 @@ class Database(object):
if isinstance(data, string_types): if isinstance(data, string_types):
data = data.encode('utf-8') data = data.encode('utf-8')
params = self._build_params(settings) params = self._build_params(settings)
r = requests.post(self.db_url, params=params, data=data, stream=stream) r = self.request_session.post(self.db_url, params=params, data=data, stream=stream, timeout=self.timeout)
if r.status_code != 200: if r.status_code != 200:
raise ServerError(r.text) raise ServerError(r.text)
return r return r

View File

@ -1,8 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import six import six
import pytz import pytz
from copy import copy, deepcopy from copy import copy, deepcopy
from math import ceil from math import ceil
from .engines import CollapsingMergeTree
from .utils import comma_join from .utils import comma_join
@ -291,6 +294,7 @@ class QuerySet(object):
self._fields = model_cls.fields().keys() self._fields = model_cls.fields().keys()
self._limits = None self._limits = None
self._distinct = False self._distinct = False
self._final = False
def __iter__(self): def __iter__(self):
""" """
@ -336,9 +340,10 @@ class QuerySet(object):
Returns the whole query as a SQL string. Returns the whole query as a SQL string.
""" """
distinct = 'DISTINCT ' if self._distinct else '' distinct = 'DISTINCT ' if self._distinct else ''
final = ' FINAL' if self._final else ''
params = (distinct, self.select_fields_as_sql(), self._model_cls.table_name()) params = (distinct, self.select_fields_as_sql(), self._model_cls.table_name(), final)
sql = u'SELECT %s%s\nFROM `%s`' % params sql = u'SELECT %s%s\nFROM `%s`%s' % params
if self._prewhere_q: if self._prewhere_q:
sql += '\nPREWHERE ' + self.conditions_as_sql(self._prewhere_q) sql += '\nPREWHERE ' + self.conditions_as_sql(self._prewhere_q)
@ -410,9 +415,10 @@ class QuerySet(object):
def _filter_or_exclude(self, *q, **kwargs): def _filter_or_exclude(self, *q, **kwargs):
reverse = kwargs.pop('reverse', False) reverse = kwargs.pop('reverse', False)
prewhere = kwargs.pop('prewhere', False) prewhere = kwargs.pop('prewhere', False)
condition = copy(self._where_q)
qs = copy(self) qs = copy(self)
condition = Q()
for q_obj in q: for q_obj in q:
condition &= q_obj condition &= q_obj
@ -422,6 +428,7 @@ class QuerySet(object):
if reverse: if reverse:
condition = ~condition condition = ~condition
condition = copy(self._prewhere_q if prewhere else self._where_q) & condition
if prewhere: if prewhere:
qs._prewhere_q = condition qs._prewhere_q = condition
else: else:
@ -479,6 +486,18 @@ class QuerySet(object):
qs._distinct = True qs._distinct = True
return qs return qs
def final(self):
"""
Adds a FINAL modifier to table, meaning data will be collapsed to final version.
Can be used with `CollapsingMergeTree` engine only.
"""
if not isinstance(self._model_cls.engine, CollapsingMergeTree):
raise TypeError('final() method can be used only with CollapsingMergeTree engine')
qs = copy(self)
qs._final = True
return qs
def aggregate(self, *args, **kwargs): def aggregate(self, *args, **kwargs):
""" """
Returns an `AggregateQuerySet` over this query, with `args` serving as Returns an `AggregateQuerySet` over this query, with `args` serving as

View File

@ -148,4 +148,4 @@ data = [
{"first_name": "Whitney", "last_name": "Scott", "birthday": "1971-07-04", "height": "1.70"}, {"first_name": "Whitney", "last_name": "Scott", "birthday": "1971-07-04", "height": "1.70"},
{"first_name": "Wynter", "last_name": "Garcia", "birthday": "1975-01-10", "height": "1.69"}, {"first_name": "Wynter", "last_name": "Garcia", "birthday": "1975-01-10", "height": "1.69"},
{"first_name": "Yolanda", "last_name": "Duke", "birthday": "1997-02-25", "height": "1.74"} {"first_name": "Yolanda", "last_name": "Duke", "birthday": "1997-02-25", "height": "1.74"}
]; ]

View File

@ -151,10 +151,15 @@ class DatabaseTestCase(TestCaseWithData):
exc = cm.exception exc = cm.exception
self.assertEqual(exc.code, 81) self.assertEqual(exc.code, 81)
self.assertEqual(exc.message, "Database db_not_here doesn't exist") self.assertEqual(exc.message, "Database db_not_here doesn't exist")
# Now create the database - should succeed # Create and delete the db twice, to ensure db_exists gets updated
db.create_database() for i in range(2):
db.create_table(Person) # Now create the database - should succeed
db.drop_database() db.create_database()
self.assertTrue(db.db_exists)
db.create_table(Person)
# Drop the database
db.drop_database()
self.assertFalse(db.db_exists)
def test_preexisting_db(self): def test_preexisting_db(self):
db = Database(self.database.db_name, autocreate=False) db = Database(self.database.db_name, autocreate=False)

View File

@ -111,6 +111,22 @@ class QuerySetTestCase(TestCaseWithData):
self._test_qs(qs.filter(birthday=date(1970, 12, 2)), 1) self._test_qs(qs.filter(birthday=date(1970, 12, 2)), 1)
self._test_qs(qs.filter(birthday__lte=date(1970, 12, 2)), 3) self._test_qs(qs.filter(birthday__lte=date(1970, 12, 2)), 3)
def test_mutiple_filter(self):
qs = Person.objects_in(self.database)
# Single filter call with multiple conditions is ANDed
self._test_qs(qs.filter(first_name='Ciaran', last_name='Carver'), 1)
# Separate filter calls are also ANDed
self._test_qs(qs.filter(first_name='Ciaran').filter(last_name='Carver'), 1)
self._test_qs(qs.filter(birthday='1970-12-02').filter(birthday='1986-01-07'), 0)
def test_multiple_exclude(self):
qs = Person.objects_in(self.database)
# Single exclude call with multiple conditions is ANDed
self._test_qs(qs.exclude(first_name='Ciaran', last_name='Carver'), 99)
# Separate exclude calls are ORed
self._test_qs(qs.exclude(first_name='Ciaran').exclude(last_name='Carver'), 98)
self._test_qs(qs.exclude(birthday='1970-12-02').exclude(birthday='1986-01-07'), 98)
def test_only(self): def test_only(self):
qs = Person.objects_in(self.database).only('first_name', 'last_name') qs = Person.objects_in(self.database).only('first_name', 'last_name')
for person in qs: for person in qs:
@ -148,6 +164,20 @@ class QuerySetTestCase(TestCaseWithData):
SampleModel(timestamp=now, num=4, color=Color.white), SampleModel(timestamp=now, num=4, color=Color.white),
]) ])
def _insert_sample_collapsing_model(self):
self.database.create_table(SampleCollapsingModel)
now = datetime.now()
self.database.insert([
SampleCollapsingModel(timestamp=now, num=1, color=Color.red),
SampleCollapsingModel(timestamp=now, num=2, color=Color.red),
SampleCollapsingModel(timestamp=now, num=2, color=Color.red, sign=-1),
SampleCollapsingModel(timestamp=now, num=2, color=Color.green),
SampleCollapsingModel(timestamp=now, num=3, color=Color.white),
SampleCollapsingModel(timestamp=now, num=4, color=Color.white, sign=1),
SampleCollapsingModel(timestamp=now, num=4, color=Color.white, sign=-1),
SampleCollapsingModel(timestamp=now, num=4, color=Color.blue, sign=1),
])
def test_filter_enum_field(self): def test_filter_enum_field(self):
self._insert_sample_model() self._insert_sample_model()
qs = SampleModel.objects_in(self.database) qs = SampleModel.objects_in(self.database)
@ -256,6 +286,17 @@ class QuerySetTestCase(TestCaseWithData):
self._test_qs(qs[70:80], 10) self._test_qs(qs[70:80], 10)
self._test_qs(qs[80:], 20) self._test_qs(qs[80:], 20)
def test_final(self):
# Final can be used with CollapsingMergeTree engine only
with self.assertRaises(TypeError):
Person.objects_in(self.database).final()
self._insert_sample_collapsing_model()
res = list(SampleCollapsingModel.objects_in(self.database).final().order_by('num'))
self.assertEqual(4, len(res))
for item, exp_color in zip(res, (Color.red, Color.green, Color.white, Color.blue)):
self.assertEqual(exp_color, item.color)
class AggregateTestCase(TestCaseWithData): class AggregateTestCase(TestCaseWithData):
@ -410,6 +451,13 @@ class SampleModel(Model):
engine = MergeTree('materialized_date', ('materialized_date',)) engine = MergeTree('materialized_date', ('materialized_date',))
class SampleCollapsingModel(SampleModel):
sign = Int8Field(default=1)
engine = CollapsingMergeTree('materialized_date', ('num',), 'sign')
class Numbers(Model): class Numbers(Model):
number = UInt64Field() number = UInt64Field()