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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ litedbmodel_runtime