clickhouse-orm 3.0.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clickhouse_orm/__init__.py +14 -0
- clickhouse_orm/database.py +457 -0
- clickhouse_orm/engines.py +346 -0
- clickhouse_orm/fields.py +665 -0
- clickhouse_orm/funcs.py +1841 -0
- clickhouse_orm/migrations.py +287 -0
- clickhouse_orm/models.py +617 -0
- clickhouse_orm/query.py +701 -0
- clickhouse_orm/system_models.py +170 -0
- clickhouse_orm/utils.py +176 -0
- clickhouse_orm-3.0.1.dist-info/METADATA +90 -0
- clickhouse_orm-3.0.1.dist-info/RECORD +14 -0
- clickhouse_orm-3.0.1.dist-info/WHEEL +5 -0
- clickhouse_orm-3.0.1.dist-info/licenses/LICENSE +27 -0
clickhouse_orm/query.py
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import copy, deepcopy
|
|
4
|
+
from math import ceil
|
|
5
|
+
|
|
6
|
+
import pytz
|
|
7
|
+
|
|
8
|
+
from .engines import CollapsingMergeTree, ReplacingMergeTree
|
|
9
|
+
from .utils import Page, arg_to_sql, comma_join, string_or_func
|
|
10
|
+
|
|
11
|
+
# TODO
|
|
12
|
+
# - check that field names are valid
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Operator:
|
|
16
|
+
"""
|
|
17
|
+
Base class for filtering operators.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def to_sql(self, model_cls, field_name, value):
|
|
21
|
+
"""
|
|
22
|
+
Subclasses should implement this method. It returns an SQL string
|
|
23
|
+
that applies this operator on the given field and value.
|
|
24
|
+
"""
|
|
25
|
+
raise NotImplementedError # pragma: no cover
|
|
26
|
+
|
|
27
|
+
def _value_to_sql(self, field, value, quote=True):
|
|
28
|
+
if isinstance(value, Cond):
|
|
29
|
+
# This is an 'in-database' value, rather than a python one
|
|
30
|
+
return value.to_sql()
|
|
31
|
+
|
|
32
|
+
return field.to_db_string(field.to_python(value, pytz.utc), quote)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SimpleOperator(Operator):
|
|
36
|
+
"""
|
|
37
|
+
A simple binary operator such as a=b, a<b, a>b etc.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, sql_operator, sql_for_null=None):
|
|
41
|
+
self._sql_operator = sql_operator
|
|
42
|
+
self._sql_for_null = sql_for_null
|
|
43
|
+
|
|
44
|
+
def to_sql(self, model_cls, field_name, value):
|
|
45
|
+
field = getattr(model_cls, field_name)
|
|
46
|
+
value = self._value_to_sql(field, value)
|
|
47
|
+
if value == "\\N" and self._sql_for_null is not None:
|
|
48
|
+
return " ".join([field_name, self._sql_for_null])
|
|
49
|
+
return " ".join([field_name, self._sql_operator, value])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InOperator(Operator):
|
|
53
|
+
"""
|
|
54
|
+
An operator that implements IN.
|
|
55
|
+
Accepts 3 different types of values:
|
|
56
|
+
- a list or tuple of simple values
|
|
57
|
+
- a string (used verbatim as the contents of the parenthesis)
|
|
58
|
+
- a queryset (subquery)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def to_sql(self, model_cls, field_name, value):
|
|
62
|
+
field = getattr(model_cls, field_name)
|
|
63
|
+
if isinstance(value, QuerySet):
|
|
64
|
+
value = value.as_sql()
|
|
65
|
+
elif isinstance(value, str):
|
|
66
|
+
pass
|
|
67
|
+
else:
|
|
68
|
+
value = comma_join([self._value_to_sql(field, v) for v in value])
|
|
69
|
+
return "%s IN (%s)" % (field_name, value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LikeOperator(Operator):
|
|
73
|
+
"""
|
|
74
|
+
A LIKE operator that matches the field to a given pattern. Can be
|
|
75
|
+
case sensitive or insensitive.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, pattern, case_sensitive=True):
|
|
79
|
+
self._pattern = pattern
|
|
80
|
+
self._case_sensitive = case_sensitive
|
|
81
|
+
|
|
82
|
+
def to_sql(self, model_cls, field_name, value):
|
|
83
|
+
field = getattr(model_cls, field_name)
|
|
84
|
+
value = self._value_to_sql(field, value, quote=False)
|
|
85
|
+
value = value.replace("\\", "\\\\").replace("%", "\\\\%").replace("_", "\\\\_")
|
|
86
|
+
pattern = self._pattern.format(value)
|
|
87
|
+
if self._case_sensitive:
|
|
88
|
+
return "%s LIKE '%s'" % (field_name, pattern)
|
|
89
|
+
else:
|
|
90
|
+
return "lowerUTF8(%s) LIKE lowerUTF8('%s')" % (field_name, pattern)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class IExactOperator(Operator):
|
|
94
|
+
"""
|
|
95
|
+
An operator for case insensitive string comparison.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def to_sql(self, model_cls, field_name, value):
|
|
99
|
+
field = getattr(model_cls, field_name)
|
|
100
|
+
value = self._value_to_sql(field, value)
|
|
101
|
+
return "lowerUTF8(%s) = lowerUTF8(%s)" % (field_name, value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class NotOperator(Operator):
|
|
105
|
+
"""
|
|
106
|
+
A wrapper around another operator, which negates it.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, base_operator):
|
|
110
|
+
self._base_operator = base_operator
|
|
111
|
+
|
|
112
|
+
def to_sql(self, model_cls, field_name, value):
|
|
113
|
+
# Negate the base operator
|
|
114
|
+
return "NOT (%s)" % self._base_operator.to_sql(model_cls, field_name, value)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BetweenOperator(Operator):
|
|
118
|
+
"""
|
|
119
|
+
An operator that implements BETWEEN.
|
|
120
|
+
Accepts list or tuple of two elements and generates sql condition:
|
|
121
|
+
- 'BETWEEN value[0] AND value[1]' if value[0] and value[1] are not None and not empty
|
|
122
|
+
Then imitations of BETWEEN, where one of two limits is missing
|
|
123
|
+
- '>= value[0]' if value[1] is None or empty
|
|
124
|
+
- '<= value[1]' if value[0] is None or empty
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def to_sql(self, model_cls, field_name, value):
|
|
128
|
+
field = getattr(model_cls, field_name)
|
|
129
|
+
value0 = self._value_to_sql(field, value[0]) if value[0] is not None or len(str(value[0])) > 0 else None
|
|
130
|
+
value1 = self._value_to_sql(field, value[1]) if value[1] is not None or len(str(value[1])) > 0 else None
|
|
131
|
+
if value0 and value1:
|
|
132
|
+
return "%s BETWEEN %s AND %s" % (field_name, value0, value1)
|
|
133
|
+
if value0 and not value1:
|
|
134
|
+
return " ".join([field_name, ">=", value0])
|
|
135
|
+
if value1 and not value0:
|
|
136
|
+
return " ".join([field_name, "<=", value1])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Define the set of builtin operators
|
|
140
|
+
|
|
141
|
+
_operators = {}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def register_operator(name, sql):
|
|
145
|
+
_operators[name] = sql
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
register_operator("eq", SimpleOperator("=", "IS NULL"))
|
|
149
|
+
register_operator("ne", SimpleOperator("!=", "IS NOT NULL"))
|
|
150
|
+
register_operator("gt", SimpleOperator(">"))
|
|
151
|
+
register_operator("gte", SimpleOperator(">="))
|
|
152
|
+
register_operator("lt", SimpleOperator("<"))
|
|
153
|
+
register_operator("lte", SimpleOperator("<="))
|
|
154
|
+
register_operator("between", BetweenOperator())
|
|
155
|
+
register_operator("in", InOperator())
|
|
156
|
+
register_operator("not_in", NotOperator(InOperator()))
|
|
157
|
+
register_operator("contains", LikeOperator("%{}%"))
|
|
158
|
+
register_operator("startswith", LikeOperator("{}%"))
|
|
159
|
+
register_operator("endswith", LikeOperator("%{}"))
|
|
160
|
+
register_operator("icontains", LikeOperator("%{}%", False))
|
|
161
|
+
register_operator("istartswith", LikeOperator("{}%", False))
|
|
162
|
+
register_operator("iendswith", LikeOperator("%{}", False))
|
|
163
|
+
register_operator("iexact", IExactOperator())
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class Cond:
|
|
167
|
+
"""
|
|
168
|
+
An abstract object for storing a single query condition Field + Operator + Value.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def to_sql(self, model_cls):
|
|
172
|
+
raise NotImplementedError
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class FieldCond(Cond):
|
|
176
|
+
"""
|
|
177
|
+
A single query condition made up of Field + Operator + Value.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(self, field_name, operator, value):
|
|
181
|
+
self._field_name = field_name
|
|
182
|
+
self._operator = _operators.get(operator)
|
|
183
|
+
if self._operator is None:
|
|
184
|
+
# The field name contains __ like my__field
|
|
185
|
+
self._field_name = field_name + "__" + operator
|
|
186
|
+
self._operator = _operators["eq"]
|
|
187
|
+
self._value = value
|
|
188
|
+
|
|
189
|
+
def to_sql(self, model_cls):
|
|
190
|
+
return self._operator.to_sql(model_cls, self._field_name, self._value)
|
|
191
|
+
|
|
192
|
+
def __deepcopy__(self, memo):
|
|
193
|
+
res = copy(self)
|
|
194
|
+
res._value = deepcopy(self._value)
|
|
195
|
+
return res
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class Q:
|
|
199
|
+
AND_MODE = "AND"
|
|
200
|
+
OR_MODE = "OR"
|
|
201
|
+
|
|
202
|
+
def __init__(self, *filter_funcs, **filter_fields):
|
|
203
|
+
self._conds = list(filter_funcs) + [self._build_cond(k, v) for k, v in filter_fields.items()]
|
|
204
|
+
self._children = []
|
|
205
|
+
self._negate = False
|
|
206
|
+
self._mode = self.AND_MODE
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def is_empty(self):
|
|
210
|
+
"""
|
|
211
|
+
Checks if there are any conditions in Q object
|
|
212
|
+
Returns: Boolean
|
|
213
|
+
"""
|
|
214
|
+
return not (self._conds or self._children)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def _construct_from(cls, l_child, r_child, mode):
|
|
218
|
+
if mode == l_child._mode and not l_child._negate:
|
|
219
|
+
q = deepcopy(l_child)
|
|
220
|
+
q._children.append(deepcopy(r_child))
|
|
221
|
+
else:
|
|
222
|
+
q = cls()
|
|
223
|
+
q._children = [l_child, r_child]
|
|
224
|
+
q._mode = mode
|
|
225
|
+
|
|
226
|
+
return q
|
|
227
|
+
|
|
228
|
+
def _build_cond(self, key, value):
|
|
229
|
+
if "__" in key:
|
|
230
|
+
field_name, operator = key.rsplit("__", 1)
|
|
231
|
+
else:
|
|
232
|
+
field_name, operator = key, "eq"
|
|
233
|
+
return FieldCond(field_name, operator, value)
|
|
234
|
+
|
|
235
|
+
def to_sql(self, model_cls):
|
|
236
|
+
condition_sql = []
|
|
237
|
+
|
|
238
|
+
if self._conds:
|
|
239
|
+
condition_sql.extend([cond.to_sql(model_cls) for cond in self._conds])
|
|
240
|
+
|
|
241
|
+
if self._children:
|
|
242
|
+
condition_sql.extend([child.to_sql(model_cls) for child in self._children if child])
|
|
243
|
+
|
|
244
|
+
if not condition_sql:
|
|
245
|
+
# Empty Q() object returns everything
|
|
246
|
+
sql = "1"
|
|
247
|
+
elif len(condition_sql) == 1:
|
|
248
|
+
# Skip not needed brackets over single condition
|
|
249
|
+
sql = condition_sql[0]
|
|
250
|
+
else:
|
|
251
|
+
# Each condition must be enclosed in brackets, or order of operations may be wrong
|
|
252
|
+
sql = "(%s)" % f") {self._mode} (".join(condition_sql)
|
|
253
|
+
|
|
254
|
+
if self._negate:
|
|
255
|
+
sql = "NOT (%s)" % sql
|
|
256
|
+
|
|
257
|
+
return sql
|
|
258
|
+
|
|
259
|
+
def __or__(self, other):
|
|
260
|
+
if not isinstance(other, Q):
|
|
261
|
+
return NotImplemented
|
|
262
|
+
|
|
263
|
+
return self.__class__._construct_from(self, other, self.OR_MODE)
|
|
264
|
+
|
|
265
|
+
def __and__(self, other):
|
|
266
|
+
if not isinstance(other, Q):
|
|
267
|
+
return NotImplemented
|
|
268
|
+
|
|
269
|
+
return self.__class__._construct_from(self, other, self.AND_MODE)
|
|
270
|
+
|
|
271
|
+
def __invert__(self):
|
|
272
|
+
q = copy(self)
|
|
273
|
+
q._negate = True
|
|
274
|
+
return q
|
|
275
|
+
|
|
276
|
+
def __bool__(self):
|
|
277
|
+
return not self.is_empty
|
|
278
|
+
|
|
279
|
+
def __deepcopy__(self, memo):
|
|
280
|
+
q = self.__class__()
|
|
281
|
+
q._conds = [deepcopy(cond) for cond in self._conds]
|
|
282
|
+
q._negate = self._negate
|
|
283
|
+
q._mode = self._mode
|
|
284
|
+
|
|
285
|
+
if self._children:
|
|
286
|
+
q._children = [deepcopy(child) for child in self._children]
|
|
287
|
+
|
|
288
|
+
return q
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class QuerySet:
|
|
292
|
+
"""
|
|
293
|
+
A queryset is an object that represents a database query using a specific `Model`.
|
|
294
|
+
It is lazy, meaning that it does not hit the database until you iterate over its
|
|
295
|
+
matching rows (model instances).
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(self, model_cls, database):
|
|
299
|
+
"""
|
|
300
|
+
Initializer. It is possible to create a queryset like this, but the standard
|
|
301
|
+
way is to use `MyModel.objects_in(database)`.
|
|
302
|
+
"""
|
|
303
|
+
self.model = model_cls
|
|
304
|
+
self._model_cls = model_cls
|
|
305
|
+
self._database = database
|
|
306
|
+
self._order_by = []
|
|
307
|
+
self._where_q = Q()
|
|
308
|
+
self._prewhere_q = Q()
|
|
309
|
+
self._grouping_fields = []
|
|
310
|
+
self._grouping_with_totals = False
|
|
311
|
+
self._fields = model_cls.fields().keys()
|
|
312
|
+
self._limits = None
|
|
313
|
+
self._limit_by = None
|
|
314
|
+
self._limit_by_fields = None
|
|
315
|
+
self._distinct = False
|
|
316
|
+
self._final = False
|
|
317
|
+
|
|
318
|
+
def __iter__(self):
|
|
319
|
+
"""
|
|
320
|
+
Iterates over the model instances matching this queryset
|
|
321
|
+
"""
|
|
322
|
+
return self._database.select(self.as_sql(), self._model_cls)
|
|
323
|
+
|
|
324
|
+
def __bool__(self):
|
|
325
|
+
"""
|
|
326
|
+
Returns true if this queryset matches any rows.
|
|
327
|
+
"""
|
|
328
|
+
return bool(self.count())
|
|
329
|
+
|
|
330
|
+
def __nonzero__(self): # Python 2 compatibility
|
|
331
|
+
return type(self).__bool__(self)
|
|
332
|
+
|
|
333
|
+
def __str__(self):
|
|
334
|
+
return self.as_sql()
|
|
335
|
+
|
|
336
|
+
def __getitem__(self, s):
|
|
337
|
+
if isinstance(s, int):
|
|
338
|
+
# Single index
|
|
339
|
+
assert s >= 0, "negative indexes are not supported"
|
|
340
|
+
qs = copy(self)
|
|
341
|
+
qs._limits = (s, 1)
|
|
342
|
+
return next(iter(qs))
|
|
343
|
+
else:
|
|
344
|
+
# Slice
|
|
345
|
+
assert s.step in (None, 1), "step is not supported in slices"
|
|
346
|
+
start = s.start or 0
|
|
347
|
+
stop = s.stop or 2**63 - 1
|
|
348
|
+
assert start >= 0 and stop >= 0, "negative indexes are not supported"
|
|
349
|
+
assert start <= stop, "start of slice cannot be smaller than its end"
|
|
350
|
+
qs = copy(self)
|
|
351
|
+
qs._limits = (start, stop - start)
|
|
352
|
+
return qs
|
|
353
|
+
|
|
354
|
+
def limit_by(self, offset_limit, *fields_or_expr):
|
|
355
|
+
"""
|
|
356
|
+
Adds a LIMIT BY clause to the query.
|
|
357
|
+
- `offset_limit`: either an integer specifying the limit, or a tuple of integers (offset, limit).
|
|
358
|
+
- `fields_or_expr`: the field names or expressions to use in the clause.
|
|
359
|
+
"""
|
|
360
|
+
if isinstance(offset_limit, int):
|
|
361
|
+
# Single limit
|
|
362
|
+
offset_limit = (0, offset_limit)
|
|
363
|
+
offset = offset_limit[0]
|
|
364
|
+
limit = offset_limit[1]
|
|
365
|
+
assert offset >= 0 and limit >= 0, "negative limits are not supported"
|
|
366
|
+
qs = copy(self)
|
|
367
|
+
qs._limit_by = (offset, limit)
|
|
368
|
+
qs._limit_by_fields = fields_or_expr
|
|
369
|
+
return qs
|
|
370
|
+
|
|
371
|
+
def select_fields_as_sql(self):
|
|
372
|
+
"""
|
|
373
|
+
Returns the selected fields or expressions as a SQL string.
|
|
374
|
+
"""
|
|
375
|
+
fields = "*"
|
|
376
|
+
if self._fields:
|
|
377
|
+
fields = comma_join("`%s`" % field for field in self._fields)
|
|
378
|
+
return fields
|
|
379
|
+
|
|
380
|
+
def as_sql(self):
|
|
381
|
+
"""
|
|
382
|
+
Returns the whole query as a SQL string.
|
|
383
|
+
"""
|
|
384
|
+
distinct = "DISTINCT " if self._distinct else ""
|
|
385
|
+
final = " FINAL" if self._final else ""
|
|
386
|
+
table_name = "`%s`" % self._model_cls.table_name()
|
|
387
|
+
if self._model_cls.is_system_model():
|
|
388
|
+
table_name = "`system`." + table_name
|
|
389
|
+
params = (distinct, self.select_fields_as_sql(), table_name, final)
|
|
390
|
+
sql = "SELECT %s%s\nFROM %s%s" % params
|
|
391
|
+
|
|
392
|
+
if self._prewhere_q and not self._prewhere_q.is_empty:
|
|
393
|
+
sql += "\nPREWHERE " + self.conditions_as_sql(prewhere=True)
|
|
394
|
+
|
|
395
|
+
if self._where_q and not self._where_q.is_empty:
|
|
396
|
+
sql += "\nWHERE " + self.conditions_as_sql(prewhere=False)
|
|
397
|
+
|
|
398
|
+
if self._grouping_fields:
|
|
399
|
+
sql += "\nGROUP BY %s" % comma_join("`%s`" % field for field in self._grouping_fields)
|
|
400
|
+
|
|
401
|
+
if self._grouping_with_totals:
|
|
402
|
+
sql += " WITH TOTALS"
|
|
403
|
+
|
|
404
|
+
if self._order_by:
|
|
405
|
+
sql += "\nORDER BY " + self.order_by_as_sql()
|
|
406
|
+
|
|
407
|
+
if self._limit_by:
|
|
408
|
+
sql += "\nLIMIT %d, %d" % self._limit_by
|
|
409
|
+
sql += " BY %s" % comma_join(string_or_func(field) for field in self._limit_by_fields)
|
|
410
|
+
|
|
411
|
+
if self._limits:
|
|
412
|
+
sql += "\nLIMIT %d, %d" % self._limits
|
|
413
|
+
|
|
414
|
+
return sql
|
|
415
|
+
|
|
416
|
+
def order_by_as_sql(self):
|
|
417
|
+
"""
|
|
418
|
+
Returns the contents of the query's `ORDER BY` clause as a string.
|
|
419
|
+
"""
|
|
420
|
+
return comma_join(
|
|
421
|
+
[
|
|
422
|
+
"%s DESC" % field[1:] if isinstance(field, str) and field[0] == "-" else str(field)
|
|
423
|
+
for field in self._order_by
|
|
424
|
+
]
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def conditions_as_sql(self, prewhere=False):
|
|
428
|
+
"""
|
|
429
|
+
Returns the contents of the query's `WHERE` or `PREWHERE` clause as a string.
|
|
430
|
+
"""
|
|
431
|
+
q_object = self._prewhere_q if prewhere else self._where_q
|
|
432
|
+
return q_object.to_sql(self._model_cls)
|
|
433
|
+
|
|
434
|
+
def count(self):
|
|
435
|
+
"""
|
|
436
|
+
Returns the number of matching model instances.
|
|
437
|
+
"""
|
|
438
|
+
if self._distinct or self._limits:
|
|
439
|
+
# Use a subquery, since a simple count won't be accurate
|
|
440
|
+
sql = "SELECT count() FROM (%s)" % self.as_sql()
|
|
441
|
+
raw = self._database.raw(sql)
|
|
442
|
+
return int(raw) if raw else 0
|
|
443
|
+
|
|
444
|
+
# Simple case
|
|
445
|
+
conditions = (self._where_q & self._prewhere_q).to_sql(self._model_cls)
|
|
446
|
+
return self._database.count(self._model_cls, conditions)
|
|
447
|
+
|
|
448
|
+
def order_by(self, *field_names):
|
|
449
|
+
"""
|
|
450
|
+
Returns a copy of this queryset with the ordering changed.
|
|
451
|
+
"""
|
|
452
|
+
qs = copy(self)
|
|
453
|
+
qs._order_by = field_names
|
|
454
|
+
return qs
|
|
455
|
+
|
|
456
|
+
def only(self, *field_names):
|
|
457
|
+
"""
|
|
458
|
+
Returns a copy of this queryset limited to the specified field names.
|
|
459
|
+
Useful when there are large fields that are not needed,
|
|
460
|
+
or for creating a subquery to use with an IN operator.
|
|
461
|
+
"""
|
|
462
|
+
qs = copy(self)
|
|
463
|
+
qs._fields = field_names
|
|
464
|
+
return qs
|
|
465
|
+
|
|
466
|
+
def _filter_or_exclude(self, *q, **kwargs):
|
|
467
|
+
inverse = kwargs.pop("_inverse", False)
|
|
468
|
+
prewhere = kwargs.pop("prewhere", False)
|
|
469
|
+
|
|
470
|
+
qs = copy(self)
|
|
471
|
+
|
|
472
|
+
condition = Q()
|
|
473
|
+
for arg in q:
|
|
474
|
+
if isinstance(arg, Q):
|
|
475
|
+
condition &= arg
|
|
476
|
+
elif isinstance(arg, Cond):
|
|
477
|
+
condition &= Q(arg)
|
|
478
|
+
else:
|
|
479
|
+
raise TypeError(f"Invalid argument '{arg}' of type '{type(arg)}' to filter")
|
|
480
|
+
|
|
481
|
+
if kwargs:
|
|
482
|
+
condition &= Q(**kwargs)
|
|
483
|
+
|
|
484
|
+
if inverse:
|
|
485
|
+
condition = ~condition
|
|
486
|
+
|
|
487
|
+
condition = copy(self._prewhere_q if prewhere else self._where_q) & condition
|
|
488
|
+
if prewhere:
|
|
489
|
+
qs._prewhere_q = condition
|
|
490
|
+
else:
|
|
491
|
+
qs._where_q = condition
|
|
492
|
+
|
|
493
|
+
return qs
|
|
494
|
+
|
|
495
|
+
def filter(self, *q, **kwargs):
|
|
496
|
+
"""
|
|
497
|
+
Returns a copy of this queryset that includes only rows matching the conditions.
|
|
498
|
+
Pass `prewhere=True` to apply the conditions as PREWHERE instead of WHERE.
|
|
499
|
+
"""
|
|
500
|
+
return self._filter_or_exclude(*q, **kwargs)
|
|
501
|
+
|
|
502
|
+
def exclude(self, *q, **kwargs):
|
|
503
|
+
"""
|
|
504
|
+
Returns a copy of this queryset that excludes all rows matching the conditions.
|
|
505
|
+
Pass `prewhere=True` to apply the conditions as PREWHERE instead of WHERE.
|
|
506
|
+
"""
|
|
507
|
+
return self._filter_or_exclude(*q, _inverse=True, **kwargs)
|
|
508
|
+
|
|
509
|
+
def paginate(self, page_num=1, page_size=100):
|
|
510
|
+
"""
|
|
511
|
+
Returns a single page of model instances that match the queryset.
|
|
512
|
+
Note that `order_by` should be used first, to ensure a correct
|
|
513
|
+
partitioning of records into pages.
|
|
514
|
+
|
|
515
|
+
- `page_num`: the page number (1-based), or -1 to get the last page.
|
|
516
|
+
- `page_size`: number of records to return per page.
|
|
517
|
+
|
|
518
|
+
The result is a namedtuple containing `objects` (list), `number_of_objects`,
|
|
519
|
+
`pages_total`, `number` (of the current page), and `page_size`.
|
|
520
|
+
"""
|
|
521
|
+
count = self.count()
|
|
522
|
+
pages_total = int(ceil(count / float(page_size)))
|
|
523
|
+
if page_num == -1:
|
|
524
|
+
page_num = pages_total
|
|
525
|
+
elif page_num < 1:
|
|
526
|
+
raise ValueError("Invalid page number: %d" % page_num)
|
|
527
|
+
offset = (page_num - 1) * page_size
|
|
528
|
+
return Page(
|
|
529
|
+
objects=list(self[offset : offset + page_size]),
|
|
530
|
+
number_of_objects=count,
|
|
531
|
+
pages_total=pages_total,
|
|
532
|
+
number=page_num,
|
|
533
|
+
page_size=page_size,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def distinct(self):
|
|
537
|
+
"""
|
|
538
|
+
Adds a DISTINCT clause to the query, meaning that any duplicate rows
|
|
539
|
+
in the results will be omitted.
|
|
540
|
+
"""
|
|
541
|
+
qs = copy(self)
|
|
542
|
+
qs._distinct = True
|
|
543
|
+
return qs
|
|
544
|
+
|
|
545
|
+
def final(self):
|
|
546
|
+
"""
|
|
547
|
+
Adds a FINAL modifier to table, meaning data will be collapsed to final version.
|
|
548
|
+
Can be used with the `CollapsingMergeTree` and `ReplacingMergeTree` engines only.
|
|
549
|
+
"""
|
|
550
|
+
if not isinstance(self._model_cls.engine, (CollapsingMergeTree, ReplacingMergeTree)):
|
|
551
|
+
raise TypeError(
|
|
552
|
+
"final() method can be used only with the CollapsingMergeTree and ReplacingMergeTree engines"
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
qs = copy(self)
|
|
556
|
+
qs._final = True
|
|
557
|
+
return qs
|
|
558
|
+
|
|
559
|
+
def delete(self):
|
|
560
|
+
"""
|
|
561
|
+
Deletes all records matched by this queryset's conditions.
|
|
562
|
+
Note that ClickHouse performs deletions in the background, so they are not immediate.
|
|
563
|
+
"""
|
|
564
|
+
self._verify_mutation_allowed()
|
|
565
|
+
conditions = (self._where_q & self._prewhere_q).to_sql(self._model_cls)
|
|
566
|
+
sql = "ALTER TABLE $db.`%s` DELETE WHERE %s" % (self._model_cls.table_name(), conditions)
|
|
567
|
+
self._database.raw(sql)
|
|
568
|
+
return self
|
|
569
|
+
|
|
570
|
+
def update(self, **kwargs):
|
|
571
|
+
"""
|
|
572
|
+
Updates all records matched by this queryset's conditions.
|
|
573
|
+
Keyword arguments specify the field names and expressions to use for the update.
|
|
574
|
+
Note that ClickHouse performs updates in the background, so they are not immediate.
|
|
575
|
+
"""
|
|
576
|
+
assert kwargs, "No fields specified for update"
|
|
577
|
+
self._verify_mutation_allowed()
|
|
578
|
+
fields = comma_join("`%s` = %s" % (name, arg_to_sql(expr)) for name, expr in kwargs.items())
|
|
579
|
+
conditions = (self._where_q & self._prewhere_q).to_sql(self._model_cls)
|
|
580
|
+
sql = "ALTER TABLE $db.`%s` UPDATE %s WHERE %s" % (self._model_cls.table_name(), fields, conditions)
|
|
581
|
+
self._database.raw(sql)
|
|
582
|
+
return self
|
|
583
|
+
|
|
584
|
+
def _verify_mutation_allowed(self):
|
|
585
|
+
"""
|
|
586
|
+
Checks that the queryset's state allows mutations. Raises an AssertionError if not.
|
|
587
|
+
"""
|
|
588
|
+
assert not self._limits, "Mutations are not allowed after slicing the queryset"
|
|
589
|
+
assert not self._limit_by, "Mutations are not allowed after calling limit_by(...)"
|
|
590
|
+
assert not self._distinct, "Mutations are not allowed after calling distinct()"
|
|
591
|
+
assert not self._final, "Mutations are not allowed after calling final()"
|
|
592
|
+
|
|
593
|
+
def aggregate(self, *args, **kwargs):
|
|
594
|
+
"""
|
|
595
|
+
Returns an `AggregateQuerySet` over this query, with `args` serving as
|
|
596
|
+
grouping fields and `kwargs` serving as calculated fields. At least one
|
|
597
|
+
calculated field is required. For example:
|
|
598
|
+
```
|
|
599
|
+
Event.objects_in(database).filter(date__gt='2017-08-01').aggregate('event_type', count='count()')
|
|
600
|
+
```
|
|
601
|
+
is equivalent to:
|
|
602
|
+
```
|
|
603
|
+
SELECT event_type, count() AS count FROM event
|
|
604
|
+
WHERE data > '2017-08-01'
|
|
605
|
+
GROUP BY event_type
|
|
606
|
+
```
|
|
607
|
+
"""
|
|
608
|
+
return AggregateQuerySet(self, args, kwargs)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class AggregateQuerySet(QuerySet):
|
|
612
|
+
"""
|
|
613
|
+
A queryset used for aggregation.
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
def __init__(self, base_qs, grouping_fields, calculated_fields):
|
|
617
|
+
"""
|
|
618
|
+
Initializer. Normally you should not call this but rather use `QuerySet.aggregate()`.
|
|
619
|
+
|
|
620
|
+
The grouping fields should be a list/tuple of field names from the model. For example:
|
|
621
|
+
```
|
|
622
|
+
('event_type', 'event_subtype')
|
|
623
|
+
```
|
|
624
|
+
The calculated fields should be a mapping from name to a ClickHouse aggregation function. For example:
|
|
625
|
+
```
|
|
626
|
+
{'weekday': 'toDayOfWeek(event_date)', 'number_of_events': 'count()'}
|
|
627
|
+
```
|
|
628
|
+
At least one calculated field is required.
|
|
629
|
+
"""
|
|
630
|
+
super().__init__(base_qs._model_cls, base_qs._database)
|
|
631
|
+
assert calculated_fields, "No calculated fields specified for aggregation"
|
|
632
|
+
self._fields = grouping_fields
|
|
633
|
+
self._grouping_fields = grouping_fields
|
|
634
|
+
self._calculated_fields = calculated_fields
|
|
635
|
+
self._order_by = list(base_qs._order_by)
|
|
636
|
+
self._where_q = base_qs._where_q
|
|
637
|
+
self._prewhere_q = base_qs._prewhere_q
|
|
638
|
+
self._limits = base_qs._limits
|
|
639
|
+
self._distinct = base_qs._distinct
|
|
640
|
+
|
|
641
|
+
def group_by(self, *args):
|
|
642
|
+
"""
|
|
643
|
+
This method lets you specify the grouping fields explicitly. The `args` must
|
|
644
|
+
be names of grouping fields or calculated fields that this queryset was
|
|
645
|
+
created with.
|
|
646
|
+
"""
|
|
647
|
+
for name in args:
|
|
648
|
+
assert name in self._fields or name in self._calculated_fields, (
|
|
649
|
+
"Cannot group by `%s` since it is not included in the query" % name
|
|
650
|
+
)
|
|
651
|
+
qs = copy(self)
|
|
652
|
+
qs._grouping_fields = args
|
|
653
|
+
return qs
|
|
654
|
+
|
|
655
|
+
def only(self, *field_names):
|
|
656
|
+
"""
|
|
657
|
+
This method is not supported on `AggregateQuerySet`.
|
|
658
|
+
"""
|
|
659
|
+
raise NotImplementedError('Cannot use "only" with AggregateQuerySet')
|
|
660
|
+
|
|
661
|
+
def aggregate(self, *args, **kwargs):
|
|
662
|
+
"""
|
|
663
|
+
This method is not supported on `AggregateQuerySet`.
|
|
664
|
+
"""
|
|
665
|
+
raise NotImplementedError("Cannot re-aggregate an AggregateQuerySet")
|
|
666
|
+
|
|
667
|
+
def select_fields_as_sql(self):
|
|
668
|
+
"""
|
|
669
|
+
Returns the selected fields or expressions as a SQL string.
|
|
670
|
+
"""
|
|
671
|
+
return comma_join(
|
|
672
|
+
[str(f) for f in self._fields] + ["%s AS %s" % (v, k) for k, v in self._calculated_fields.items()]
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def __iter__(self):
|
|
676
|
+
return self._database.select(self.as_sql()) # using an ad-hoc model
|
|
677
|
+
|
|
678
|
+
def count(self):
|
|
679
|
+
"""
|
|
680
|
+
Returns the number of rows after aggregation.
|
|
681
|
+
"""
|
|
682
|
+
sql = "SELECT count() FROM (%s)" % self.as_sql()
|
|
683
|
+
raw = self._database.raw(sql)
|
|
684
|
+
return int(raw) if raw else 0
|
|
685
|
+
|
|
686
|
+
def with_totals(self):
|
|
687
|
+
"""
|
|
688
|
+
Adds WITH TOTALS modifier ot GROUP BY, making query return extra row
|
|
689
|
+
with aggregate function calculated across all the rows. More information:
|
|
690
|
+
https://clickhouse.tech/docs/en/query_language/select/#with-totals-modifier
|
|
691
|
+
"""
|
|
692
|
+
qs = copy(self)
|
|
693
|
+
qs._grouping_with_totals = True
|
|
694
|
+
return qs
|
|
695
|
+
|
|
696
|
+
def _verify_mutation_allowed(self):
|
|
697
|
+
raise AssertionError("Cannot mutate an AggregateQuerySet")
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# Expose only relevant classes in import *
|
|
701
|
+
__all__ = [c.__name__ for c in [Q, QuerySet, AggregateQuerySet]]
|