j-perm-sql 0.2.0__tar.gz → 0.4.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.2.0 → j_perm_sql-0.4.0}/PKG-INFO +25 -4
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/README.md +23 -2
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/pyproject.toml +2 -2
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql/constructs.py +4 -3
- j_perm_sql-0.4.0/src/j_perm_sql/handler.py +193 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql/render.py +62 -1
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql.egg-info/PKG-INFO +25 -4
- j_perm_sql-0.4.0/src/j_perm_sql.egg-info/requires.txt +1 -0
- j_perm_sql-0.2.0/src/j_perm_sql/handler.py +0 -86
- j_perm_sql-0.2.0/src/j_perm_sql.egg-info/requires.txt +0 -1
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/setup.cfg +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql/__init__.py +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql/constructs_write.py +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql/dialect.py +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql/install.py +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql/pipeline.py +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql.egg-info/SOURCES.txt +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.0}/src/j_perm_sql.egg-info/dependency_links.txt +0 -0
- {j_perm_sql-0.2.0 → j_perm_sql-0.4.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.4.0
|
|
4
4
|
Summary: j-perm plugin for SQL
|
|
5
5
|
Author-email: Roman <kuschanow@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -10,7 +10,7 @@ Project-URL: Tracker, https://github.com/kuschanow/j-perm/issues
|
|
|
10
10
|
Project-URL: Documentation, https://github.com/kuschanow/j-perm
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: j-perm>=1.
|
|
13
|
+
Requires-Dist: j-perm>=1.11.0
|
|
14
14
|
|
|
15
15
|
# j-perm-sql
|
|
16
16
|
|
|
@@ -39,8 +39,10 @@ A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
|
|
|
39
39
|
pip install j-perm-sql
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Requires `j-perm >= 1.9.0
|
|
43
|
-
|
|
42
|
+
Requires `j-perm >= 1.10.0`: 1.9.0 made `run_pipeline` a passthrough invoker
|
|
43
|
+
(which this plugin relies on), and 1.10.0 added the `nested_spec_pipeline`
|
|
44
|
+
compile hook and per-pipeline `CompiledSpec` execution that let `op: sql` be
|
|
45
|
+
compiled end-to-end (see [Compilation](#compilation)).
|
|
44
46
|
|
|
45
47
|
## Quick start
|
|
46
48
|
|
|
@@ -202,6 +204,25 @@ install_sql(engine, run_sql) # async
|
|
|
202
204
|
await engine.apply_async(spec, source=…, dest=…)
|
|
203
205
|
```
|
|
204
206
|
|
|
207
|
+
## Compilation
|
|
208
|
+
|
|
209
|
+
`op: sql` / `op: sql_write` are compilable. `engine.compile(spec)` compiles the
|
|
210
|
+
`query` subtree against the isolated SQL pipeline (the engine never needs to
|
|
211
|
+
understand the SQL constructs — it routes the nested spec through the registered
|
|
212
|
+
pipeline by name), and the rendered tree is dispatched through the compiled path
|
|
213
|
+
with per-node memoisation. Re-applying the same `CompiledSpec` keeps every node
|
|
214
|
+
compiled; only `$val` data is re-bound from the live context on each run.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
compiled = engine.compile([{"op": "sql", "to": "/rows", "query": query}])
|
|
218
|
+
compiled.apply(source={"wanted": 1}, dest={}) # renders + executes, fully compiled
|
|
219
|
+
compiled.apply(source={"wanted": 2}, dest={}) # reuses compiled nodes, re-binds values
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
This requires the `nested_spec_pipeline` compile hook and per-pipeline
|
|
223
|
+
`CompiledSpec` execution added in the core engine (see the "What gets compiled"
|
|
224
|
+
section of the main j-perm README).
|
|
225
|
+
|
|
205
226
|
## Construct reference
|
|
206
227
|
|
|
207
228
|
**Query**
|
|
@@ -25,8 +25,10 @@ A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
|
|
|
25
25
|
pip install j-perm-sql
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Requires `j-perm >= 1.9.0
|
|
29
|
-
|
|
28
|
+
Requires `j-perm >= 1.10.0`: 1.9.0 made `run_pipeline` a passthrough invoker
|
|
29
|
+
(which this plugin relies on), and 1.10.0 added the `nested_spec_pipeline`
|
|
30
|
+
compile hook and per-pipeline `CompiledSpec` execution that let `op: sql` be
|
|
31
|
+
compiled end-to-end (see [Compilation](#compilation)).
|
|
30
32
|
|
|
31
33
|
## Quick start
|
|
32
34
|
|
|
@@ -188,6 +190,25 @@ install_sql(engine, run_sql) # async
|
|
|
188
190
|
await engine.apply_async(spec, source=…, dest=…)
|
|
189
191
|
```
|
|
190
192
|
|
|
193
|
+
## Compilation
|
|
194
|
+
|
|
195
|
+
`op: sql` / `op: sql_write` are compilable. `engine.compile(spec)` compiles the
|
|
196
|
+
`query` subtree against the isolated SQL pipeline (the engine never needs to
|
|
197
|
+
understand the SQL constructs — it routes the nested spec through the registered
|
|
198
|
+
pipeline by name), and the rendered tree is dispatched through the compiled path
|
|
199
|
+
with per-node memoisation. Re-applying the same `CompiledSpec` keeps every node
|
|
200
|
+
compiled; only `$val` data is re-bound from the live context on each run.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
compiled = engine.compile([{"op": "sql", "to": "/rows", "query": query}])
|
|
204
|
+
compiled.apply(source={"wanted": 1}, dest={}) # renders + executes, fully compiled
|
|
205
|
+
compiled.apply(source={"wanted": 2}, dest={}) # reuses compiled nodes, re-binds values
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This requires the `nested_spec_pipeline` compile hook and per-pipeline
|
|
209
|
+
`CompiledSpec` execution added in the core engine (see the "What gets compiled"
|
|
210
|
+
section of the main j-perm README).
|
|
211
|
+
|
|
191
212
|
## Construct reference
|
|
192
213
|
|
|
193
214
|
**Query**
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "j-perm-sql"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.4.0"
|
|
10
10
|
description = "j-perm plugin for SQL"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Roman", email = "kuschanow@gmail.com" },
|
|
@@ -18,7 +18,7 @@ requires-python = ">=3.10"
|
|
|
18
18
|
license = { text = "MIT" }
|
|
19
19
|
|
|
20
20
|
dependencies = [
|
|
21
|
-
"j-perm>=1.
|
|
21
|
+
"j-perm>=1.11.0",
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
[tool.pytest.ini_options]
|
|
@@ -23,6 +23,7 @@ from .render import (
|
|
|
23
23
|
render_operand,
|
|
24
24
|
render_operands,
|
|
25
25
|
render_subquery,
|
|
26
|
+
resolve_value,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -74,7 +75,7 @@ def col(node, ctx, *, opts: RenderOptions) -> dict:
|
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
def val(node, ctx, *, opts: RenderOptions) -> dict:
|
|
77
|
-
value =
|
|
78
|
+
value = resolve_value(node["$val"], ctx)
|
|
78
79
|
return fragment(PLACEHOLDER, [value])
|
|
79
80
|
|
|
80
81
|
|
|
@@ -219,7 +220,7 @@ def _in(node, ctx, *, opts: RenderOptions, negate: bool) -> dict:
|
|
|
219
220
|
if is_query(right):
|
|
220
221
|
sub = render_subquery(right, ctx)
|
|
221
222
|
return fragment(f"{lf['sql']} {kw} {sub['sql']}", lf["params"] + sub["params"])
|
|
222
|
-
values =
|
|
223
|
+
values = resolve_value(right, ctx)
|
|
223
224
|
if not isinstance(values, (list, tuple)):
|
|
224
225
|
values = [values]
|
|
225
226
|
if not values:
|
|
@@ -529,7 +530,7 @@ def values(node, ctx, *, opts: RenderOptions) -> dict:
|
|
|
529
530
|
raise ValueError("all $values rows must have the same length")
|
|
530
531
|
rendered_rows.append("(" + ", ".join(PLACEHOLDER for _ in row) + ")")
|
|
531
532
|
for cell in row:
|
|
532
|
-
params.append(
|
|
533
|
+
params.append(resolve_value(cell, ctx))
|
|
533
534
|
return fragment("VALUES " + ", ".join(rendered_rows), params)
|
|
534
535
|
|
|
535
536
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""The single top-level ``op: sql`` operation (sync + async).
|
|
2
|
+
|
|
3
|
+
Schema::
|
|
4
|
+
|
|
5
|
+
{"op": "sql", "query": <SQL construct tree>, "to": "/dest/path"}
|
|
6
|
+
|
|
7
|
+
* ``query`` — the SQL construct tree (rendered via the isolated SQL pipeline).
|
|
8
|
+
* ``to`` — optional destination pointer; the executor's result is written
|
|
9
|
+
there (template-expanded). If omitted, the result is discarded.
|
|
10
|
+
|
|
11
|
+
Rendering is identical for sync and async; only the executor call differs
|
|
12
|
+
(call vs ``await``). A single class can't expose both a sync and an async
|
|
13
|
+
``execute`` (same method name), and ``Pipeline.run_async`` dispatches to the
|
|
14
|
+
async path only for :class:`AsyncActionHandler` — hence two classes over one
|
|
15
|
+
shared :class:`SqlRenderer`.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from j_perm import ActionHandler, AsyncActionHandler
|
|
20
|
+
from j_perm.core import Compound
|
|
21
|
+
|
|
22
|
+
from .dialect import RenderOptions
|
|
23
|
+
from .render import (
|
|
24
|
+
ACTIVE_PIPELINE_KEY,
|
|
25
|
+
ASYNC_VALUE_CACHE_KEY,
|
|
26
|
+
COMPILE_CACHE_KEY,
|
|
27
|
+
SQL_PIPELINE_NAME,
|
|
28
|
+
_NeedAsyncValue,
|
|
29
|
+
is_fragment,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
#: Attribute under which a compiled query spec carries its per-node compilation
|
|
33
|
+
#: cache (see :func:`j_perm_sql.render.render`). Stored on the spec — not in the
|
|
34
|
+
#: context — so it survives across runs of the same compiled query.
|
|
35
|
+
_NODE_CACHE_ATTR = "_sql_node_cache"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SqlRenderer:
|
|
39
|
+
"""Render a SQL construct tree to ``(sql, params)`` for a target dialect.
|
|
40
|
+
|
|
41
|
+
*pipeline_name* selects which isolated pipeline the tree (and all its
|
|
42
|
+
recursion) is dispatched through — the read-only ``"sql"`` pipeline by
|
|
43
|
+
default, or the write pipeline for ``op: sql_write``.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, opts: RenderOptions, pipeline_name: str = SQL_PIPELINE_NAME) -> None:
|
|
47
|
+
self.opts = opts
|
|
48
|
+
self.pipeline_name = pipeline_name
|
|
49
|
+
|
|
50
|
+
def _render_ctx(self, ctx, extra_metadata: dict | None = None):
|
|
51
|
+
# Render against a scratch dest so the real document is never clobbered,
|
|
52
|
+
# but expose the real dest under _real_dest so @: pointers inside $val
|
|
53
|
+
# can still read the document being built. The active pipeline name is
|
|
54
|
+
# threaded through metadata so recursion stays in this pipeline.
|
|
55
|
+
metadata = {
|
|
56
|
+
**ctx.metadata,
|
|
57
|
+
"_real_dest": ctx.dest,
|
|
58
|
+
ACTIVE_PIPELINE_KEY: self.pipeline_name,
|
|
59
|
+
}
|
|
60
|
+
if extra_metadata:
|
|
61
|
+
metadata.update(extra_metadata)
|
|
62
|
+
return ctx.copy(new_dest={}, new_metadata=metadata)
|
|
63
|
+
|
|
64
|
+
def _finalize(self, frag) -> tuple:
|
|
65
|
+
if not is_fragment(frag):
|
|
66
|
+
raise ValueError("top-level SQL query must be a SQL construct")
|
|
67
|
+
return self.opts.finalize(frag["sql"], frag["params"])
|
|
68
|
+
|
|
69
|
+
def render(self, query, ctx) -> tuple:
|
|
70
|
+
"""Render *query* through the interpreted SQL pipeline."""
|
|
71
|
+
render_ctx = self._render_ctx(ctx)
|
|
72
|
+
frag = ctx.engine.run_pipeline(self.pipeline_name, query, render_ctx).dest
|
|
73
|
+
return self._finalize(frag)
|
|
74
|
+
|
|
75
|
+
def render_compiled(self, compiled_query, ctx) -> tuple:
|
|
76
|
+
"""Render a pre-compiled query spec, reusing per-node compilation.
|
|
77
|
+
|
|
78
|
+
*compiled_query* is the :class:`~j_perm.core.CompiledSpec` produced for
|
|
79
|
+
the ``query`` key at compile time (its top node is already resolved).
|
|
80
|
+
Nested nodes are compiled lazily on first run and memoised on the spec,
|
|
81
|
+
so repeated executions of the same compiled query stay fully compiled.
|
|
82
|
+
"""
|
|
83
|
+
cache = getattr(compiled_query, _NODE_CACHE_ATTR, None)
|
|
84
|
+
if cache is None:
|
|
85
|
+
cache = {}
|
|
86
|
+
setattr(compiled_query, _NODE_CACHE_ATTR, cache)
|
|
87
|
+
render_ctx = self._render_ctx(ctx, {COMPILE_CACHE_KEY: cache})
|
|
88
|
+
ctx.engine.get_pipeline(self.pipeline_name).run_compiled(compiled_query, render_ctx)
|
|
89
|
+
return self._finalize(render_ctx.dest)
|
|
90
|
+
|
|
91
|
+
async def render_async(self, query, ctx) -> tuple:
|
|
92
|
+
"""Async twin of :meth:`render`.
|
|
93
|
+
|
|
94
|
+
Renders synchronously, but each embedded engine value (``$val`` etc.) is
|
|
95
|
+
resolved via ``process_value_async``: an unresolved value raises
|
|
96
|
+
:class:`~j_perm_sql.render._NeedAsyncValue`, we ``await`` it, cache it by
|
|
97
|
+
``id(node)``, and restart the render. After *k* distinct value nodes the
|
|
98
|
+
render runs ``k+1`` times (cheap — pure string building over a scratch
|
|
99
|
+
dest), and never resolves values through the (async) value pipeline
|
|
100
|
+
synchronously.
|
|
101
|
+
"""
|
|
102
|
+
cache: dict = {}
|
|
103
|
+
while True:
|
|
104
|
+
render_ctx = self._render_ctx(ctx, {ASYNC_VALUE_CACHE_KEY: cache})
|
|
105
|
+
try:
|
|
106
|
+
frag = ctx.engine.run_pipeline(self.pipeline_name, query, render_ctx).dest
|
|
107
|
+
return self._finalize(frag)
|
|
108
|
+
except _NeedAsyncValue as exc:
|
|
109
|
+
cache[id(exc.node)] = await ctx.engine.process_value_async(exc.node, render_ctx)
|
|
110
|
+
|
|
111
|
+
async def render_compiled_async(self, compiled_query, ctx) -> tuple:
|
|
112
|
+
"""Async twin of :meth:`render_compiled` (same restart protocol)."""
|
|
113
|
+
node_cache = getattr(compiled_query, _NODE_CACHE_ATTR, None)
|
|
114
|
+
if node_cache is None:
|
|
115
|
+
node_cache = {}
|
|
116
|
+
setattr(compiled_query, _NODE_CACHE_ATTR, node_cache)
|
|
117
|
+
value_cache: dict = {}
|
|
118
|
+
pipeline = ctx.engine.get_pipeline(self.pipeline_name)
|
|
119
|
+
while True:
|
|
120
|
+
render_ctx = self._render_ctx(
|
|
121
|
+
ctx, {COMPILE_CACHE_KEY: node_cache, ASYNC_VALUE_CACHE_KEY: value_cache})
|
|
122
|
+
try:
|
|
123
|
+
pipeline.run_compiled(compiled_query, render_ctx)
|
|
124
|
+
return self._finalize(render_ctx.dest)
|
|
125
|
+
except _NeedAsyncValue as exc:
|
|
126
|
+
value_cache[id(exc.node)] = await ctx.engine.process_value_async(exc.node, render_ctx)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _SqlCompound(Compound):
|
|
130
|
+
"""Mixin marking the ``op: sql`` handlers as compilable.
|
|
131
|
+
|
|
132
|
+
The ``query`` subtree is compiled against the handler's isolated SQL
|
|
133
|
+
pipeline (read or write) rather than the main pipeline, so the engine never
|
|
134
|
+
needs to know the SQL constructs.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
_renderer: SqlRenderer
|
|
138
|
+
|
|
139
|
+
def nested_spec_keys(self, step) -> list[str]:
|
|
140
|
+
return ["query"] if "query" in step else []
|
|
141
|
+
|
|
142
|
+
def nested_spec_pipeline(self, step, key) -> str:
|
|
143
|
+
return self._renderer.pipeline_name
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _write_result(step, ctx, result):
|
|
147
|
+
if "to" in step:
|
|
148
|
+
path = ctx.engine.process_value(step["to"], ctx)
|
|
149
|
+
ctx.engine.processor.set(path, ctx, result)
|
|
150
|
+
return ctx.dest
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def _write_result_async(step, ctx, result):
|
|
154
|
+
if "to" in step:
|
|
155
|
+
path = await ctx.engine.process_value_async(step["to"], ctx)
|
|
156
|
+
ctx.engine.processor.set(path, ctx, result)
|
|
157
|
+
return ctx.dest
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SqlHandler(ActionHandler, _SqlCompound):
|
|
161
|
+
"""``op: sql`` with a synchronous executor ``executor(sql, params) -> result``."""
|
|
162
|
+
|
|
163
|
+
def __init__(self, executor, renderer: SqlRenderer) -> None:
|
|
164
|
+
self._executor = executor
|
|
165
|
+
self._renderer = renderer
|
|
166
|
+
|
|
167
|
+
def execute(self, step, ctx):
|
|
168
|
+
sql, params = self._renderer.render(step["query"], ctx)
|
|
169
|
+
result = self._executor(sql, params)
|
|
170
|
+
return _write_result(step, ctx, result)
|
|
171
|
+
|
|
172
|
+
def execute_compiled(self, step, ctx, nested):
|
|
173
|
+
sql, params = self._renderer.render_compiled(nested["query"], ctx)
|
|
174
|
+
result = self._executor(sql, params)
|
|
175
|
+
return _write_result(step, ctx, result)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class AsyncSqlHandler(AsyncActionHandler, _SqlCompound):
|
|
179
|
+
"""``op: sql`` with an async executor ``await executor(sql, params) -> result``."""
|
|
180
|
+
|
|
181
|
+
def __init__(self, executor, renderer: SqlRenderer) -> None:
|
|
182
|
+
self._executor = executor
|
|
183
|
+
self._renderer = renderer
|
|
184
|
+
|
|
185
|
+
async def execute(self, step, ctx):
|
|
186
|
+
sql, params = await self._renderer.render_async(step["query"], ctx)
|
|
187
|
+
result = await self._executor(sql, params)
|
|
188
|
+
return await _write_result_async(step, ctx, result)
|
|
189
|
+
|
|
190
|
+
async def execute_compiled(self, step, ctx, nested):
|
|
191
|
+
sql, params = await self._renderer.render_compiled_async(nested["query"], ctx)
|
|
192
|
+
result = await self._executor(sql, params)
|
|
193
|
+
return await _write_result_async(step, ctx, result)
|
|
@@ -17,17 +17,31 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
+
from j_perm.handlers.signals import ControlFlowSignal
|
|
21
|
+
|
|
20
22
|
from .dialect import PLACEHOLDER, RenderOptions
|
|
21
23
|
|
|
22
24
|
#: Name the read-only SQL value-pipeline is registered under on the engine.
|
|
23
25
|
SQL_PIPELINE_NAME = "sql"
|
|
24
26
|
|
|
27
|
+
#: Metadata key carrying the async value cache (``{id(node): resolved}``) on the
|
|
28
|
+
#: *async* render path. Its presence is what switches :func:`resolve_value` from
|
|
29
|
+
#: resolving inline (sync) to the resolve-on-demand-with-restart protocol used by
|
|
30
|
+
#: :meth:`j_perm_sql.handler.SqlRenderer.render_async`.
|
|
31
|
+
ASYNC_VALUE_CACHE_KEY = "_sql_async_value_cache"
|
|
32
|
+
|
|
25
33
|
#: Metadata key carrying the name of the pipeline that recursion should dispatch
|
|
26
34
|
#: through. Set by :class:`~j_perm_sql.handler.SqlRenderer` so a write
|
|
27
35
|
#: statement's sub-parts resolve in the write pipeline; defaults to the read
|
|
28
36
|
#: pipeline when absent.
|
|
29
37
|
ACTIVE_PIPELINE_KEY = "_sql_pipeline"
|
|
30
38
|
|
|
39
|
+
#: Metadata key carrying the per-node compilation cache (``{id(node): CompiledSpec}``)
|
|
40
|
+
#: for the *compiled* render path. Present only when the top-level ``op: sql``
|
|
41
|
+
#: was reached through a compiled pipeline; absent for the plain interpreted path
|
|
42
|
+
#: (then :func:`render` dispatches through ``run_pipeline`` as before).
|
|
43
|
+
COMPILE_CACHE_KEY = "_sql_compile_cache"
|
|
44
|
+
|
|
31
45
|
#: Keys whose presence marks a node as a *query* (must be parenthesised when
|
|
32
46
|
#: used as an operand, subquery, or derived table).
|
|
33
47
|
_QUERY_KEYS = frozenset(
|
|
@@ -35,6 +49,37 @@ _QUERY_KEYS = frozenset(
|
|
|
35
49
|
)
|
|
36
50
|
|
|
37
51
|
|
|
52
|
+
class _NeedAsyncValue(ControlFlowSignal):
|
|
53
|
+
"""Raised mid-render to ask the async driver to resolve a value node.
|
|
54
|
+
|
|
55
|
+
Subclasses :class:`~j_perm.handlers.signals.ControlFlowSignal` so it
|
|
56
|
+
propagates straight up through the (synchronous) SQL pipeline without being
|
|
57
|
+
annotated or logged, and is caught only by
|
|
58
|
+
:meth:`j_perm_sql.handler.SqlRenderer.render_async`.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, node: Any) -> None:
|
|
62
|
+
self.node = node
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_value(node: Any, ctx) -> Any:
|
|
66
|
+
"""Resolve an embedded engine value (``$val`` / ``$in`` / ``$values`` cell).
|
|
67
|
+
|
|
68
|
+
* Sync render (no async cache in metadata) → resolve inline via
|
|
69
|
+
``process_value``.
|
|
70
|
+
* Async render → consult the per-render cache keyed by ``id(node)``; on a
|
|
71
|
+
miss, raise :class:`_NeedAsyncValue` so the async driver can
|
|
72
|
+
``await process_value_async`` and restart the render with the value cached.
|
|
73
|
+
"""
|
|
74
|
+
cache = ctx.metadata.get(ASYNC_VALUE_CACHE_KEY)
|
|
75
|
+
if cache is None:
|
|
76
|
+
return ctx.engine.process_value(node, ctx)
|
|
77
|
+
key = id(node)
|
|
78
|
+
if key in cache:
|
|
79
|
+
return cache[key]
|
|
80
|
+
raise _NeedAsyncValue(node)
|
|
81
|
+
|
|
82
|
+
|
|
38
83
|
def fragment(sql: str, params: list | None = None) -> dict:
|
|
39
84
|
"""Build a SQL fragment dict."""
|
|
40
85
|
return {"sql": sql, "params": list(params) if params else []}
|
|
@@ -56,9 +101,25 @@ def render(node: Any, ctx) -> Any:
|
|
|
56
101
|
The active pipeline name is read from ``ctx.metadata`` (set by the renderer)
|
|
57
102
|
so recursion stays within the same pipeline the top-level operation chose;
|
|
58
103
|
it defaults to the read-only :data:`SQL_PIPELINE_NAME`.
|
|
104
|
+
|
|
105
|
+
When a compilation cache is active (the top-level ``op: sql`` was compiled),
|
|
106
|
+
each node is compiled once against the SQL pipeline and the resulting
|
|
107
|
+
:class:`~j_perm.core.CompiledSpec` is memoised on the cache, so subsequent
|
|
108
|
+
runs of the same compiled query skip stage processing and handler resolution
|
|
109
|
+
for every node. The cache lives on the top-level compiled query spec, so it
|
|
110
|
+
is bounded by that query's nodes and freed together with it.
|
|
59
111
|
"""
|
|
60
112
|
name = ctx.metadata.get(ACTIVE_PIPELINE_KEY, SQL_PIPELINE_NAME)
|
|
61
|
-
|
|
113
|
+
cache = ctx.metadata.get(COMPILE_CACHE_KEY)
|
|
114
|
+
if cache is None:
|
|
115
|
+
return ctx.engine.run_pipeline(name, node, ctx).dest
|
|
116
|
+
pipeline = ctx.engine.get_pipeline(name)
|
|
117
|
+
compiled = cache.get(id(node))
|
|
118
|
+
if compiled is None:
|
|
119
|
+
compiled = pipeline.compile(node, ctx)
|
|
120
|
+
cache[id(node)] = compiled
|
|
121
|
+
pipeline.run_compiled(compiled, ctx)
|
|
122
|
+
return ctx.dest
|
|
62
123
|
|
|
63
124
|
|
|
64
125
|
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.4.0
|
|
4
4
|
Summary: j-perm plugin for SQL
|
|
5
5
|
Author-email: Roman <kuschanow@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -10,7 +10,7 @@ Project-URL: Tracker, https://github.com/kuschanow/j-perm/issues
|
|
|
10
10
|
Project-URL: Documentation, https://github.com/kuschanow/j-perm
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: j-perm>=1.
|
|
13
|
+
Requires-Dist: j-perm>=1.11.0
|
|
14
14
|
|
|
15
15
|
# j-perm-sql
|
|
16
16
|
|
|
@@ -39,8 +39,10 @@ A [j-perm](https://github.com/kuschanow/j-perm) plugin that builds and executes
|
|
|
39
39
|
pip install j-perm-sql
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Requires `j-perm >= 1.9.0
|
|
43
|
-
|
|
42
|
+
Requires `j-perm >= 1.10.0`: 1.9.0 made `run_pipeline` a passthrough invoker
|
|
43
|
+
(which this plugin relies on), and 1.10.0 added the `nested_spec_pipeline`
|
|
44
|
+
compile hook and per-pipeline `CompiledSpec` execution that let `op: sql` be
|
|
45
|
+
compiled end-to-end (see [Compilation](#compilation)).
|
|
44
46
|
|
|
45
47
|
## Quick start
|
|
46
48
|
|
|
@@ -202,6 +204,25 @@ install_sql(engine, run_sql) # async
|
|
|
202
204
|
await engine.apply_async(spec, source=…, dest=…)
|
|
203
205
|
```
|
|
204
206
|
|
|
207
|
+
## Compilation
|
|
208
|
+
|
|
209
|
+
`op: sql` / `op: sql_write` are compilable. `engine.compile(spec)` compiles the
|
|
210
|
+
`query` subtree against the isolated SQL pipeline (the engine never needs to
|
|
211
|
+
understand the SQL constructs — it routes the nested spec through the registered
|
|
212
|
+
pipeline by name), and the rendered tree is dispatched through the compiled path
|
|
213
|
+
with per-node memoisation. Re-applying the same `CompiledSpec` keeps every node
|
|
214
|
+
compiled; only `$val` data is re-bound from the live context on each run.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
compiled = engine.compile([{"op": "sql", "to": "/rows", "query": query}])
|
|
218
|
+
compiled.apply(source={"wanted": 1}, dest={}) # renders + executes, fully compiled
|
|
219
|
+
compiled.apply(source={"wanted": 2}, dest={}) # reuses compiled nodes, re-binds values
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
This requires the `nested_spec_pipeline` compile hook and per-pipeline
|
|
223
|
+
`CompiledSpec` execution added in the core engine (see the "What gets compiled"
|
|
224
|
+
section of the main j-perm README).
|
|
225
|
+
|
|
205
226
|
## Construct reference
|
|
206
227
|
|
|
207
228
|
**Query**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
j-perm>=1.11.0
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
"""The single top-level ``op: sql`` operation (sync + async).
|
|
2
|
-
|
|
3
|
-
Schema::
|
|
4
|
-
|
|
5
|
-
{"op": "sql", "query": <SQL construct tree>, "to": "/dest/path"}
|
|
6
|
-
|
|
7
|
-
* ``query`` — the SQL construct tree (rendered via the isolated SQL pipeline).
|
|
8
|
-
* ``to`` — optional destination pointer; the executor's result is written
|
|
9
|
-
there (template-expanded). If omitted, the result is discarded.
|
|
10
|
-
|
|
11
|
-
Rendering is identical for sync and async; only the executor call differs
|
|
12
|
-
(call vs ``await``). A single class can't expose both a sync and an async
|
|
13
|
-
``execute`` (same method name), and ``Pipeline.run_async`` dispatches to the
|
|
14
|
-
async path only for :class:`AsyncActionHandler` — hence two classes over one
|
|
15
|
-
shared :class:`SqlRenderer`.
|
|
16
|
-
"""
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from j_perm import ActionHandler, AsyncActionHandler
|
|
20
|
-
|
|
21
|
-
from .dialect import RenderOptions
|
|
22
|
-
from .render import ACTIVE_PIPELINE_KEY, SQL_PIPELINE_NAME, is_fragment
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class SqlRenderer:
|
|
26
|
-
"""Render a SQL construct tree to ``(sql, params)`` for a target dialect.
|
|
27
|
-
|
|
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:
|
|
34
|
-
self.opts = opts
|
|
35
|
-
self.pipeline_name = pipeline_name
|
|
36
|
-
|
|
37
|
-
def render(self, query, ctx) -> tuple:
|
|
38
|
-
# Render against a scratch dest so the real document is never clobbered,
|
|
39
|
-
# but expose the real dest under _real_dest so @: pointers inside $val
|
|
40
|
-
# can still read the document being built. The active pipeline name is
|
|
41
|
-
# threaded through metadata so recursion stays in this pipeline.
|
|
42
|
-
render_ctx = ctx.copy(
|
|
43
|
-
new_dest={},
|
|
44
|
-
new_metadata={
|
|
45
|
-
**ctx.metadata,
|
|
46
|
-
"_real_dest": ctx.dest,
|
|
47
|
-
ACTIVE_PIPELINE_KEY: self.pipeline_name,
|
|
48
|
-
},
|
|
49
|
-
)
|
|
50
|
-
frag = ctx.engine.run_pipeline(self.pipeline_name, query, render_ctx).dest
|
|
51
|
-
if not is_fragment(frag):
|
|
52
|
-
raise ValueError("top-level SQL query must be a SQL construct")
|
|
53
|
-
return self.opts.finalize(frag["sql"], frag["params"])
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _write_result(step, ctx, result):
|
|
57
|
-
if "to" in step:
|
|
58
|
-
path = ctx.engine.process_value(step["to"], ctx)
|
|
59
|
-
ctx.engine.processor.set(path, ctx, result)
|
|
60
|
-
return ctx.dest
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class SqlHandler(ActionHandler):
|
|
64
|
-
"""``op: sql`` with a synchronous executor ``executor(sql, params) -> result``."""
|
|
65
|
-
|
|
66
|
-
def __init__(self, executor, renderer: SqlRenderer) -> None:
|
|
67
|
-
self._executor = executor
|
|
68
|
-
self._renderer = renderer
|
|
69
|
-
|
|
70
|
-
def execute(self, step, ctx):
|
|
71
|
-
sql, params = self._renderer.render(step["query"], ctx)
|
|
72
|
-
result = self._executor(sql, params)
|
|
73
|
-
return _write_result(step, ctx, result)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class AsyncSqlHandler(AsyncActionHandler):
|
|
77
|
-
"""``op: sql`` with an async executor ``await executor(sql, params) -> result``."""
|
|
78
|
-
|
|
79
|
-
def __init__(self, executor, renderer: SqlRenderer) -> None:
|
|
80
|
-
self._executor = executor
|
|
81
|
-
self._renderer = renderer
|
|
82
|
-
|
|
83
|
-
async def execute(self, step, ctx):
|
|
84
|
-
sql, params = self._renderer.render(step["query"], ctx)
|
|
85
|
-
result = await self._executor(sql, params)
|
|
86
|
-
return _write_result(step, ctx, result)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
j-perm>=1.9.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|