sqlrules-postgresql 1.0.1__py3-none-any.whl → 2.0.0__py3-none-any.whl
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_postgresql/__init__.py +60 -8
- sqlrules_postgresql/jsonb.py +5 -1
- sqlrules_postgresql-2.0.0.dist-info/METADATA +83 -0
- sqlrules_postgresql-2.0.0.dist-info/RECORD +10 -0
- {sqlrules_postgresql-1.0.1.dist-info → sqlrules_postgresql-2.0.0.dist-info}/WHEEL +1 -1
- sqlrules_postgresql/type_check.py +0 -250
- sqlrules_postgresql-1.0.1.dist-info/METADATA +0 -98
- sqlrules_postgresql-1.0.1.dist-info/RECORD +0 -11
- {sqlrules_postgresql-1.0.1.dist-info → sqlrules_postgresql-2.0.0.dist-info}/licenses/LICENSE +0 -0
sqlrules_postgresql/__init__.py
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
7
|
+
|
|
8
|
+
from sqlrules.backend import prepare_scalar
|
|
9
|
+
from sqlrules.errors import CapabilityError
|
|
10
|
+
from sqlrules.ir import CompilationContext, PreparedValue, RuleField
|
|
3
11
|
from sqlrules.plugins import PLUGIN_API_VERSION
|
|
4
12
|
from sqlrules.translators import TranslatorRegistry
|
|
5
13
|
from sqlrules_postgresql.array import translate_array_contains, translate_array_overlap
|
|
6
14
|
from sqlrules_postgresql.jsonb import translate_json_contains, translate_json_has_key
|
|
7
15
|
from sqlrules_postgresql.pattern import translate_pattern
|
|
8
16
|
from sqlrules_postgresql.range import translate_range_contains, translate_range_overlap
|
|
9
|
-
from sqlrules_postgresql.type_check import translate_type_check
|
|
10
17
|
|
|
11
|
-
__version__ = "
|
|
18
|
+
__version__ = "2.0.0"
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
class PostgresPlugin:
|
|
@@ -17,17 +24,63 @@ class PostgresPlugin:
|
|
|
17
24
|
name = "postgresql"
|
|
18
25
|
api_version = PLUGIN_API_VERSION
|
|
19
26
|
|
|
27
|
+
def __init__(self, *, server_version: tuple[int, ...] | None = None) -> None:
|
|
28
|
+
self.server_version = tuple(server_version) if server_version is not None else None
|
|
29
|
+
|
|
30
|
+
def capabilities(self) -> Mapping[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"backend": self.name,
|
|
33
|
+
"server_version": self.server_version,
|
|
34
|
+
"native_scalar_types": (
|
|
35
|
+
"bool",
|
|
36
|
+
"int",
|
|
37
|
+
"float",
|
|
38
|
+
"decimal",
|
|
39
|
+
"str",
|
|
40
|
+
"date",
|
|
41
|
+
"datetime",
|
|
42
|
+
"time",
|
|
43
|
+
"uuid",
|
|
44
|
+
),
|
|
45
|
+
"safe_text_numeric_conversion": self.server_version is not None
|
|
46
|
+
and self.server_version >= (16, 0),
|
|
47
|
+
"safe_text_numeric_targets": (
|
|
48
|
+
("int", "float", "decimal")
|
|
49
|
+
if self.server_version is not None and self.server_version >= (16, 0)
|
|
50
|
+
else ()
|
|
51
|
+
),
|
|
52
|
+
"assumptions": ("PostgreSQL native column types provide logical type evidence.",),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def prepare_value(
|
|
56
|
+
self,
|
|
57
|
+
column: Any,
|
|
58
|
+
field: RuleField,
|
|
59
|
+
context: CompilationContext,
|
|
60
|
+
) -> PreparedValue:
|
|
61
|
+
if (
|
|
62
|
+
field.python_type is dict
|
|
63
|
+
and any(
|
|
64
|
+
item.operator in {"json_contains", "json_has_key"} for item in field.constraints
|
|
65
|
+
)
|
|
66
|
+
and not isinstance(column.type, JSONB)
|
|
67
|
+
):
|
|
68
|
+
raise CapabilityError(
|
|
69
|
+
self.name,
|
|
70
|
+
field.name,
|
|
71
|
+
"jsonb",
|
|
72
|
+
type(column.type).__name__,
|
|
73
|
+
"PostgreSQL JSON markers require a JSONB column; generic JSON does not provide "
|
|
74
|
+
"the containment and key operators used by SQLRules.",
|
|
75
|
+
)
|
|
76
|
+
return prepare_scalar(column, field, context, backend=self.name)
|
|
77
|
+
|
|
20
78
|
def register(self, registry: TranslatorRegistry) -> None:
|
|
21
79
|
registry.register_constraint(
|
|
22
80
|
"pattern",
|
|
23
81
|
translate_pattern,
|
|
24
82
|
on_conflict="replace",
|
|
25
83
|
)
|
|
26
|
-
registry.register_constraint(
|
|
27
|
-
"type_check",
|
|
28
|
-
translate_type_check,
|
|
29
|
-
on_conflict="replace",
|
|
30
|
-
)
|
|
31
84
|
for operator, translator in (
|
|
32
85
|
("json_contains", translate_json_contains),
|
|
33
86
|
("json_has_key", translate_json_has_key),
|
|
@@ -49,5 +102,4 @@ __all__ = [
|
|
|
49
102
|
"translate_pattern",
|
|
50
103
|
"translate_range_contains",
|
|
51
104
|
"translate_range_overlap",
|
|
52
|
-
"translate_type_check",
|
|
53
105
|
]
|
sqlrules_postgresql/jsonb.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, cast
|
|
4
4
|
|
|
5
|
+
from sqlalchemy import func
|
|
5
6
|
from sqlalchemy.sql.elements import ColumnElement
|
|
6
7
|
|
|
7
8
|
from sqlrules.ir import CompilationContext, Constraint
|
|
@@ -22,4 +23,7 @@ def translate_json_has_key(
|
|
|
22
23
|
context: CompilationContext,
|
|
23
24
|
) -> ColumnElement[bool]:
|
|
24
25
|
"""Translate ``json_has_key`` to JSONB ``?`` / ``has_key``."""
|
|
25
|
-
return cast(
|
|
26
|
+
return cast(
|
|
27
|
+
ColumnElement[bool],
|
|
28
|
+
(func.jsonb_typeof(column) == "object") & column.has_key(constraint.value),
|
|
29
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sqlrules-postgresql
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: PostgreSQL dialect plugin for SQLRules (regex, JSONB, ARRAY, range).
|
|
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: postgresql,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-postgresql
|
|
24
|
+
|
|
25
|
+
PostgreSQL backend provider for [SQLRules](https://github.com/eddiethedean/sqlrules).
|
|
26
|
+
The package version follows the core 2.x line.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install "sqlrules>=2,<3" "sqlrules-postgresql>=2,<3"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from typing import Annotated, Any
|
|
38
|
+
|
|
39
|
+
from pydantic import Field
|
|
40
|
+
from sqlalchemy import Column, MetaData, String, Table
|
|
41
|
+
from sqlalchemy.dialects.postgresql import ARRAY, INT4RANGE, JSONB
|
|
42
|
+
|
|
43
|
+
from sqlrules import ArrayContains, Compiler, JsonContains, RangeContains, RuleSchema, where
|
|
44
|
+
from sqlrules_postgresql import PostgresPlugin
|
|
45
|
+
|
|
46
|
+
rows = Table(
|
|
47
|
+
"rows",
|
|
48
|
+
MetaData(),
|
|
49
|
+
Column("name", String),
|
|
50
|
+
Column("meta", JSONB),
|
|
51
|
+
Column("tags", ARRAY(String)),
|
|
52
|
+
Column("span", INT4RANGE),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RowRules(RuleSchema):
|
|
57
|
+
name: Annotated[str, Field(pattern=r"^A")]
|
|
58
|
+
meta: Annotated[dict[str, Any], JsonContains({"active": True})]
|
|
59
|
+
tags: Annotated[list[str], ArrayContains(["admin"])]
|
|
60
|
+
span: Annotated[int, RangeContains(5)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
compiled = Compiler(plugins=[PostgresPlugin(server_version=(16, 0))]).compile(RowRules, rows)
|
|
64
|
+
statement = rows.select().where(*where(compiled))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Capabilities
|
|
68
|
+
|
|
69
|
+
- `pattern`: PostgreSQL `~` / `~*`
|
|
70
|
+
- JSONB containment and key membership
|
|
71
|
+
- ARRAY containment and overlap
|
|
72
|
+
- Range containment and overlap
|
|
73
|
+
- Safe lax text-to-int/float/Decimal parsing on PostgreSQL 16+
|
|
74
|
+
|
|
75
|
+
String Literal and Enum fields require a column with `C` or `POSIX`
|
|
76
|
+
collation. Review the [type support matrix](https://sqlrules.readthedocs.io/en/latest/TYPE_SUPPORT.html)
|
|
77
|
+
for the exact conversion profile and limitations.
|
|
78
|
+
|
|
79
|
+
## Pattern cost
|
|
80
|
+
|
|
81
|
+
Untrusted regular expressions can cause expensive engine-side evaluation.
|
|
82
|
+
Prefer patterns authored with the rule model. See the SQLRules
|
|
83
|
+
[security notes](https://sqlrules.readthedocs.io/en/latest/SECURITY.html).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sqlrules_postgresql/__init__.py,sha256=PFGK_zUbwUWHt-PE-3njuzw0C5zZIWTZBTf7zXycM-w,3684
|
|
2
|
+
sqlrules_postgresql/array.py,sha256=Kzin-DQRwidhVsL6hL8c04ejSjEfGjcMZewjSHmH_3s,764
|
|
3
|
+
sqlrules_postgresql/jsonb.py,sha256=KYl8AyZAdGqjFT7hjeWm4yLpJXbKuMYLDPomZJuMVuc,843
|
|
4
|
+
sqlrules_postgresql/pattern.py,sha256=swLYUSmd0sgW-XSYunYBBnoT2QpwkzDsXfuL38cgXqU,599
|
|
5
|
+
sqlrules_postgresql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
sqlrules_postgresql/range.py,sha256=WBvd5yHpkP2UNmU_TxCNdHj-HdTMYVPO_PztqWj6TUs,774
|
|
7
|
+
sqlrules_postgresql-2.0.0.dist-info/METADATA,sha256=Fip_8BMPGyf6l-KlxGsxkyZhsnajXqCa1WQRX3kFUxg,2657
|
|
8
|
+
sqlrules_postgresql-2.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
sqlrules_postgresql-2.0.0.dist-info/licenses/LICENSE,sha256=cLMlkCH6RhjWAkXBWR7LazV98TOG0WkPtCKB5zqqkAs,1078
|
|
10
|
+
sqlrules_postgresql-2.0.0.dist-info/RECORD,,
|
|
@@ -1,250 +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
|
-
# Integer-like text (Pydantic lax int accepts digit strings).
|
|
18
|
-
_INT_TEXT = r"^[+-]?(0|[1-9]\d*)$"
|
|
19
|
-
# Float / Decimal-like text (simplified; not full Pydantic parity).
|
|
20
|
-
_FLOAT_TEXT = r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$"
|
|
21
|
-
# ISO-ish date / datetime / time (approximate).
|
|
22
|
-
_DATE_TEXT = r"^\d{4}-\d{2}-\d{2}$"
|
|
23
|
-
_DATETIME_TEXT = r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:?\d{2}|Z)?$"
|
|
24
|
-
_TIME_TEXT = r"^\d{2}:\d{2}:\d{2}(\.\d+)?$"
|
|
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 _col_type(column: ColumnElement[Any]) -> TypeEngine[Any]:
|
|
32
|
-
return column.type
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _is_type(column: ColumnElement[Any], *bases: type[TypeEngine[Any]]) -> bool:
|
|
36
|
-
col_type = _col_type(column)
|
|
37
|
-
if isinstance(col_type, NullType):
|
|
38
|
-
return False
|
|
39
|
-
return isinstance(col_type, bases)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _regex(column: ColumnElement[Any], pattern: str) -> ColumnElement[bool]:
|
|
43
|
-
return cast(ColumnElement[bool], column.op("~")(pattern))
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _wrap_none(
|
|
47
|
-
column: ColumnElement[Any],
|
|
48
|
-
predicate: ColumnElement[bool],
|
|
49
|
-
*,
|
|
50
|
-
allow_none: bool,
|
|
51
|
-
) -> ColumnElement[bool]:
|
|
52
|
-
if allow_none:
|
|
53
|
-
return cast(ColumnElement[bool], column.is_(None) | predicate)
|
|
54
|
-
return predicate
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _unsupported(field: str, spec: TypeSpec, suggestion: str) -> None:
|
|
58
|
-
raise UnsupportedConstraintError(
|
|
59
|
-
field=field,
|
|
60
|
-
operator="type_check",
|
|
61
|
-
value=spec,
|
|
62
|
-
suggestion=suggestion,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _predicate_int(
|
|
67
|
-
column: ColumnElement[Any],
|
|
68
|
-
spec: TypeSpec,
|
|
69
|
-
field: str,
|
|
70
|
-
) -> ColumnElement[bool]:
|
|
71
|
-
if _is_type(column, Integer):
|
|
72
|
-
# Typed integer column: every non-null value is already an int.
|
|
73
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
74
|
-
if spec.strict:
|
|
75
|
-
_unsupported(
|
|
76
|
-
field,
|
|
77
|
-
spec,
|
|
78
|
-
"PostgreSQL strict int type_check requires an Integer column "
|
|
79
|
-
"(Pydantic strict mode rejects string/bool coercion).",
|
|
80
|
-
)
|
|
81
|
-
if _is_type(column, String):
|
|
82
|
-
return _regex(column, _INT_TEXT)
|
|
83
|
-
if _is_type(column, Float, Numeric):
|
|
84
|
-
# Whole numbers only (approximate Pydantic float→int when no fractional part).
|
|
85
|
-
return cast(ColumnElement[bool], column == column.cast(Integer).cast(Float))
|
|
86
|
-
_unsupported(
|
|
87
|
-
field,
|
|
88
|
-
spec,
|
|
89
|
-
"PostgreSQL lax int type_check supports Integer, String, Float, or Numeric columns.",
|
|
90
|
-
)
|
|
91
|
-
raise AssertionError("unreachable")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _predicate_bool(
|
|
95
|
-
column: ColumnElement[Any],
|
|
96
|
-
spec: TypeSpec,
|
|
97
|
-
field: str,
|
|
98
|
-
) -> ColumnElement[bool]:
|
|
99
|
-
if spec.strict:
|
|
100
|
-
if _is_type(column, Boolean):
|
|
101
|
-
return cast(ColumnElement[bool], column.in_((True, False)))
|
|
102
|
-
_unsupported(
|
|
103
|
-
field,
|
|
104
|
-
spec,
|
|
105
|
-
"PostgreSQL strict bool type_check requires a Boolean column.",
|
|
106
|
-
)
|
|
107
|
-
_unsupported(
|
|
108
|
-
field,
|
|
109
|
-
spec,
|
|
110
|
-
"PostgreSQL lax bool type_check is not supported (coercion set is not "
|
|
111
|
-
"deterministically expressible). Use Field(strict=True) or model_config "
|
|
112
|
-
"strict=True with a Boolean column.",
|
|
113
|
-
)
|
|
114
|
-
raise AssertionError("unreachable")
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _predicate_str(
|
|
118
|
-
column: ColumnElement[Any],
|
|
119
|
-
spec: TypeSpec,
|
|
120
|
-
field: str,
|
|
121
|
-
) -> ColumnElement[bool]:
|
|
122
|
-
if _is_type(column, String):
|
|
123
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
124
|
-
_unsupported(
|
|
125
|
-
field,
|
|
126
|
-
spec,
|
|
127
|
-
"PostgreSQL str type_check requires a String/Text column "
|
|
128
|
-
"(Pydantic does not coerce other SQL types to str by default).",
|
|
129
|
-
)
|
|
130
|
-
raise AssertionError("unreachable")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _predicate_float(
|
|
134
|
-
column: ColumnElement[Any],
|
|
135
|
-
spec: TypeSpec,
|
|
136
|
-
field: str,
|
|
137
|
-
) -> ColumnElement[bool]:
|
|
138
|
-
if _is_type(column, Float, Numeric, Integer):
|
|
139
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
140
|
-
if spec.strict:
|
|
141
|
-
_unsupported(
|
|
142
|
-
field,
|
|
143
|
-
spec,
|
|
144
|
-
"PostgreSQL strict float/Decimal type_check requires a numeric column.",
|
|
145
|
-
)
|
|
146
|
-
if _is_type(column, String):
|
|
147
|
-
return _regex(column, _FLOAT_TEXT)
|
|
148
|
-
_unsupported(
|
|
149
|
-
field,
|
|
150
|
-
spec,
|
|
151
|
-
"PostgreSQL float/Decimal type_check supports numeric or String columns.",
|
|
152
|
-
)
|
|
153
|
-
raise AssertionError("unreachable")
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _predicate_temporal(
|
|
157
|
-
column: ColumnElement[Any],
|
|
158
|
-
spec: TypeSpec,
|
|
159
|
-
field: str,
|
|
160
|
-
*,
|
|
161
|
-
sa_types: tuple[type[TypeEngine[Any]], ...],
|
|
162
|
-
text_pattern: str,
|
|
163
|
-
) -> ColumnElement[bool]:
|
|
164
|
-
if _is_type(column, *sa_types):
|
|
165
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
166
|
-
if spec.strict:
|
|
167
|
-
type_name = spec.python_type.__name__
|
|
168
|
-
_unsupported(
|
|
169
|
-
field,
|
|
170
|
-
spec,
|
|
171
|
-
f"PostgreSQL strict {type_name} type_check requires a typed {type_name} column.",
|
|
172
|
-
)
|
|
173
|
-
if _is_type(column, String):
|
|
174
|
-
return _regex(column, text_pattern)
|
|
175
|
-
type_name = spec.python_type.__name__
|
|
176
|
-
_unsupported(
|
|
177
|
-
field,
|
|
178
|
-
spec,
|
|
179
|
-
f"PostgreSQL {type_name} type_check supports typed {type_name} or String columns.",
|
|
180
|
-
)
|
|
181
|
-
raise AssertionError("unreachable")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def _predicate_uuid(
|
|
185
|
-
column: ColumnElement[Any],
|
|
186
|
-
spec: TypeSpec,
|
|
187
|
-
field: str,
|
|
188
|
-
) -> ColumnElement[bool]:
|
|
189
|
-
col_type = _col_type(column)
|
|
190
|
-
type_name = type(col_type).__name__.lower()
|
|
191
|
-
if "uuid" in type_name:
|
|
192
|
-
return cast(ColumnElement[bool], column.isnot(None))
|
|
193
|
-
if spec.strict:
|
|
194
|
-
_unsupported(
|
|
195
|
-
field,
|
|
196
|
-
spec,
|
|
197
|
-
"PostgreSQL strict UUID type_check requires a UUID column.",
|
|
198
|
-
)
|
|
199
|
-
if _is_type(column, String):
|
|
200
|
-
return _regex(column, _UUID_TEXT)
|
|
201
|
-
_unsupported(
|
|
202
|
-
field,
|
|
203
|
-
spec,
|
|
204
|
-
"PostgreSQL UUID type_check supports UUID or String columns.",
|
|
205
|
-
)
|
|
206
|
-
raise AssertionError("unreachable")
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def _build_predicate(
|
|
210
|
-
column: ColumnElement[Any],
|
|
211
|
-
spec: TypeSpec,
|
|
212
|
-
field: str,
|
|
213
|
-
) -> ColumnElement[bool]:
|
|
214
|
-
python_type = spec.python_type
|
|
215
|
-
if python_type is int:
|
|
216
|
-
return _predicate_int(column, spec, field)
|
|
217
|
-
if python_type is bool:
|
|
218
|
-
return _predicate_bool(column, spec, field)
|
|
219
|
-
if python_type is str:
|
|
220
|
-
return _predicate_str(column, spec, field)
|
|
221
|
-
if python_type in {float, Decimal}:
|
|
222
|
-
return _predicate_float(column, spec, field)
|
|
223
|
-
if python_type is date:
|
|
224
|
-
return _predicate_temporal(column, spec, field, sa_types=(Date,), text_pattern=_DATE_TEXT)
|
|
225
|
-
if python_type is datetime:
|
|
226
|
-
return _predicate_temporal(
|
|
227
|
-
column, spec, field, sa_types=(DateTime,), text_pattern=_DATETIME_TEXT
|
|
228
|
-
)
|
|
229
|
-
if python_type is time:
|
|
230
|
-
return _predicate_temporal(column, spec, field, sa_types=(Time,), text_pattern=_TIME_TEXT)
|
|
231
|
-
if python_type is UUID:
|
|
232
|
-
return _predicate_uuid(column, spec, field)
|
|
233
|
-
_unsupported(
|
|
234
|
-
field,
|
|
235
|
-
spec,
|
|
236
|
-
f"PostgreSQL type_check has no translator for {python_type!r}.",
|
|
237
|
-
)
|
|
238
|
-
raise AssertionError("unreachable")
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def translate_type_check(
|
|
242
|
-
constraint: Constraint,
|
|
243
|
-
column: ColumnElement[Any],
|
|
244
|
-
context: CompilationContext,
|
|
245
|
-
) -> ColumnElement[bool]:
|
|
246
|
-
"""Translate ``type_check`` into a PostgreSQL shape/type predicate."""
|
|
247
|
-
del context # dialect hint unused; plugin is already PostgreSQL-specific
|
|
248
|
-
spec = type_spec(constraint.value)
|
|
249
|
-
predicate = _build_predicate(column, spec, constraint.field)
|
|
250
|
-
return _wrap_none(column, predicate, allow_none=spec.allow_none)
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqlrules-postgresql
|
|
3
|
-
Version: 1.0.1
|
|
4
|
-
Summary: PostgreSQL dialect plugin for SQLRules (regex, JSONB, ARRAY, range).
|
|
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: postgresql,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-postgresql
|
|
24
|
-
|
|
25
|
-
PostgreSQL dialect plugin for [SQLRules](https://github.com/eddiethedean/sqlrules).
|
|
26
|
-
|
|
27
|
-
## Install
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pip install sqlrules-postgresql
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Usage
|
|
34
|
-
|
|
35
|
-
```python
|
|
36
|
-
import re
|
|
37
|
-
from typing import Annotated, Any
|
|
38
|
-
|
|
39
|
-
from pydantic import BaseModel, Field
|
|
40
|
-
from sqlalchemy import Column, MetaData, Table
|
|
41
|
-
from sqlalchemy.dialects.postgresql import ARRAY, INT4RANGE, JSONB, TEXT
|
|
42
|
-
|
|
43
|
-
from sqlrules import ArrayContains, Compiler, JsonContains, RangeContains
|
|
44
|
-
from sqlrules_postgresql import PostgresPlugin
|
|
45
|
-
|
|
46
|
-
class RowFilter(BaseModel):
|
|
47
|
-
name: Annotated[str, Field(pattern=re.compile(r"^a", re.I))]
|
|
48
|
-
meta: Annotated[dict[str, Any], JsonContains({"active": True})]
|
|
49
|
-
tags: Annotated[list[str], ArrayContains(["admin"])]
|
|
50
|
-
span: Annotated[int, RangeContains(5)]
|
|
51
|
-
|
|
52
|
-
table = Table(
|
|
53
|
-
"rows",
|
|
54
|
-
MetaData(),
|
|
55
|
-
Column("name", TEXT),
|
|
56
|
-
Column("meta", JSONB),
|
|
57
|
-
Column("tags", ARRAY(TEXT)),
|
|
58
|
-
Column("span", INT4RANGE),
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
compiler = Compiler(plugins=[PostgresPlugin()], dialect="postgresql")
|
|
62
|
-
rules = compiler.compile(RowFilter, table)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Operators
|
|
66
|
-
|
|
67
|
-
| IR operator | SQLAlchemy / PostgreSQL |
|
|
68
|
-
|---|---|
|
|
69
|
-
| `pattern` | `~` or `~*` (when `PatternSpec.ignore_case`) |
|
|
70
|
-
| `type_check` | Shape/type predicates from `TypeSpec` (see matrix below) |
|
|
71
|
-
| `json_contains` | JSONB `contains` / `@>` |
|
|
72
|
-
| `json_has_key` | JSONB `has_key` / `?` |
|
|
73
|
-
| `array_contains` | array `contains` |
|
|
74
|
-
| `array_overlap` | array `overlap` / `&&` |
|
|
75
|
-
| `range_contains` | range `@>` |
|
|
76
|
-
| `range_overlap` | range `&&` |
|
|
77
|
-
|
|
78
|
-
### `type_check` matrix (approximate)
|
|
79
|
-
|
|
80
|
-
Enable with `Compiler(..., emit_type_checks=True)`. Not full Pydantic
|
|
81
|
-
parity — inexpressible pairs raise.
|
|
82
|
-
|
|
83
|
-
| Python type | Lax | Strict |
|
|
84
|
-
|---|---|---|
|
|
85
|
-
| `int` | Integer column; String `~` digit pattern; numeric whole-number | Integer column only |
|
|
86
|
-
| `bool` | unsupported (raise) | Boolean `IN (true, false)` |
|
|
87
|
-
| `str` | String/Text `IS NOT NULL` | same |
|
|
88
|
-
| `float` / `Decimal` | numeric column; String float-ish `~` | numeric column |
|
|
89
|
-
| `date` / `datetime` / `time` / `UUID` | typed column or String format `~` | typed column |
|
|
90
|
-
|
|
91
|
-
`Optional[T]` → `(column IS NULL) OR <predicate>`.
|
|
92
|
-
|
|
93
|
-
## Security note
|
|
94
|
-
|
|
95
|
-
`pattern` becomes a PostgreSQL regex (`~` / `~*`). Untrusted pattern strings
|
|
96
|
-
can cause expensive engine-side evaluation (ReDoS-class cost). Prefer
|
|
97
|
-
static/allowlisted patterns. See
|
|
98
|
-
[SECURITY](https://sqlrules.readthedocs.io/en/latest/SECURITY.html).
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
sqlrules_postgresql/__init__.py,sha256=Axgd7Vsg1PKCIMCi4iU9SaEtK3sePkM14v9dOVqLOL4,1805
|
|
2
|
-
sqlrules_postgresql/array.py,sha256=Kzin-DQRwidhVsL6hL8c04ejSjEfGjcMZewjSHmH_3s,764
|
|
3
|
-
sqlrules_postgresql/jsonb.py,sha256=SKuAD5S8RZZ27-ntbXSi1jVJ3b7Yw7K956DOUYFYUvU,750
|
|
4
|
-
sqlrules_postgresql/pattern.py,sha256=swLYUSmd0sgW-XSYunYBBnoT2QpwkzDsXfuL38cgXqU,599
|
|
5
|
-
sqlrules_postgresql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
sqlrules_postgresql/range.py,sha256=WBvd5yHpkP2UNmU_TxCNdHj-HdTMYVPO_PztqWj6TUs,774
|
|
7
|
-
sqlrules_postgresql/type_check.py,sha256=q7AA5hMiVLgDmdhfWt64123sBHcGG3ntkmprf-w58s8,7927
|
|
8
|
-
sqlrules_postgresql-1.0.1.dist-info/METADATA,sha256=ILbicMQXIGVkPQ3gtYYNgLJvdf4FnvRTmlrMSJLHMeI,3232
|
|
9
|
-
sqlrules_postgresql-1.0.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
10
|
-
sqlrules_postgresql-1.0.1.dist-info/licenses/LICENSE,sha256=cLMlkCH6RhjWAkXBWR7LazV98TOG0WkPtCKB5zqqkAs,1078
|
|
11
|
-
sqlrules_postgresql-1.0.1.dist-info/RECORD,,
|
{sqlrules_postgresql-1.0.1.dist-info → sqlrules_postgresql-2.0.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|