litedbmodel 2.0.2 → 2.1.0

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.
Files changed (80) hide show
  1. package/dist/decorators.d.ts.map +1 -1
  2. package/dist/decorators.js +44 -24
  3. package/dist/decorators.js.map +1 -1
  4. package/dist/drivers/mysql.d.ts.map +1 -1
  5. package/dist/drivers/mysql.js +6 -0
  6. package/dist/drivers/mysql.js.map +1 -1
  7. package/dist/drivers/postgres.d.ts.map +1 -1
  8. package/dist/drivers/postgres.js +7 -0
  9. package/dist/drivers/postgres.js.map +1 -1
  10. package/dist/drivers/sqlite.d.ts +2 -0
  11. package/dist/drivers/sqlite.d.ts.map +1 -1
  12. package/dist/drivers/sqlite.js +37 -2
  13. package/dist/drivers/sqlite.js.map +1 -1
  14. package/dist/scp/authoring.d.ts +66 -3
  15. package/dist/scp/authoring.d.ts.map +1 -1
  16. package/dist/scp/authoring.js +194 -5
  17. package/dist/scp/authoring.js.map +1 -1
  18. package/dist/scp/catalog.d.ts +2 -1
  19. package/dist/scp/catalog.d.ts.map +1 -1
  20. package/dist/scp/catalog.js +12 -1
  21. package/dist/scp/catalog.js.map +1 -1
  22. package/dist/scp/codegen.d.ts +43 -31
  23. package/dist/scp/codegen.d.ts.map +1 -1
  24. package/dist/scp/codegen.js +279 -95
  25. package/dist/scp/codegen.js.map +1 -1
  26. package/dist/scp/coltype.d.ts +83 -0
  27. package/dist/scp/coltype.d.ts.map +1 -1
  28. package/dist/scp/coltype.js +220 -0
  29. package/dist/scp/coltype.js.map +1 -1
  30. package/dist/scp/guard.d.ts +1 -1
  31. package/dist/scp/guard.d.ts.map +1 -1
  32. package/dist/scp/guard.js +6 -0
  33. package/dist/scp/guard.js.map +1 -1
  34. package/dist/scp/index.cjs +7149 -9379
  35. package/dist/scp/index.d.ts +7 -6
  36. package/dist/scp/index.d.ts.map +1 -1
  37. package/dist/scp/index.js +21 -6
  38. package/dist/scp/index.js.map +1 -1
  39. package/dist/scp/index.mjs +729 -178
  40. package/dist/scp/makesql/authoring-compile.d.ts.map +1 -1
  41. package/dist/scp/makesql/authoring-compile.js +11 -0
  42. package/dist/scp/makesql/authoring-compile.js.map +1 -1
  43. package/dist/scp/makesql/index.d.ts +2 -3
  44. package/dist/scp/makesql/index.d.ts.map +1 -1
  45. package/dist/scp/makesql/index.js +6 -6
  46. package/dist/scp/makesql/index.js.map +1 -1
  47. package/dist/scp/makesql/json-batch.d.ts.map +1 -1
  48. package/dist/scp/makesql/json-batch.js +6 -1
  49. package/dist/scp/makesql/json-batch.js.map +1 -1
  50. package/dist/scp/makesql/outtype.d.ts +61 -6
  51. package/dist/scp/makesql/outtype.d.ts.map +1 -1
  52. package/dist/scp/makesql/outtype.js +164 -33
  53. package/dist/scp/makesql/outtype.js.map +1 -1
  54. package/dist/scp/makesql/pool-executor.d.ts +70 -0
  55. package/dist/scp/makesql/pool-executor.d.ts.map +1 -1
  56. package/dist/scp/makesql/pool-executor.js +73 -5
  57. package/dist/scp/makesql/pool-executor.js.map +1 -1
  58. package/dist/scp/makesql/static-bundle.d.ts +39 -30
  59. package/dist/scp/makesql/static-bundle.d.ts.map +1 -1
  60. package/dist/scp/makesql/static-bundle.js +269 -138
  61. package/dist/scp/makesql/static-bundle.js.map +1 -1
  62. package/dist/scp/makesql/writeouttype.d.ts +5 -6
  63. package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
  64. package/dist/scp/makesql/writeouttype.js +5 -6
  65. package/dist/scp/makesql/writeouttype.js.map +1 -1
  66. package/dist/scp/relation.d.ts +15 -1
  67. package/dist/scp/relation.d.ts.map +1 -1
  68. package/dist/scp/relation.js +61 -2
  69. package/dist/scp/relation.js.map +1 -1
  70. package/dist/scp/runtime.d.ts +6 -5
  71. package/dist/scp/runtime.d.ts.map +1 -1
  72. package/dist/scp/runtime.js +27 -5
  73. package/dist/scp/runtime.js.map +1 -1
  74. package/docs/BENCHMARK.md +58 -56
  75. package/docs/benchmark-chart.svg +166 -166
  76. package/package.json +8 -2
  77. package/dist/scp/makesql/ir.d.ts +0 -42
  78. package/dist/scp/makesql/ir.d.ts.map +0 -1
  79. package/dist/scp/makesql/ir.js +0 -66
  80. package/dist/scp/makesql/ir.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"static-bundle.d.ts","sourceRoot":"","sources":["../../../src/scp/makesql/static-bundle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"static-bundle.d.ts","sourceRoot":"","sources":["../../../src/scp/makesql/static-bundle.ts"],"names":[],"mappings":"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"}
@@ -51,6 +51,7 @@ const handler_1 = require("./handler");
51
51
  const tx_2 = require("./tx");
52
52
  const find_filter_guard_1 = require("../find-filter-guard");
53
53
  const outtype_1 = require("./outtype");
54
+ const coltype_1 = require("../coltype");
54
55
  const errors_1 = require("../errors");
55
56
  const DBConditions_1 = require("../../DBConditions");
56
57
  const DBValues_1 = require("../../DBValues");
@@ -61,6 +62,10 @@ const authoring_sql_1 = require("../authoring-sql");
61
62
  function isMap(n) {
62
63
  return 'map' in n;
63
64
  }
65
+ /** A bc 0.7.3+ `FanoutNode` (connection fan-out). litedbmodel never emits these. */
66
+ function isFanout(n) {
67
+ return 'fanout' in n;
68
+ }
64
69
  function nodeRef(n) {
65
70
  return isMap(n) ? { component: n.map.component, ports: n.map.ports } : { component: n.component, ports: n.ports };
66
71
  }
@@ -578,6 +583,11 @@ function compileStaticBundle(contract, dialect, entry) {
578
583
  for (const n of component.body) {
579
584
  if ('cond' in n)
580
585
  continue;
586
+ if (isFanout(n)) {
587
+ // bc 0.7.3+ `FanoutNode`. litedbmodel never emits fanout — reject fail-closed rather
588
+ // than mistake it for the primary catalog `componentRef`.
589
+ throw new Error(`static-bundle: behavior '${component.name}' node '${n.id}' is a fanout node, not supported by litedbmodel (bc FanoutNode)`);
590
+ }
581
591
  if (isMap(n))
582
592
  continue;
583
593
  if (primary === undefined)
@@ -642,6 +652,8 @@ function collectRefOptHeads(node, into) {
642
652
  function skipGuardHeads(n, into) {
643
653
  if ('cond' in n)
644
654
  return;
655
+ if (isFanout(n))
656
+ return; // bc FanoutNode carries no where-ports SKIP guard (litedbmodel never emits it)
645
657
  const ports = isMap(n) ? n.map.ports : n.ports;
646
658
  const where = ports.where;
647
659
  if (typeof where !== 'object' || where === null || !('arr' in where))
@@ -845,62 +857,14 @@ function executeStaticWrite(bundle, input, db) {
845
857
  }
846
858
  }
847
859
  // ============================================================================
848
- // Step 0 — the bc-runBehavior read executor: bc composes (map / Φ-merge / wiring),
849
- // makeSQL executes. A read behavior method whose output is a Φ-merge of SQL nodes
850
- // (`{obj:{posts:ref[n0], authors:map(n0)}}`) is executed by rewriting each SQL node
851
- // to a `makeSQL` node carrying a synthetic `__scope` port and running bc `runBehavior`
852
- // with a handler that renders that node's static statements against the evaluated scope.
853
- // This gives map iteration + Φ output + wiring FOR FREE (bc owns orchestration).
860
+ // #12 — the NATIVE read-graph walker (interpreter-free): litedbmodel composes map /
861
+ // Φ-merge / wiring ITSELF over `compileBehaviors`' REAL `Select`/`Count`/map node IR
862
+ // (no `__makeSqlNode`/`__scope` surrogate, no bc `runBehavior`). Each body node's SQL
863
+ // comes from its `statementsById[id]` fragment templates (rendered against the walk
864
+ // scope); topology (`map.over`/`as`, `plan.groups`, `output` Φ) is read off the real
865
+ // nodes. This mirrors the rust/go/py/php native walkers — one shared orchestration model
866
+ // across all 5 runtimes, `runBehavior` invoked on NONE of them.
854
867
  // ============================================================================
855
- /** The synthetic port that carries a SQL node's render scope (bc evaluates it in-scope). */
856
- const SCOPE_PORT = '__scope';
857
- /** The makeSQL catalog leaf name every rewritten SQL node references. */
858
- const NODE_COMPONENT = '__makeSqlNode';
859
- /** Collect every ref/refOpt path HEAD used inside an Expression IR node. */
860
- function collectAllHeads(node, heads) {
861
- if (node === null || typeof node !== 'object')
862
- return;
863
- if (Array.isArray(node)) {
864
- for (const el of node)
865
- collectAllHeads(el, heads);
866
- return;
867
- }
868
- const keys = Object.keys(node);
869
- if (keys.length === 1 && (keys[0] === 'ref' || keys[0] === 'refOpt')) {
870
- const path = node[keys[0]];
871
- if (Array.isArray(path) && typeof path[0] === 'string')
872
- heads.add(path[0]);
873
- return;
874
- }
875
- for (const k of keys)
876
- collectAllHeads(node[k], heads);
877
- }
878
- /** Every binding head a node's compiled statements reference (params + skip). */
879
- function statementHeads(statements) {
880
- const heads = new Set();
881
- for (const stmt of statements) {
882
- for (const p of stmt.params)
883
- collectAllHeads(p, heads);
884
- if (stmt.skip !== undefined)
885
- collectAllHeads(stmt.skip, heads);
886
- }
887
- return heads;
888
- }
889
- /**
890
- * Build the surrogate `__scope` port for a node: a bc `{obj:{…}}` re-exporting each referenced
891
- * head as `{ref:[head]}` (excluding the IN-sentinel column marker). bc evaluates this in ITS
892
- * scope (so `input`, sibling wire results and the `map` element `as` all resolve), then the
893
- * handler renders the node's static statements against the resulting plain scope.
894
- */
895
- function scopePort(statements) {
896
- const obj = {};
897
- for (const head of statementHeads(statements)) {
898
- if (head === tx_1.IN_SENTINEL)
899
- continue;
900
- obj[head] = { ref: [head] };
901
- }
902
- return { obj };
903
- }
904
868
  /** Compile ONE authored SQL node (Select or CRUD write) into its static statements. */
905
869
  function compileNodeStatements(node, dialect) {
906
870
  const component = 'map' in node ? node.map.component : node.component;
@@ -910,10 +874,12 @@ function compileNodeStatements(node, dialect) {
910
874
  return [{ sql: op.sql, params: op.params }];
911
875
  }
912
876
  /**
913
- * Compile a READ behavior method into a portable {@link ReadGraph}: rewrite each authored SQL
914
- * body node to a `makeSQL` node carrying a synthetic `__scope` port (bc evaluates it in-scope),
915
- * and compile each node's static statements ONCE. SYMBOLIC — no concrete input. bc will own map
916
- * iteration / wire binding / Φ output at execute time; the handler renders each node's statements.
877
+ * Compile a READ behavior method into a portable {@link ReadGraph} (#12 de-surrogated): keep
878
+ * `compileBehaviors`' REAL `Select`/`Count`/map body nodes (only additively annotate each with its
879
+ * read `outType`), and compile each node's static `makeSQL` statements ONCE into the `statementsById`
880
+ * sidecar (keyed by the real body-node id). SYMBOLIC — no concrete input. The native walker
881
+ * ({@link executeReadGraph}) owns map iteration / wire binding / Φ output at execute time, rendering
882
+ * each node's statements against the walk scope — never bc `runBehavior`, never a `__scope` surrogate.
917
883
  */
918
884
  function compileReadGraph(contract, dialect, entry, findFilterModel, resolveColumnType) {
919
885
  const component = entry ? contract.components.find((c) => c.name === entry) : contract.components[0];
@@ -929,6 +895,11 @@ function compileReadGraph(contract, dialect, entry, findFilterModel, resolveColu
929
895
  for (const n of component.body) {
930
896
  if ('cond' in n)
931
897
  continue;
898
+ if (isFanout(n)) {
899
+ // bc 0.7.3+ `FanoutNode`. litedbmodel never emits fanout — reject fail-closed rather
900
+ // than mis-read it as a Select/Count ref and skip the FIND_FILTER fold check.
901
+ throw new Error(`static-bundle: read component '${component.name}' node '${n.id}' is a fanout node, not supported by litedbmodel (bc FanoutNode)`);
902
+ }
932
903
  const ref = 'map' in n ? n.map : n;
933
904
  if (ref.component !== 'Select' && ref.component !== 'Count')
934
905
  continue;
@@ -939,115 +910,273 @@ function compileReadGraph(contract, dialect, entry, findFilterModel, resolveColu
939
910
  for (const n of component.body) {
940
911
  if ('cond' in n)
941
912
  continue;
913
+ if (isFanout(n)) {
914
+ // bc 0.7.3+ `FanoutNode`. litedbmodel never emits fanout — reject fail-closed rather
915
+ // than feed it to `compileNodeStatements` as a catalog ref.
916
+ throw new Error(`static-bundle: read component '${component.name}' node '${n.id}' is a fanout node, not supported by litedbmodel (bc FanoutNode)`);
917
+ }
942
918
  statementsById[n.id] = compileNodeStatements(n, dialect);
943
919
  }
944
- // Typed-codegen `outType`/`outputType` annotations (spec §4.1; #58/#56). Derived from the ORIGINAL
945
- // body (real `table`/`select` ports) so bc's typed-raw de-box emitters can materialize concrete
946
- // row structs instead of hard-failing ("nothing to de-box"). Only when a column-type resolver (the
947
- // schema/DDL SoT) is supplied — without it the read graph stays un-annotated (interpret path only;
948
- // typed codegen then fail-closes downstream, never silently boxes). Any ambiguity throws here.
949
- const outTypes = resolveColumnType !== undefined ? (0, outtype_1.deriveReadOutTypes)(component, resolveColumnType) : undefined;
950
- const surrogateBody = component.body.map((n) => {
920
+ // SINGLE column-type resolution (issue #59 audit — unified read+codegen): `deriveReadOutTypes`
921
+ // resolves every projected column of the read ONCE (via the SHARED projection parser, all shapes:
922
+ // bare / qualified / aliased; `*` / computed / undeclared → HARD ERROR, spec §4.1) and returns BOTH
923
+ // the codegen `outType`/`outputType` annotations AND the TS read-path `materializersByNode` — two
924
+ // projections of the ONE resolution, so they cannot diverge (there is no second, weaker read-path
925
+ // pass). The column types come from the model's inline `static columns` (carried on the contract as
926
+ // `resolveColumnType`, always present for a read model), so this runs for EVERY read: codegen writes
927
+ // the outType IR annotations, the runtime read path consumes the materializers. Any ambiguity /
928
+ // undeclared column / `*` / computed projection throws here.
929
+ const readTypes = resolveColumnType !== undefined ? (0, outtype_1.deriveReadOutTypes)(component, resolveColumnType) : undefined;
930
+ const materializers = readTypes?.materializersByNode;
931
+ // #12 (de-surrogation): the read-graph IR is `compileBehaviors`' REAL component — the authored
932
+ // `Select`/`Count`/map nodes with their own `table`/`select`/`where`/… ports, NOT a hand-built
933
+ // `__makeSqlNode` + `__scope` surrogate. litedbmodel constructs NO `ComponentGraphIR`/`BodyNode`
934
+ // literal here: it only ADDITIVELY annotates each node with its read `outType` (issue #59) and the
935
+ // component with its `outputType` — the node SHAPE/ports are `compileBehaviors`' output untouched.
936
+ // The compiled SQL fragment templates ride the sidecar `statementsById` (opaque, v1-`DBConditions`-
937
+ // sourced TEXT), keyed by the SAME real body-node id; every language runtime walks this real-node
938
+ // IR natively (topology from the real nodes; SQL from `statementsById[id]`), never bc `runBehavior`.
939
+ const annotatedBody = component.body.map((n) => {
951
940
  if ('cond' in n)
952
941
  return n;
953
- const stmts = statementsById[n.id];
954
- const outType = outTypes?.byNode.get(n.id);
955
- const outAnn = outType !== undefined ? { outType } : {};
956
- if ('map' in n) {
957
- return { ...n, ...outAnn, map: { ...n.map, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } } };
958
- }
959
- return { ...n, ...outAnn, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } };
942
+ const outType = readTypes?.byNode.get(n.id);
943
+ return outType !== undefined ? { ...n, outType } : n;
960
944
  });
961
- const surrogate = {
945
+ const irComponent = {
962
946
  ...component,
963
- body: surrogateBody,
964
- ...(outTypes !== undefined ? { outputType: outTypes.outputType } : {}),
947
+ body: annotatedBody,
948
+ ...(readTypes !== undefined ? { outputType: readTypes.outputType } : {}),
965
949
  };
966
- const ir = { irVersion: 1, exprVersion: 2, components: [surrogate] };
950
+ // Preserve `compileBehaviors`' OWN IR envelope (irVersion/exprVersion + any other metadata) — the
951
+ // read graph IR is the compiler's output with ONLY the single entry component swapped for its
952
+ // outType-annotated twin (additive). litedbmodel fabricates no `ComponentGraphIR`/`BodyNode`.
953
+ const ir = { ...contract.ir, components: [irComponent] };
967
954
  return {
968
955
  dialect,
969
956
  name: component.name,
970
957
  ir,
971
958
  statementsById,
972
959
  optionalHeads: [...optionalHeadsOfComponent(component, dialect)],
960
+ ...(materializers !== undefined && materializers.size > 0 ? { materializersByNode: Object.fromEntries(materializers) } : {}),
973
961
  };
974
962
  }
975
963
  /**
976
- * Execute a compiled {@link ReadGraph} via bc `runBehavior` + a makeSQL handler: bc drives map
977
- * iteration / wire binding / Φ output; the handler renders each node's static statements against
978
- * the evaluated `__scope` and runs REAL SQLite. Returns the component's Φ output. This is the
979
- * design's "bc composes, makeSQL executes" — the SAME path a per-language runtime follows.
964
+ * Apply a node's per-column materializers (issue #59) to its raw driver rows: coerce each cell to
965
+ * the JS form its SQL column type declares (BIGINT→bigint, INT→number, DATE→string, BOOLEAN→
966
+ * boolean). A no-op when the node has no materializer map (read compiled without a column-type
967
+ * resolver) or a column has no entry (defensive — only projected columns are typed). Mutates + returns
968
+ * the same row objects (they are freshly produced by the driver, so mutation is safe).
980
969
  */
981
- function executeReadGraph(graph, input, db) {
982
- const handle = (ports, ctx) => {
983
- const stmts = graph.statementsById[ctx.nodeId];
984
- if (stmts === undefined)
985
- return { error: `static-bundle: no statements for node '${ctx.nodeId}'` };
986
- const scope = ports[SCOPE_PORT];
987
- if (scope === null || typeof scope !== 'object' || Array.isArray(scope)) {
988
- return { error: `static-bundle: node '${ctx.nodeId}' surrogate scope did not evaluate to an object` };
970
+ function materializeRows(rows, cols, safeIntegersOn = false) {
971
+ if (cols === undefined && !safeIntegersOn)
972
+ return rows;
973
+ for (const row of rows) {
974
+ for (const key of Object.keys(row)) {
975
+ const klass = cols?.[key];
976
+ if (klass !== undefined) {
977
+ row[key] = (0, coltype_1.materializeCell)(row[key], klass);
978
+ continue;
979
+ }
980
+ // Defensive (SQLite safeIntegers mode): when the statement was put in safe-integer mode for a
981
+ // BIGINT column, EVERY int column arrives as a bigint — including columns the resolver didn't
982
+ // type. Narrow a stray bigint back to a number when it fits safely, else to an exact string
983
+ // (so a value never leaks out as a raw bigint, which is not JSON-safe).
984
+ if (safeIntegersOn && typeof row[key] === 'bigint') {
985
+ const v = row[key];
986
+ row[key] = v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v.toString();
987
+ }
989
988
  }
990
- const { sql, params } = renderStatements(stmts, graph.dialect, scope);
991
- try {
992
- const rows = db.prepare(sql).all(...params);
993
- return { ok: rows };
989
+ }
990
+ return rows;
991
+ }
992
+ /** True iff any projected column of this node is a 64-bit int (needs the driver in exact mode). */
993
+ function hasInt64(cols) {
994
+ for (const k of Object.keys(cols))
995
+ if (cols[k] === 'int64')
996
+ return true;
997
+ return false;
998
+ }
999
+ /** The plan stages (`groups`) as body-index lists, or one-node-per-stage in declaration order. */
1000
+ function planStages(component) {
1001
+ const plan = component.plan;
1002
+ const groups = plan?.groups;
1003
+ if (Array.isArray(groups)) {
1004
+ return groups.map((st) => (Array.isArray(st) ? st.filter((i) => typeof i === 'number') : []));
1005
+ }
1006
+ return component.body.map((_n, i) => [i]);
1007
+ }
1008
+ /** The plan's `concurrency` bound (default 1 — serial when absent/≤1), for the async fan-out. */
1009
+ function planConcurrency(component) {
1010
+ const plan = component.plan;
1011
+ const c = plan?.concurrency;
1012
+ return typeof c === 'number' && c >= 1 ? c : 1;
1013
+ }
1014
+ /**
1015
+ * Bounded-parallel map preserving INPUT ORDER in the result. At most `limit` tasks run at once
1016
+ * (`limit <= 1` ⇒ strictly serial). Deterministic: the output array is indexed by input position,
1017
+ * regardless of completion order — mirrors bc `runPlanAsync`'s in-order commit, so the async walker's
1018
+ * assembled result equals the serial walker's byte-for-byte.
1019
+ */
1020
+ async function boundedMap(items, limit, fn) {
1021
+ const out = new Array(items.length);
1022
+ const cap = Math.max(1, Math.floor(limit));
1023
+ let next = 0;
1024
+ async function worker() {
1025
+ for (;;) {
1026
+ const i = next++;
1027
+ if (i >= items.length)
1028
+ return;
1029
+ out[i] = await fn(items[i], i);
994
1030
  }
995
- catch (e) {
996
- return { error: (0, errors_1.mapSqliteError)(e).message };
1031
+ }
1032
+ await Promise.all(Array.from({ length: Math.min(cap, items.length) }, () => worker()));
1033
+ return out;
1034
+ }
1035
+ /** Render + execute a node's `statementsById` fragments against `scope`, sync driver, with #59 de-box. */
1036
+ function execReadNodeSync(graph, nodeId, scope, db) {
1037
+ const stmts = graph.statementsById[nodeId];
1038
+ if (stmts === undefined)
1039
+ throw new Error(`static-bundle: no statements for node '${nodeId}'`);
1040
+ const { sql, params } = renderStatements(stmts, graph.dialect, scope);
1041
+ const cols = graph.materializersByNode?.[nodeId];
1042
+ try {
1043
+ const stmt = db.prepare(sql);
1044
+ // A BIGINT-projecting node runs in SQLite safe-integer mode so int8 comes back as an EXACT
1045
+ // bigint (a JS number would round it — the #59 hole); `materializeRows` then narrows/stringifies.
1046
+ let safeIntegersOn = false;
1047
+ if (cols !== undefined && hasInt64(cols) && typeof stmt.safeIntegers === 'function') {
1048
+ stmt.safeIntegers(true);
1049
+ safeIntegersOn = true;
997
1050
  }
998
- };
999
- const handlers = { [NODE_COMPONENT]: handle };
1000
- const normalized = { ...input };
1051
+ const rows = stmt.all(...params);
1052
+ return materializeRows(rows, cols, safeIntegersOn);
1053
+ }
1054
+ catch (e) {
1055
+ throw (0, errors_1.mapSqliteError)(e);
1056
+ }
1057
+ }
1058
+ /** Compute ONE read-graph body node's value against `base`: a `cond` join, a `map` (per-element
1059
+ * render+execute under the `as` binding), or a componentRef (render+execute). Sync driver. */
1060
+ function computeReadNodeSync(graph, node, base, db) {
1061
+ if (node.cond !== undefined)
1062
+ return (0, behavior_contracts_1.evaluateExpression)(node.cond, base);
1063
+ if (node.map !== undefined) {
1064
+ const over = (0, behavior_contracts_1.evaluateExpression)(node.map.over, base);
1065
+ if (!Array.isArray(over))
1066
+ throw new Error(`static-bundle: map '${node.id}': 'over' is not an array`);
1067
+ const asName = node.map.as ?? '$';
1068
+ const out = [];
1069
+ for (const el of over) {
1070
+ out.push(execReadNodeSync(graph, node.id, { ...base, [asName]: el }, db));
1071
+ }
1072
+ return out;
1073
+ }
1074
+ return execReadNodeSync(graph, node.id, base, db);
1075
+ }
1076
+ /** Normalize omitted OPTIONAL heads to present-as-null (absent-key SKIP; SSoT-driven). */
1077
+ function normalizeReadInput(graph, input) {
1078
+ const out = { ...input };
1001
1079
  for (const head of graph.optionalHeads)
1002
- if (!(head in normalized))
1003
- normalized[head] = null;
1080
+ if (!(head in out))
1081
+ out[head] = null;
1082
+ return out;
1083
+ }
1084
+ /**
1085
+ * Execute a compiled {@link ReadGraph} via the NATIVE walker (#12): walk `compileBehaviors`' REAL
1086
+ * `Select`/`Count`/map node IR in `plan.groups` stage order — computing each node's rows from its
1087
+ * `statementsById` fragments (rendered against the walk scope) and committing `(nodeId → value)` —
1088
+ * then evaluate the component's `output` Φ expression. NO bc `runBehavior`, NO `__makeSqlNode`
1089
+ * surrogate; litedbmodel owns map iteration / wire binding / Φ output. This is the SAME native
1090
+ * orchestration model the rust/go/py/php runtimes follow — one shape, all 5 languages, interpreter-free.
1091
+ */
1092
+ function executeReadGraph(graph, input, db) {
1093
+ const component = graph.ir.components[0];
1094
+ const body = component.body;
1095
+ const output = component.output ?? null;
1096
+ const normalized = normalizeReadInput(graph, input);
1004
1097
  try {
1005
- return (0, behavior_contracts_1.runBehavior)(graph.ir, handlers, normalized, graph.name);
1098
+ const results = [];
1099
+ for (const stage of planStages(component)) {
1100
+ // Commit stage members in ascending body index (deterministic failure precedence).
1101
+ const ordered = [...stage].sort((a, b) => a - b);
1102
+ const base = { ...normalized };
1103
+ for (const [id, v] of results)
1104
+ base[id] = v;
1105
+ for (const idx of ordered) {
1106
+ const node = body[idx];
1107
+ base[node.id] = results[results.push([node.id, computeReadNodeSync(graph, node, base, db)]) - 1][1];
1108
+ }
1109
+ }
1110
+ const scope = { ...normalized };
1111
+ for (const [id, v] of results)
1112
+ scope[id] = v;
1113
+ return (0, behavior_contracts_1.evaluateExpression)(output, scope);
1006
1114
  }
1007
1115
  catch (e) {
1008
1116
  throw reErrorToSqlFailure(e);
1009
1117
  }
1010
1118
  }
1119
+ /** Render + execute a node's `statementsById` fragments against `scope`, async driver, with #59 de-box. */
1120
+ async function execReadNodeAsync(graph, nodeId, scope, exec) {
1121
+ const stmts = graph.statementsById[nodeId];
1122
+ if (stmts === undefined)
1123
+ throw new Error(`static-bundle: no statements for node '${nodeId}'`);
1124
+ const { sql, params } = renderStatements(stmts, graph.dialect, scope);
1125
+ try {
1126
+ const rows = await exec(sql, params);
1127
+ // #59 materializers: the pooled PG/MySQL drivers are configured so int8/BIGINT arrive as a string
1128
+ // (→ bigint) and BOOLEAN as a JS boolean; `materializeCell` coerces each to the SQL-type JS form.
1129
+ return materializeRows(rows, graph.materializersByNode?.[nodeId]);
1130
+ }
1131
+ catch (e) {
1132
+ throw (0, errors_1.mapSqliteError)(e);
1133
+ }
1134
+ }
1135
+ /** Async twin of {@link computeReadNodeSync}: dispatches a map stage's per-element queries in bounded
1136
+ * parallel (the live PG/MySQL fan-out, capped at `concurrency`); a componentRef runs one query; a
1137
+ * `cond` is pure. */
1138
+ async function computeReadNodeAsync(graph, node, base, exec, concurrency) {
1139
+ if (node.cond !== undefined)
1140
+ return (0, behavior_contracts_1.evaluateExpression)(node.cond, base);
1141
+ if (node.map !== undefined) {
1142
+ const over = (0, behavior_contracts_1.evaluateExpression)(node.map.over, base);
1143
+ if (!Array.isArray(over))
1144
+ throw new Error(`static-bundle: map '${node.id}': 'over' is not an array`);
1145
+ const asName = node.map.as ?? '$';
1146
+ return (await boundedMap(over, concurrency, (el) => execReadNodeAsync(graph, node.id, { ...base, [asName]: el }, exec)));
1147
+ }
1148
+ return execReadNodeAsync(graph, node.id, base, exec);
1149
+ }
1011
1150
  /**
1012
- * Async twin of {@link executeReadGraph} — the PG / MySQL execution model (#40).
1013
- *
1014
- * Byte-identical composition (SAME IR, SAME per-node SQL text + params, SAME Φ output), but bc's
1015
- * `runBehaviorAsync` drives it: the makeSQL handler returns a Promise, and bc dispatches the
1016
- * INDEPENDENT sibling nodes of a plan stage on `runPlanAsync`'s bounded-parallel path (bc#23),
1017
- * bounded by the plan's `concurrency` (default 16). Against a pooled async driver (`pg` / `mysql2`,
1018
- * each `exec` call resolving on its own pooled connection), that becomes REAL parallel read-relation
1019
- * DB I/O — the plan's declared concurrency finally cashes out as concurrent dispatch. The result is
1020
- * deterministic (bc commits stage outcomes in declaration order), so it equals the serial
1021
- * `executeReadGraph` output exactly; only the wall-clock changes.
1022
- *
1023
- * The conformance bar stays on the SYNC in-proc better-sqlite3 path (`executeReadGraph`); this async
1024
- * path is the live PG/MySQL execution model and is proven by the latency-injecting concurrency test.
1151
+ * Async twin of {@link executeReadGraph} — the live PG / MySQL execution model (#40). Byte-identical
1152
+ * composition (SAME real-node IR, SAME per-node SQL text + params, SAME Φ output); within a plan stage
1153
+ * the INDEPENDENT sibling nodes are dispatched concurrently against the pooled async executor (each
1154
+ * `exec` resolving on its own pooled connection), and a map node's per-element queries fan out in
1155
+ * bounded parallel. The result is deterministic (stage outcomes commit in declaration order), so it
1156
+ * equals the serial `executeReadGraph` output exactly; only the wall-clock changes.
1025
1157
  */
1026
1158
  async function executeReadGraphAsync(graph, input, exec) {
1027
- const handle = async (ports, ctx) => {
1028
- const stmts = graph.statementsById[ctx.nodeId];
1029
- if (stmts === undefined)
1030
- return { error: `static-bundle: no statements for node '${ctx.nodeId}'` };
1031
- const scope = ports[SCOPE_PORT];
1032
- if (scope === null || typeof scope !== 'object' || Array.isArray(scope)) {
1033
- return { error: `static-bundle: node '${ctx.nodeId}' surrogate scope did not evaluate to an object` };
1034
- }
1035
- const { sql, params } = renderStatements(stmts, graph.dialect, scope);
1036
- try {
1037
- const rows = await exec(sql, params);
1038
- return { ok: rows };
1039
- }
1040
- catch (e) {
1041
- return { error: (0, errors_1.mapSqliteError)(e).message };
1042
- }
1043
- };
1044
- const handlers = { [NODE_COMPONENT]: handle };
1045
- const normalized = { ...input };
1046
- for (const head of graph.optionalHeads)
1047
- if (!(head in normalized))
1048
- normalized[head] = null;
1159
+ const component = graph.ir.components[0];
1160
+ const body = component.body;
1161
+ const output = component.output ?? null;
1162
+ const concurrency = planConcurrency(component);
1163
+ const normalized = normalizeReadInput(graph, input);
1049
1164
  try {
1050
- return await (0, behavior_contracts_1.runBehaviorAsync)(graph.ir, handlers, normalized, graph.name);
1165
+ const results = [];
1166
+ for (const stage of planStages(component)) {
1167
+ const ordered = [...stage].sort((a, b) => a - b);
1168
+ const base = { ...normalized };
1169
+ for (const [id, v] of results)
1170
+ base[id] = v;
1171
+ // Independent siblings of a stage run in bounded parallel (capped at the plan concurrency;
1172
+ // concurrency ≤ 1 ⇒ serial); committed in ascending body index order for determinism.
1173
+ const vals = await boundedMap(ordered, concurrency, (idx) => computeReadNodeAsync(graph, body[idx], base, exec, concurrency));
1174
+ ordered.forEach((idx, i) => results.push([body[idx].id, vals[i]]));
1175
+ }
1176
+ const scope = { ...normalized };
1177
+ for (const [id, v] of results)
1178
+ scope[id] = v;
1179
+ return (0, behavior_contracts_1.evaluateExpression)(output, scope);
1051
1180
  }
1052
1181
  catch (e) {
1053
1182
  throw reErrorToSqlFailure(e);
@@ -1069,7 +1198,7 @@ function executeReadBehavior(contract, input, dialect, db, entry) {
1069
1198
  */
1070
1199
  function renderReadPrimary(graph, input) {
1071
1200
  const ids = Object.keys(graph.statementsById);
1072
- // The primary node is the first body node in the surrogate IR order (map nodes reference it).
1201
+ // The primary node is the first body node in the real-node IR order (map nodes reference it).
1073
1202
  const bodyIds = graph.ir.components[0].body.map((n) => n.id).filter((id) => ids.includes(id));
1074
1203
  const primaryId = bodyIds[0];
1075
1204
  if (primaryId === undefined)
@@ -1092,6 +1221,8 @@ function optionalHeadsOfComponent(component, dialect) {
1092
1221
  for (const n of component.body) {
1093
1222
  if ('cond' in n)
1094
1223
  continue;
1224
+ if (isFanout(n))
1225
+ continue; // bc FanoutNode: litedbmodel never emits it; carries no ref-opt heads.
1095
1226
  const stmts = compileNodeStatements(n, dialect);
1096
1227
  for (const stmt of stmts) {
1097
1228
  for (const p of stmt.params)