Skip to content

Commit

Permalink
Add support for LIMIT BY clause
Browse files Browse the repository at this point in the history
  • Loading branch information
ods committed Jun 16, 2020
1 parent e0b6f69 commit 240f7eb
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 2 deletions.
20 changes: 20 additions & 0 deletions clickhouse_sqlalchemy/drivers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ def visit_if__func(self, func, **kw):
self.process(func.clauses.clauses[2], **kw)
)

def limit_by_clause(self, select, **kw):
text = ''
limit_by_clause = select._limit_by_clause
if limit_by_clause:
text += ' LIMIT '
if limit_by_clause.offset is not None:
text += self.process(limit_by_clause.offset, **kw) + ', '
text += self.process(limit_by_clause.limit, **kw)
limit_by_exprs = limit_by_clause.by_clauses._compiler_dispatch(
self, **kw,
)
text += ' BY ' + limit_by_exprs

return text

def limit_clause(self, select, **kw):
text = ''
if select._limit_clause is not None:
Expand Down Expand Up @@ -277,6 +292,11 @@ def _compose_select_body(
if select._order_by_clause.clauses:
text += self.order_by_clause(select, **kwargs)

limit_by_clause = getattr(select, '_limit_by_clause', None)

if limit_by_clause is not None:
text += self.limit_by_clause(select, **kwargs)

if (select._limit_clause is not None or
select._offset_clause is not None):
text += self.limit_clause(select, **kwargs)
Expand Down
15 changes: 15 additions & 0 deletions clickhouse_sqlalchemy/ext/clauses.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from sqlalchemy import util, exc
from sqlalchemy.sql import type_api
from sqlalchemy.sql.elements import (
_literal_as_label_reference,
BindParameter,
ColumnElement,
ClauseList
)
from sqlalchemy.sql.selectable import _offset_or_limit_clause
from sqlalchemy.sql.visitors import Visitable


Expand All @@ -29,6 +31,19 @@ def sample_clause(element):
return SampleParam(None, element, unique=True)


class LimitByClause:

def __init__(self, by_clauses, offset, limit):
self.by_clauses = ClauseList(
*by_clauses, _literal_as_text=_literal_as_label_reference,
)
self.offset = _offset_or_limit_clause(offset)
self.limit = _offset_or_limit_clause(limit)

def __bool__(self):
return bool(self.by_clauses.clauses)


class Lambda(ColumnElement):
"""Represent a lambda function, ``Lambda(lambda x: 2 * x)``."""

Expand Down
8 changes: 7 additions & 1 deletion clickhouse_sqlalchemy/orm/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
from sqlalchemy.orm.util import _ORMJoin as _StandardORMJoin

from ..ext.clauses import (
sample_clause,
ArrayJoin,
LimitByClause,
sample_clause,
)


class Query(BaseQuery):
_with_totals = False
_final = None
_sample = None
_limit_by = None
_array_join = None

def _compile_context(self, labels=True):
Expand All @@ -25,6 +27,7 @@ def _compile_context(self, labels=True):
statement._with_totals = self._with_totals
statement._final_clause = self._final
statement._sample_clause = sample_clause(self._sample)
statement._limit_by_clause = self._limit_by
statement._array_join = self._array_join

return context
Expand All @@ -51,6 +54,9 @@ def final(self):
def sample(self, sample):
self._sample = sample

@_generative()
def limit_by(self, *by_clauses, offset=None, limit):
self._limit_by = LimitByClause(by_clauses, offset, limit)

def join(self, *props, **kwargs):
type = kwargs.pop('type', None)
Expand Down
11 changes: 10 additions & 1 deletion clickhouse_sqlalchemy/sql/selectable.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
Join as StandardJoin,
)

from ..ext.clauses import ArrayJoin, sample_clause
from ..ext.clauses import (
ArrayJoin,
LimitByClause,
sample_clause,
)


__all__ = ('Select', 'select')
Expand All @@ -30,6 +34,7 @@ class Select(StandardSelect):
_with_totals = False
_final_clause = None
_sample_clause = None
_limit_by_clause = None
_array_join = None

@_generative
Expand All @@ -44,6 +49,10 @@ def final(self):
def sample(self, sample):
self._sample_clause = sample_clause(sample)

@_generative
def limit_by(self, *by_clauses, offset=None, limit):
self._limit_by_clause = LimitByClause(by_clauses, offset, limit)

@_generative
def array_join(self, *columns):
self._array_join = ArrayJoin(*columns)
Expand Down
15 changes: 15 additions & 0 deletions tests/orm/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ def test_final(self):
'SELECT t1.x AS t1_x FROM t1 FINAL GROUP BY t1.x'
)

def test_limit_by(self):
table = self._make_table()

query = self.session.query(table.c.x).order_by(table.c.x)\
.limit_by(table.c.x, limit=1)
self.assertEqual(
self.compile(query),
'SELECT t1.x AS t1_x FROM t1 ORDER BY t1.x '
'LIMIT %(param_1)s BY t1.x'
)
self.assertEqual(
self.compile(query, literal_binds=True),
'SELECT t1.x AS t1_x FROM t1 ORDER BY t1.x LIMIT 1 BY t1.x'
)

def test_lambda_functions(self):
query = self.session.query(
func.arrayFilter(
Expand Down
14 changes: 14 additions & 0 deletions tests/sql/test_selectable.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ def test_final(self):
'SELECT t1.x FROM t1 FINAL GROUP BY t1.x'
)

def test_limit_by(self):
table = self._make_table()

query = select([table.c.x]).order_by(table.c.x)\
.limit_by(table.c.x, limit=1)
self.assertEqual(
self.compile(query),
'SELECT t1.x FROM t1 ORDER BY t1.x LIMIT %(param_1)s BY t1.x'
)
self.assertEqual(
self.compile(query, literal_binds=True),
'SELECT t1.x FROM t1 ORDER BY t1.x LIMIT 1 BY t1.x'
)

def test_nested_type(self):
table = self._make_table(
't1',
Expand Down

0 comments on commit 240f7eb

Please sign in to comment.