Skip to content

Commit 4de4c9f

Browse files
committed
SQLAlchemy DQL: Use CrateDB's native ILIKE operator
Instead of using SA's generic implementation `lower() LIKE lower()`, use CrateDB's native one.
1 parent 3376b5c commit 4de4c9f

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Unreleased
88
- SQLAlchemy: Rename leftover occurrences of ``Object``. The new symbol to represent
99
CrateDB's ``OBJECT`` column type is now ``ObjectType``.
1010

11+
- SQLAlchemy DQL: Use CrateDB's native ``ILIKE`` operator instead of using SA's
12+
generic implementation ``lower() LIKE lower()``. Thanks, @hlcianfagna.
13+
1114

1215
2023/07/06 0.32.0
1316
=================

src/crate/client/sqlalchemy/compiler.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,40 @@ def visit_any(self, element, **kw):
244244
self.process(element.right, **kw)
245245
)
246246

247+
def visit_ilike_case_insensitive_operand(self, element, **kw):
248+
"""
249+
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
250+
"""
251+
return element.element._compiler_dispatch(self, **kw)
252+
253+
def visit_ilike_op_binary(self, binary, operator, **kw):
254+
"""
255+
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
256+
257+
Do not implement the `ESCAPE` functionality, because it is not
258+
supported by CrateDB.
259+
"""
260+
if binary.modifiers.get("escape", None) is not None:
261+
raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
262+
return "%s ILIKE %s" % (
263+
self.process(binary.left, **kw),
264+
self.process(binary.right, **kw),
265+
)
266+
267+
def visit_not_ilike_op_binary(self, binary, operator, **kw):
268+
"""
269+
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
270+
271+
Do not implement the `ESCAPE` functionality, because it is not
272+
supported by CrateDB.
273+
"""
274+
if binary.modifiers.get("escape", None) is not None:
275+
raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
276+
return "%s NOT ILIKE %s" % (
277+
self.process(binary.left, **kw),
278+
self.process(binary.right, **kw),
279+
)
280+
247281
def limit_clause(self, select, **kw):
248282
"""
249283
Generate OFFSET / LIMIT clause, PostgreSQL-compatible.

src/crate/client/sqlalchemy/tests/compiler_test.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# However, if you have executed another commercial license agreement
1919
# with Crate these terms will supersede the license and you may use the
2020
# software solely pursuant to the terms of the relevant commercial agreement.
21-
21+
from textwrap import dedent
2222
from unittest import mock, TestCase, skipIf
2323

2424
from crate.client.sqlalchemy.compiler import crate_before_execute
@@ -71,6 +71,58 @@ def test_bulk_update_on_builtin_type(self):
7171

7272
self.assertFalse(hasattr(clauseelement, '_crate_specific'))
7373

74+
def test_select_with_ilike(self):
75+
"""
76+
Verify the compiler uses CrateDB's native `ILIKE` method.
77+
"""
78+
selectable = self.mytable.select().where(self.mytable.c.name.ilike("%foo%"))
79+
statement = str(selectable.compile(bind=self.crate_engine))
80+
self.assertEqual(statement, dedent("""
81+
SELECT mytable.name, mytable.data
82+
FROM mytable
83+
WHERE mytable.name ILIKE ?
84+
""").strip()) # noqa: W291
85+
86+
def test_select_with_not_ilike(self):
87+
"""
88+
Verify the compiler uses CrateDB's native `ILIKE` method.
89+
"""
90+
selectable = self.mytable.select().where(self.mytable.c.name.notilike("%foo%"))
91+
statement = str(selectable.compile(bind=self.crate_engine))
92+
if SA_VERSION < SA_1_4:
93+
self.assertEqual(statement, dedent("""
94+
SELECT mytable.name, mytable.data
95+
FROM mytable
96+
WHERE lower(mytable.name) NOT LIKE lower(?)
97+
""").strip()) # noqa: W291
98+
else:
99+
self.assertEqual(statement, dedent("""
100+
SELECT mytable.name, mytable.data
101+
FROM mytable
102+
WHERE mytable.name NOT ILIKE ?
103+
""").strip()) # noqa: W291
104+
105+
def test_select_with_ilike_and_escape(self):
106+
"""
107+
Verify the compiler fails when using CrateDB's native `ILIKE` method together with `ESCAPE`.
108+
"""
109+
110+
selectable = self.mytable.select().where(self.mytable.c.name.ilike("%foo%", escape='\\'))
111+
with self.assertRaises(NotImplementedError) as cmex:
112+
selectable.compile(bind=self.crate_engine)
113+
self.assertEqual(str(cmex.exception), "Unsupported feature: ESCAPE is not supported")
114+
115+
@skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 and earlier do not support native `NOT ILIKE` compilation")
116+
def test_select_with_not_ilike_and_escape(self):
117+
"""
118+
Verify the compiler fails when using CrateDB's native `ILIKE` method together with `ESCAPE`.
119+
"""
120+
121+
selectable = self.mytable.select().where(self.mytable.c.name.notilike("%foo%", escape='\\'))
122+
with self.assertRaises(NotImplementedError) as cmex:
123+
selectable.compile(bind=self.crate_engine)
124+
self.assertEqual(str(cmex.exception), "Unsupported feature: ESCAPE is not supported")
125+
74126
def test_select_with_offset(self):
75127
"""
76128
Verify the `CrateCompiler.limit_clause` method, with offset.

0 commit comments

Comments
 (0)