sqlrules-mysql 1.0.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/.gitignore +1 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/PKG-INFO +1 -1
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/pyproject.toml +1 -1
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/src/sqlrules_mysql/__init__.py +1 -1
- sqlrules_mysql-1.0.1/tests/test_mysql_plugin.py +128 -0
- sqlrules_mysql-1.0.0/tests/test_mysql_plugin.py +0 -73
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/LICENSE +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/README.md +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/src/sqlrules_mysql/fulltext.py +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/src/sqlrules_mysql/json.py +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/src/sqlrules_mysql/pattern.py +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/src/sqlrules_mysql/py.typed +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-1.0.1}/src/sqlrules_mysql/type_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlrules-mysql
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: MySQL/MariaDB dialect plugin for SQLRules (REGEXP, JSON, full-text).
|
|
5
5
|
Project-URL: Homepage, https://github.com/eddiethedean/sqlrules
|
|
6
6
|
Project-URL: Repository, https://github.com/eddiethedean/sqlrules
|
|
@@ -7,7 +7,7 @@ from sqlrules_mysql.json import translate_json_contains, translate_json_has_key
|
|
|
7
7
|
from sqlrules_mysql.pattern import translate_pattern
|
|
8
8
|
from sqlrules_mysql.type_check import translate_type_check
|
|
9
9
|
|
|
10
|
-
__version__ = "1.0.
|
|
10
|
+
__version__ = "1.0.1"
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class MysqlPlugin:
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Annotated, Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from sqlalchemy import Column, MetaData, String, Table, Text
|
|
8
|
+
from sqlalchemy.dialects import mysql
|
|
9
|
+
from sqlrules_mysql import MysqlPlugin, __version__
|
|
10
|
+
|
|
11
|
+
from sqlrules import Compiler, FullTextMatch, JsonContains, JsonHasKey
|
|
12
|
+
from sqlrules.conformance import run_basic_conformance
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _sql(expr: object, *, literal_binds: bool = True) -> str:
|
|
16
|
+
return str(
|
|
17
|
+
expr.compile( # type: ignore[union-attr]
|
|
18
|
+
dialect=mysql.dialect(),
|
|
19
|
+
compile_kwargs={"literal_binds": literal_binds} if literal_binds else {},
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_version() -> None:
|
|
25
|
+
assert __version__ == "1.0.1"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_conformance() -> None:
|
|
29
|
+
run_basic_conformance(MysqlPlugin(), operator="pattern")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_pattern_and_json_compile() -> None:
|
|
33
|
+
"""MySQL REGEXP is CI for non-binary collations; ignore_case is intentionally unused."""
|
|
34
|
+
|
|
35
|
+
class Filter(BaseModel):
|
|
36
|
+
name: Annotated[str, Field(pattern=r"^A")]
|
|
37
|
+
meta: Annotated[dict[str, Any], JsonContains({"active": True}), JsonHasKey("active")]
|
|
38
|
+
body: Annotated[str, FullTextMatch("hello")]
|
|
39
|
+
|
|
40
|
+
table = Table(
|
|
41
|
+
"items",
|
|
42
|
+
MetaData(),
|
|
43
|
+
Column("name", String),
|
|
44
|
+
Column("meta", Text),
|
|
45
|
+
Column("body", Text),
|
|
46
|
+
)
|
|
47
|
+
rules = Compiler(
|
|
48
|
+
plugins=[MysqlPlugin()],
|
|
49
|
+
dialect="mysql",
|
|
50
|
+
cache=False,
|
|
51
|
+
).compile(Filter, table)
|
|
52
|
+
|
|
53
|
+
assert _sql(rules["name"][0]) == "items.name REGEXP '^A'"
|
|
54
|
+
|
|
55
|
+
contains_sql = _sql(rules["meta"][0]).lower()
|
|
56
|
+
assert "json_contains(items.meta" in contains_sql
|
|
57
|
+
assert '{"active":true}' in contains_sql.replace(" ", "")
|
|
58
|
+
assert "json_contains_path" not in contains_sql
|
|
59
|
+
|
|
60
|
+
has_key_sql = _sql(rules["meta"][1]).lower()
|
|
61
|
+
assert "json_contains_path(items.meta" in has_key_sql
|
|
62
|
+
assert '$."active"' in has_key_sql or "$" in has_key_sql
|
|
63
|
+
|
|
64
|
+
body_sql = _sql(rules["body"][0]).lower()
|
|
65
|
+
assert "match" in body_sql and "against" in body_sql
|
|
66
|
+
assert "hello" in body_sql
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_pattern_ignore_case_does_not_invent_binary() -> None:
|
|
70
|
+
"""Case-insensitive PatternSpec must not invent REGEXP BINARY (collation owns CI)."""
|
|
71
|
+
|
|
72
|
+
class Filter(BaseModel):
|
|
73
|
+
name: Annotated[str, Field(pattern=re.compile(r"^A", re.I))]
|
|
74
|
+
|
|
75
|
+
table = Table("items", MetaData(), Column("name", String))
|
|
76
|
+
rules = Compiler(
|
|
77
|
+
plugins=[MysqlPlugin()],
|
|
78
|
+
dialect="mysql",
|
|
79
|
+
cache=False,
|
|
80
|
+
).compile(Filter, table)
|
|
81
|
+
compiled = _sql(rules["name"][0])
|
|
82
|
+
assert compiled == "items.name REGEXP '^A'"
|
|
83
|
+
assert "BINARY" not in compiled.upper()
|
|
84
|
+
assert "(?i)" not in compiled
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_type_check_int_and_lax_string() -> None:
|
|
88
|
+
from sqlalchemy import Integer
|
|
89
|
+
|
|
90
|
+
class Typed(BaseModel):
|
|
91
|
+
age: int
|
|
92
|
+
|
|
93
|
+
class Textual(BaseModel):
|
|
94
|
+
age: int
|
|
95
|
+
|
|
96
|
+
typed_table = Table("users", MetaData(), Column("age", Integer))
|
|
97
|
+
text_table = Table("rows", MetaData(), Column("age", String))
|
|
98
|
+
compiler = Compiler(
|
|
99
|
+
plugins=[MysqlPlugin()],
|
|
100
|
+
dialect="mysql",
|
|
101
|
+
emit_type_checks=True,
|
|
102
|
+
cache=False,
|
|
103
|
+
)
|
|
104
|
+
typed_sql = _sql(compiler.compile(Typed, typed_table)["age"][0], literal_binds=False)
|
|
105
|
+
text_sql = _sql(compiler.compile(Textual, text_table)["age"][0])
|
|
106
|
+
assert "users.age" in typed_sql or "`users`.age" in typed_sql
|
|
107
|
+
assert "IS NOT NULL" in typed_sql.upper()
|
|
108
|
+
assert "age REGEXP" in text_sql.replace("`", "")
|
|
109
|
+
assert "REGEXP" in text_sql
|
|
110
|
+
assert "^[+-]?(0|[1-9][0-9]*)$" in text_sql
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_type_check_optional_allow_none() -> None:
|
|
114
|
+
from sqlalchemy import Integer
|
|
115
|
+
|
|
116
|
+
class Filter(BaseModel):
|
|
117
|
+
age: int | None = None
|
|
118
|
+
|
|
119
|
+
table = Table("users", MetaData(), Column("age", Integer))
|
|
120
|
+
rules = Compiler(
|
|
121
|
+
plugins=[MysqlPlugin()],
|
|
122
|
+
dialect="mysql",
|
|
123
|
+
emit_type_checks=True,
|
|
124
|
+
cache=False,
|
|
125
|
+
).compile(Filter, table)
|
|
126
|
+
compiled = _sql(rules["age"][0], literal_binds=False).upper()
|
|
127
|
+
assert "IS NULL" in compiled
|
|
128
|
+
assert "IS NOT NULL" in compiled
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Annotated, Any
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
from sqlalchemy import Column, MetaData, String, Table, Text
|
|
7
|
-
from sqlalchemy.dialects import mysql
|
|
8
|
-
from sqlrules_mysql import MysqlPlugin, __version__
|
|
9
|
-
|
|
10
|
-
from sqlrules import Compiler, FullTextMatch, JsonContains, JsonHasKey
|
|
11
|
-
from sqlrules.conformance import run_basic_conformance
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_version() -> None:
|
|
15
|
-
assert __version__ == "1.0.0"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_conformance() -> None:
|
|
19
|
-
run_basic_conformance(MysqlPlugin(), operator="pattern")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_pattern_and_json_compile() -> None:
|
|
23
|
-
class Filter(BaseModel):
|
|
24
|
-
name: Annotated[str, Field(pattern=r"^A")]
|
|
25
|
-
meta: Annotated[dict[str, Any], JsonContains({"active": True}), JsonHasKey("active")]
|
|
26
|
-
body: Annotated[str, FullTextMatch("hello")]
|
|
27
|
-
|
|
28
|
-
table = Table(
|
|
29
|
-
"items",
|
|
30
|
-
MetaData(),
|
|
31
|
-
Column("name", String),
|
|
32
|
-
Column("meta", Text),
|
|
33
|
-
Column("body", Text),
|
|
34
|
-
)
|
|
35
|
-
rules = Compiler(
|
|
36
|
-
plugins=[MysqlPlugin()],
|
|
37
|
-
dialect="mysql",
|
|
38
|
-
cache=False,
|
|
39
|
-
).compile(Filter, table)
|
|
40
|
-
dialect = mysql.dialect()
|
|
41
|
-
assert "REGEXP" in str(rules["name"][0].compile(dialect=dialect))
|
|
42
|
-
|
|
43
|
-
contains_sql = str(rules["meta"][0].compile(dialect=dialect)).lower()
|
|
44
|
-
has_key_sql = str(rules["meta"][1].compile(dialect=dialect)).lower()
|
|
45
|
-
assert "json_contains(" in contains_sql
|
|
46
|
-
assert "json_contains_path" not in contains_sql
|
|
47
|
-
assert "json_contains_path" in has_key_sql
|
|
48
|
-
|
|
49
|
-
body_sql = str(rules["body"][0].compile(dialect=dialect)).lower()
|
|
50
|
-
assert "match" in body_sql and "against" in body_sql
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_type_check_int_and_lax_string() -> None:
|
|
54
|
-
from sqlalchemy import Integer
|
|
55
|
-
|
|
56
|
-
class Typed(BaseModel):
|
|
57
|
-
age: int
|
|
58
|
-
|
|
59
|
-
class Textual(BaseModel):
|
|
60
|
-
age: int
|
|
61
|
-
|
|
62
|
-
typed_table = Table("users", MetaData(), Column("age", Integer))
|
|
63
|
-
text_table = Table("rows", MetaData(), Column("age", String))
|
|
64
|
-
compiler = Compiler(
|
|
65
|
-
plugins=[MysqlPlugin()],
|
|
66
|
-
dialect="mysql",
|
|
67
|
-
emit_type_checks=True,
|
|
68
|
-
cache=False,
|
|
69
|
-
)
|
|
70
|
-
typed_sql = str(compiler.compile(Typed, typed_table)["age"][0].compile(dialect=mysql.dialect()))
|
|
71
|
-
text_sql = str(compiler.compile(Textual, text_table)["age"][0].compile(dialect=mysql.dialect()))
|
|
72
|
-
assert "IS NOT NULL" in typed_sql.upper()
|
|
73
|
-
assert "REGEXP" in text_sql
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|