sqlrules-mysql 1.0.0__tar.gz → 2.0.0__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-2.0.0}/.gitignore +1 -0
- sqlrules_mysql-2.0.0/PKG-INFO +83 -0
- sqlrules_mysql-2.0.0/README.md +61 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-2.0.0}/pyproject.toml +4 -4
- sqlrules_mysql-2.0.0/src/sqlrules_mysql/__init__.py +87 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-2.0.0}/src/sqlrules_mysql/json.py +1 -3
- sqlrules_mysql-2.0.0/src/sqlrules_mysql/length.py +29 -0
- sqlrules_mysql-2.0.0/src/sqlrules_mysql/pattern.py +25 -0
- sqlrules_mysql-2.0.0/tests/test_mysql_plugin.py +56 -0
- sqlrules_mysql-1.0.0/PKG-INFO +0 -66
- sqlrules_mysql-1.0.0/README.md +0 -44
- sqlrules_mysql-1.0.0/src/sqlrules_mysql/__init__.py +0 -46
- sqlrules_mysql-1.0.0/src/sqlrules_mysql/pattern.py +0 -22
- sqlrules_mysql-1.0.0/src/sqlrules_mysql/type_check.py +0 -238
- sqlrules_mysql-1.0.0/tests/test_mysql_plugin.py +0 -73
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-2.0.0}/LICENSE +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-2.0.0}/src/sqlrules_mysql/fulltext.py +0 -0
- {sqlrules_mysql-1.0.0 → sqlrules_mysql-2.0.0}/src/sqlrules_mysql/py.typed +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sqlrules-mysql
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: MySQL dialect plugin for SQLRules (REGEXP, JSON, full-text).
|
|
5
|
+
Project-URL: Homepage, https://github.com/eddiethedean/sqlrules
|
|
6
|
+
Project-URL: Repository, https://github.com/eddiethedean/sqlrules
|
|
7
|
+
Project-URL: Issues, https://github.com/eddiethedean/sqlrules/issues
|
|
8
|
+
Author: SQLRules Contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: mysql,pydantic,sqlalchemy,sqlrules
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
18
|
+
Requires-Dist: sqlrules<3,>=2
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# sqlrules-mysql
|
|
24
|
+
|
|
25
|
+
MySQL backend provider for [SQLRules](https://github.com/eddiethedean/sqlrules).
|
|
26
|
+
The package version follows the core 2.x line.
|
|
27
|
+
|
|
28
|
+
This provider targets MySQL 8.0+. MariaDB is outside the supported server
|
|
29
|
+
matrix because its regular-expression function API differs from MySQL's.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "sqlrules>=2,<3" "sqlrules-mysql>=2,<3"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Use
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from typing import Annotated, Any
|
|
41
|
+
|
|
42
|
+
from pydantic import Field
|
|
43
|
+
from sqlalchemy import Column, JSON, MetaData, String, Table
|
|
44
|
+
|
|
45
|
+
from sqlrules import Compiler, FullTextMatch, JsonContains, RuleSchema, where
|
|
46
|
+
from sqlrules_mysql import MysqlPlugin
|
|
47
|
+
|
|
48
|
+
rows = Table(
|
|
49
|
+
"rows",
|
|
50
|
+
MetaData(),
|
|
51
|
+
Column("name", String),
|
|
52
|
+
Column("meta", JSON),
|
|
53
|
+
Column("body", String),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RowRules(RuleSchema):
|
|
58
|
+
name: Annotated[str, Field(pattern=r"^A")]
|
|
59
|
+
meta: Annotated[dict[str, Any], JsonContains({"active": True})]
|
|
60
|
+
body: Annotated[str, FullTextMatch("sqlrules")]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
provider = MysqlPlugin(server_version=(8, 0, 36))
|
|
64
|
+
compiled = Compiler(plugins=[provider]).compile(RowRules, rows)
|
|
65
|
+
statement = rows.select().where(*where(compiled))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Capabilities
|
|
69
|
+
|
|
70
|
+
- `pattern`: MySQL `REGEXP`
|
|
71
|
+
- JSON containment and key lookup
|
|
72
|
+
- Full-text matching (requires a matching FULLTEXT index)
|
|
73
|
+
- Safe lax text-to-int conversion on MySQL 8.0+
|
|
74
|
+
|
|
75
|
+
Text-to-float and text-to-Decimal are compile-time capability errors. String
|
|
76
|
+
Literal and Enum fields need an explicit binary or case-sensitive collation.
|
|
77
|
+
See the [type support matrix](https://sqlrules.readthedocs.io/en/latest/TYPE_SUPPORT.html).
|
|
78
|
+
|
|
79
|
+
## Pattern cost
|
|
80
|
+
|
|
81
|
+
Untrusted regular expressions and full-text queries can be expensive. Prefer
|
|
82
|
+
static or allowlisted values. See the SQLRules
|
|
83
|
+
[security notes](https://sqlrules.readthedocs.io/en/latest/SECURITY.html).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# sqlrules-mysql
|
|
2
|
+
|
|
3
|
+
MySQL backend provider for [SQLRules](https://github.com/eddiethedean/sqlrules).
|
|
4
|
+
The package version follows the core 2.x line.
|
|
5
|
+
|
|
6
|
+
This provider targets MySQL 8.0+. MariaDB is outside the supported server
|
|
7
|
+
matrix because its regular-expression function API differs from MySQL's.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install "sqlrules>=2,<3" "sqlrules-mysql>=2,<3"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from typing import Annotated, Any
|
|
19
|
+
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
from sqlalchemy import Column, JSON, MetaData, String, Table
|
|
22
|
+
|
|
23
|
+
from sqlrules import Compiler, FullTextMatch, JsonContains, RuleSchema, where
|
|
24
|
+
from sqlrules_mysql import MysqlPlugin
|
|
25
|
+
|
|
26
|
+
rows = Table(
|
|
27
|
+
"rows",
|
|
28
|
+
MetaData(),
|
|
29
|
+
Column("name", String),
|
|
30
|
+
Column("meta", JSON),
|
|
31
|
+
Column("body", String),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RowRules(RuleSchema):
|
|
36
|
+
name: Annotated[str, Field(pattern=r"^A")]
|
|
37
|
+
meta: Annotated[dict[str, Any], JsonContains({"active": True})]
|
|
38
|
+
body: Annotated[str, FullTextMatch("sqlrules")]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
provider = MysqlPlugin(server_version=(8, 0, 36))
|
|
42
|
+
compiled = Compiler(plugins=[provider]).compile(RowRules, rows)
|
|
43
|
+
statement = rows.select().where(*where(compiled))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Capabilities
|
|
47
|
+
|
|
48
|
+
- `pattern`: MySQL `REGEXP`
|
|
49
|
+
- JSON containment and key lookup
|
|
50
|
+
- Full-text matching (requires a matching FULLTEXT index)
|
|
51
|
+
- Safe lax text-to-int conversion on MySQL 8.0+
|
|
52
|
+
|
|
53
|
+
Text-to-float and text-to-Decimal are compile-time capability errors. String
|
|
54
|
+
Literal and Enum fields need an explicit binary or case-sensitive collation.
|
|
55
|
+
See the [type support matrix](https://sqlrules.readthedocs.io/en/latest/TYPE_SUPPORT.html).
|
|
56
|
+
|
|
57
|
+
## Pattern cost
|
|
58
|
+
|
|
59
|
+
Untrusted regular expressions and full-text queries can be expensive. Prefer
|
|
60
|
+
static or allowlisted values. See the SQLRules
|
|
61
|
+
[security notes](https://sqlrules.readthedocs.io/en/latest/SECURITY.html).
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlrules-mysql"
|
|
7
|
-
version = "
|
|
8
|
-
description = "MySQL
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "MySQL dialect plugin for SQLRules (REGEXP, JSON, full-text)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
authors = [{ name = "SQLRules Contributors" }]
|
|
13
|
-
keywords = ["sqlrules", "mysql", "
|
|
13
|
+
keywords = ["sqlrules", "mysql", "pydantic", "sqlalchemy"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"License :: OSI Approved :: MIT License",
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Typing :: Typed",
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
"sqlrules>=
|
|
21
|
+
"sqlrules>=2,<3",
|
|
22
22
|
"sqlalchemy>=2.0,<3",
|
|
23
23
|
]
|
|
24
24
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sqlrules.backend import prepare_scalar
|
|
7
|
+
from sqlrules.ir import CompilationContext, PreparedValue, RuleField
|
|
8
|
+
from sqlrules.plugins import PLUGIN_API_VERSION
|
|
9
|
+
from sqlrules.translators import TranslatorRegistry
|
|
10
|
+
from sqlrules_mysql.fulltext import translate_fulltext_match
|
|
11
|
+
from sqlrules_mysql.json import translate_json_contains, translate_json_has_key
|
|
12
|
+
from sqlrules_mysql.length import translate_max_length, translate_min_length
|
|
13
|
+
from sqlrules_mysql.pattern import translate_pattern
|
|
14
|
+
|
|
15
|
+
__version__ = "2.0.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MysqlPlugin:
|
|
19
|
+
"""Register MySQL constraint translators."""
|
|
20
|
+
|
|
21
|
+
name = "mysql"
|
|
22
|
+
api_version = PLUGIN_API_VERSION
|
|
23
|
+
|
|
24
|
+
def __init__(self, *, server_version: tuple[int, ...] | None = None) -> None:
|
|
25
|
+
self.server_version = tuple(server_version) if server_version is not None else None
|
|
26
|
+
|
|
27
|
+
def capabilities(self) -> Mapping[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"backend": self.name,
|
|
30
|
+
"server_version": self.server_version,
|
|
31
|
+
"native_scalar_types": (
|
|
32
|
+
"bool",
|
|
33
|
+
"int",
|
|
34
|
+
"float",
|
|
35
|
+
"decimal",
|
|
36
|
+
"str",
|
|
37
|
+
"date",
|
|
38
|
+
"datetime",
|
|
39
|
+
"time",
|
|
40
|
+
),
|
|
41
|
+
"safe_text_numeric_conversion": self.server_version is not None
|
|
42
|
+
and self.server_version >= (8, 0),
|
|
43
|
+
"safe_text_numeric_targets": (
|
|
44
|
+
("int",)
|
|
45
|
+
if self.server_version is not None and self.server_version >= (8, 0)
|
|
46
|
+
else ()
|
|
47
|
+
),
|
|
48
|
+
"assumptions": (
|
|
49
|
+
"MySQL 8.0 REGEXP and native column types provide the advertised type evidence.",
|
|
50
|
+
"Lax integer/float/Decimal conversions accept the documented lexical profile.",
|
|
51
|
+
),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def prepare_value(
|
|
55
|
+
self,
|
|
56
|
+
column: Any,
|
|
57
|
+
field: RuleField,
|
|
58
|
+
context: CompilationContext,
|
|
59
|
+
) -> PreparedValue:
|
|
60
|
+
return prepare_scalar(column, field, context, backend=self.name)
|
|
61
|
+
|
|
62
|
+
def register(self, registry: TranslatorRegistry) -> None:
|
|
63
|
+
registry.register_constraint(
|
|
64
|
+
"pattern",
|
|
65
|
+
translate_pattern,
|
|
66
|
+
on_conflict="replace",
|
|
67
|
+
)
|
|
68
|
+
for operator, translator in (
|
|
69
|
+
("min_length", translate_min_length),
|
|
70
|
+
("max_length", translate_max_length),
|
|
71
|
+
("json_contains", translate_json_contains),
|
|
72
|
+
("json_has_key", translate_json_has_key),
|
|
73
|
+
("fulltext_match", translate_fulltext_match),
|
|
74
|
+
):
|
|
75
|
+
registry.register_constraint(operator, translator, on_conflict="replace")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"MysqlPlugin",
|
|
80
|
+
"__version__",
|
|
81
|
+
"translate_fulltext_match",
|
|
82
|
+
"translate_max_length",
|
|
83
|
+
"translate_min_length",
|
|
84
|
+
"translate_json_contains",
|
|
85
|
+
"translate_json_has_key",
|
|
86
|
+
"translate_pattern",
|
|
87
|
+
]
|
|
@@ -22,9 +22,7 @@ def translate_json_contains(
|
|
|
22
22
|
context: CompilationContext,
|
|
23
23
|
) -> ColumnElement[bool]:
|
|
24
24
|
"""Translate ``json_contains`` to MySQL ``JSON_CONTAINS``."""
|
|
25
|
-
payload = constraint.value
|
|
26
|
-
if not isinstance(payload, str):
|
|
27
|
-
payload = json.dumps(payload, separators=(",", ":"))
|
|
25
|
+
payload = json.dumps(constraint.value, separators=(",", ":"))
|
|
28
26
|
return cast(ColumnElement[bool], func.json_contains(column, payload) == 1)
|
|
29
27
|
|
|
30
28
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import func
|
|
6
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
7
|
+
|
|
8
|
+
from sqlrules.ir import CompilationContext, Constraint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def translate_min_length(
|
|
12
|
+
constraint: Constraint,
|
|
13
|
+
column: ColumnElement[Any],
|
|
14
|
+
context: CompilationContext,
|
|
15
|
+
) -> ColumnElement[bool]:
|
|
16
|
+
"""Compare MySQL character count, not encoded byte length."""
|
|
17
|
+
return cast(ColumnElement[bool], func.char_length(column) >= constraint.value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def translate_max_length(
|
|
21
|
+
constraint: Constraint,
|
|
22
|
+
column: ColumnElement[Any],
|
|
23
|
+
context: CompilationContext,
|
|
24
|
+
) -> ColumnElement[bool]:
|
|
25
|
+
"""Compare MySQL character count, not encoded byte length."""
|
|
26
|
+
return cast(ColumnElement[bool], func.char_length(column) <= constraint.value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["translate_max_length", "translate_min_length"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import func
|
|
6
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
7
|
+
|
|
8
|
+
from sqlrules.constraints import pattern_text
|
|
9
|
+
from sqlrules.ir import CompilationContext, Constraint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def translate_pattern(
|
|
13
|
+
constraint: Constraint,
|
|
14
|
+
column: ColumnElement[Any],
|
|
15
|
+
context: CompilationContext,
|
|
16
|
+
) -> ColumnElement[bool]:
|
|
17
|
+
"""Translate ``pattern`` to MySQL ``REGEXP_LIKE``.
|
|
18
|
+
|
|
19
|
+
MySQL's default matching follows the expression collation. Use
|
|
20
|
+
``REGEXP_LIKE``'s match type to keep SQLRules patterns case-sensitive by
|
|
21
|
+
default and preserve an explicit ``re.IGNORECASE`` flag.
|
|
22
|
+
"""
|
|
23
|
+
pattern, ignore_case = pattern_text(constraint.value)
|
|
24
|
+
match_type = "i" if ignore_case else "c"
|
|
25
|
+
return cast(ColumnElement[bool], func.regexp_like(column, pattern, match_type))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
from sqlalchemy import JSON, Column, MetaData, String, Table
|
|
7
|
+
from sqlalchemy.dialects.mysql import dialect
|
|
8
|
+
from sqlrules_mysql import MysqlPlugin, __version__
|
|
9
|
+
|
|
10
|
+
from sqlrules import Compiler, FullTextMatch, JsonContains, RuleSchema
|
|
11
|
+
from sqlrules.conformance import run_basic_conformance
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_version_and_plugin_conformance() -> None:
|
|
15
|
+
assert __version__ == "2.0.0"
|
|
16
|
+
run_basic_conformance(MysqlPlugin(), operator="pattern")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_pattern_json_and_fulltext_constraints_compile() -> None:
|
|
20
|
+
class Rules(RuleSchema):
|
|
21
|
+
name: Annotated[str, Field(pattern=r"^A")]
|
|
22
|
+
meta: Annotated[dict[str, Any], JsonContains({"active": True})]
|
|
23
|
+
body: Annotated[str, FullTextMatch("sqlrules")]
|
|
24
|
+
|
|
25
|
+
table = Table(
|
|
26
|
+
"rows",
|
|
27
|
+
MetaData(),
|
|
28
|
+
Column("name", String),
|
|
29
|
+
Column("meta", JSON),
|
|
30
|
+
Column("body", String),
|
|
31
|
+
)
|
|
32
|
+
compiled = Compiler(plugins=[MysqlPlugin(server_version=(8, 0, 36))]).compile(Rules, table)
|
|
33
|
+
statement = compiled.predicate.compile(dialect=dialect())
|
|
34
|
+
sql = str(statement).lower()
|
|
35
|
+
assert "regexp_like" in sql
|
|
36
|
+
assert "c" in statement.params.values()
|
|
37
|
+
assert "json_contains" in sql
|
|
38
|
+
assert "match" in sql
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_integer_text_profile_is_server_version_gated() -> None:
|
|
42
|
+
class Rules(RuleSchema):
|
|
43
|
+
count: int
|
|
44
|
+
|
|
45
|
+
table = Table("rows", MetaData(), Column("count", String))
|
|
46
|
+
from sqlrules import CapabilityError
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
Compiler(plugins=[MysqlPlugin()]).compile(Rules, table)
|
|
50
|
+
except CapabilityError as exc:
|
|
51
|
+
assert "MySQL 8.0+" in str(exc)
|
|
52
|
+
else: # pragma: no cover - assertion is the capability contract
|
|
53
|
+
raise AssertionError("text-to-integer needs a configured MySQL version")
|
|
54
|
+
|
|
55
|
+
compiled = Compiler(plugins=[MysqlPlugin(server_version=(8, 0, 36))]).compile(Rules, table)
|
|
56
|
+
assert compiled.fields[0].coercion == "text-to-int"
|
sqlrules_mysql-1.0.0/PKG-INFO
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqlrules-mysql
|
|
3
|
-
Version: 1.0.0
|
|
4
|
-
Summary: MySQL/MariaDB dialect plugin for SQLRules (REGEXP, JSON, full-text).
|
|
5
|
-
Project-URL: Homepage, https://github.com/eddiethedean/sqlrules
|
|
6
|
-
Project-URL: Repository, https://github.com/eddiethedean/sqlrules
|
|
7
|
-
Project-URL: Issues, https://github.com/eddiethedean/sqlrules/issues
|
|
8
|
-
Author: SQLRules Contributors
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: mariadb,mysql,pydantic,sqlalchemy,sqlrules
|
|
12
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Typing :: Typed
|
|
16
|
-
Requires-Python: >=3.10
|
|
17
|
-
Requires-Dist: sqlalchemy<3,>=2.0
|
|
18
|
-
Requires-Dist: sqlrules<2,>=1
|
|
19
|
-
Provides-Extra: dev
|
|
20
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
-
Description-Content-Type: text/markdown
|
|
22
|
-
|
|
23
|
-
# sqlrules-mysql
|
|
24
|
-
|
|
25
|
-
MySQL / MariaDB dialect plugin for [SQLRules](https://github.com/eddiethedean/sqlrules).
|
|
26
|
-
|
|
27
|
-
## Install
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pip install sqlrules-mysql
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Usage
|
|
34
|
-
|
|
35
|
-
```python
|
|
36
|
-
from typing import Annotated, Any
|
|
37
|
-
|
|
38
|
-
from pydantic import BaseModel, Field
|
|
39
|
-
|
|
40
|
-
from sqlrules import Compiler, FullTextMatch, JsonContains
|
|
41
|
-
from sqlrules_mysql import MysqlPlugin
|
|
42
|
-
|
|
43
|
-
class RowFilter(BaseModel):
|
|
44
|
-
name: Annotated[str, Field(pattern=r"^A")]
|
|
45
|
-
meta: Annotated[dict[str, Any], JsonContains({"active": True})]
|
|
46
|
-
body: Annotated[str, FullTextMatch("sqlrules")]
|
|
47
|
-
|
|
48
|
-
compiler = Compiler(plugins=[MysqlPlugin()], dialect="mysql")
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Operators
|
|
52
|
-
|
|
53
|
-
| IR operator | Notes |
|
|
54
|
-
|---|---|
|
|
55
|
-
| `pattern` | `REGEXP` (case-insensitive under typical collations) |
|
|
56
|
-
| `type_check` | Shape/type predicates from `TypeSpec` (partial matrix) |
|
|
57
|
-
| `json_contains` | `JSON_CONTAINS(column, payload) = 1` |
|
|
58
|
-
| `json_has_key` | `JSON_CONTAINS_PATH(column, 'one', '$.key') = 1` |
|
|
59
|
-
| `fulltext_match` | `MATCH(column) AGAINST (value)` — requires a FULLTEXT index |
|
|
60
|
-
|
|
61
|
-
## Security note
|
|
62
|
-
|
|
63
|
-
`pattern` / `fulltext_match` values are bound parameters, but evaluation cost
|
|
64
|
-
is engine-dependent. Prefer static/allowlisted patterns and queries from
|
|
65
|
-
untrusted input. See
|
|
66
|
-
[SECURITY](https://sqlrules.readthedocs.io/en/latest/SECURITY.html).
|
sqlrules_mysql-1.0.0/README.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# sqlrules-mysql
|
|
2
|
-
|
|
3
|
-
MySQL / MariaDB dialect plugin for [SQLRules](https://github.com/eddiethedean/sqlrules).
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install sqlrules-mysql
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
from typing import Annotated, Any
|
|
15
|
-
|
|
16
|
-
from pydantic import BaseModel, Field
|
|
17
|
-
|
|
18
|
-
from sqlrules import Compiler, FullTextMatch, JsonContains
|
|
19
|
-
from sqlrules_mysql import MysqlPlugin
|
|
20
|
-
|
|
21
|
-
class RowFilter(BaseModel):
|
|
22
|
-
name: Annotated[str, Field(pattern=r"^A")]
|
|
23
|
-
meta: Annotated[dict[str, Any], JsonContains({"active": True})]
|
|
24
|
-
body: Annotated[str, FullTextMatch("sqlrules")]
|
|
25
|
-
|
|
26
|
-
compiler = Compiler(plugins=[MysqlPlugin()], dialect="mysql")
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Operators
|
|
30
|
-
|
|
31
|
-
| IR operator | Notes |
|
|
32
|
-
|---|---|
|
|
33
|
-
| `pattern` | `REGEXP` (case-insensitive under typical collations) |
|
|
34
|
-
| `type_check` | Shape/type predicates from `TypeSpec` (partial matrix) |
|
|
35
|
-
| `json_contains` | `JSON_CONTAINS(column, payload) = 1` |
|
|
36
|
-
| `json_has_key` | `JSON_CONTAINS_PATH(column, 'one', '$.key') = 1` |
|
|
37
|
-
| `fulltext_match` | `MATCH(column) AGAINST (value)` — requires a FULLTEXT index |
|
|
38
|
-
|
|
39
|
-
## Security note
|
|
40
|
-
|
|
41
|
-
`pattern` / `fulltext_match` values are bound parameters, but evaluation cost
|
|
42
|
-
is engine-dependent. Prefer static/allowlisted patterns and queries from
|
|
43
|
-
untrusted input. See
|
|
44
|
-
[SECURITY](https://sqlrules.readthedocs.io/en/latest/SECURITY.html).
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from sqlrules.plugins import PLUGIN_API_VERSION
|
|
4
|
-
from sqlrules.translators import TranslatorRegistry
|
|
5
|
-
from sqlrules_mysql.fulltext import translate_fulltext_match
|
|
6
|
-
from sqlrules_mysql.json import translate_json_contains, translate_json_has_key
|
|
7
|
-
from sqlrules_mysql.pattern import translate_pattern
|
|
8
|
-
from sqlrules_mysql.type_check import translate_type_check
|
|
9
|
-
|
|
10
|
-
__version__ = "1.0.0"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class MysqlPlugin:
|
|
14
|
-
"""Register MySQL / MariaDB constraint translators."""
|
|
15
|
-
|
|
16
|
-
name = "mysql"
|
|
17
|
-
api_version = PLUGIN_API_VERSION
|
|
18
|
-
|
|
19
|
-
def register(self, registry: TranslatorRegistry) -> None:
|
|
20
|
-
registry.register_constraint(
|
|
21
|
-
"pattern",
|
|
22
|
-
translate_pattern,
|
|
23
|
-
on_conflict="replace",
|
|
24
|
-
)
|
|
25
|
-
registry.register_constraint(
|
|
26
|
-
"type_check",
|
|
27
|
-
translate_type_check,
|
|
28
|
-
on_conflict="replace",
|
|
29
|
-
)
|
|
30
|
-
for operator, translator in (
|
|
31
|
-
("json_contains", translate_json_contains),
|
|
32
|
-
("json_has_key", translate_json_has_key),
|
|
33
|
-
("fulltext_match", translate_fulltext_match),
|
|
34
|
-
):
|
|
35
|
-
registry.register_constraint(operator, translator, on_conflict="replace")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
__all__ = [
|
|
39
|
-
"MysqlPlugin",
|
|
40
|
-
"__version__",
|
|
41
|
-
"translate_fulltext_match",
|
|
42
|
-
"translate_json_contains",
|
|
43
|
-
"translate_json_has_key",
|
|
44
|
-
"translate_pattern",
|
|
45
|
-
"translate_type_check",
|
|
46
|
-
]
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any, cast
|
|
4
|
-
|
|
5
|
-
from sqlalchemy.sql.elements import ColumnElement
|
|
6
|
-
|
|
7
|
-
from sqlrules.constraints import pattern_text
|
|
8
|
-
from sqlrules.ir import CompilationContext, Constraint
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def translate_pattern(
|
|
12
|
-
constraint: Constraint,
|
|
13
|
-
column: ColumnElement[Any],
|
|
14
|
-
context: CompilationContext,
|
|
15
|
-
) -> ColumnElement[bool]:
|
|
16
|
-
"""Translate ``pattern`` to MySQL/MariaDB ``REGEXP``.
|
|
17
|
-
|
|
18
|
-
MySQL ``REGEXP`` is case-insensitive for non-binary collations. SQLRules
|
|
19
|
-
follows that dialect behavior rather than inventing ``REGEXP BINARY``.
|
|
20
|
-
"""
|
|
21
|
-
pattern, _ignore_case = pattern_text(constraint.value)
|
|
22
|
-
return cast(ColumnElement[bool], column.op("REGEXP")(pattern))
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from datetime import date, datetime, time
|
|
4
|
-
from decimal import Decimal
|
|
5
|
-
from typing import Any, cast
|
|
6
|
-
from uuid import UUID
|
|
7
|
-
|
|
8
|
-
from sqlalchemy import Boolean, Date, DateTime, Float, Integer, Numeric, String, Time
|
|
9
|
-
from sqlalchemy.sql.elements import ColumnElement
|
|
10
|
-
from sqlalchemy.sql.sqltypes import NullType
|
|
11
|
-
from sqlalchemy.types import TypeEngine
|
|
12
|
-
|
|
13
|
-
from sqlrules.constraints import type_spec
|
|
14
|
-
from sqlrules.errors import UnsupportedConstraintError
|
|
15
|
-
from sqlrules.ir import CompilationContext, Constraint, TypeSpec
|
|
16
|
-
|
|
17
|
-
_INT_TEXT = r"^[+-]?(0|[1-9][0-9]*)$"
|
|
18
|
-
_FLOAT_TEXT = r"^[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?$"
|
|
19
|
-
_DATE_TEXT = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
20
|
-
_DATETIME_TEXT = (
|
|
21
|
-
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}"
|
|
22
|
-
r"(\.[0-9]+)?([+-][0-9]{2}:?[0-9]{2}|Z)?$"
|
|
23
|
-
)
|
|
24
|
-
_TIME_TEXT = r"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?$"
|
|
25
|
-
_UUID_TEXT = (
|
|
26
|
-
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
27
|
-
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _is_type(column: ColumnElement[Any], *bases: type[TypeEngine[Any]]) -> bool:
|
|
32
|
-
col_type = column.type
|
|
33
|
-
if isinstance(col_type, NullType):
|
|
34
|
-
return False
|
|
35
|
-
return isinstance(col_type, bases)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _regexp(column: ColumnElement[Any], pattern: str) -> ColumnElement[bool]:
|
|
39
|
-
return cast(ColumnElement[bool], column.op("REGEXP")(pattern))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _wrap_none(
|
|
43
|
-
column: ColumnElement[Any],
|
|
44
|
-
predicate: ColumnElement[bool],
|
|
45
|
-
*,
|
|
46
|
-
allow_none: bool,
|
|
47
|
-
) -> ColumnElement[bool]:
|
|
48
|
-
if allow_none:
|
|
49
|
-
return cast(ColumnElement[bool], column.is_(None) | predicate)
|
|
50
|
-
return predicate
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _unsupported(field: str, spec: TypeSpec, suggestion: str) -> None:
|
|
54
|
-
raise UnsupportedConstraintError(
|
|
55
|
-
field=field,
|
|
56
|
-
operator="type_check",
|
|
57
|
-
value=spec,
|
|
58
|
-
suggestion=suggestion,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _predicate_int(
|
|
63
|
-
column: ColumnElement[Any],
|
|
64
|
-
spec: TypeSpec,
|
|
65
|
-
field: str,
|
|
66
|
-
) -> ColumnElement[bool]:
|
|
67
|
-
if _is_type(column, Integer):
|
|
68
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
69
|
-
if spec.strict:
|
|
70
|
-
_unsupported(
|
|
71
|
-
field,
|
|
72
|
-
spec,
|
|
73
|
-
"MySQL strict int type_check requires an Integer column.",
|
|
74
|
-
)
|
|
75
|
-
if _is_type(column, String):
|
|
76
|
-
return _regexp(column, _INT_TEXT)
|
|
77
|
-
if _is_type(column, Float, Numeric):
|
|
78
|
-
return cast(ColumnElement[bool], column == column.cast(Integer).cast(Float))
|
|
79
|
-
_unsupported(
|
|
80
|
-
field,
|
|
81
|
-
spec,
|
|
82
|
-
"MySQL lax int type_check supports Integer, String, Float, or Numeric columns.",
|
|
83
|
-
)
|
|
84
|
-
raise AssertionError("unreachable")
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _predicate_bool(
|
|
88
|
-
column: ColumnElement[Any],
|
|
89
|
-
spec: TypeSpec,
|
|
90
|
-
field: str,
|
|
91
|
-
) -> ColumnElement[bool]:
|
|
92
|
-
if spec.strict:
|
|
93
|
-
if _is_type(column, Boolean, Integer):
|
|
94
|
-
return cast(ColumnElement[bool], column.in_((True, False, 0, 1)))
|
|
95
|
-
_unsupported(
|
|
96
|
-
field,
|
|
97
|
-
spec,
|
|
98
|
-
"MySQL strict bool type_check requires a Boolean/Integer column.",
|
|
99
|
-
)
|
|
100
|
-
_unsupported(
|
|
101
|
-
field,
|
|
102
|
-
spec,
|
|
103
|
-
"MySQL lax bool type_check is not supported. Use strict=True.",
|
|
104
|
-
)
|
|
105
|
-
raise AssertionError("unreachable")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _predicate_str(
|
|
109
|
-
column: ColumnElement[Any],
|
|
110
|
-
spec: TypeSpec,
|
|
111
|
-
field: str,
|
|
112
|
-
) -> ColumnElement[bool]:
|
|
113
|
-
if _is_type(column, String):
|
|
114
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
115
|
-
_unsupported(
|
|
116
|
-
field,
|
|
117
|
-
spec,
|
|
118
|
-
"MySQL str type_check requires a String/Text column.",
|
|
119
|
-
)
|
|
120
|
-
raise AssertionError("unreachable")
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _predicate_float(
|
|
124
|
-
column: ColumnElement[Any],
|
|
125
|
-
spec: TypeSpec,
|
|
126
|
-
field: str,
|
|
127
|
-
) -> ColumnElement[bool]:
|
|
128
|
-
if _is_type(column, Float, Numeric, Integer):
|
|
129
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
130
|
-
if spec.strict:
|
|
131
|
-
_unsupported(
|
|
132
|
-
field,
|
|
133
|
-
spec,
|
|
134
|
-
"MySQL strict float/Decimal type_check requires a numeric column.",
|
|
135
|
-
)
|
|
136
|
-
if _is_type(column, String):
|
|
137
|
-
return _regexp(column, _FLOAT_TEXT)
|
|
138
|
-
_unsupported(
|
|
139
|
-
field,
|
|
140
|
-
spec,
|
|
141
|
-
"MySQL float/Decimal type_check supports numeric or String columns.",
|
|
142
|
-
)
|
|
143
|
-
raise AssertionError("unreachable")
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _predicate_temporal(
|
|
147
|
-
column: ColumnElement[Any],
|
|
148
|
-
spec: TypeSpec,
|
|
149
|
-
field: str,
|
|
150
|
-
*,
|
|
151
|
-
sa_types: tuple[type[TypeEngine[Any]], ...],
|
|
152
|
-
text_pattern: str,
|
|
153
|
-
) -> ColumnElement[bool]:
|
|
154
|
-
if _is_type(column, *sa_types):
|
|
155
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
156
|
-
if spec.strict:
|
|
157
|
-
type_name = spec.python_type.__name__
|
|
158
|
-
_unsupported(
|
|
159
|
-
field,
|
|
160
|
-
spec,
|
|
161
|
-
f"MySQL strict {type_name} type_check requires a typed {type_name} column.",
|
|
162
|
-
)
|
|
163
|
-
if _is_type(column, String):
|
|
164
|
-
return _regexp(column, text_pattern)
|
|
165
|
-
type_name = spec.python_type.__name__
|
|
166
|
-
_unsupported(
|
|
167
|
-
field,
|
|
168
|
-
spec,
|
|
169
|
-
f"MySQL {type_name} type_check supports typed {type_name} or String columns.",
|
|
170
|
-
)
|
|
171
|
-
raise AssertionError("unreachable")
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _predicate_uuid(
|
|
175
|
-
column: ColumnElement[Any],
|
|
176
|
-
spec: TypeSpec,
|
|
177
|
-
field: str,
|
|
178
|
-
) -> ColumnElement[bool]:
|
|
179
|
-
if "uuid" in type(column.type).__name__.lower():
|
|
180
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
181
|
-
if spec.strict:
|
|
182
|
-
_unsupported(
|
|
183
|
-
field,
|
|
184
|
-
spec,
|
|
185
|
-
"MySQL strict UUID type_check requires a UUID column.",
|
|
186
|
-
)
|
|
187
|
-
if _is_type(column, String):
|
|
188
|
-
return _regexp(column, _UUID_TEXT)
|
|
189
|
-
_unsupported(
|
|
190
|
-
field,
|
|
191
|
-
spec,
|
|
192
|
-
"MySQL UUID type_check supports UUID or String columns.",
|
|
193
|
-
)
|
|
194
|
-
raise AssertionError("unreachable")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def _build_predicate(
|
|
198
|
-
column: ColumnElement[Any],
|
|
199
|
-
spec: TypeSpec,
|
|
200
|
-
field: str,
|
|
201
|
-
) -> ColumnElement[bool]:
|
|
202
|
-
python_type = spec.python_type
|
|
203
|
-
if python_type is int:
|
|
204
|
-
return _predicate_int(column, spec, field)
|
|
205
|
-
if python_type is bool:
|
|
206
|
-
return _predicate_bool(column, spec, field)
|
|
207
|
-
if python_type is str:
|
|
208
|
-
return _predicate_str(column, spec, field)
|
|
209
|
-
if python_type in {float, Decimal}:
|
|
210
|
-
return _predicate_float(column, spec, field)
|
|
211
|
-
if python_type is date:
|
|
212
|
-
return _predicate_temporal(column, spec, field, sa_types=(Date,), text_pattern=_DATE_TEXT)
|
|
213
|
-
if python_type is datetime:
|
|
214
|
-
return _predicate_temporal(
|
|
215
|
-
column, spec, field, sa_types=(DateTime,), text_pattern=_DATETIME_TEXT
|
|
216
|
-
)
|
|
217
|
-
if python_type is time:
|
|
218
|
-
return _predicate_temporal(column, spec, field, sa_types=(Time,), text_pattern=_TIME_TEXT)
|
|
219
|
-
if python_type is UUID:
|
|
220
|
-
return _predicate_uuid(column, spec, field)
|
|
221
|
-
_unsupported(
|
|
222
|
-
field,
|
|
223
|
-
spec,
|
|
224
|
-
f"MySQL type_check has no translator for {python_type!r}.",
|
|
225
|
-
)
|
|
226
|
-
raise AssertionError("unreachable")
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def translate_type_check(
|
|
230
|
-
constraint: Constraint,
|
|
231
|
-
column: ColumnElement[Any],
|
|
232
|
-
context: CompilationContext,
|
|
233
|
-
) -> ColumnElement[bool]:
|
|
234
|
-
"""Translate ``type_check`` into MySQL shape/type predicates."""
|
|
235
|
-
del context
|
|
236
|
-
spec = type_spec(constraint.value)
|
|
237
|
-
predicate = _build_predicate(column, spec, constraint.field)
|
|
238
|
-
return _wrap_none(column, predicate, allow_none=spec.allow_none)
|
|
@@ -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
|