Skip to content

Commit e186dd6

Browse files
committed
Add limit offset caching support
1 parent 974465e commit e186dd6

File tree

2 files changed

+100
-12
lines changed

2 files changed

+100
-12
lines changed

sqlalchemy_redshift/dialect.py

+45-12
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from packaging.version import Version
55
import pkg_resources
66
import sqlalchemy as sa
7-
from sqlalchemy import inspect
7+
from sqlalchemy import inspect, exc as sa_exc
88
from sqlalchemy.dialects.postgresql.base import (
99
PGCompiler, PGDDLCompiler, PGIdentifierPreparer, PGTypeCompiler,
1010
PGExecutionContext, PGDialect
@@ -560,7 +560,7 @@ class RedshiftDialectMixin(DefaultDialect):
560560
max_identifier_length = 127
561561
# explicitly disables statement cache to disable warnings in logs
562562
# ref: https://docs.sqlalchemy.org/en/14/core/connections.html#caching-for-third-party-dialects # noqa
563-
supports_statement_cache = False
563+
supports_statement_cache = True
564564

565565
statement_compiler = RedshiftCompiler
566566
ddl_compiler = RedshiftDDLCompiler
@@ -1053,16 +1053,49 @@ class RedshiftDialect_redshift_connector(RedshiftDialectMixin, PGDialect):
10531053

10541054
class RedshiftCompiler_redshift_connector(RedshiftCompiler, PGCompiler):
10551055
def limit_clause(self, select, **kw):
1056-
text = ""
1057-
if select._limit_clause is not None:
1058-
# an integer value for limit is retrieved
1059-
text += " \n LIMIT " + str(select._limit)
1060-
if select._offset_clause is not None:
1061-
if select._limit_clause is None:
1062-
text += "\n LIMIT ALL"
1063-
# an integer value for offset is retrieved
1064-
text += " OFFSET " + str(select._offset)
1065-
return text
1056+
if sa_version >= Version('1.4.0'):
1057+
text = ""
1058+
1059+
limit_clause = select._limit_clause
1060+
offset_clause = select._offset_clause
1061+
1062+
if select._simple_int_clause(limit_clause):
1063+
text += " \n LIMIT %s" % (
1064+
self.process(
1065+
limit_clause.render_literal_execute(),
1066+
**kw
1067+
)
1068+
)
1069+
elif limit_clause is not None:
1070+
raise sa_exc.CompileError(
1071+
"dialect 'redshift-dialect' can only \
1072+
render simple integers for LIMIT"
1073+
)
1074+
if select._simple_int_clause(offset_clause):
1075+
text += " \n OFFSET %s" % (
1076+
self.process(
1077+
offset_clause.render_literal_execute(),
1078+
**kw
1079+
)
1080+
)
1081+
elif offset_clause is not None:
1082+
raise sa_exc.CompileError(
1083+
"dialect 'redshift-dialect' can only \
1084+
render simple integers for OFFSET"
1085+
)
1086+
1087+
return text
1088+
else:
1089+
text = ""
1090+
if select._limit_clause is not None:
1091+
# an integer value for limit is retrieved
1092+
text += " \n LIMIT " + str(select._limit)
1093+
if select._offset_clause is not None:
1094+
if select._limit_clause is None:
1095+
text += "\n LIMIT ALL"
1096+
# an integer value for offset is retrieved
1097+
text += " OFFSET " + str(select._offset)
1098+
return text
10661099

10671100
def visit_mod_binary(self, binary, operator, **kw):
10681101
return (

tests/test_limit_offset.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
__author__ = "James Waterworth"
2+
"""
3+
Tests to valdiate that LIMIT and OFFSET values are cached
4+
at the appropriate stage of query generation
5+
"""
6+
7+
import sqlalchemy as sa
8+
from packaging.version import Version
9+
10+
from rs_sqla_test_utils.utils import clean, compile_query
11+
12+
sa_version = Version(sa.__version__)
13+
14+
meta = sa.MetaData()
15+
16+
items = sa.Table(
17+
"items",
18+
meta,
19+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=False),
20+
sa.Column("order_id", sa.Integer),
21+
sa.Column("product_id", sa.Integer),
22+
sa.Column("name", sa.String(255)),
23+
sa.Column("qty", sa.Numeric(12, 4)),
24+
sa.Column("price", sa.Numeric(12, 4)),
25+
sa.Column("total_invoiced", sa.Numeric(12, 4)),
26+
sa.Column("discount_invoiced", sa.Numeric(12, 4)),
27+
sa.Column("grandtotal_invoiced", sa.Numeric(12, 4)),
28+
sa.Column("created_at", sa.DateTime),
29+
sa.Column("updated_at", sa.DateTime),
30+
)
31+
32+
33+
def test_select_1_item(stub_redshift_dialect):
34+
query = sa.select(items.c.id).select_from(items).limit(1).offset(0)
35+
36+
assert (
37+
clean(compile_query(query, stub_redshift_dialect))
38+
== "SELECT items.id FROM items LIMIT 1 OFFSET 0"
39+
)
40+
41+
42+
def test_limit_offset_values_not_cached(stub_redshift_dialect):
43+
query = sa.select(items.c.id).select_from(items).limit(1).offset(0)
44+
45+
# compile our first query to get it cached
46+
compile_query(query, stub_redshift_dialect)
47+
48+
second_query = sa.select(items.c.id).select_from(items).limit(2).offset(1)
49+
50+
# perform the same query, it should use the cached version but add the
51+
# limit, offset values seperately
52+
assert (
53+
clean(compile_query(second_query, stub_redshift_dialect))
54+
== "SELECT items.id FROM items LIMIT 2 OFFSET 1"
55+
)

0 commit comments

Comments
 (0)