Skip to content

Commit

Permalink
SQLAlchemy DQL: Use CrateDB's native ILIKE operator only on >= 4.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
amotl committed Jul 17, 2023
1 parent 4de4c9f commit 17e6ebb
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 19 deletions.
27 changes: 18 additions & 9 deletions src/crate/client/sqlalchemy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,10 @@ def visit_ilike_case_insensitive_operand(self, element, **kw):
"""
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
"""
return element.element._compiler_dispatch(self, **kw)
if self.dialect.has_ilike_operator():
return element.element._compiler_dispatch(self, **kw)
else:
return super().visit_ilike_case_insensitive_operand(element, **kw)

def visit_ilike_op_binary(self, binary, operator, **kw):
"""
Expand All @@ -259,10 +262,13 @@ def visit_ilike_op_binary(self, binary, operator, **kw):
"""
if binary.modifiers.get("escape", None) is not None:
raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
return "%s ILIKE %s" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw),
)
if self.dialect.has_ilike_operator():
return "%s ILIKE %s" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw),
)
else:
return super().visit_ilike_op_binary(binary, operator, **kw)

def visit_not_ilike_op_binary(self, binary, operator, **kw):
"""
Expand All @@ -273,10 +279,13 @@ def visit_not_ilike_op_binary(self, binary, operator, **kw):
"""
if binary.modifiers.get("escape", None) is not None:
raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
return "%s NOT ILIKE %s" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw),
)
if self.dialect.has_ilike_operator():
return "%s NOT ILIKE %s" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw),
)
else:
return super().visit_not_ilike_op_binary(binary, operator, **kw)

def limit_clause(self, select, **kw):
"""
Expand Down
7 changes: 7 additions & 0 deletions src/crate/client/sqlalchemy/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ def _create_column_info(self, row):
def _resolve_type(self, type_):
return TYPES_MAP.get(type_, sqltypes.UserDefinedType)

def has_ilike_operator(self):
"""
Only CrateDB 4.1.0 and higher implements the `ILIKE` operator.
"""
server_version_info = self.server_version_info
return server_version_info is not None and server_version_info >= (4, 1, 0)


class DateTrunc(functions.GenericFunction):
name = "date_trunc"
Expand Down
4 changes: 4 additions & 0 deletions src/crate/client/sqlalchemy/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ..compat.api13 import monkeypatch_amend_select_sa14, monkeypatch_add_connectionfairy_driver_connection
from ..sa_version import SA_1_4, SA_VERSION
from ...test_util import ParametrizedTestCase

# `sql.select()` of SQLAlchemy 1.3 uses old calling semantics,
# but the test cases already need the modern ones.
Expand Down Expand Up @@ -32,6 +33,9 @@ def test_suite_unit():
tests.addTest(makeSuite(SqlAlchemyDictTypeTest))
tests.addTest(makeSuite(SqlAlchemyDateAndDateTimeTest))
tests.addTest(makeSuite(SqlAlchemyCompilerTest))
tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": None}))
tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 0, 12)}))
tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 1, 10)}))
tests.addTest(makeSuite(SqlAlchemyUpdateTest))
tests.addTest(makeSuite(SqlAlchemyMatchTest))
tests.addTest(makeSuite(SqlAlchemyCreateTableTest))
Expand Down
31 changes: 21 additions & 10 deletions src/crate/client/sqlalchemy/tests/compiler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# with Crate these terms will supersede the license and you may use the
# software solely pursuant to the terms of the relevant commercial agreement.
from textwrap import dedent
from unittest import mock, TestCase, skipIf
from unittest import mock, skipIf

from crate.client.sqlalchemy.compiler import crate_before_execute

Expand All @@ -28,12 +28,16 @@

from crate.client.sqlalchemy.sa_version import SA_VERSION, SA_1_4, SA_2_0
from crate.client.sqlalchemy.types import ObjectType
from crate.client.test_util import ParametrizedTestCase


class SqlAlchemyCompilerTest(TestCase):
class SqlAlchemyCompilerTest(ParametrizedTestCase):

def setUp(self):
self.crate_engine = sa.create_engine('crate://')
if isinstance(self.param, dict) and "server_version_info" in self.param:
server_version_info = self.param["server_version_info"]
self.crate_engine.dialect.server_version_info = server_version_info
self.sqlite_engine = sa.create_engine('sqlite://')
self.metadata = sa.MetaData()
self.mytable = sa.Table('mytable', self.metadata,
Expand Down Expand Up @@ -71,25 +75,32 @@ def test_bulk_update_on_builtin_type(self):

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

def test_select_with_ilike(self):
def test_select_with_ilike_no_escape(self):
"""
Verify the compiler uses CrateDB's native `ILIKE` method.
"""
selectable = self.mytable.select().where(self.mytable.c.name.ilike("%foo%"))
statement = str(selectable.compile(bind=self.crate_engine))
self.assertEqual(statement, dedent("""
SELECT mytable.name, mytable.data
FROM mytable
WHERE mytable.name ILIKE ?
""").strip()) # noqa: W291
if self.crate_engine.dialect.has_ilike_operator():
self.assertEqual(statement, dedent("""
SELECT mytable.name, mytable.data
FROM mytable
WHERE mytable.name ILIKE ?
""").strip()) # noqa: W291
else:
self.assertEqual(statement, dedent("""
SELECT mytable.name, mytable.data
FROM mytable
WHERE lower(mytable.name) LIKE lower(?)
""").strip()) # noqa: W291

def test_select_with_not_ilike(self):
def test_select_with_not_ilike_no_escape(self):
"""
Verify the compiler uses CrateDB's native `ILIKE` method.
"""
selectable = self.mytable.select().where(self.mytable.c.name.notilike("%foo%"))
statement = str(selectable.compile(bind=self.crate_engine))
if SA_VERSION < SA_1_4:
if SA_VERSION < SA_1_4 or not self.crate_engine.dialect.has_ilike_operator():
self.assertEqual(statement, dedent("""
SELECT mytable.name, mytable.data
FROM mytable
Expand Down
25 changes: 25 additions & 0 deletions src/crate/client/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# However, if you have executed another commercial license agreement
# with Crate these terms will supersede the license and you may use the
# software solely pursuant to the terms of the relevant commercial agreement.
import unittest


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

def close(self):
pass


class ParametrizedTestCase(unittest.TestCase):
"""
TestCase classes that want to be parametrized should
inherit from this class.
https://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases
"""
def __init__(self, methodName="runTest", param=None):
super(ParametrizedTestCase, self).__init__(methodName)
self.param = param

@staticmethod
def parametrize(testcase_klass, param=None):
""" Create a suite containing all tests taken from the given
subclass, passing them the parameter 'param'.
"""
testloader = unittest.TestLoader()
testnames = testloader.getTestCaseNames(testcase_klass)
suite = unittest.TestSuite()
for name in testnames:
suite.addTest(testcase_klass(name, param=param))
return suite

0 comments on commit 17e6ebb

Please sign in to comment.