j-perm-sql 0.1.0__tar.gz → 0.2.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.
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/PKG-INFO +103 -17
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/README.md +102 -16
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/pyproject.toml +1 -1
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql/__init__.py +12 -2
- j_perm_sql-0.2.0/src/j_perm_sql/constructs_write.py +138 -0
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql/handler.py +17 -6
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql/install.py +46 -12
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql/pipeline.py +24 -5
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql/render.py +15 -3
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql.egg-info/PKG-INFO +103 -17
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql.egg-info/SOURCES.txt +1 -0
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/setup.cfg +0 -0
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql/constructs.py +0 -0
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql/dialect.py +0 -0
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql.egg-info/dependency_links.txt +0 -0
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql.egg-info/requires.txt +0 -0
- {j_perm_sql-0.1.0 → j_perm_sql-0.2.0}/src/j_perm_sql.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: j-perm-sql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: j-perm plugin for SQL
|
|
5
5
|
Author-email: Roman <kuschanow@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -15,18 +15,23 @@ Requires-Dist: j-perm>=1.9.0
|
|
|
15
15
|
# j-perm-sql
|
|
16
16
|
|
|
17
17
|
A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
|
|
18
|
-
**SQL
|
|
18
|
+
**SQL queries** from j-perm constructs.
|
|
19
19
|
|
|
20
20
|
* SQL is described with a tree of `$`-constructs (`$select`, `$col`, `$val`,
|
|
21
21
|
predicates, joins, …).
|
|
22
|
-
* A
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
* A top-level operation renders that tree to a **parameterized** `(sql, params)`
|
|
23
|
+
pair and hands it to a configurable executor (any ORM's raw-execute function):
|
|
24
|
+
`op: sql` for read-only `SELECT`, and (opt-in) `op: sql_write` for
|
|
25
|
+
`INSERT`/`UPDATE`/`DELETE`.
|
|
26
|
+
* The SQL constructs live in **isolated** named pipelines: they mean nothing
|
|
27
|
+
outside those operations. `{"$select": …}` used as an ordinary value is just a
|
|
28
|
+
dict.
|
|
29
|
+
|
|
30
|
+
> Scope: the full standard **`SELECT`** surface (read-only) via `install_sql`,
|
|
31
|
+
> plus standard **`INSERT`/`UPDATE`/`DELETE`** (row content only) via
|
|
32
|
+
> `install_sql_write`. Schema DDL (`CREATE`/`ALTER`/`DROP` of tables/columns)
|
|
33
|
+
> and non-universal DML (`RETURNING`, `ON CONFLICT`/upsert, `UPDATE … FROM`,
|
|
34
|
+
> `DELETE … USING`) are intentionally out of scope.
|
|
30
35
|
|
|
31
36
|
## Install
|
|
32
37
|
|
|
@@ -78,12 +83,79 @@ plugins.
|
|
|
78
83
|
* `to` — optional destination pointer (template-expanded); the executor's
|
|
79
84
|
result is written there. If omitted, the result is discarded.
|
|
80
85
|
|
|
86
|
+
## Writing data (`INSERT`/`UPDATE`/`DELETE`)
|
|
87
|
+
|
|
88
|
+
Writing is a **separate, opt-in install** — you don't get it unless you ask for
|
|
89
|
+
it, and `op: sql` stays guaranteed read-only even when both are installed:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from j_perm_sql import install_sql, install_sql_write
|
|
93
|
+
|
|
94
|
+
install_sql(engine, run_sql) # read-only op: sql (optional)
|
|
95
|
+
install_sql_write(engine, run_sql) # write op: sql_write
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`install_sql_write(engine, executor, *, paramstyle="qmark", dialect=None,
|
|
99
|
+
op="sql_write")` registers an isolated **write pipeline** (the full `SELECT`
|
|
100
|
+
surface *plus* the DML statements, so `WHERE` predicates, `SET` expressions and
|
|
101
|
+
`INSERT … SELECT` subqueries all work) and the `op: sql_write` operation. It is
|
|
102
|
+
independent of `install_sql` — either may be installed alone, in any order. It
|
|
103
|
+
selects the sync/async handler the same way (by `asyncio.iscoroutinefunction`).
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
{"op": "sql_write", "query": <DML construct tree>, "to": "/dest/path"}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**`$insert`** — exactly one of `values` / `query`:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
{"$insert": {
|
|
113
|
+
"into": "users", # or {"table": "users", "schema": "app"}
|
|
114
|
+
"columns": ["name", "age"], # optional
|
|
115
|
+
"values": [[{"$val": "Ann"}, {"$val": 30}], ...], # cells are operands ($val to bind)
|
|
116
|
+
# ── OR ──
|
|
117
|
+
"query": {"$select": {...}}, # INSERT … SELECT
|
|
118
|
+
}}
|
|
119
|
+
# → INSERT INTO "users" ("name", "age") VALUES (?, ?) params=["Ann", 30]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**`$update`** — single table:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
{"$update": {
|
|
126
|
+
"table": "users", # or {"table": "users", "schema": "app"}
|
|
127
|
+
"set": {"name": {"$val": "Bob"},
|
|
128
|
+
"visits": {"$add": [{"$col": "visits"}, {"$val": 1}]}},
|
|
129
|
+
"where": {"$eq": [{"$col": "id"}, {"$val": 5}]}, # or "all": true
|
|
130
|
+
}}
|
|
131
|
+
# → UPDATE "users" SET "name" = ?, "visits" = ("visits" + ?) WHERE "id" = ?
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**`$delete`** — single table:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
{"$delete": {
|
|
138
|
+
"from": "sessions", # or {"table": "sessions", "schema": "app"}
|
|
139
|
+
"where": {"$lt": [{"$col": "last_seen"}, {"$val": "2020-01-01"}]}, # or "all": true
|
|
140
|
+
}}
|
|
141
|
+
# → DELETE FROM "sessions" WHERE "last_seen" < ?
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`set` values, `$insert` cells, and `where` predicates are ordinary read
|
|
145
|
+
constructs, so the full expression/predicate/subquery surface (including
|
|
146
|
+
correlated subqueries) is available, and data is always bound as parameters.
|
|
147
|
+
|
|
148
|
+
> **WHERE guard.** A `$update` / `$delete` **without** a `where` raises unless
|
|
149
|
+
> you pass an explicit `"all": true`. This prevents an accidental full-table
|
|
150
|
+
> update/delete.
|
|
151
|
+
|
|
81
152
|
## Parameterization & injection safety
|
|
82
153
|
|
|
83
154
|
Data values are **always bound as parameters**, never interpolated:
|
|
84
155
|
|
|
85
|
-
* `$val` (and the data sides of `$in`, `$between`, `$values`
|
|
86
|
-
and add the value to
|
|
156
|
+
* `$val` (and the data sides of `$in`, `$between`, `$values`, `$update`'s `set`
|
|
157
|
+
values, and `$insert`'s row cells) emit a placeholder and add the value to
|
|
158
|
+
`params`.
|
|
87
159
|
* Identifiers (table/column/alias names) are validated against a conservative
|
|
88
160
|
charset and quoted.
|
|
89
161
|
* Function names, CAST types, join types, sort directions, etc. are validated
|
|
@@ -94,8 +166,14 @@ Data values are **always bound as parameters**, never interpolated:
|
|
|
94
166
|
# → '"name" = ?' with the (possibly malicious) value safely in params
|
|
95
167
|
```
|
|
96
168
|
|
|
97
|
-
|
|
98
|
-
|
|
169
|
+
**Parameters come from inside j-perm — no external param source.** Inside `$val`
|
|
170
|
+
the value expression is resolved with j-perm's normal value pipeline, so `$ref`,
|
|
171
|
+
`${…}` templates, and `@:` dest-pointers all work. The `params` list handed to
|
|
172
|
+
the executor is simply the values j-perm itself computed from the document; the
|
|
173
|
+
parameter binding exists only for injection safety and the driver's paramstyle.
|
|
174
|
+
The whole flow (source → SQL → bound values) is self-contained in one j-perm
|
|
175
|
+
run; the executor is just the pipe to the driver. This applies equally to read
|
|
176
|
+
(`op: sql`) and write (`op: sql_write`).
|
|
99
177
|
|
|
100
178
|
## Dialect / `RenderOptions`
|
|
101
179
|
|
|
@@ -114,9 +192,9 @@ install_sql(engine, run_sql, dialect=RenderOptions(
|
|
|
114
192
|
|
|
115
193
|
## Sync & async
|
|
116
194
|
|
|
117
|
-
`install_sql`
|
|
118
|
-
handler (use with `engine.apply_async`); a regular
|
|
119
|
-
handler (use with `engine.apply`).
|
|
195
|
+
Both `install_sql` and `install_sql_write` inspect the executor: a coroutine
|
|
196
|
+
function registers the async handler (use with `engine.apply_async`); a regular
|
|
197
|
+
function registers the sync handler (use with `engine.apply`).
|
|
120
198
|
|
|
121
199
|
```python
|
|
122
200
|
async def run_sql(sql, params): ...
|
|
@@ -134,6 +212,14 @@ await engine.apply_async(spec, source=…, dest=…)
|
|
|
134
212
|
| `$union` / `$union_all` / `$intersect` / `$except` | `{"$union": [q1, q2, …], order_by?, limit?…}` |
|
|
135
213
|
| `$values` | `{"$values": [[…row…], …]}` (table source or `IN`) |
|
|
136
214
|
|
|
215
|
+
**Write (DML)** — only via `install_sql_write` / `op: sql_write`
|
|
216
|
+
|
|
217
|
+
| Construct | Form |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `$insert` | `{into, columns?, values \| query}` (exactly one of `values`/`query`) |
|
|
220
|
+
| `$update` | `{table, set: {col: operand}, where? \| "all": true}` |
|
|
221
|
+
| `$delete` | `{from, where? \| "all": true}` |
|
|
222
|
+
|
|
137
223
|
**Projection / expressions**
|
|
138
224
|
|
|
139
225
|
| Construct | Renders |
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
# j-perm-sql
|
|
2
2
|
|
|
3
3
|
A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
|
|
4
|
-
**SQL
|
|
4
|
+
**SQL queries** from j-perm constructs.
|
|
5
5
|
|
|
6
6
|
* SQL is described with a tree of `$`-constructs (`$select`, `$col`, `$val`,
|
|
7
7
|
predicates, joins, …).
|
|
8
|
-
* A
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
* A top-level operation renders that tree to a **parameterized** `(sql, params)`
|
|
9
|
+
pair and hands it to a configurable executor (any ORM's raw-execute function):
|
|
10
|
+
`op: sql` for read-only `SELECT`, and (opt-in) `op: sql_write` for
|
|
11
|
+
`INSERT`/`UPDATE`/`DELETE`.
|
|
12
|
+
* The SQL constructs live in **isolated** named pipelines: they mean nothing
|
|
13
|
+
outside those operations. `{"$select": …}` used as an ordinary value is just a
|
|
14
|
+
dict.
|
|
15
|
+
|
|
16
|
+
> Scope: the full standard **`SELECT`** surface (read-only) via `install_sql`,
|
|
17
|
+
> plus standard **`INSERT`/`UPDATE`/`DELETE`** (row content only) via
|
|
18
|
+
> `install_sql_write`. Schema DDL (`CREATE`/`ALTER`/`DROP` of tables/columns)
|
|
19
|
+
> and non-universal DML (`RETURNING`, `ON CONFLICT`/upsert, `UPDATE … FROM`,
|
|
20
|
+
> `DELETE … USING`) are intentionally out of scope.
|
|
16
21
|
|
|
17
22
|
## Install
|
|
18
23
|
|
|
@@ -64,12 +69,79 @@ plugins.
|
|
|
64
69
|
* `to` — optional destination pointer (template-expanded); the executor's
|
|
65
70
|
result is written there. If omitted, the result is discarded.
|
|
66
71
|
|
|
72
|
+
## Writing data (`INSERT`/`UPDATE`/`DELETE`)
|
|
73
|
+
|
|
74
|
+
Writing is a **separate, opt-in install** — you don't get it unless you ask for
|
|
75
|
+
it, and `op: sql` stays guaranteed read-only even when both are installed:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from j_perm_sql import install_sql, install_sql_write
|
|
79
|
+
|
|
80
|
+
install_sql(engine, run_sql) # read-only op: sql (optional)
|
|
81
|
+
install_sql_write(engine, run_sql) # write op: sql_write
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`install_sql_write(engine, executor, *, paramstyle="qmark", dialect=None,
|
|
85
|
+
op="sql_write")` registers an isolated **write pipeline** (the full `SELECT`
|
|
86
|
+
surface *plus* the DML statements, so `WHERE` predicates, `SET` expressions and
|
|
87
|
+
`INSERT … SELECT` subqueries all work) and the `op: sql_write` operation. It is
|
|
88
|
+
independent of `install_sql` — either may be installed alone, in any order. It
|
|
89
|
+
selects the sync/async handler the same way (by `asyncio.iscoroutinefunction`).
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
{"op": "sql_write", "query": <DML construct tree>, "to": "/dest/path"}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**`$insert`** — exactly one of `values` / `query`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
{"$insert": {
|
|
99
|
+
"into": "users", # or {"table": "users", "schema": "app"}
|
|
100
|
+
"columns": ["name", "age"], # optional
|
|
101
|
+
"values": [[{"$val": "Ann"}, {"$val": 30}], ...], # cells are operands ($val to bind)
|
|
102
|
+
# ── OR ──
|
|
103
|
+
"query": {"$select": {...}}, # INSERT … SELECT
|
|
104
|
+
}}
|
|
105
|
+
# → INSERT INTO "users" ("name", "age") VALUES (?, ?) params=["Ann", 30]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**`$update`** — single table:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
{"$update": {
|
|
112
|
+
"table": "users", # or {"table": "users", "schema": "app"}
|
|
113
|
+
"set": {"name": {"$val": "Bob"},
|
|
114
|
+
"visits": {"$add": [{"$col": "visits"}, {"$val": 1}]}},
|
|
115
|
+
"where": {"$eq": [{"$col": "id"}, {"$val": 5}]}, # or "all": true
|
|
116
|
+
}}
|
|
117
|
+
# → UPDATE "users" SET "name" = ?, "visits" = ("visits" + ?) WHERE "id" = ?
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**`$delete`** — single table:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
{"$delete": {
|
|
124
|
+
"from": "sessions", # or {"table": "sessions", "schema": "app"}
|
|
125
|
+
"where": {"$lt": [{"$col": "last_seen"}, {"$val": "2020-01-01"}]}, # or "all": true
|
|
126
|
+
}}
|
|
127
|
+
# → DELETE FROM "sessions" WHERE "last_seen" < ?
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`set` values, `$insert` cells, and `where` predicates are ordinary read
|
|
131
|
+
constructs, so the full expression/predicate/subquery surface (including
|
|
132
|
+
correlated subqueries) is available, and data is always bound as parameters.
|
|
133
|
+
|
|
134
|
+
> **WHERE guard.** A `$update` / `$delete` **without** a `where` raises unless
|
|
135
|
+
> you pass an explicit `"all": true`. This prevents an accidental full-table
|
|
136
|
+
> update/delete.
|
|
137
|
+
|
|
67
138
|
## Parameterization & injection safety
|
|
68
139
|
|
|
69
140
|
Data values are **always bound as parameters**, never interpolated:
|
|
70
141
|
|
|
71
|
-
* `$val` (and the data sides of `$in`, `$between`, `$values`
|
|
72
|
-
and add the value to
|
|
142
|
+
* `$val` (and the data sides of `$in`, `$between`, `$values`, `$update`'s `set`
|
|
143
|
+
values, and `$insert`'s row cells) emit a placeholder and add the value to
|
|
144
|
+
`params`.
|
|
73
145
|
* Identifiers (table/column/alias names) are validated against a conservative
|
|
74
146
|
charset and quoted.
|
|
75
147
|
* Function names, CAST types, join types, sort directions, etc. are validated
|
|
@@ -80,8 +152,14 @@ Data values are **always bound as parameters**, never interpolated:
|
|
|
80
152
|
# → '"name" = ?' with the (possibly malicious) value safely in params
|
|
81
153
|
```
|
|
82
154
|
|
|
83
|
-
|
|
84
|
-
|
|
155
|
+
**Parameters come from inside j-perm — no external param source.** Inside `$val`
|
|
156
|
+
the value expression is resolved with j-perm's normal value pipeline, so `$ref`,
|
|
157
|
+
`${…}` templates, and `@:` dest-pointers all work. The `params` list handed to
|
|
158
|
+
the executor is simply the values j-perm itself computed from the document; the
|
|
159
|
+
parameter binding exists only for injection safety and the driver's paramstyle.
|
|
160
|
+
The whole flow (source → SQL → bound values) is self-contained in one j-perm
|
|
161
|
+
run; the executor is just the pipe to the driver. This applies equally to read
|
|
162
|
+
(`op: sql`) and write (`op: sql_write`).
|
|
85
163
|
|
|
86
164
|
## Dialect / `RenderOptions`
|
|
87
165
|
|
|
@@ -100,9 +178,9 @@ install_sql(engine, run_sql, dialect=RenderOptions(
|
|
|
100
178
|
|
|
101
179
|
## Sync & async
|
|
102
180
|
|
|
103
|
-
`install_sql`
|
|
104
|
-
handler (use with `engine.apply_async`); a regular
|
|
105
|
-
handler (use with `engine.apply`).
|
|
181
|
+
Both `install_sql` and `install_sql_write` inspect the executor: a coroutine
|
|
182
|
+
function registers the async handler (use with `engine.apply_async`); a regular
|
|
183
|
+
function registers the sync handler (use with `engine.apply`).
|
|
106
184
|
|
|
107
185
|
```python
|
|
108
186
|
async def run_sql(sql, params): ...
|
|
@@ -120,6 +198,14 @@ await engine.apply_async(spec, source=…, dest=…)
|
|
|
120
198
|
| `$union` / `$union_all` / `$intersect` / `$except` | `{"$union": [q1, q2, …], order_by?, limit?…}` |
|
|
121
199
|
| `$values` | `{"$values": [[…row…], …]}` (table source or `IN`) |
|
|
122
200
|
|
|
201
|
+
**Write (DML)** — only via `install_sql_write` / `op: sql_write`
|
|
202
|
+
|
|
203
|
+
| Construct | Form |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `$insert` | `{into, columns?, values \| query}` (exactly one of `values`/`query`) |
|
|
206
|
+
| `$update` | `{table, set: {col: operand}, where? \| "all": true}` |
|
|
207
|
+
| `$delete` | `{from, where? \| "all": true}` |
|
|
208
|
+
|
|
123
209
|
**Projection / expressions**
|
|
124
210
|
|
|
125
211
|
| Construct | Renders |
|
|
@@ -25,22 +25,32 @@ Quick start::
|
|
|
25
25
|
)
|
|
26
26
|
"""
|
|
27
27
|
from .constructs import build_sql_specials
|
|
28
|
+
from .constructs_write import build_sql_write_specials
|
|
28
29
|
from .dialect import PLACEHOLDER, RenderOptions
|
|
29
30
|
from .handler import AsyncSqlHandler, SqlHandler, SqlRenderer
|
|
30
|
-
from .install import install_sql
|
|
31
|
-
from .pipeline import
|
|
31
|
+
from .install import install_sql, install_sql_write
|
|
32
|
+
from .pipeline import (
|
|
33
|
+
SQL_PIPELINE_NAME,
|
|
34
|
+
SQL_WRITE_PIPELINE_NAME,
|
|
35
|
+
build_sql_pipeline,
|
|
36
|
+
build_sql_write_pipeline,
|
|
37
|
+
)
|
|
32
38
|
from .render import fragment, is_fragment, is_query, render
|
|
33
39
|
|
|
34
40
|
__all__ = [
|
|
35
41
|
"install_sql",
|
|
42
|
+
"install_sql_write",
|
|
36
43
|
"RenderOptions",
|
|
37
44
|
"PLACEHOLDER",
|
|
38
45
|
"SqlHandler",
|
|
39
46
|
"AsyncSqlHandler",
|
|
40
47
|
"SqlRenderer",
|
|
41
48
|
"build_sql_pipeline",
|
|
49
|
+
"build_sql_write_pipeline",
|
|
42
50
|
"build_sql_specials",
|
|
51
|
+
"build_sql_write_specials",
|
|
43
52
|
"SQL_PIPELINE_NAME",
|
|
53
|
+
"SQL_WRITE_PIPELINE_NAME",
|
|
44
54
|
"fragment",
|
|
45
55
|
"is_fragment",
|
|
46
56
|
"is_query",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""DML construct handlers — standard ``INSERT`` / ``UPDATE`` / ``DELETE``.
|
|
2
|
+
|
|
3
|
+
These build on the read-only constructs in :mod:`.constructs`: a write
|
|
4
|
+
statement's sub-parts (``WHERE`` predicates, ``SET`` values, ``INSERT … SELECT``
|
|
5
|
+
subqueries) are ordinary read constructs that recurse through the same pipeline.
|
|
6
|
+
|
|
7
|
+
Only the standard, broadly portable forms are supported. Non-standard surface
|
|
8
|
+
(``RETURNING``, ``ON CONFLICT``/upsert, ``UPDATE … FROM``, ``DELETE … USING``)
|
|
9
|
+
is intentionally out of scope.
|
|
10
|
+
|
|
11
|
+
Safety:
|
|
12
|
+
|
|
13
|
+
* Data values are always bound as parameters (``$val`` / operands), never
|
|
14
|
+
interpolated — exactly as for ``SELECT``.
|
|
15
|
+
* Table and column names are validated and quoted as identifiers.
|
|
16
|
+
* ``UPDATE`` / ``DELETE`` without a ``where`` raise unless an explicit
|
|
17
|
+
``"all": true`` flag is given, to guard against accidental full-table writes.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from functools import partial
|
|
22
|
+
|
|
23
|
+
from .constructs import build_sql_specials
|
|
24
|
+
from .dialect import RenderOptions
|
|
25
|
+
from .render import (
|
|
26
|
+
fragment,
|
|
27
|
+
is_query,
|
|
28
|
+
render_construct,
|
|
29
|
+
render_operand,
|
|
30
|
+
render_operands,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _table_target(spec, opts: RenderOptions) -> str:
|
|
35
|
+
"""Quote a plain table target (``INSERT INTO`` / ``UPDATE`` / ``DELETE FROM``).
|
|
36
|
+
|
|
37
|
+
Standard DML targets a single named table (optionally schema-qualified) — no
|
|
38
|
+
alias and no subquery. Accepts a string name or ``{"table", "schema"?}``.
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(spec, str):
|
|
41
|
+
return opts.quote_ref(spec)
|
|
42
|
+
if isinstance(spec, dict) and "table" in spec and not is_query(spec):
|
|
43
|
+
parts = ([spec["schema"]] if spec.get("schema") else []) + [spec["table"]]
|
|
44
|
+
return ".".join(opts.quote_identifier(p) for p in parts)
|
|
45
|
+
raise ValueError(f"invalid table target: {spec!r}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _where_clause(spec, ctx) -> tuple[str, list]:
|
|
49
|
+
"""Render the optional ``WHERE`` of an UPDATE/DELETE, enforcing the guard.
|
|
50
|
+
|
|
51
|
+
Returns ``(sql_suffix, params)``. Without a ``where`` key an explicit
|
|
52
|
+
``"all": true`` is required, else :class:`ValueError`.
|
|
53
|
+
"""
|
|
54
|
+
if "where" in spec:
|
|
55
|
+
wf = render_construct(spec["where"], ctx)
|
|
56
|
+
return f" WHERE {wf['sql']}", wf["params"]
|
|
57
|
+
if spec.get("all") is True:
|
|
58
|
+
return "", []
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"UPDATE/DELETE without 'where' requires an explicit \"all\": true"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def insert(node, ctx, *, opts: RenderOptions) -> dict:
|
|
65
|
+
spec = node["$insert"]
|
|
66
|
+
sql = f"INSERT INTO {_table_target(spec['into'], opts)}"
|
|
67
|
+
if spec.get("columns"):
|
|
68
|
+
cols = ", ".join(opts.quote_identifier(c) for c in spec["columns"])
|
|
69
|
+
sql += f" ({cols})"
|
|
70
|
+
has_values = "values" in spec
|
|
71
|
+
has_query = "query" in spec
|
|
72
|
+
if has_values == has_query:
|
|
73
|
+
raise ValueError("$insert requires exactly one of 'values' or 'query'")
|
|
74
|
+
params: list = []
|
|
75
|
+
if has_values:
|
|
76
|
+
rows = spec["values"]
|
|
77
|
+
if not rows:
|
|
78
|
+
raise ValueError("$insert 'values' requires at least one row")
|
|
79
|
+
rendered_rows: list[str] = []
|
|
80
|
+
width: int | None = None
|
|
81
|
+
for row in rows:
|
|
82
|
+
if width is None:
|
|
83
|
+
width = len(row)
|
|
84
|
+
elif len(row) != width:
|
|
85
|
+
raise ValueError("all $insert rows must have the same length")
|
|
86
|
+
frag = render_operands(row, ctx, opts)
|
|
87
|
+
rendered_rows.append(f"({frag['sql']})")
|
|
88
|
+
params += frag["params"]
|
|
89
|
+
sql += " VALUES " + ", ".join(rendered_rows)
|
|
90
|
+
else:
|
|
91
|
+
if not is_query(spec["query"]):
|
|
92
|
+
raise ValueError("$insert 'query' must be a SELECT/set-op query construct")
|
|
93
|
+
q = render_construct(spec["query"], ctx)
|
|
94
|
+
sql += f" {q['sql']}"
|
|
95
|
+
params += q["params"]
|
|
96
|
+
return fragment(sql, params)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _render_set(spec, ctx, opts: RenderOptions) -> dict:
|
|
100
|
+
if not isinstance(spec, dict) or not spec:
|
|
101
|
+
raise ValueError("$update 'set' must be a non-empty mapping of column -> value")
|
|
102
|
+
assignments: list[str] = []
|
|
103
|
+
params: list = []
|
|
104
|
+
for col_name, value in spec.items():
|
|
105
|
+
rhs = render_operand(value, ctx, opts)
|
|
106
|
+
assignments.append(f"{opts.quote_identifier(col_name)} = {rhs['sql']}")
|
|
107
|
+
params += rhs["params"]
|
|
108
|
+
return fragment(", ".join(assignments), params)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def update(node, ctx, *, opts: RenderOptions) -> dict:
|
|
112
|
+
spec = node["$update"]
|
|
113
|
+
set_frag = _render_set(spec["set"], ctx, opts)
|
|
114
|
+
sql = f"UPDATE {_table_target(spec['table'], opts)} SET {set_frag['sql']}"
|
|
115
|
+
params = list(set_frag["params"])
|
|
116
|
+
where_sql, where_params = _where_clause(spec, ctx)
|
|
117
|
+
return fragment(sql + where_sql, params + where_params)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def delete(node, ctx, *, opts: RenderOptions) -> dict:
|
|
121
|
+
spec = node["$delete"]
|
|
122
|
+
sql = f"DELETE FROM {_table_target(spec['from'], opts)}"
|
|
123
|
+
where_sql, where_params = _where_clause(spec, ctx)
|
|
124
|
+
return fragment(sql + where_sql, list(where_params))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_sql_write_specials(opts: RenderOptions) -> dict:
|
|
128
|
+
"""Build the ``{key: handler}`` mapping for the write SQL pipeline.
|
|
129
|
+
|
|
130
|
+
The write pipeline is a superset of the read pipeline: all ``SELECT``
|
|
131
|
+
constructs plus the DML statements, so subqueries / predicates / ``SET``
|
|
132
|
+
expressions resolve in the same pipeline.
|
|
133
|
+
"""
|
|
134
|
+
specials = dict(build_sql_specials(opts))
|
|
135
|
+
specials["$insert"] = partial(insert, opts=opts)
|
|
136
|
+
specials["$update"] = partial(update, opts=opts)
|
|
137
|
+
specials["$delete"] = partial(delete, opts=opts)
|
|
138
|
+
return specials
|
|
@@ -19,24 +19,35 @@ from __future__ import annotations
|
|
|
19
19
|
from j_perm import ActionHandler, AsyncActionHandler
|
|
20
20
|
|
|
21
21
|
from .dialect import RenderOptions
|
|
22
|
-
from .render import SQL_PIPELINE_NAME, is_fragment
|
|
22
|
+
from .render import ACTIVE_PIPELINE_KEY, SQL_PIPELINE_NAME, is_fragment
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class SqlRenderer:
|
|
26
|
-
"""Render a SQL construct tree to ``(sql, params)`` for a target dialect.
|
|
26
|
+
"""Render a SQL construct tree to ``(sql, params)`` for a target dialect.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
*pipeline_name* selects which isolated pipeline the tree (and all its
|
|
29
|
+
recursion) is dispatched through — the read-only ``"sql"`` pipeline by
|
|
30
|
+
default, or the write pipeline for ``op: sql_write``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, opts: RenderOptions, pipeline_name: str = SQL_PIPELINE_NAME) -> None:
|
|
29
34
|
self.opts = opts
|
|
35
|
+
self.pipeline_name = pipeline_name
|
|
30
36
|
|
|
31
37
|
def render(self, query, ctx) -> tuple:
|
|
32
38
|
# Render against a scratch dest so the real document is never clobbered,
|
|
33
39
|
# but expose the real dest under _real_dest so @: pointers inside $val
|
|
34
|
-
# can still read the document being built.
|
|
40
|
+
# can still read the document being built. The active pipeline name is
|
|
41
|
+
# threaded through metadata so recursion stays in this pipeline.
|
|
35
42
|
render_ctx = ctx.copy(
|
|
36
43
|
new_dest={},
|
|
37
|
-
new_metadata={
|
|
44
|
+
new_metadata={
|
|
45
|
+
**ctx.metadata,
|
|
46
|
+
"_real_dest": ctx.dest,
|
|
47
|
+
ACTIVE_PIPELINE_KEY: self.pipeline_name,
|
|
48
|
+
},
|
|
38
49
|
)
|
|
39
|
-
frag = ctx.engine.run_pipeline(
|
|
50
|
+
frag = ctx.engine.run_pipeline(self.pipeline_name, query, render_ctx).dest
|
|
40
51
|
if not is_fragment(frag):
|
|
41
52
|
raise ValueError("top-level SQL query must be a SQL construct")
|
|
42
53
|
return self.opts.finalize(frag["sql"], frag["params"])
|
|
@@ -12,14 +12,29 @@ from j_perm import ActionNode, OpMatcher
|
|
|
12
12
|
|
|
13
13
|
from .dialect import RenderOptions
|
|
14
14
|
from .handler import AsyncSqlHandler, SqlHandler, SqlRenderer
|
|
15
|
-
from .pipeline import
|
|
16
|
-
|
|
15
|
+
from .pipeline import (
|
|
16
|
+
SQL_PIPELINE_NAME,
|
|
17
|
+
SQL_WRITE_PIPELINE_NAME,
|
|
18
|
+
build_sql_pipeline,
|
|
19
|
+
build_sql_write_pipeline,
|
|
20
|
+
)
|
|
17
21
|
|
|
18
|
-
__all__ = ["install_sql"]
|
|
22
|
+
__all__ = ["install_sql", "install_sql_write"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _register_op(engine, executor, renderer, op):
|
|
26
|
+
"""Register an ``op`` handler, choosing sync/async by the executor."""
|
|
27
|
+
if asyncio.iscoroutinefunction(executor):
|
|
28
|
+
handler = AsyncSqlHandler(executor, renderer)
|
|
29
|
+
else:
|
|
30
|
+
handler = SqlHandler(executor, renderer)
|
|
31
|
+
engine.main_pipeline.registry.register(
|
|
32
|
+
ActionNode(name=op, priority=10, matcher=OpMatcher(op), handler=handler)
|
|
33
|
+
)
|
|
19
34
|
|
|
20
35
|
|
|
21
36
|
def install_sql(engine, executor, *, paramstyle: str = "qmark", dialect=None, op: str = "sql"):
|
|
22
|
-
"""Install SQL support into *engine*.
|
|
37
|
+
"""Install read-only SQL (``SELECT``) support into *engine*.
|
|
23
38
|
|
|
24
39
|
Args:
|
|
25
40
|
engine: a built ``j_perm`` engine (e.g. from ``build_default_engine``).
|
|
@@ -36,12 +51,31 @@ def install_sql(engine, executor, *, paramstyle: str = "qmark", dialect=None, op
|
|
|
36
51
|
"""
|
|
37
52
|
opts = dialect if dialect is not None else RenderOptions(paramstyle=paramstyle)
|
|
38
53
|
engine.register_pipeline(SQL_PIPELINE_NAME, build_sql_pipeline(opts))
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
engine
|
|
45
|
-
|
|
46
|
-
)
|
|
54
|
+
_register_op(engine, executor, SqlRenderer(opts, SQL_PIPELINE_NAME), op)
|
|
55
|
+
return engine
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def install_sql_write(
|
|
59
|
+
engine, executor, *, paramstyle: str = "qmark", dialect=None, op: str = "sql_write"
|
|
60
|
+
):
|
|
61
|
+
"""Install write (DML) SQL support — ``INSERT`` / ``UPDATE`` / ``DELETE``.
|
|
62
|
+
|
|
63
|
+
This is a separate, opt-in install: it registers an isolated write pipeline
|
|
64
|
+
(read constructs + DML) and a distinct ``op`` so that ``op: sql`` — if also
|
|
65
|
+
installed — stays guaranteed read-only. Independent of :func:`install_sql`
|
|
66
|
+
(either may be installed alone, in any order).
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
engine: a built ``j_perm`` engine.
|
|
70
|
+
executor: ``executor(sql, params) -> result`` (sync or coroutine).
|
|
71
|
+
paramstyle: placeholder style when *dialect* is not given.
|
|
72
|
+
dialect: an explicit :class:`RenderOptions`; overrides *paramstyle*.
|
|
73
|
+
op: the operation name to register (default ``"sql_write"``).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The same *engine*, for chaining.
|
|
77
|
+
"""
|
|
78
|
+
opts = dialect if dialect is not None else RenderOptions(paramstyle=paramstyle)
|
|
79
|
+
engine.register_pipeline(SQL_WRITE_PIPELINE_NAME, build_sql_write_pipeline(opts))
|
|
80
|
+
_register_op(engine, executor, SqlRenderer(opts, SQL_WRITE_PIPELINE_NAME), op)
|
|
47
81
|
return engine
|
|
@@ -17,16 +17,23 @@ from j_perm import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from .constructs import build_sql_specials
|
|
20
|
+
from .constructs_write import build_sql_write_specials
|
|
20
21
|
from .dialect import RenderOptions
|
|
21
22
|
from .render import SQL_PIPELINE_NAME
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
#: Name the write (DML) SQL pipeline is registered under on the engine.
|
|
25
|
+
SQL_WRITE_PIPELINE_NAME = "sql_write"
|
|
24
26
|
|
|
27
|
+
__all__ = [
|
|
28
|
+
"build_sql_pipeline",
|
|
29
|
+
"build_sql_write_pipeline",
|
|
30
|
+
"SQL_PIPELINE_NAME",
|
|
31
|
+
"SQL_WRITE_PIPELINE_NAME",
|
|
32
|
+
]
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
specials = build_sql_specials(opts)
|
|
34
|
+
|
|
35
|
+
def _build_pipeline(specials: dict) -> Pipeline:
|
|
36
|
+
"""Assemble an isolated SQL pipeline from a ``{key: handler}`` mapping."""
|
|
30
37
|
registry = ActionTypeRegistry()
|
|
31
38
|
registry.register(
|
|
32
39
|
ActionNode(
|
|
@@ -45,3 +52,15 @@ def build_sql_pipeline(opts: RenderOptions | None = None) -> Pipeline:
|
|
|
45
52
|
)
|
|
46
53
|
)
|
|
47
54
|
return Pipeline(registry=registry, track_execution=True)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_sql_pipeline(opts: RenderOptions | None = None) -> Pipeline:
|
|
58
|
+
"""Build the isolated read-only SQL pipeline for the given dialect options."""
|
|
59
|
+
opts = opts if opts is not None else RenderOptions()
|
|
60
|
+
return _build_pipeline(build_sql_specials(opts))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_sql_write_pipeline(opts: RenderOptions | None = None) -> Pipeline:
|
|
64
|
+
"""Build the isolated write (DML) SQL pipeline (read constructs + DML)."""
|
|
65
|
+
opts = opts if opts is not None else RenderOptions()
|
|
66
|
+
return _build_pipeline(build_sql_write_specials(opts))
|
|
@@ -19,9 +19,15 @@ from typing import Any
|
|
|
19
19
|
|
|
20
20
|
from .dialect import PLACEHOLDER, RenderOptions
|
|
21
21
|
|
|
22
|
-
#: Name the SQL value-pipeline is registered under on the engine.
|
|
22
|
+
#: Name the read-only SQL value-pipeline is registered under on the engine.
|
|
23
23
|
SQL_PIPELINE_NAME = "sql"
|
|
24
24
|
|
|
25
|
+
#: Metadata key carrying the name of the pipeline that recursion should dispatch
|
|
26
|
+
#: through. Set by :class:`~j_perm_sql.handler.SqlRenderer` so a write
|
|
27
|
+
#: statement's sub-parts resolve in the write pipeline; defaults to the read
|
|
28
|
+
#: pipeline when absent.
|
|
29
|
+
ACTIVE_PIPELINE_KEY = "_sql_pipeline"
|
|
30
|
+
|
|
25
31
|
#: Keys whose presence marks a node as a *query* (must be parenthesised when
|
|
26
32
|
#: used as an operand, subquery, or derived table).
|
|
27
33
|
_QUERY_KEYS = frozenset(
|
|
@@ -45,8 +51,14 @@ def is_query(node: Any) -> bool:
|
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
def render(node: Any, ctx) -> Any:
|
|
48
|
-
"""Dispatch *node* through the
|
|
49
|
-
|
|
54
|
+
"""Dispatch *node* through the active SQL pipeline and return the result.
|
|
55
|
+
|
|
56
|
+
The active pipeline name is read from ``ctx.metadata`` (set by the renderer)
|
|
57
|
+
so recursion stays within the same pipeline the top-level operation chose;
|
|
58
|
+
it defaults to the read-only :data:`SQL_PIPELINE_NAME`.
|
|
59
|
+
"""
|
|
60
|
+
name = ctx.metadata.get(ACTIVE_PIPELINE_KEY, SQL_PIPELINE_NAME)
|
|
61
|
+
return ctx.engine.run_pipeline(name, node, ctx).dest
|
|
50
62
|
|
|
51
63
|
|
|
52
64
|
def render_construct(node: Any, ctx) -> dict:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: j-perm-sql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: j-perm plugin for SQL
|
|
5
5
|
Author-email: Roman <kuschanow@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -15,18 +15,23 @@ Requires-Dist: j-perm>=1.9.0
|
|
|
15
15
|
# j-perm-sql
|
|
16
16
|
|
|
17
17
|
A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
|
|
18
|
-
**SQL
|
|
18
|
+
**SQL queries** from j-perm constructs.
|
|
19
19
|
|
|
20
20
|
* SQL is described with a tree of `$`-constructs (`$select`, `$col`, `$val`,
|
|
21
21
|
predicates, joins, …).
|
|
22
|
-
* A
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
* A top-level operation renders that tree to a **parameterized** `(sql, params)`
|
|
23
|
+
pair and hands it to a configurable executor (any ORM's raw-execute function):
|
|
24
|
+
`op: sql` for read-only `SELECT`, and (opt-in) `op: sql_write` for
|
|
25
|
+
`INSERT`/`UPDATE`/`DELETE`.
|
|
26
|
+
* The SQL constructs live in **isolated** named pipelines: they mean nothing
|
|
27
|
+
outside those operations. `{"$select": …}` used as an ordinary value is just a
|
|
28
|
+
dict.
|
|
29
|
+
|
|
30
|
+
> Scope: the full standard **`SELECT`** surface (read-only) via `install_sql`,
|
|
31
|
+
> plus standard **`INSERT`/`UPDATE`/`DELETE`** (row content only) via
|
|
32
|
+
> `install_sql_write`. Schema DDL (`CREATE`/`ALTER`/`DROP` of tables/columns)
|
|
33
|
+
> and non-universal DML (`RETURNING`, `ON CONFLICT`/upsert, `UPDATE … FROM`,
|
|
34
|
+
> `DELETE … USING`) are intentionally out of scope.
|
|
30
35
|
|
|
31
36
|
## Install
|
|
32
37
|
|
|
@@ -78,12 +83,79 @@ plugins.
|
|
|
78
83
|
* `to` — optional destination pointer (template-expanded); the executor's
|
|
79
84
|
result is written there. If omitted, the result is discarded.
|
|
80
85
|
|
|
86
|
+
## Writing data (`INSERT`/`UPDATE`/`DELETE`)
|
|
87
|
+
|
|
88
|
+
Writing is a **separate, opt-in install** — you don't get it unless you ask for
|
|
89
|
+
it, and `op: sql` stays guaranteed read-only even when both are installed:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from j_perm_sql import install_sql, install_sql_write
|
|
93
|
+
|
|
94
|
+
install_sql(engine, run_sql) # read-only op: sql (optional)
|
|
95
|
+
install_sql_write(engine, run_sql) # write op: sql_write
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`install_sql_write(engine, executor, *, paramstyle="qmark", dialect=None,
|
|
99
|
+
op="sql_write")` registers an isolated **write pipeline** (the full `SELECT`
|
|
100
|
+
surface *plus* the DML statements, so `WHERE` predicates, `SET` expressions and
|
|
101
|
+
`INSERT … SELECT` subqueries all work) and the `op: sql_write` operation. It is
|
|
102
|
+
independent of `install_sql` — either may be installed alone, in any order. It
|
|
103
|
+
selects the sync/async handler the same way (by `asyncio.iscoroutinefunction`).
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
{"op": "sql_write", "query": <DML construct tree>, "to": "/dest/path"}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**`$insert`** — exactly one of `values` / `query`:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
{"$insert": {
|
|
113
|
+
"into": "users", # or {"table": "users", "schema": "app"}
|
|
114
|
+
"columns": ["name", "age"], # optional
|
|
115
|
+
"values": [[{"$val": "Ann"}, {"$val": 30}], ...], # cells are operands ($val to bind)
|
|
116
|
+
# ── OR ──
|
|
117
|
+
"query": {"$select": {...}}, # INSERT … SELECT
|
|
118
|
+
}}
|
|
119
|
+
# → INSERT INTO "users" ("name", "age") VALUES (?, ?) params=["Ann", 30]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**`$update`** — single table:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
{"$update": {
|
|
126
|
+
"table": "users", # or {"table": "users", "schema": "app"}
|
|
127
|
+
"set": {"name": {"$val": "Bob"},
|
|
128
|
+
"visits": {"$add": [{"$col": "visits"}, {"$val": 1}]}},
|
|
129
|
+
"where": {"$eq": [{"$col": "id"}, {"$val": 5}]}, # or "all": true
|
|
130
|
+
}}
|
|
131
|
+
# → UPDATE "users" SET "name" = ?, "visits" = ("visits" + ?) WHERE "id" = ?
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**`$delete`** — single table:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
{"$delete": {
|
|
138
|
+
"from": "sessions", # or {"table": "sessions", "schema": "app"}
|
|
139
|
+
"where": {"$lt": [{"$col": "last_seen"}, {"$val": "2020-01-01"}]}, # or "all": true
|
|
140
|
+
}}
|
|
141
|
+
# → DELETE FROM "sessions" WHERE "last_seen" < ?
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`set` values, `$insert` cells, and `where` predicates are ordinary read
|
|
145
|
+
constructs, so the full expression/predicate/subquery surface (including
|
|
146
|
+
correlated subqueries) is available, and data is always bound as parameters.
|
|
147
|
+
|
|
148
|
+
> **WHERE guard.** A `$update` / `$delete` **without** a `where` raises unless
|
|
149
|
+
> you pass an explicit `"all": true`. This prevents an accidental full-table
|
|
150
|
+
> update/delete.
|
|
151
|
+
|
|
81
152
|
## Parameterization & injection safety
|
|
82
153
|
|
|
83
154
|
Data values are **always bound as parameters**, never interpolated:
|
|
84
155
|
|
|
85
|
-
* `$val` (and the data sides of `$in`, `$between`, `$values`
|
|
86
|
-
and add the value to
|
|
156
|
+
* `$val` (and the data sides of `$in`, `$between`, `$values`, `$update`'s `set`
|
|
157
|
+
values, and `$insert`'s row cells) emit a placeholder and add the value to
|
|
158
|
+
`params`.
|
|
87
159
|
* Identifiers (table/column/alias names) are validated against a conservative
|
|
88
160
|
charset and quoted.
|
|
89
161
|
* Function names, CAST types, join types, sort directions, etc. are validated
|
|
@@ -94,8 +166,14 @@ Data values are **always bound as parameters**, never interpolated:
|
|
|
94
166
|
# → '"name" = ?' with the (possibly malicious) value safely in params
|
|
95
167
|
```
|
|
96
168
|
|
|
97
|
-
|
|
98
|
-
|
|
169
|
+
**Parameters come from inside j-perm — no external param source.** Inside `$val`
|
|
170
|
+
the value expression is resolved with j-perm's normal value pipeline, so `$ref`,
|
|
171
|
+
`${…}` templates, and `@:` dest-pointers all work. The `params` list handed to
|
|
172
|
+
the executor is simply the values j-perm itself computed from the document; the
|
|
173
|
+
parameter binding exists only for injection safety and the driver's paramstyle.
|
|
174
|
+
The whole flow (source → SQL → bound values) is self-contained in one j-perm
|
|
175
|
+
run; the executor is just the pipe to the driver. This applies equally to read
|
|
176
|
+
(`op: sql`) and write (`op: sql_write`).
|
|
99
177
|
|
|
100
178
|
## Dialect / `RenderOptions`
|
|
101
179
|
|
|
@@ -114,9 +192,9 @@ install_sql(engine, run_sql, dialect=RenderOptions(
|
|
|
114
192
|
|
|
115
193
|
## Sync & async
|
|
116
194
|
|
|
117
|
-
`install_sql`
|
|
118
|
-
handler (use with `engine.apply_async`); a regular
|
|
119
|
-
handler (use with `engine.apply`).
|
|
195
|
+
Both `install_sql` and `install_sql_write` inspect the executor: a coroutine
|
|
196
|
+
function registers the async handler (use with `engine.apply_async`); a regular
|
|
197
|
+
function registers the sync handler (use with `engine.apply`).
|
|
120
198
|
|
|
121
199
|
```python
|
|
122
200
|
async def run_sql(sql, params): ...
|
|
@@ -134,6 +212,14 @@ await engine.apply_async(spec, source=…, dest=…)
|
|
|
134
212
|
| `$union` / `$union_all` / `$intersect` / `$except` | `{"$union": [q1, q2, …], order_by?, limit?…}` |
|
|
135
213
|
| `$values` | `{"$values": [[…row…], …]}` (table source or `IN`) |
|
|
136
214
|
|
|
215
|
+
**Write (DML)** — only via `install_sql_write` / `op: sql_write`
|
|
216
|
+
|
|
217
|
+
| Construct | Form |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `$insert` | `{into, columns?, values \| query}` (exactly one of `values`/`query`) |
|
|
220
|
+
| `$update` | `{table, set: {col: operand}, where? \| "all": true}` |
|
|
221
|
+
| `$delete` | `{from, where? \| "all": true}` |
|
|
222
|
+
|
|
137
223
|
**Projection / expressions**
|
|
138
224
|
|
|
139
225
|
| Construct | Renders |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|