Skip to content

Commit 927dc3c

Browse files
committed
Support for LIMIT BY clause
1 parent 31660a1 commit 927dc3c

File tree

5 files changed

+172
-31
lines changed

5 files changed

+172
-31
lines changed

aiochsa/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .client import Client
44
from .exc import DBException, ProtocolError
55
from .pool import connect, create_pool
6+
from .sql import select
67

78
try:
89
__version__ = get_distribution(__name__).version

aiochsa/dialect.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from sqlalchemy.sql import crud
55
from sqlalchemy.sql.compiler import SQLCompiler
66

7+
from .sql import _ORDER_BY_SENTINEL
8+
79

810
class ClickhouseSaSQLCompiler(ClickHouseCompiler):
911

@@ -12,6 +14,25 @@ class ClickhouseSaSQLCompiler(ClickHouseCompiler):
1214
def get_from_hint_text(self, table, text):
1315
return text
1416

17+
def order_by_clause(self, select, **kw):
18+
text = ''
19+
if select._order_by_clause is not _ORDER_BY_SENTINEL:
20+
text = super().order_by_clause(select, **kw)
21+
22+
# Hack to add LIMIT BY clause
23+
limit_by_clause = getattr(select, '_limit_by_clause', None)
24+
if limit_by_clause is not None and limit_by_clause.clauses.clauses:
25+
text += ' LIMIT '
26+
if limit_by_clause.offset is not None:
27+
text += f'{self.process(limit_by_clause.offset, **kw)}, '
28+
text += self.process(limit_by_clause.limit, **kw)
29+
limit_by_exprs = limit_by_clause.clauses._compiler_dispatch(
30+
self, **kw,
31+
)
32+
text += f' BY {limit_by_exprs}'
33+
34+
return text
35+
1536
def visit_insert(self, insert_stmt, asfrom=False, **kw):
1637
assert not self.stack # INSERT only at top level
1738

aiochsa/sql.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from types import SimpleNamespace
2+
3+
from clickhouse_sqlalchemy.sql import Select as _BaseSelect
4+
from sqlalchemy.sql.base import _generative
5+
from sqlalchemy.sql.elements import ClauseList, _literal_as_label_reference
6+
from sqlalchemy.sql.selectable import _offset_or_limit_clause
7+
8+
9+
class LimitByClause:
10+
11+
def __init__(self, clauses, offset, limit):
12+
self.clauses = ClauseList(
13+
*clauses, _literal_as_text=_literal_as_label_reference,
14+
)
15+
self.offset = _offset_or_limit_clause(offset)
16+
self.limit = _offset_or_limit_clause(limit)
17+
18+
19+
20+
# Minimal value to trick SQLAlchemy into believing that there is a ORDER BY
21+
# clause
22+
_ORDER_BY_SENTINEL = SimpleNamespace(clauses=True)
23+
24+
25+
class Select(_BaseSelect):
26+
27+
_limit_by_clause = None
28+
29+
@_generative
30+
def limit_by(self, *clauses, offset=None, limit):
31+
"""return a new selectable with the given
32+
`LIMIT [offset,] limit BY clauses` criterion applied.
33+
34+
e.g.::
35+
36+
stmt = (
37+
select([table.c.name, table.c.amount])
38+
.order_by(table.c.amount.desc())
39+
.limit_by(table.c.name, limit=1)
40+
)
41+
"""
42+
# LIMIT BY clause comes just after ORDER BY, so we hack the later's
43+
# machinery to add former too. Alternative would be redefining
44+
# whole `_compose_select_body` method just to insert 2 lines of code.
45+
# The worst here is that this method is redefined in
46+
# clickhouse_sqlalchemy and still is under active development.
47+
if not self._order_by_clause.clauses:
48+
self._order_by_clause = _ORDER_BY_SENTINEL
49+
50+
self._limit_by_clause = LimitByClause(clauses, offset, limit)
51+
52+
def append_order_by(self, *clauses):
53+
if self._order_by_clause is _ORDER_BY_SENTINEL:
54+
self._order_by_clause = ClauseList()
55+
super().append_order_by(*clauses)
56+
if (
57+
self._limit_by_clause is not None and
58+
not self._order_by_clause.clauses
59+
):
60+
self._order_by_clause = _ORDER_BY_SENTINEL
61+
62+
63+
select = Select

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import datetime
33
from decimal import Decimal
44

5+
import clickhouse_sqlalchemy
56
import pytest
67
import sqlalchemy as sa
78

@@ -180,3 +181,12 @@ async def _create(sa_type):
180181
)
181182

182183
return _create
184+
185+
186+
@pytest.fixture(params=[
187+
sa.select,
188+
clickhouse_sqlalchemy.select,
189+
aiochsa.select,
190+
])
191+
def any_select(request):
192+
return request.param

0 commit comments

Comments
 (0)