Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8ac9b28

Browse files
Add support for case insensitive regexp + add support for REGEXP SQLite module (tortoise#1737)
* reformat postgres regex enums and support case insentive regexp # Conflicts: # tortoise/contrib/postgres/regex.py * add regexp supprot for sqlite * update docs * fx wrong order of case insenstivie regexp operator * add tests * fix sqlite iregexp # Conflicts: # tortoise/backends/sqlite/client.py * remove unused import * add cast to varchar # Conflicts: # tortoise/contrib/postgres/regex.py * fix tests for mysql * add null field test * remove type-ignore statement from mysql regex impl * add coalesce * make style and make lint fixes * fix mysql tests * make sqlite regexp function installation option, add capability for it # Conflicts: # tortoise/backends/sqlite/client.py * make style & make lint * fix import issue after rebase
1 parent 84c9a5b commit 8ac9b28

17 files changed

Lines changed: 224 additions & 23 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ test: deps
5252
test_sqlite:
5353
$(py_warn) TORTOISE_TEST_DB=sqlite://:memory: pytest --cov-report= $(pytest_opts)
5454

55+
test_sqlite_regexp:
56+
$(py_warn) TORTOISE_TEST_DB=sqlite://:memory:?install_regexp_functions=True pytest --cov-report= $(pytest_opts)
57+
5558
test_postgres_asyncpg:
5659
python -V | grep PyPy || $(py_warn) TORTOISE_TEST_DB="asyncpg://postgres:$(TORTOISE_POSTGRES_PASS)@127.0.0.1:5432/test_\{\}" pytest $(pytest_opts) --cov-append --cov-report=
5760

docs/query.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,9 @@ The ``filter`` option allows you to filter the JSON object by its keys and value
271271
obj5 = await JSONModel.filter(data__filter={"owner__name__isnull": True}).first()
272272
obj6 = await JSONModel.filter(data__filter={"owner__last__not_isnull": False}).first()
273273
274-
In PostgreSQL and MySQL, you can use ``postgres_posix_regex`` to make comparisons using POSIX regular expressions:
275-
In PostgreSQL, this is done with the ``~`` operator, while in MySQL the ``REGEXP`` operator is used.
274+
In PostgreSQL and MySQL and SQLite, you can use ``posix_regex`` to make comparisons using POSIX regular expressions:
275+
On PostgreSQL, this uses the ``~`` operator, on MySQL and SQLite it uses the ``REGEXP`` operator.
276+
PostgreSQL and SQLite also support ``iposix_regex``, which makes case insensive comparisons.
276277

277278

278279
.. code-block:: python3
@@ -281,6 +282,7 @@ In PostgreSQL, this is done with the ``~`` operator, while in MySQL the ``REGEXP
281282
282283
await DemoModel.create(demo_text="Hello World")
283284
obj = await DemoModel.filter(demo_text__posix_regex="^Hello World$").first()
285+
obj = await DemoModel.filter(demo_text__iposix_regex="^hello world$").first()
284286
285287
286288
In PostgreSQL, ``filter`` supports additional lookup types:

tests/test_connection.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,21 @@ def test_clear_storage(self, mocked_get_storage: Mock):
7171
@patch("tortoise.connection.importlib.import_module")
7272
def test_discover_client_class_proper_impl(self, mocked_import_module: Mock):
7373
mocked_import_module.return_value = Mock(client_class="some_class")
74-
client_class = self.conn_handler._discover_client_class("blah")
74+
del mocked_import_module.return_value.get_client_class
75+
client_class = self.conn_handler._discover_client_class({"engine": "blah"})
76+
7577
mocked_import_module.assert_called_once_with("blah")
7678
self.assertEqual(client_class, "some_class")
7779

7880
@patch("tortoise.connection.importlib.import_module")
7981
def test_discover_client_class_improper_impl(self, mocked_import_module: Mock):
8082
del mocked_import_module.return_value.client_class
83+
del mocked_import_module.return_value.get_client_class
8184
engine = "some_engine"
8285
with self.assertRaises(
8386
ConfigurationError, msg=f'Backend for engine "{engine}" does not implement db client'
8487
):
85-
_ = self.conn_handler._discover_client_class(engine)
88+
_ = self.conn_handler._discover_client_class({"engine": engine})
8689

8790
@patch("tortoise.connection.ConnectionHandler.db_config", new_callable=PropertyMock)
8891
def test_get_db_info_present(self, mocked_db_config: Mock):
@@ -156,7 +159,9 @@ def test_create_connection_db_info_str(
156159

157160
mocked_get_db_info.assert_called_once_with(alias)
158161
mocked_expand_db_url.assert_called_once_with("some_db_url")
159-
mocked_discover_client_class.assert_called_once_with("some_engine")
162+
mocked_discover_client_class.assert_called_once_with(
163+
{"engine": "some_engine", "credentials": {"cred_key": "some_val"}}
164+
)
160165
expected_client_class.assert_called_once_with(**expected_db_params)
161166
self.assertEqual(ret_val, "some_connection")
162167

@@ -182,7 +187,9 @@ def test_create_connection_db_info_not_str(
182187

183188
mocked_get_db_info.assert_called_once_with(alias)
184189
mocked_expand_db_url.assert_not_called()
185-
mocked_discover_client_class.assert_called_once_with("some_engine")
190+
mocked_discover_client_class.assert_called_once_with(
191+
{"engine": "some_engine", "credentials": {"cred_key": "some_val"}}
192+
)
186193
expected_client_class.assert_called_once_with(**expected_db_params)
187194
self.assertEqual(ret_val, "some_connection")
188195

tests/test_posix_regex_filter.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
from tortoise.contrib import test
33

44

5+
class RegexTestCase(test.TestCase):
6+
async def asyncSetUp(self) -> None:
7+
await super().asyncSetUp()
8+
9+
510
class TestPosixRegexFilter(test.TestCase):
611

7-
@test.requireCapability(dialect="mysql")
8-
@test.requireCapability(dialect="postgres")
12+
@test.requireCapability(support_for_posix_regex_queries=True)
913
async def test_regex_filter(self):
1014
author = await testmodels.Author.create(name="Johann Wolfgang von Goethe")
1115
self.assertEqual(
@@ -16,3 +20,55 @@ async def test_regex_filter(self):
1620
),
1721
{author.name},
1822
)
23+
24+
@test.requireCapability(dialect="postgres", support_for_posix_regex_queries=True)
25+
async def test_regex_filter_works_with_null_field_postgres(self):
26+
await testmodels.Tournament.create(name="Test")
27+
print(testmodels.Tournament.filter(desc__posix_regex="^test$").sql())
28+
self.assertEqual(
29+
set(
30+
await testmodels.Tournament.filter(desc__posix_regex="^test$").values_list(
31+
"name", flat=True
32+
)
33+
),
34+
set(),
35+
)
36+
37+
@test.requireCapability(dialect="sqlite", support_for_posix_regex_queries=True)
38+
async def test_regex_filter_works_with_null_field_sqlite(self):
39+
await testmodels.Tournament.create(name="Test")
40+
print(testmodels.Tournament.filter(desc__posix_regex="^test$").sql())
41+
self.assertEqual(
42+
set(
43+
await testmodels.Tournament.filter(desc__posix_regex="^test$").values_list(
44+
"name", flat=True
45+
)
46+
),
47+
set(),
48+
)
49+
50+
51+
class TestCaseInsensitivePosixRegexFilter(test.TestCase):
52+
@test.requireCapability(dialect="postgres", support_for_posix_regex_queries=True)
53+
async def test_case_insensitive_regex_filter_postgres(self):
54+
author = await testmodels.Author.create(name="Johann Wolfgang von Goethe")
55+
self.assertEqual(
56+
set(
57+
await testmodels.Author.filter(
58+
name__iposix_regex="^johann [a-zA-Z]+ Von goethe$"
59+
).values_list("name", flat=True)
60+
),
61+
{author.name},
62+
)
63+
64+
@test.requireCapability(dialect="sqlite", support_for_posix_regex_queries=True)
65+
async def test_case_insensitive_regex_filter_sqlite(self):
66+
author = await testmodels.Author.create(name="Johann Wolfgang von Goethe")
67+
self.assertEqual(
68+
set(
69+
await testmodels.Author.filter(
70+
name__iposix_regex="^johann [a-zA-Z]+ Von goethe$"
71+
).values_list("name", flat=True)
72+
),
73+
{author.name},
74+
)

tortoise/backends/base/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Capabilities:
4646
:param support_for_update: Indicates that this DB supports SELECT ... FOR UPDATE SQL statement.
4747
:param support_index_hint: Support force index or use index.
4848
:param support_update_limit_order_by: support update/delete with limit and order by.
49+
:param: support_for_posix_regex_queries: indicated if the db supports posix regex queries
4950
"""
5051

5152
def __init__(
@@ -63,6 +64,7 @@ def __init__(
6364
support_index_hint: bool = False,
6465
# support update/delete with limit and order by
6566
support_update_limit_order_by: bool = True,
67+
support_for_posix_regex_queries: bool = False,
6668
) -> None:
6769
super().__setattr__("_mutable", True)
6870

@@ -74,6 +76,7 @@ def __init__(
7476
self.support_for_update = support_for_update
7577
self.support_index_hint = support_index_hint
7678
self.support_update_limit_order_by = support_update_limit_order_by
79+
self.support_for_posix_regex_queries = support_for_posix_regex_queries
7780
super().__setattr__("_mutable", False)
7881

7982
def __setattr__(self, attr: str, value: Any) -> None:

tortoise/backends/base/config_generator.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@
6262
"skip_first_char": False,
6363
"vmap": {"path": "file_path"},
6464
"defaults": {"journal_mode": "WAL", "journal_size_limit": 16384},
65-
"cast": {"journal_size_limit": int},
65+
"cast": {
66+
"journal_size_limit": int,
67+
"install_regexp_functions": bool,
68+
},
6669
},
6770
"mysql": {
6871
"engine": "tortoise.backends.mysql",

tortoise/backends/base_postgres/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ class BasePostgresClient(BaseDBAsyncClient, abc.ABC):
5353
query_class: Type[PostgreSQLQuery] = PostgreSQLQuery
5454
executor_class: Type[BasePostgresExecutor] = BasePostgresExecutor
5555
schema_generator: Type[BasePostgresSchemaGenerator] = BasePostgresSchemaGenerator
56-
capabilities = Capabilities("postgres", support_update_limit_order_by=False)
56+
capabilities = Capabilities(
57+
"postgres", support_update_limit_order_by=False, support_for_posix_regex_queries=True
58+
)
5759
connection_class: "Optional[Union[AsyncConnection, Connection]]" = None
5860
loop: Optional[AbstractEventLoop] = None
5961
_pool: Optional[Any] = None

tortoise/backends/base_postgres/executor.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
postgres_json_contains,
1212
postgres_json_filter,
1313
)
14-
from tortoise.contrib.postgres.regex import postgres_posix_regex
14+
from tortoise.contrib.postgres.regex import (
15+
postgres_insensitive_posix_regex,
16+
postgres_posix_regex,
17+
)
1518
from tortoise.contrib.postgres.search import SearchCriterion
1619
from tortoise.filters import (
20+
insensitive_posix_regex,
1721
json_contained_by,
1822
json_contains,
1923
json_filter,
@@ -35,6 +39,7 @@ class BasePostgresExecutor(BaseExecutor):
3539
json_contained_by: postgres_json_contained_by,
3640
json_filter: postgres_json_filter,
3741
posix_regex: postgres_posix_regex,
42+
insensitive_posix_regex: postgres_insensitive_posix_regex,
3843
}
3944

4045
def _prepare_insert_statement(

tortoise/backends/mysql/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ class MySQLClient(BaseDBAsyncClient):
7474
executor_class = MySQLExecutor
7575
schema_generator = MySQLSchemaGenerator
7676
capabilities = Capabilities(
77-
"mysql", requires_limit=True, inline_comment=True, support_index_hint=True
77+
"mysql",
78+
requires_limit=True,
79+
inline_comment=True,
80+
support_index_hint=True,
81+
support_for_posix_regex_queries=True,
7882
)
7983

8084
def __init__(

tortoise/backends/mysql/executor.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import enum
2+
13
from pypika_tortoise import functions
24
from pypika_tortoise.enums import SqlTypes
5+
from pypika_tortoise.functions import Cast, Coalesce
36
from pypika_tortoise.terms import BasicCriterion, Criterion
47
from pypika_tortoise.utils import format_quotes
58

@@ -31,6 +34,10 @@
3134
)
3235

3336

37+
class MySQLRegexpComparators(enum.Enum):
38+
REGEXP = " REGEXP "
39+
40+
3441
class StrWrapper(ValueWrapper):
3542
"""
3643
Naive str wrapper that doesn't use the monkey-patched pypika ValueWrapper for MySQL
@@ -97,7 +104,9 @@ def mysql_search(field: Term, value: str) -> SearchCriterion:
97104

98105

99106
def mysql_posix_regex(field: Term, value: str) -> BasicCriterion:
100-
return BasicCriterion(" REGEXP ", field, StrWrapper(value)) # type:ignore[arg-type]
107+
return BasicCriterion(
108+
MySQLRegexpComparators.REGEXP, Coalesce(Cast(field, SqlTypes.CHAR)), StrWrapper(value)
109+
)
101110

102111

103112
class MySQLExecutor(BaseExecutor):

0 commit comments

Comments
 (0)