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.
@@ -11,6 +11,7 @@ __pycache__/
11
11
  htmlcov/
12
12
  dist/
13
13
  dist-plugins/
14
+ dist-missing/
14
15
  build/
15
16
  *.egg-info/
16
17
  .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlrules-mysql
3
- Version: 1.0.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlrules-mysql"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "MySQL/MariaDB dialect plugin for SQLRules (REGEXP, JSON, full-text)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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.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