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.
@@ -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]]