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.
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/PKG-INFO +2 -2
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/pyproject.toml +2 -2
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/constructs.py +4 -3
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/handler.py +50 -4
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/render.py +39 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql.egg-info/PKG-INFO +2 -2
- j_perm_sql-0.4.0/src/j_perm_sql.egg-info/requires.txt +1 -0
- j_perm_sql-0.3.0/src/j_perm_sql.egg-info/requires.txt +0 -1
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/README.md +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/setup.cfg +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/__init__.py +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/constructs_write.py +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/dialect.py +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/install.py +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql/pipeline.py +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql.egg-info/SOURCES.txt +0 -0
- {j_perm_sql-0.3.0 → j_perm_sql-0.4.0}/src/j_perm_sql.egg-info/dependency_links.txt +0 -0
- {j_perm_sql-0.3.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
|
|
|
@@ -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
|
|
|
@@ -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.
|
|
186
|
+
sql, params = await self._renderer.render_async(step["query"], ctx)
|
|
141
187
|
result = await self._executor(sql, params)
|
|
142
|
-
return
|
|
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.
|
|
191
|
+
sql, params = await self._renderer.render_compiled_async(nested["query"], ctx)
|
|
146
192
|
result = await self._executor(sql, params)
|
|
147
|
-
return
|
|
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
|
+
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
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
j-perm>=1.11.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
j-perm>=1.10.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
|
|
File without changes
|