j-perm-sql 0.2.0__tar.gz → 0.3.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.3.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.10.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.3.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.10.0",
22
22
  ]
23
23
 
24
24
  [tool.pytest.ini_options]
@@ -17,9 +17,20 @@ shared :class:`SqlRenderer`.
17
17
  from __future__ import annotations
18
18
 
19
19
  from j_perm import ActionHandler, AsyncActionHandler
20
+ from j_perm.core import Compound
20
21
 
21
22
  from .dialect import RenderOptions
22
- from .render import ACTIVE_PIPELINE_KEY, SQL_PIPELINE_NAME, is_fragment
23
+ from .render import (
24
+ ACTIVE_PIPELINE_KEY,
25
+ COMPILE_CACHE_KEY,
26
+ SQL_PIPELINE_NAME,
27
+ is_fragment,
28
+ )
29
+
30
+ #: Attribute under which a compiled query spec carries its per-node compilation
31
+ #: cache (see :func:`j_perm_sql.render.render`). Stored on the spec — not in the
32
+ #: context — so it survives across runs of the same compiled query.
33
+ _NODE_CACHE_ATTR = "_sql_node_cache"
23
34
 
24
35
 
25
36
  class SqlRenderer:
@@ -34,24 +45,64 @@ class SqlRenderer:
34
45
  self.opts = opts
35
46
  self.pipeline_name = pipeline_name
36
47
 
37
- def render(self, query, ctx) -> tuple:
48
+ def _render_ctx(self, ctx, extra_metadata: dict | None = None):
38
49
  # Render against a scratch dest so the real document is never clobbered,
39
50
  # but expose the real dest under _real_dest so @: pointers inside $val
40
51
  # can still read the document being built. The active pipeline name is
41
52
  # 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
53
+ metadata = {
54
+ **ctx.metadata,
55
+ "_real_dest": ctx.dest,
56
+ ACTIVE_PIPELINE_KEY: self.pipeline_name,
57
+ }
58
+ if extra_metadata:
59
+ metadata.update(extra_metadata)
60
+ return ctx.copy(new_dest={}, new_metadata=metadata)
61
+
62
+ def _finalize(self, frag) -> tuple:
51
63
  if not is_fragment(frag):
52
64
  raise ValueError("top-level SQL query must be a SQL construct")
53
65
  return self.opts.finalize(frag["sql"], frag["params"])
54
66
 
67
+ def render(self, query, ctx) -> tuple:
68
+ """Render *query* through the interpreted SQL pipeline."""
69
+ render_ctx = self._render_ctx(ctx)
70
+ frag = ctx.engine.run_pipeline(self.pipeline_name, query, render_ctx).dest
71
+ return self._finalize(frag)
72
+
73
+ def render_compiled(self, compiled_query, ctx) -> tuple:
74
+ """Render a pre-compiled query spec, reusing per-node compilation.
75
+
76
+ *compiled_query* is the :class:`~j_perm.core.CompiledSpec` produced for
77
+ the ``query`` key at compile time (its top node is already resolved).
78
+ Nested nodes are compiled lazily on first run and memoised on the spec,
79
+ so repeated executions of the same compiled query stay fully compiled.
80
+ """
81
+ cache = getattr(compiled_query, _NODE_CACHE_ATTR, None)
82
+ if cache is None:
83
+ cache = {}
84
+ setattr(compiled_query, _NODE_CACHE_ATTR, cache)
85
+ render_ctx = self._render_ctx(ctx, {COMPILE_CACHE_KEY: cache})
86
+ ctx.engine.get_pipeline(self.pipeline_name).run_compiled(compiled_query, render_ctx)
87
+ return self._finalize(render_ctx.dest)
88
+
89
+
90
+ class _SqlCompound(Compound):
91
+ """Mixin marking the ``op: sql`` handlers as compilable.
92
+
93
+ The ``query`` subtree is compiled against the handler's isolated SQL
94
+ pipeline (read or write) rather than the main pipeline, so the engine never
95
+ needs to know the SQL constructs.
96
+ """
97
+
98
+ _renderer: SqlRenderer
99
+
100
+ def nested_spec_keys(self, step) -> list[str]:
101
+ return ["query"] if "query" in step else []
102
+
103
+ def nested_spec_pipeline(self, step, key) -> str:
104
+ return self._renderer.pipeline_name
105
+
55
106
 
56
107
  def _write_result(step, ctx, result):
57
108
  if "to" in step:
@@ -60,7 +111,7 @@ def _write_result(step, ctx, result):
60
111
  return ctx.dest
61
112
 
62
113
 
63
- class SqlHandler(ActionHandler):
114
+ class SqlHandler(ActionHandler, _SqlCompound):
64
115
  """``op: sql`` with a synchronous executor ``executor(sql, params) -> result``."""
65
116
 
66
117
  def __init__(self, executor, renderer: SqlRenderer) -> None:
@@ -72,8 +123,13 @@ class SqlHandler(ActionHandler):
72
123
  result = self._executor(sql, params)
73
124
  return _write_result(step, ctx, result)
74
125
 
126
+ def execute_compiled(self, step, ctx, nested):
127
+ sql, params = self._renderer.render_compiled(nested["query"], ctx)
128
+ result = self._executor(sql, params)
129
+ return _write_result(step, ctx, result)
130
+
75
131
 
76
- class AsyncSqlHandler(AsyncActionHandler):
132
+ class AsyncSqlHandler(AsyncActionHandler, _SqlCompound):
77
133
  """``op: sql`` with an async executor ``await executor(sql, params) -> result``."""
78
134
 
79
135
  def __init__(self, executor, renderer: SqlRenderer) -> None:
@@ -84,3 +140,8 @@ class AsyncSqlHandler(AsyncActionHandler):
84
140
  sql, params = self._renderer.render(step["query"], ctx)
85
141
  result = await self._executor(sql, params)
86
142
  return _write_result(step, ctx, result)
143
+
144
+ async def execute_compiled(self, step, ctx, nested):
145
+ sql, params = self._renderer.render_compiled(nested["query"], ctx)
146
+ result = await self._executor(sql, params)
147
+ return _write_result(step, ctx, result)
@@ -28,6 +28,12 @@ SQL_PIPELINE_NAME = "sql"
28
28
  #: pipeline when absent.
29
29
  ACTIVE_PIPELINE_KEY = "_sql_pipeline"
30
30
 
31
+ #: Metadata key carrying the per-node compilation cache (``{id(node): CompiledSpec}``)
32
+ #: for the *compiled* render path. Present only when the top-level ``op: sql``
33
+ #: was reached through a compiled pipeline; absent for the plain interpreted path
34
+ #: (then :func:`render` dispatches through ``run_pipeline`` as before).
35
+ COMPILE_CACHE_KEY = "_sql_compile_cache"
36
+
31
37
  #: Keys whose presence marks a node as a *query* (must be parenthesised when
32
38
  #: used as an operand, subquery, or derived table).
33
39
  _QUERY_KEYS = frozenset(
@@ -56,9 +62,25 @@ def render(node: Any, ctx) -> Any:
56
62
  The active pipeline name is read from ``ctx.metadata`` (set by the renderer)
57
63
  so recursion stays within the same pipeline the top-level operation chose;
58
64
  it defaults to the read-only :data:`SQL_PIPELINE_NAME`.
65
+
66
+ When a compilation cache is active (the top-level ``op: sql`` was compiled),
67
+ each node is compiled once against the SQL pipeline and the resulting
68
+ :class:`~j_perm.core.CompiledSpec` is memoised on the cache, so subsequent
69
+ runs of the same compiled query skip stage processing and handler resolution
70
+ for every node. The cache lives on the top-level compiled query spec, so it
71
+ is bounded by that query's nodes and freed together with it.
59
72
  """
60
73
  name = ctx.metadata.get(ACTIVE_PIPELINE_KEY, SQL_PIPELINE_NAME)
61
- return ctx.engine.run_pipeline(name, node, ctx).dest
74
+ cache = ctx.metadata.get(COMPILE_CACHE_KEY)
75
+ if cache is None:
76
+ return ctx.engine.run_pipeline(name, node, ctx).dest
77
+ pipeline = ctx.engine.get_pipeline(name)
78
+ compiled = cache.get(id(node))
79
+ if compiled is None:
80
+ compiled = pipeline.compile(node, ctx)
81
+ cache[id(node)] = compiled
82
+ pipeline.run_compiled(compiled, ctx)
83
+ return ctx.dest
62
84
 
63
85
 
64
86
  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.3.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.10.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.10.0
@@ -1 +0,0 @@
1
- j-perm>=1.9.0
File without changes