j-perm-sql 0.1.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.
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: j-perm-sql
3
+ Version: 0.1.0
4
+ Summary: j-perm plugin for SQL
5
+ Author-email: Roman <kuschanow@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kuschanow/j-perm
8
+ Project-URL: Source, https://github.com/kuschanow/j-perm
9
+ Project-URL: Tracker, https://github.com/kuschanow/j-perm/issues
10
+ Project-URL: Documentation, https://github.com/kuschanow/j-perm
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: j-perm>=1.9.0
14
+
15
+ # j-perm-sql
16
+
17
+ A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
18
+ **SQL `SELECT` queries** from j-perm constructs.
19
+
20
+ * SQL is described with a tree of `$`-constructs (`$select`, `$col`, `$val`,
21
+ predicates, joins, …).
22
+ * A single new top-level operation — `op: sql` — renders that tree to a
23
+ **parameterized** `(sql, params)` pair and hands it to a configurable
24
+ executor (any ORM's raw-execute function).
25
+ * The SQL constructs live in an **isolated** named pipeline: they mean nothing
26
+ outside `op: sql`. `{"$select": …}` used as an ordinary value is just a dict.
27
+
28
+ > v1 scope: the full standard **`SELECT`** surface (read-only). DDL/DML
29
+ > (`CREATE`/`ALTER`/`INSERT`/`UPDATE`/`DELETE`) is intentionally out of scope.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install j-perm-sql
35
+ ```
36
+
37
+ Requires `j-perm >= 1.9.0` (the version that made `run_pipeline` a passthrough
38
+ invoker, which this plugin relies on).
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from j_perm import build_default_engine
44
+ from j_perm_sql import install_sql
45
+
46
+ def run_sql(sql, params):
47
+ # any ORM's raw execute: cursor.execute(sql, params); return rows
48
+ ...
49
+
50
+ engine = build_default_engine()
51
+ install_sql(engine, run_sql, paramstyle="qmark")
52
+
53
+ engine.apply(
54
+ {"op": "sql", "to": "/rows", "query": {"$select": {
55
+ "columns": [{"$col": {"name": "id"}}, {"$col": {"name": "name"}}],
56
+ "from": {"table": "users"},
57
+ "where": {"$gte": [{"$col": {"name": "age"}}, {"$val": 18}]},
58
+ "order_by": [{"expr": {"$col": {"name": "name"}}}],
59
+ "limit": 50,
60
+ }}},
61
+ source={}, dest={},
62
+ )
63
+ # run_sql receives: ('SELECT "id", "name" FROM "users" WHERE "age" >= ? ORDER BY "name" LIMIT 50', [18])
64
+ # result is written to dest at /rows
65
+ ```
66
+
67
+ `install_sql` **patches an existing engine** — it registers the isolated SQL
68
+ pipeline and the `op: sql` operation. It composes with any engine and any other
69
+ plugins.
70
+
71
+ ## The `op: sql` operation
72
+
73
+ ```js
74
+ {"op": "sql", "query": <SQL construct tree>, "to": "/dest/path"}
75
+ ```
76
+
77
+ * `query` — the SQL construct tree.
78
+ * `to` — optional destination pointer (template-expanded); the executor's
79
+ result is written there. If omitted, the result is discarded.
80
+
81
+ ## Parameterization & injection safety
82
+
83
+ Data values are **always bound as parameters**, never interpolated:
84
+
85
+ * `$val` (and the data sides of `$in`, `$between`, `$values`) emit a placeholder
86
+ and add the value to `params`.
87
+ * Identifiers (table/column/alias names) are validated against a conservative
88
+ charset and quoted.
89
+ * Function names, CAST types, join types, sort directions, etc. are validated
90
+ against whitelists.
91
+
92
+ ```python
93
+ {"$eq": [{"$col": {"name": "name"}}, {"$val": {"$ref": "/user_input"}}]}
94
+ # → '"name" = ?' with the (possibly malicious) value safely in params
95
+ ```
96
+
97
+ Inside `$val`, the value expression is resolved with j-perm's normal value
98
+ pipeline, so `$ref`, `${…}` templates, and `@:` dest-pointers all work.
99
+
100
+ ## Dialect / `RenderOptions`
101
+
102
+ Everything that genuinely differs between databases is configurable:
103
+
104
+ ```python
105
+ from j_perm_sql import RenderOptions
106
+
107
+ install_sql(engine, run_sql, dialect=RenderOptions(
108
+ paramstyle="numeric", # qmark (?) | format (%s) | numeric ($1) | named (:p1)
109
+ identifier_quote='"', # e.g. "`" for MySQL
110
+ pagination="fetch", # "limit" (LIMIT n OFFSET m) | "fetch" (OFFSET m ROWS FETCH FIRST n ROWS ONLY)
111
+ concat_operator="||", # "||" or "+"
112
+ ))
113
+ ```
114
+
115
+ ## Sync & async
116
+
117
+ `install_sql` inspects the executor: a coroutine function registers the async
118
+ handler (use with `engine.apply_async`); a regular function registers the sync
119
+ handler (use with `engine.apply`).
120
+
121
+ ```python
122
+ async def run_sql(sql, params): ...
123
+ install_sql(engine, run_sql) # async
124
+ await engine.apply_async(spec, source=…, dest=…)
125
+ ```
126
+
127
+ ## Construct reference
128
+
129
+ **Query**
130
+
131
+ | Construct | Form |
132
+ |---|---|
133
+ | `$select` | `{with?, distinct?, columns?, from?, joins?, where?, group_by?, having?, order_by?, limit?/offset? \| fetch?}` |
134
+ | `$union` / `$union_all` / `$intersect` / `$except` | `{"$union": [q1, q2, …], order_by?, limit?…}` |
135
+ | `$values` | `{"$values": [[…row…], …]}` (table source or `IN`) |
136
+
137
+ **Projection / expressions**
138
+
139
+ | Construct | Renders |
140
+ |---|---|
141
+ | `$col` | `"t"."name" [AS "alias"]`; `"id"`; `*`; `"t".*` |
142
+ | `$val` | a bound parameter |
143
+ | `$func` / `$call` | `NAME([DISTINCT ]args)[ OVER (…)][ AS "alias"]` (use `"*"` for `COUNT(*)`) |
144
+ | `$cast` | `CAST(expr AS TYPE)` |
145
+ | `$case` | searched `CASE WHEN … THEN … [ELSE …] END` |
146
+ | `$concat` | `(a || b …)` |
147
+ | `$add` `$sub` `$mul` `$div` `$mod` | `(a op b …)` |
148
+
149
+ Projection items may also be `{"expr": <operand>, "as": "alias"}`.
150
+
151
+ **Predicates** (WHERE / HAVING / ON)
152
+
153
+ `$and` `$or` `$not` · `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` ·
154
+ `$in`/`$not_in` (list or subquery) · `$between`/`$not_between` ·
155
+ `$like`/`$not_like` (+ `escape`) · `$is_null`/`$is_not_null` ·
156
+ `$exists`/`$not_exists` · `$any`/`$all`/`$some` (quantified).
157
+
158
+ **FROM / JOIN** — *table source* = a table name (string), a
159
+ `{table, as?, schema?}` dict, a nested `$select`/`$values` (derived table, needs
160
+ `as`), or `lateral: true`. `$join`: `{type, table, as?, on? | using?, natural?}`
161
+ with type `inner`/`left`/`right`/`full`/`cross`.
162
+
163
+ **Windows** — `over` on `$func`: `{partition_by?, order_by?, frame?}` where
164
+ `frame` is `{type: "rows"|"range", start, end?}` and a bound is
165
+ `"unbounded preceding" | "unbounded following" | "current row" |
166
+ {preceding: n} | {following: n}`.
167
+
168
+ **GROUP BY** — a list of expressions, or `{"$rollup": […]}` /
169
+ `{"$cube": […]}` / `{"$grouping_sets": [[…], …]}`.
170
+
171
+ **CTE** — `with`: `[{name, columns?, recursive?, query: $select}]`.
172
+
173
+ See `tests/` for end-to-end examples.
174
+
175
+ ## Portability caveats
176
+
177
+ The DSL renders standard SQL and does **not** validate that a target database
178
+ supports every feature — portability is the query author's responsibility:
179
+
180
+ * `LIMIT/OFFSET` vs `OFFSET/FETCH`, `RIGHT/FULL JOIN`, `INTERSECT/EXCEPT`,
181
+ `NATURAL JOIN`, `NULLS FIRST/LAST`, `LATERAL`, `GROUPING SETS/ROLLUP/CUBE`,
182
+ and the concatenation operator (`||` vs `+`) are not universal.
183
+ * CTEs and window functions are standard but require recent versions
184
+ (e.g. MySQL ≥ 8, SQLite ≥ 3.25 for windows / ≥ 3.8.3 for CTEs).
185
+
186
+ Use `RenderOptions` to match the target dialect's placeholder style, identifier
187
+ quoting, pagination form, and concatenation operator.
188
+
189
+ ## License
190
+
191
+ MIT
@@ -0,0 +1,177 @@
1
+ # j-perm-sql
2
+
3
+ A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
4
+ **SQL `SELECT` queries** from j-perm constructs.
5
+
6
+ * SQL is described with a tree of `$`-constructs (`$select`, `$col`, `$val`,
7
+ predicates, joins, …).
8
+ * A single new top-level operation — `op: sql` — renders that tree to a
9
+ **parameterized** `(sql, params)` pair and hands it to a configurable
10
+ executor (any ORM's raw-execute function).
11
+ * The SQL constructs live in an **isolated** named pipeline: they mean nothing
12
+ outside `op: sql`. `{"$select": …}` used as an ordinary value is just a dict.
13
+
14
+ > v1 scope: the full standard **`SELECT`** surface (read-only). DDL/DML
15
+ > (`CREATE`/`ALTER`/`INSERT`/`UPDATE`/`DELETE`) is intentionally out of scope.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install j-perm-sql
21
+ ```
22
+
23
+ Requires `j-perm >= 1.9.0` (the version that made `run_pipeline` a passthrough
24
+ invoker, which this plugin relies on).
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ from j_perm import build_default_engine
30
+ from j_perm_sql import install_sql
31
+
32
+ def run_sql(sql, params):
33
+ # any ORM's raw execute: cursor.execute(sql, params); return rows
34
+ ...
35
+
36
+ engine = build_default_engine()
37
+ install_sql(engine, run_sql, paramstyle="qmark")
38
+
39
+ engine.apply(
40
+ {"op": "sql", "to": "/rows", "query": {"$select": {
41
+ "columns": [{"$col": {"name": "id"}}, {"$col": {"name": "name"}}],
42
+ "from": {"table": "users"},
43
+ "where": {"$gte": [{"$col": {"name": "age"}}, {"$val": 18}]},
44
+ "order_by": [{"expr": {"$col": {"name": "name"}}}],
45
+ "limit": 50,
46
+ }}},
47
+ source={}, dest={},
48
+ )
49
+ # run_sql receives: ('SELECT "id", "name" FROM "users" WHERE "age" >= ? ORDER BY "name" LIMIT 50', [18])
50
+ # result is written to dest at /rows
51
+ ```
52
+
53
+ `install_sql` **patches an existing engine** — it registers the isolated SQL
54
+ pipeline and the `op: sql` operation. It composes with any engine and any other
55
+ plugins.
56
+
57
+ ## The `op: sql` operation
58
+
59
+ ```js
60
+ {"op": "sql", "query": <SQL construct tree>, "to": "/dest/path"}
61
+ ```
62
+
63
+ * `query` — the SQL construct tree.
64
+ * `to` — optional destination pointer (template-expanded); the executor's
65
+ result is written there. If omitted, the result is discarded.
66
+
67
+ ## Parameterization & injection safety
68
+
69
+ Data values are **always bound as parameters**, never interpolated:
70
+
71
+ * `$val` (and the data sides of `$in`, `$between`, `$values`) emit a placeholder
72
+ and add the value to `params`.
73
+ * Identifiers (table/column/alias names) are validated against a conservative
74
+ charset and quoted.
75
+ * Function names, CAST types, join types, sort directions, etc. are validated
76
+ against whitelists.
77
+
78
+ ```python
79
+ {"$eq": [{"$col": {"name": "name"}}, {"$val": {"$ref": "/user_input"}}]}
80
+ # → '"name" = ?' with the (possibly malicious) value safely in params
81
+ ```
82
+
83
+ Inside `$val`, the value expression is resolved with j-perm's normal value
84
+ pipeline, so `$ref`, `${…}` templates, and `@:` dest-pointers all work.
85
+
86
+ ## Dialect / `RenderOptions`
87
+
88
+ Everything that genuinely differs between databases is configurable:
89
+
90
+ ```python
91
+ from j_perm_sql import RenderOptions
92
+
93
+ install_sql(engine, run_sql, dialect=RenderOptions(
94
+ paramstyle="numeric", # qmark (?) | format (%s) | numeric ($1) | named (:p1)
95
+ identifier_quote='"', # e.g. "`" for MySQL
96
+ pagination="fetch", # "limit" (LIMIT n OFFSET m) | "fetch" (OFFSET m ROWS FETCH FIRST n ROWS ONLY)
97
+ concat_operator="||", # "||" or "+"
98
+ ))
99
+ ```
100
+
101
+ ## Sync & async
102
+
103
+ `install_sql` inspects the executor: a coroutine function registers the async
104
+ handler (use with `engine.apply_async`); a regular function registers the sync
105
+ handler (use with `engine.apply`).
106
+
107
+ ```python
108
+ async def run_sql(sql, params): ...
109
+ install_sql(engine, run_sql) # async
110
+ await engine.apply_async(spec, source=…, dest=…)
111
+ ```
112
+
113
+ ## Construct reference
114
+
115
+ **Query**
116
+
117
+ | Construct | Form |
118
+ |---|---|
119
+ | `$select` | `{with?, distinct?, columns?, from?, joins?, where?, group_by?, having?, order_by?, limit?/offset? \| fetch?}` |
120
+ | `$union` / `$union_all` / `$intersect` / `$except` | `{"$union": [q1, q2, …], order_by?, limit?…}` |
121
+ | `$values` | `{"$values": [[…row…], …]}` (table source or `IN`) |
122
+
123
+ **Projection / expressions**
124
+
125
+ | Construct | Renders |
126
+ |---|---|
127
+ | `$col` | `"t"."name" [AS "alias"]`; `"id"`; `*`; `"t".*` |
128
+ | `$val` | a bound parameter |
129
+ | `$func` / `$call` | `NAME([DISTINCT ]args)[ OVER (…)][ AS "alias"]` (use `"*"` for `COUNT(*)`) |
130
+ | `$cast` | `CAST(expr AS TYPE)` |
131
+ | `$case` | searched `CASE WHEN … THEN … [ELSE …] END` |
132
+ | `$concat` | `(a || b …)` |
133
+ | `$add` `$sub` `$mul` `$div` `$mod` | `(a op b …)` |
134
+
135
+ Projection items may also be `{"expr": <operand>, "as": "alias"}`.
136
+
137
+ **Predicates** (WHERE / HAVING / ON)
138
+
139
+ `$and` `$or` `$not` · `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` ·
140
+ `$in`/`$not_in` (list or subquery) · `$between`/`$not_between` ·
141
+ `$like`/`$not_like` (+ `escape`) · `$is_null`/`$is_not_null` ·
142
+ `$exists`/`$not_exists` · `$any`/`$all`/`$some` (quantified).
143
+
144
+ **FROM / JOIN** — *table source* = a table name (string), a
145
+ `{table, as?, schema?}` dict, a nested `$select`/`$values` (derived table, needs
146
+ `as`), or `lateral: true`. `$join`: `{type, table, as?, on? | using?, natural?}`
147
+ with type `inner`/`left`/`right`/`full`/`cross`.
148
+
149
+ **Windows** — `over` on `$func`: `{partition_by?, order_by?, frame?}` where
150
+ `frame` is `{type: "rows"|"range", start, end?}` and a bound is
151
+ `"unbounded preceding" | "unbounded following" | "current row" |
152
+ {preceding: n} | {following: n}`.
153
+
154
+ **GROUP BY** — a list of expressions, or `{"$rollup": […]}` /
155
+ `{"$cube": […]}` / `{"$grouping_sets": [[…], …]}`.
156
+
157
+ **CTE** — `with`: `[{name, columns?, recursive?, query: $select}]`.
158
+
159
+ See `tests/` for end-to-end examples.
160
+
161
+ ## Portability caveats
162
+
163
+ The DSL renders standard SQL and does **not** validate that a target database
164
+ supports every feature — portability is the query author's responsibility:
165
+
166
+ * `LIMIT/OFFSET` vs `OFFSET/FETCH`, `RIGHT/FULL JOIN`, `INTERSECT/EXCEPT`,
167
+ `NATURAL JOIN`, `NULLS FIRST/LAST`, `LATERAL`, `GROUPING SETS/ROLLUP/CUBE`,
168
+ and the concatenation operator (`||` vs `+`) are not universal.
169
+ * CTEs and window functions are standard but require recent versions
170
+ (e.g. MySQL ≥ 8, SQLite ≥ 3.25 for windows / ≥ 3.8.3 for CTEs).
171
+
172
+ Use `RenderOptions` to match the target dialect's placeholder style, identifier
173
+ quoting, pagination form, and concatenation operator.
174
+
175
+ ## License
176
+
177
+ MIT
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=68"
4
+ ]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "j-perm-sql"
9
+ version = "0.1.0"
10
+ description = "j-perm plugin for SQL"
11
+ authors = [
12
+ { name = "Roman", email = "kuschanow@gmail.com" },
13
+ ]
14
+ readme = "README.md"
15
+
16
+ requires-python = ">=3.10"
17
+
18
+ license = { text = "MIT" }
19
+
20
+ dependencies = [
21
+ "j-perm>=1.9.0",
22
+ ]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+ pythonpath = ["src"]
27
+ addopts = "--cov=src/j_perm_sql --cov-report=term-missing"
28
+ asyncio_mode = "auto"
29
+
30
+ [tool.coverage.run]
31
+ source = ["src/j_perm_sql"]
32
+ omit = []
33
+
34
+ [tool.coverage.report]
35
+ fail_under = 100
36
+ exclude_lines = [
37
+ "pragma: no cover",
38
+ "if TYPE_CHECKING:",
39
+ "@abstractmethod",
40
+ "^\\s*\\.\\.\\.\\s*$",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/kuschanow/j-perm"
45
+ Source = "https://github.com/kuschanow/j-perm"
46
+ Tracker = "https://github.com/kuschanow/j-perm/issues"
47
+ Documentation = "https://github.com/kuschanow/j-perm"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,48 @@
1
+ """j_perm_sql — build and execute SQL queries from j-perm constructs.
2
+
3
+ SQL is described with a tree of ``$``-constructs (``$select``, ``$col``,
4
+ ``$val``, predicates, joins, …) and rendered by an **isolated** named pipeline.
5
+ A single top-level operation, ``op: sql``, renders that tree to a parameterized
6
+ ``(sql, params)`` pair and hands it to a configurable executor (any ORM's raw
7
+ execute). The SQL constructs are never visible to the engine's normal value
8
+ pipeline — they only mean anything inside ``op: sql``.
9
+
10
+ Quick start::
11
+
12
+ from j_perm import build_default_engine
13
+ from j_perm_sql import install_sql
14
+
15
+ engine = build_default_engine()
16
+ install_sql(engine, my_orm_raw_execute, paramstyle="qmark")
17
+
18
+ engine.apply(
19
+ {"op": "sql", "to": "/rows", "query": {"$select": {
20
+ "columns": [{"$col": {"name": "id"}}],
21
+ "from": {"table": "users"},
22
+ "where": {"$gte": [{"$col": {"name": "age"}}, {"$val": 18}]},
23
+ }}},
24
+ source={}, dest={},
25
+ )
26
+ """
27
+ from .constructs import build_sql_specials
28
+ from .dialect import PLACEHOLDER, RenderOptions
29
+ from .handler import AsyncSqlHandler, SqlHandler, SqlRenderer
30
+ from .install import install_sql
31
+ from .pipeline import SQL_PIPELINE_NAME, build_sql_pipeline
32
+ from .render import fragment, is_fragment, is_query, render
33
+
34
+ __all__ = [
35
+ "install_sql",
36
+ "RenderOptions",
37
+ "PLACEHOLDER",
38
+ "SqlHandler",
39
+ "AsyncSqlHandler",
40
+ "SqlRenderer",
41
+ "build_sql_pipeline",
42
+ "build_sql_specials",
43
+ "SQL_PIPELINE_NAME",
44
+ "fragment",
45
+ "is_fragment",
46
+ "is_query",
47
+ "render",
48
+ ]