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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: j-perm-sql
3
- Version: 0.2.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.9.0
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` (the version that made `run_pipeline` a passthrough
43
- invoker, which this plugin relies on).
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` (the version that made `run_pipeline` a passthrough
29
- invoker, which this plugin relies on).
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.2.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.9.0",
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 = ctx.engine.process_value(node["$val"], ctx)
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 = ctx.engine.process_value(right, ctx)
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(ctx.engine.process_value(cell, ctx))
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
- return ctx.engine.run_pipeline(name, node, ctx).dest
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.2.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.9.0
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` (the version that made `run_pipeline` a passthrough
43
- invoker, which this plugin relies on).
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