Skip to content

Commit 17e6ebb

Browse files
committed
SQLAlchemy DQL: Use CrateDB's native ILIKE operator only on >= 4.1.0
1 parent 4de4c9f commit 17e6ebb

File tree

5 files changed

+75
-19
lines changed

5 files changed

+75
-19
lines changed

src/crate/client/sqlalchemy/compiler.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ def visit_ilike_case_insensitive_operand(self, element, **kw):
248248
"""
249249
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
250250
"""
251-
return element.element._compiler_dispatch(self, **kw)
251+
if self.dialect.has_ilike_operator():
252+
return element.element._compiler_dispatch(self, **kw)
253+
else:
254+
return super().visit_ilike_case_insensitive_operand(element, **kw)
252255

253256
def visit_ilike_op_binary(self, binary, operator, **kw):
254257
"""
@@ -259,10 +262,13 @@ def visit_ilike_op_binary(self, binary, operator, **kw):
259262
"""
260263
if binary.modifiers.get("escape", None) is not None:
261264
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-
)
265+
if self.dialect.has_ilike_operator():
266+
return "%s ILIKE %s" % (
267+
self.process(binary.left, **kw),
268+
self.process(binary.right, **kw),
269+
)
270+
else:
271+
return super().visit_ilike_op_binary(binary, operator, **kw)
266272

267273
def visit_not_ilike_op_binary(self, binary, operator, **kw):
268274
"""
@@ -273,10 +279,13 @@ def visit_not_ilike_op_binary(self, binary, operator, **kw):
273279
"""
274280
if binary.modifiers.get("escape", None) is not None:
275281
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-
)
282+
if self.dialect.has_ilike_operator():
283+
return "%s NOT ILIKE %s" % (
284+
self.process(binary.left, **kw),
285+
self.process(binary.right, **kw),
286+
)
287+
else:
288+
return super().visit_not_ilike_op_binary(binary, operator, **kw)
280289

281290
def limit_clause(self, select, **kw):
282291
"""

src/crate/client/sqlalchemy/dialect.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,13 @@ def _create_column_info(self, row):
356356
def _resolve_type(self, type_):
357357
return TYPES_MAP.get(type_, sqltypes.UserDefinedType)
358358

359+
def has_ilike_operator(self):
360+
"""
361+
Only CrateDB 4.1.0 and higher implements the `ILIKE` operator.
362+
"""
363+
server_version_info = self.server_version_info
364+
return server_version_info is not None and server_version_info >= (4, 1, 0)
365+
359366

360367
class DateTrunc(functions.GenericFunction):
361368
name = "date_trunc"

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ..compat.api13 import monkeypatch_amend_select_sa14, monkeypatch_add_connectionfairy_driver_connection
44
from ..sa_version import SA_1_4, SA_VERSION
5+
from ...test_util import ParametrizedTestCase
56

67
# `sql.select()` of SQLAlchemy 1.3 uses old calling semantics,
78
# but the test cases already need the modern ones.
@@ -32,6 +33,9 @@ def test_suite_unit():
3233
tests.addTest(makeSuite(SqlAlchemyDictTypeTest))
3334
tests.addTest(makeSuite(SqlAlchemyDateAndDateTimeTest))
3435
tests.addTest(makeSuite(SqlAlchemyCompilerTest))
36+
tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": None}))
37+
tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 0, 12)}))
38+
tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 1, 10)}))
3539
tests.addTest(makeSuite(SqlAlchemyUpdateTest))
3640
tests.addTest(makeSuite(SqlAlchemyMatchTest))
3741
tests.addTest(makeSuite(SqlAlchemyCreateTableTest))

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
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.
2121
from textwrap import dedent
22-
from unittest import mock, TestCase, skipIf
22+
from unittest import mock, skipIf
2323

2424
from crate.client.sqlalchemy.compiler import crate_before_execute
2525

@@ -28,12 +28,16 @@
2828

2929
from crate.client.sqlalchemy.sa_version import SA_VERSION, SA_1_4, SA_2_0
3030
from crate.client.sqlalchemy.types import ObjectType
31+
from crate.client.test_util import ParametrizedTestCase
3132

3233

33-
class SqlAlchemyCompilerTest(TestCase):
34+
class SqlAlchemyCompilerTest(ParametrizedTestCase):
3435

3536
def setUp(self):
3637
self.crate_engine = sa.create_engine('crate://')
38+
if isinstance(self.param, dict) and "server_version_info" in self.param:
39+
server_version_info = self.param["server_version_info"]
40+
self.crate_engine.dialect.server_version_info = server_version_info
3741
self.sqlite_engine = sa.create_engine('sqlite://')
3842
self.metadata = sa.MetaData()
3943
self.mytable = sa.Table('mytable', self.metadata,
@@ -71,25 +75,32 @@ def test_bulk_update_on_builtin_type(self):
7175

7276
self.assertFalse(hasattr(clauseelement, '_crate_specific'))
7377

74-
def test_select_with_ilike(self):
78+
def test_select_with_ilike_no_escape(self):
7579
"""
7680
Verify the compiler uses CrateDB's native `ILIKE` method.
7781
"""
7882
selectable = self.mytable.select().where(self.mytable.c.name.ilike("%foo%"))
7983
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
84+
if self.crate_engine.dialect.has_ilike_operator():
85+
self.assertEqual(statement, dedent("""
86+
SELECT mytable.name, mytable.data
87+
FROM mytable
88+
WHERE mytable.name ILIKE ?
89+
""").strip()) # noqa: W291
90+
else:
91+
self.assertEqual(statement, dedent("""
92+
SELECT mytable.name, mytable.data
93+
FROM mytable
94+
WHERE lower(mytable.name) LIKE lower(?)
95+
""").strip()) # noqa: W291
8596

86-
def test_select_with_not_ilike(self):
97+
def test_select_with_not_ilike_no_escape(self):
8798
"""
8899
Verify the compiler uses CrateDB's native `ILIKE` method.
89100
"""
90101
selectable = self.mytable.select().where(self.mytable.c.name.notilike("%foo%"))
91102
statement = str(selectable.compile(bind=self.crate_engine))
92-
if SA_VERSION < SA_1_4:
103+
if SA_VERSION < SA_1_4 or not self.crate_engine.dialect.has_ilike_operator():
93104
self.assertEqual(statement, dedent("""
94105
SELECT mytable.name, mytable.data
95106
FROM mytable

src/crate/client/test_util.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +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+
import unittest
2122

2223

2324
class ClientMocked(object):
@@ -42,3 +43,27 @@ def set_next_server_infos(self, server, server_name, version):
4243

4344
def close(self):
4445
pass
46+
47+
48+
class ParametrizedTestCase(unittest.TestCase):
49+
"""
50+
TestCase classes that want to be parametrized should
51+
inherit from this class.
52+
53+
https://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases
54+
"""
55+
def __init__(self, methodName="runTest", param=None):
56+
super(ParametrizedTestCase, self).__init__(methodName)
57+
self.param = param
58+
59+
@staticmethod
60+
def parametrize(testcase_klass, param=None):
61+
""" Create a suite containing all tests taken from the given
62+
subclass, passing them the parameter 'param'.
63+
"""
64+
testloader = unittest.TestLoader()
65+
testnames = testloader.getTestCaseNames(testcase_klass)
66+
suite = unittest.TestSuite()
67+
for name in testnames:
68+
suite.addTest(testcase_klass(name, param=param))
69+
return suite

0 commit comments

Comments
 (0)