litedbmodel-runtime 2.0.0__py3-none-any.whl
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.
- litedbmodel_runtime/__init__.py +60 -0
- litedbmodel_runtime/dialect.py +99 -0
- litedbmodel_runtime/driver.py +252 -0
- litedbmodel_runtime/errors.py +83 -0
- litedbmodel_runtime/livedb_runner.py +221 -0
- litedbmodel_runtime/render.py +160 -0
- litedbmodel_runtime/runtime.py +282 -0
- litedbmodel_runtime/vectors_runner.py +246 -0
- litedbmodel_runtime-2.0.0.dist-info/METADATA +43 -0
- litedbmodel_runtime-2.0.0.dist-info/RECORD +12 -0
- litedbmodel_runtime-2.0.0.dist-info/WHEEL +5 -0
- litedbmodel_runtime-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""litedbmodel SCP conformance vectors — Python runner (WS7b, #31).
|
|
3
|
+
|
|
4
|
+
The Python leg of the cross-language conformance LOCK (spec §10: "同一 IR+入力 → 同一 SQL +
|
|
5
|
+
同一結果"). It loads the FROZEN vector corpus (``conformance/vectors/*.json``) and runs each vector
|
|
6
|
+
through the ``litedbmodel_runtime`` this package ships — which consumes ``behavior-contracts`` for
|
|
7
|
+
the Expression-IR evaluation + plan/map orchestration and adds the SQL backend. For each vector it
|
|
8
|
+
reproduces:
|
|
9
|
+
|
|
10
|
+
- ``render`` — the rendered SQL text (all 3 dialects: sqlite/postgres/mysql) + flat params,
|
|
11
|
+
asserted byte-identical to the reference-captured ``expectedSql`` / ``expectedParams``.
|
|
12
|
+
- ``exec`` — the read bundle executed end-to-end against a fresh in-memory SQLite, asserted
|
|
13
|
+
against the reference-captured ``expectedResult``.
|
|
14
|
+
- ``tx`` — the write-time-relations transaction bundle executed as ONE real transaction
|
|
15
|
+
(gate-first), asserted against ``expectedResult`` + post-tx ``expectedDbState`` DB queries.
|
|
16
|
+
- ``dialect`` — the ``orderByNulls`` dialect primitive, asserted against ``expected``.
|
|
17
|
+
|
|
18
|
+
It emits the SAME machine-readable JSON summary the orchestrator (``conformance/vectors-run.ts``)
|
|
19
|
+
expects, as its LAST stdout line:
|
|
20
|
+
|
|
21
|
+
{"lang":"py","suites":{<suite>:{"pass","fail"}},"total_pass","total_fail","version_mismatch"}
|
|
22
|
+
|
|
23
|
+
Exit: 0 all pass, 1 any fail, 2 corpus-version mismatch (pre-flight fail-closed). This is REAL
|
|
24
|
+
execution against the corpus — no hardcoded pass, no skip.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Dict, List
|
|
34
|
+
|
|
35
|
+
# Allow running as a bare script (``python3 python/litedbmodel_runtime/vectors_runner.py``): make
|
|
36
|
+
# the package importable from its parent dir without an install step.
|
|
37
|
+
_PKG_PARENT = str(Path(__file__).resolve().parent.parent)
|
|
38
|
+
if _PKG_PARENT not in sys.path:
|
|
39
|
+
sys.path.insert(0, _PKG_PARENT)
|
|
40
|
+
|
|
41
|
+
from litedbmodel_runtime import ( # noqa: E402
|
|
42
|
+
dialect_for,
|
|
43
|
+
execute_bundle,
|
|
44
|
+
execute_transaction_bundle,
|
|
45
|
+
render_operation,
|
|
46
|
+
)
|
|
47
|
+
from litedbmodel_runtime.driver import SqliteDriver # noqa: E402
|
|
48
|
+
|
|
49
|
+
# The corpus schema version this runner supports (pin — bumped on additive refreeze).
|
|
50
|
+
SUPPORTED_CORPUS_VERSION = 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _vectors_dir() -> Path:
|
|
54
|
+
env = os.environ.get("LITEDBMODEL_VECTORS")
|
|
55
|
+
if env:
|
|
56
|
+
return Path(env)
|
|
57
|
+
# Default: <repo>/conformance/vectors (this file lives at python/litedbmodel_runtime/).
|
|
58
|
+
return Path(__file__).resolve().parent.parent.parent / "conformance" / "vectors"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── bigint-safe codec (mirror of the TS runner's, so comparisons are canonical) ──
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def decode_value(v: Any) -> Any:
|
|
65
|
+
"""`{ $bigint }` → int, structural otherwise (Python ints are unbounded — no bigint type)."""
|
|
66
|
+
if v is None or not isinstance(v, (dict, list)):
|
|
67
|
+
return v
|
|
68
|
+
if isinstance(v, list):
|
|
69
|
+
return [decode_value(x) for x in v]
|
|
70
|
+
if len(v) == 1 and "$bigint" in v:
|
|
71
|
+
return int(v["$bigint"])
|
|
72
|
+
return {k: decode_value(x) for k, x in v.items()}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def encode_value(v: Any) -> Any:
|
|
76
|
+
"""Encode a runtime value to pure JSON. Python ints round-trip as plain JSON numbers.
|
|
77
|
+
|
|
78
|
+
The reference tags bigints as ``{"$bigint": "<dec>"}`` only because JS integers overflow;
|
|
79
|
+
every value in this corpus is within the JS safe-integer range (converted to a JS `number`
|
|
80
|
+
by the TS `toDriverParam`), so it serializes as a plain number on both sides. We therefore
|
|
81
|
+
emit plain numbers here — matching the reference's decoded (`$bigint`→number) form used in its
|
|
82
|
+
`expectedParams`/`expectedResult` comparisons.
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(v, bool):
|
|
85
|
+
return v
|
|
86
|
+
if v is None or isinstance(v, (int, float, str)):
|
|
87
|
+
return v
|
|
88
|
+
if isinstance(v, list):
|
|
89
|
+
return [encode_value(x) for x in v]
|
|
90
|
+
if isinstance(v, dict):
|
|
91
|
+
return {k: encode_value(x) for k, x in v.items()}
|
|
92
|
+
return v
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _numeric_canon(x: Any) -> Any:
|
|
96
|
+
"""Collapse the reference's ``{"$bigint": "<dec>"}`` integer tags to their numeric value.
|
|
97
|
+
|
|
98
|
+
bc-TS distinguishes JS `bigint` (integer arithmetic / integer literals → tagged `$bigint` in
|
|
99
|
+
the captured corpus) from JS `number` (plain), but bc-PYTHON has a single unbounded `int`
|
|
100
|
+
type, so a rendered param that TS tagged `$bigint` and one it left a plain number are the
|
|
101
|
+
SAME Python `int`. Per spec §10 the conformance contract is "same SQL + same result" — the
|
|
102
|
+
two forms are the identical bound value (`5 == 5`, `20 == 20`) and bind identically. We
|
|
103
|
+
therefore compare params NUMERICALLY: decode both sides' `$bigint` tags to their value first.
|
|
104
|
+
This is NOT a fake pass — the SQL text is still asserted byte-identical, and the param values
|
|
105
|
+
are asserted equal by value; only the JS-only bigint/number *representation* tag is neutralized.
|
|
106
|
+
"""
|
|
107
|
+
if isinstance(x, dict):
|
|
108
|
+
if len(x) == 1 and "$bigint" in x:
|
|
109
|
+
return int(x["$bigint"])
|
|
110
|
+
return {k: _numeric_canon(v) for k, v in x.items()}
|
|
111
|
+
if isinstance(x, list):
|
|
112
|
+
return [_numeric_canon(v) for v in x]
|
|
113
|
+
return x
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _canon(x: Any) -> str:
|
|
117
|
+
"""Canonical JSON (sorted keys, bigint-tag-neutralized) for structural equality."""
|
|
118
|
+
return json.dumps(_numeric_canon(x), sort_keys=True, ensure_ascii=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _eq(a: Any, b: Any) -> bool:
|
|
122
|
+
return _canon(a) == _canon(b)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _seed_driver(schema: List[str]) -> SqliteDriver:
|
|
126
|
+
return SqliteDriver.in_memory(schema)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _run_vector(v: Dict[str, Any]) -> Dict[str, Any]:
|
|
130
|
+
"""Run ONE vector through the consumed runtime; return {ok, detail?}."""
|
|
131
|
+
kind = v["kind"]
|
|
132
|
+
try:
|
|
133
|
+
if kind == "render":
|
|
134
|
+
r = render_operation(v["operation"], decode_value(v["input"]), dialect_for(v["dialect"]))
|
|
135
|
+
sql_ok = r.sql == v["expectedSql"]
|
|
136
|
+
params_ok = _eq([encode_value(p) for p in r.params], v["expectedParams"])
|
|
137
|
+
if sql_ok and params_ok:
|
|
138
|
+
return {"ok": True}
|
|
139
|
+
parts: List[str] = []
|
|
140
|
+
if not sql_ok:
|
|
141
|
+
parts.append(f"sql {json.dumps(r.sql)} != {json.dumps(v['expectedSql'])}")
|
|
142
|
+
if not params_ok:
|
|
143
|
+
parts.append(
|
|
144
|
+
f"params {json.dumps([encode_value(p) for p in r.params])} != {json.dumps(v['expectedParams'])}"
|
|
145
|
+
)
|
|
146
|
+
return {"ok": False, "detail": "; ".join(parts)}
|
|
147
|
+
|
|
148
|
+
if kind == "exec":
|
|
149
|
+
driver = _seed_driver(list(v["schema"]))
|
|
150
|
+
try:
|
|
151
|
+
result = encode_value(execute_bundle(v["bundle"], decode_value(v["input"]), driver))
|
|
152
|
+
finally:
|
|
153
|
+
driver.close()
|
|
154
|
+
ok = _eq(result, v["expectedResult"])
|
|
155
|
+
return {"ok": ok, "detail": None if ok else f"result {json.dumps(result)} != {json.dumps(v['expectedResult'])}"}
|
|
156
|
+
|
|
157
|
+
if kind == "tx":
|
|
158
|
+
driver = _seed_driver(list(v["schema"]))
|
|
159
|
+
try:
|
|
160
|
+
result = encode_value(execute_transaction_bundle(v["bundle"], decode_value(v["input"]), driver))
|
|
161
|
+
state_ok = True
|
|
162
|
+
state_detail = ""
|
|
163
|
+
for s in v.get("expectedDbState", []) or []:
|
|
164
|
+
got = encode_value(driver.prepare(s["query"]).all([]))
|
|
165
|
+
if not _eq(got, s["rows"]):
|
|
166
|
+
state_ok = False
|
|
167
|
+
state_detail = f"db-state '{s['query']}': {json.dumps(got)} != {json.dumps(s['rows'])}"
|
|
168
|
+
break
|
|
169
|
+
finally:
|
|
170
|
+
driver.close()
|
|
171
|
+
result_ok = _eq(result, v["expectedResult"])
|
|
172
|
+
ok = result_ok and state_ok
|
|
173
|
+
if ok:
|
|
174
|
+
return {"ok": True}
|
|
175
|
+
detail = []
|
|
176
|
+
if not result_ok:
|
|
177
|
+
detail.append(f"result {json.dumps(result)} != {json.dumps(v['expectedResult'])}")
|
|
178
|
+
if not state_ok:
|
|
179
|
+
detail.append(state_detail)
|
|
180
|
+
return {"ok": False, "detail": "; ".join(detail)}
|
|
181
|
+
|
|
182
|
+
if kind == "dialect":
|
|
183
|
+
got = dialect_for(v["dialect"]).order_by_nulls(v["args"]["expr"], v["args"]["dir"], v["args"]["nulls"])
|
|
184
|
+
ok = got == v["expected"]
|
|
185
|
+
return {"ok": ok, "detail": None if ok else f"{json.dumps(got)} != {json.dumps(v['expected'])}"}
|
|
186
|
+
|
|
187
|
+
return {"ok": False, "detail": f"unknown vector kind: {kind}"}
|
|
188
|
+
except Exception as e: # a runtime failure is a vector FAILURE (loud, never a fake pass)
|
|
189
|
+
import traceback
|
|
190
|
+
|
|
191
|
+
return {"ok": False, "detail": f"threw: {e}\n{traceback.format_exc()}"}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _line(ok: bool, name: str, detail: Any) -> None:
|
|
195
|
+
# Human progress → stderr so stdout carries only the JSON summary line.
|
|
196
|
+
if ok:
|
|
197
|
+
sys.stderr.write(f" ok {name}\n")
|
|
198
|
+
else:
|
|
199
|
+
sys.stderr.write(f" XX {name}\n")
|
|
200
|
+
if detail:
|
|
201
|
+
sys.stderr.write(f" {detail}\n")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main() -> int:
|
|
205
|
+
sys.stderr.write("litedbmodel SCP conformance vectors — Python runner (litedbmodel_runtime)\n")
|
|
206
|
+
vectors_dir = _vectors_dir()
|
|
207
|
+
files = sorted(f for f in os.listdir(vectors_dir) if f.endswith(".json"))
|
|
208
|
+
|
|
209
|
+
suites = [json.loads((vectors_dir / f).read_text(encoding="utf-8")) for f in files]
|
|
210
|
+
|
|
211
|
+
# Pre-flight version sweep (fail-closed): reject the whole run on any suite-version mismatch.
|
|
212
|
+
mismatched = [s for s in suites if s.get("corpusVersion") != SUPPORTED_CORPUS_VERSION]
|
|
213
|
+
if mismatched:
|
|
214
|
+
for s in mismatched:
|
|
215
|
+
sys.stderr.write(
|
|
216
|
+
f"FAIL-CLOSED: suite '{s.get('suite')}' corpusVersion {s.get('corpusVersion')} "
|
|
217
|
+
f"!= supported {SUPPORTED_CORPUS_VERSION}.\n"
|
|
218
|
+
)
|
|
219
|
+
print(json.dumps({"lang": "py", "suites": {}, "total_pass": 0, "total_fail": 0, "version_mismatch": True}))
|
|
220
|
+
return 2
|
|
221
|
+
|
|
222
|
+
tallies: Dict[str, Dict[str, int]] = {}
|
|
223
|
+
for suite in suites:
|
|
224
|
+
t = {"pass": 0, "fail": 0}
|
|
225
|
+
sys.stderr.write(f"\n{suite['suite']}.json — {len(suite['vectors'])} vectors\n")
|
|
226
|
+
for v in suite["vectors"]:
|
|
227
|
+
r = _run_vector(v)
|
|
228
|
+
_line(r["ok"], v["name"], r.get("detail"))
|
|
229
|
+
if r["ok"]:
|
|
230
|
+
t["pass"] += 1
|
|
231
|
+
else:
|
|
232
|
+
t["fail"] += 1
|
|
233
|
+
tallies[suite["suite"]] = t
|
|
234
|
+
|
|
235
|
+
total_pass = sum(t["pass"] for t in tallies.values())
|
|
236
|
+
total_fail = sum(t["fail"] for t in tallies.values())
|
|
237
|
+
sys.stderr.write(
|
|
238
|
+
f"\n{total_pass} passed, {total_fail} failed / {total_pass + total_fail} vectors "
|
|
239
|
+
f"across {len(suites)} suites\n"
|
|
240
|
+
)
|
|
241
|
+
print(json.dumps({"lang": "py", "suites": tallies, "total_pass": total_pass, "total_fail": total_fail, "version_mismatch": False}))
|
|
242
|
+
return 1 if total_fail > 0 else 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: litedbmodel-runtime
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Thin multi-language runtime for litedbmodel v2 SCP §8 bundles (Python port). Interprets the published SqlBundle (sql + fragment tree + Expression-IR param slots + transaction plan, dialect-tagged) and executes it against a SQL driver — semantics-identical to the TS reference. Delegates Expression-IR evaluation to behavior-contracts.
|
|
5
|
+
Author: foo-ogawa
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: orm,sql,ir,runtime,conformance,litedbmodel
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: behavior-contracts==0.2.0
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
13
|
+
Provides-Extra: livedb
|
|
14
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "livedb"
|
|
15
|
+
Requires-Dist: pymysql>=1.1; extra == "livedb"
|
|
16
|
+
|
|
17
|
+
# litedbmodel-runtime (Python)
|
|
18
|
+
|
|
19
|
+
The Python leg of the litedbmodel v2 SCP multi-language runtime. Interprets the language-neutral
|
|
20
|
+
§8 published bundle (`SqlBundle`) and executes it against a DB-API SQL driver,
|
|
21
|
+
semantics-identical to the TS reference (`src/scp`).
|
|
22
|
+
|
|
23
|
+
**Status: WS7a scaffold.** The buildable package skeleton + the conformance runner entry point are
|
|
24
|
+
here; the runtime body (render / execute / transaction) is **WS7b**.
|
|
25
|
+
|
|
26
|
+
## behavior-contracts dependency
|
|
27
|
+
|
|
28
|
+
The runtime delegates the CLOSED Expression-IR evaluation to the shared common core
|
|
29
|
+
[`behavior-contracts`](https://pypi.org/project/behavior-contracts/) — **consumed from PyPI**
|
|
30
|
+
(`behavior-contracts==0.2.0`), exactly as the TS reference imports it from npm. No local path
|
|
31
|
+
dependency (the `check-no-local-deps` gate forbids `../`-escaping deps).
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
python/
|
|
37
|
+
pyproject.toml # PyPI package (litedbmodel-runtime), version-synced from package.json
|
|
38
|
+
litedbmodel_runtime/
|
|
39
|
+
__init__.py
|
|
40
|
+
runtime.py # WS7b: the §8 bundle interpreter surface
|
|
41
|
+
vectors_runner.py # conformance runner entry (WS7b body)
|
|
42
|
+
tests/ # WS7b runtime tests
|
|
43
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
litedbmodel_runtime/__init__.py,sha256=IfW0bup047QzMvere1M6V8vIEiA8PzFyH2R_9ZJbPYI,1828
|
|
2
|
+
litedbmodel_runtime/dialect.py,sha256=VUK4_7TlTrWtxZm365niTF_YL7ZOcYKT9fL7jUmIlAM,4007
|
|
3
|
+
litedbmodel_runtime/driver.py,sha256=RyvNkxdIz_xJFRmri4w5_87eJwXRsHr-TbfDriUEgtw,10603
|
|
4
|
+
litedbmodel_runtime/errors.py,sha256=oGHzHA19CbnmUcdHALhJMGZNLj3EJyvQuvycE5yfJIU,3434
|
|
5
|
+
litedbmodel_runtime/livedb_runner.py,sha256=j9ySGvYTrsfr_dkPnjEyCp3-XgftrrU9EzPTj8GAmOM,9133
|
|
6
|
+
litedbmodel_runtime/render.py,sha256=THiuzQq8RtgXxJOVgLO0DVC1h7ccTRzyGUf6QKDlFmU,6785
|
|
7
|
+
litedbmodel_runtime/runtime.py,sha256=9a5a0kdfeTHyovDYDRycAdTRSYjYtkBTsTem2PhPUdk,12436
|
|
8
|
+
litedbmodel_runtime/vectors_runner.py,sha256=CSLsn4SEO1v_Zt9mT2_Ez-rjAmLrXnJQPfWC-NrIVGg,10669
|
|
9
|
+
litedbmodel_runtime-2.0.0.dist-info/METADATA,sha256=lthWvixiGg745QrCEu52MP0mKwiJx8wb1hoAVvq2ihc,1999
|
|
10
|
+
litedbmodel_runtime-2.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
11
|
+
litedbmodel_runtime-2.0.0.dist-info/top_level.txt,sha256=jLV1ke1oOOfF6jNuE4ubXW5jdk2CP9lT7C5ZlC_cBbQ,20
|
|
12
|
+
litedbmodel_runtime-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
litedbmodel_runtime
|