j-perm-sql 0.3.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.3.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.10.0
13
+ Requires-Dist: j-perm>=1.11.0
14
14
 
15
15
  # j-perm-sql
16
16
 
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "j-perm-sql"
9
- version = "0.3.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.10.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
 
@@ -22,8 +22,10 @@ from j_perm.core import Compound
22
22
  from .dialect import RenderOptions
23
23
  from .render import (
24
24
  ACTIVE_PIPELINE_KEY,
25
+ ASYNC_VALUE_CACHE_KEY,
25
26
  COMPILE_CACHE_KEY,
26
27
  SQL_PIPELINE_NAME,
28
+ _NeedAsyncValue,
27
29
  is_fragment,
28
30
  )
29
31
 
@@ -86,6 +88,43 @@ class SqlRenderer:
86
88
  ctx.engine.get_pipeline(self.pipeline_name).run_compiled(compiled_query, render_ctx)
87
89
  return self._finalize(render_ctx.dest)
88
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
+
89
128
 
90
129
  class _SqlCompound(Compound):
91
130
  """Mixin marking the ``op: sql`` handlers as compilable.
@@ -111,6 +150,13 @@ def _write_result(step, ctx, result):
111
150
  return ctx.dest
112
151
 
113
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
+
114
160
  class SqlHandler(ActionHandler, _SqlCompound):
115
161
  """``op: sql`` with a synchronous executor ``executor(sql, params) -> result``."""
116
162
 
@@ -137,11 +183,11 @@ class AsyncSqlHandler(AsyncActionHandler, _SqlCompound):
137
183
  self._renderer = renderer
138
184
 
139
185
  async def execute(self, step, ctx):
140
- sql, params = self._renderer.render(step["query"], ctx)
186
+ sql, params = await self._renderer.render_async(step["query"], ctx)
141
187
  result = await self._executor(sql, params)
142
- return _write_result(step, ctx, result)
188
+ return await _write_result_async(step, ctx, result)
143
189
 
144
190
  async def execute_compiled(self, step, ctx, nested):
145
- sql, params = self._renderer.render_compiled(nested["query"], ctx)
191
+ sql, params = await self._renderer.render_compiled_async(nested["query"], ctx)
146
192
  result = await self._executor(sql, params)
147
- return _write_result(step, ctx, result)
193
+ return await _write_result_async(step, ctx, result)
@@ -17,11 +17,19 @@ 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
@@ -41,6 +49,37 @@ _QUERY_KEYS = frozenset(
41
49
  )
42
50
 
43
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
+
44
83
  def fragment(sql: str, params: list | None = None) -> dict:
45
84
  """Build a SQL fragment dict."""
46
85
  return {"sql": sql, "params": list(params) if params else []}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: j-perm-sql
3
- Version: 0.3.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.10.0
13
+ Requires-Dist: j-perm>=1.11.0
14
14
 
15
15
  # j-perm-sql
16
16
 
@@ -0,0 +1 @@
1
+ j-perm>=1.11.0
@@ -1 +0,0 @@
1
- j-perm>=1.10.0
File without changes
File without changes