litedbmodel 1.2.10 → 2.0.1

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 (143) hide show
  1. package/README.md +1 -1
  2. package/dist/DBModel.d.ts.map +1 -1
  3. package/dist/DBModel.js +6 -0
  4. package/dist/DBModel.js.map +1 -1
  5. package/dist/scp/authoring-sql.d.ts +134 -0
  6. package/dist/scp/authoring-sql.d.ts.map +1 -0
  7. package/dist/scp/authoring-sql.js +191 -0
  8. package/dist/scp/authoring-sql.js.map +1 -0
  9. package/dist/scp/authoring.d.ts +155 -0
  10. package/dist/scp/authoring.d.ts.map +1 -0
  11. package/dist/scp/authoring.js +162 -0
  12. package/dist/scp/authoring.js.map +1 -0
  13. package/dist/scp/catalog.d.ts +91 -0
  14. package/dist/scp/catalog.d.ts.map +1 -0
  15. package/dist/scp/catalog.js +230 -0
  16. package/dist/scp/catalog.js.map +1 -0
  17. package/dist/scp/codegen.d.ts +95 -0
  18. package/dist/scp/codegen.d.ts.map +1 -0
  19. package/dist/scp/codegen.js +94 -0
  20. package/dist/scp/codegen.js.map +1 -0
  21. package/dist/scp/dialect.d.ts +141 -0
  22. package/dist/scp/dialect.d.ts.map +1 -0
  23. package/dist/scp/dialect.js +194 -0
  24. package/dist/scp/dialect.js.map +1 -0
  25. package/dist/scp/errors.d.ts +36 -0
  26. package/dist/scp/errors.d.ts.map +1 -0
  27. package/dist/scp/errors.js +72 -0
  28. package/dist/scp/errors.js.map +1 -0
  29. package/dist/scp/find-filter-guard.d.ts +68 -0
  30. package/dist/scp/find-filter-guard.d.ts.map +1 -0
  31. package/dist/scp/find-filter-guard.js +88 -0
  32. package/dist/scp/find-filter-guard.js.map +1 -0
  33. package/dist/scp/fragment/assemble.d.ts +61 -0
  34. package/dist/scp/fragment/assemble.d.ts.map +1 -0
  35. package/dist/scp/fragment/assemble.js +148 -0
  36. package/dist/scp/fragment/assemble.js.map +1 -0
  37. package/dist/scp/fragment/compile-pg.d.ts +107 -0
  38. package/dist/scp/fragment/compile-pg.d.ts.map +1 -0
  39. package/dist/scp/fragment/compile-pg.js +142 -0
  40. package/dist/scp/fragment/compile-pg.js.map +1 -0
  41. package/dist/scp/fragment/index.d.ts +16 -0
  42. package/dist/scp/fragment/index.d.ts.map +1 -0
  43. package/dist/scp/fragment/index.js +34 -0
  44. package/dist/scp/fragment/index.js.map +1 -0
  45. package/dist/scp/fragment/model.d.ts +119 -0
  46. package/dist/scp/fragment/model.d.ts.map +1 -0
  47. package/dist/scp/fragment/model.js +66 -0
  48. package/dist/scp/fragment/model.js.map +1 -0
  49. package/dist/scp/fragment/pg-array-type.d.ts +13 -0
  50. package/dist/scp/fragment/pg-array-type.d.ts.map +1 -0
  51. package/dist/scp/fragment/pg-array-type.js +34 -0
  52. package/dist/scp/fragment/pg-array-type.js.map +1 -0
  53. package/dist/scp/guard.d.ts +43 -0
  54. package/dist/scp/guard.d.ts.map +1 -0
  55. package/dist/scp/guard.js +134 -0
  56. package/dist/scp/guard.js.map +1 -0
  57. package/dist/scp/index.cjs +12202 -0
  58. package/dist/scp/index.d.ts +64 -0
  59. package/dist/scp/index.d.ts.map +1 -0
  60. package/dist/scp/index.js +238 -0
  61. package/dist/scp/index.js.map +1 -0
  62. package/dist/scp/index.mjs +4819 -0
  63. package/dist/scp/makesql/authoring-compile.d.ts +90 -0
  64. package/dist/scp/makesql/authoring-compile.d.ts.map +1 -0
  65. package/dist/scp/makesql/authoring-compile.js +494 -0
  66. package/dist/scp/makesql/authoring-compile.js.map +1 -0
  67. package/dist/scp/makesql/compile-crud.d.ts +133 -0
  68. package/dist/scp/makesql/compile-crud.d.ts.map +1 -0
  69. package/dist/scp/makesql/compile-crud.js +288 -0
  70. package/dist/scp/makesql/compile-crud.js.map +1 -0
  71. package/dist/scp/makesql/compile-relation.d.ts +157 -0
  72. package/dist/scp/makesql/compile-relation.d.ts.map +1 -0
  73. package/dist/scp/makesql/compile-relation.js +425 -0
  74. package/dist/scp/makesql/compile-relation.js.map +1 -0
  75. package/dist/scp/makesql/compile-select.d.ts +45 -0
  76. package/dist/scp/makesql/compile-select.d.ts.map +1 -0
  77. package/dist/scp/makesql/compile-select.js +58 -0
  78. package/dist/scp/makesql/compile-select.js.map +1 -0
  79. package/dist/scp/makesql/compile.d.ts +63 -0
  80. package/dist/scp/makesql/compile.d.ts.map +1 -0
  81. package/dist/scp/makesql/compile.js +99 -0
  82. package/dist/scp/makesql/compile.js.map +1 -0
  83. package/dist/scp/makesql/handler.d.ts +58 -0
  84. package/dist/scp/makesql/handler.d.ts.map +1 -0
  85. package/dist/scp/makesql/handler.js +113 -0
  86. package/dist/scp/makesql/handler.js.map +1 -0
  87. package/dist/scp/makesql/index.d.ts +33 -0
  88. package/dist/scp/makesql/index.d.ts.map +1 -0
  89. package/dist/scp/makesql/index.js +114 -0
  90. package/dist/scp/makesql/index.js.map +1 -0
  91. package/dist/scp/makesql/ir.d.ts +42 -0
  92. package/dist/scp/makesql/ir.d.ts.map +1 -0
  93. package/dist/scp/makesql/ir.js +66 -0
  94. package/dist/scp/makesql/ir.js.map +1 -0
  95. package/dist/scp/makesql/json-array.d.ts +84 -0
  96. package/dist/scp/makesql/json-array.d.ts.map +1 -0
  97. package/dist/scp/makesql/json-array.js +118 -0
  98. package/dist/scp/makesql/json-array.js.map +1 -0
  99. package/dist/scp/makesql/json-batch.d.ts +102 -0
  100. package/dist/scp/makesql/json-batch.d.ts.map +1 -0
  101. package/dist/scp/makesql/json-batch.js +269 -0
  102. package/dist/scp/makesql/json-batch.js.map +1 -0
  103. package/dist/scp/makesql/makesql.d.ts +99 -0
  104. package/dist/scp/makesql/makesql.d.ts.map +1 -0
  105. package/dist/scp/makesql/makesql.js +122 -0
  106. package/dist/scp/makesql/makesql.js.map +1 -0
  107. package/dist/scp/makesql/pool-executor.d.ts +47 -0
  108. package/dist/scp/makesql/pool-executor.d.ts.map +1 -0
  109. package/dist/scp/makesql/pool-executor.js +50 -0
  110. package/dist/scp/makesql/pool-executor.js.map +1 -0
  111. package/dist/scp/makesql/static-bundle.d.ts +185 -0
  112. package/dist/scp/makesql/static-bundle.d.ts.map +1 -0
  113. package/dist/scp/makesql/static-bundle.js +1024 -0
  114. package/dist/scp/makesql/static-bundle.js.map +1 -0
  115. package/dist/scp/makesql/tx.d.ts +216 -0
  116. package/dist/scp/makesql/tx.d.ts.map +1 -0
  117. package/dist/scp/makesql/tx.js +743 -0
  118. package/dist/scp/makesql/tx.js.map +1 -0
  119. package/dist/scp/relation.d.ts +148 -0
  120. package/dist/scp/relation.d.ts.map +1 -0
  121. package/dist/scp/relation.js +257 -0
  122. package/dist/scp/relation.js.map +1 -0
  123. package/dist/scp/runtime.d.ts +231 -0
  124. package/dist/scp/runtime.d.ts.map +1 -0
  125. package/dist/scp/runtime.js +381 -0
  126. package/dist/scp/runtime.js.map +1 -0
  127. package/dist/scp/typed-object.d.ts +99 -0
  128. package/dist/scp/typed-object.d.ts.map +1 -0
  129. package/dist/scp/typed-object.js +184 -0
  130. package/dist/scp/typed-object.js.map +1 -0
  131. package/dist/scp/writes.d.ts +251 -0
  132. package/dist/scp/writes.d.ts.map +1 -0
  133. package/dist/scp/writes.js +176 -0
  134. package/dist/scp/writes.js.map +1 -0
  135. package/docs/proposal/contracts-architecture-feasibility.md +328 -0
  136. package/docs/proposal/contracts-architecture.md +579 -0
  137. package/docs/proposal/litedbmodel-v2-scp-architecture.md +382 -0
  138. package/docs/proposal/sql-dynamic-expansion-spec.md +175 -0
  139. package/docs/proposal/v0-coverage-matrix.md +157 -0
  140. package/docs/proposal/v2-completeness-plan.md +90 -0
  141. package/docs/proposal/v2-sql-parity-checklist.md +162 -0
  142. package/package.json +27 -5
  143. package/docs/README_REVIEW.md +0 -23
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ /**
3
+ * litedbmodel v2 SCP — dialect strategy table (WS6, #26; spec §4/§5/§8/§10).
4
+ *
5
+ * The SINGLE SOURCE OF TRUTH for every SQL-dialect difference the Backend Compile + render
6
+ * pipeline needs. The dialect axis is compiled ONCE, TS-side (spec §10: "方言軸はコンパイル時
7
+ * に TS 側で1回検証"). This module encodes the differences as a closed strategy record — NO
8
+ * scattered `?:` in the engine code (the hard rule). A backend compile / renderer asks the
9
+ * {@link Dialect} for the dialect-specific text and never branches on the dialect name inline.
10
+ *
11
+ * ## What actually differs (verified against the v1 SqlBuilders — the golden bar)
12
+ *
13
+ * For the CRUD + write-tx + relation SQL the SCP Backend Compile emits (single-row VALUES,
14
+ * `?` placeholders, `col = ?` WHERE, `col IN (?, …)`), the dialect surface is small and
15
+ * closed:
16
+ *
17
+ * - **INSERT conflict clause**: SQLite `INSERT OR IGNORE … ` / `… ON CONFLICT (cols) DO
18
+ * UPDATE SET c = excluded.c`; Postgres `… ON CONFLICT (cols) DO NOTHING` / `… DO UPDATE
19
+ * SET c = EXCLUDED.c`; MySQL `INSERT IGNORE …` / `… ON DUPLICATE KEY UPDATE c = VALUES(c)`.
20
+ * These are byte-identical to `SqliteSqlBuilder` / `PostgresSqlBuilder` / `MysqlSqlBuilder`
21
+ * `buildInsert` for the single-record case (see the WS6 dialect-golden test).
22
+ * - **Bare `ON CONFLICT DO NOTHING`** (no column list — the gate-first idempotency/unique
23
+ * guard INSERTs of the write-tx plan, spec §6): SQLite/Postgres emit `ON CONFLICT DO
24
+ * NOTHING`; MySQL emits `INSERT IGNORE`.
25
+ * - **Placeholder style**: all dialects author with `?`; Postgres converts `?`→`$1,$2,…` in a
26
+ * final left-to-right one-pass AFTER the SQL text is fully assembled (spec §8 — the
27
+ * number-reassignment problem is designed out because the pass runs once over the final
28
+ * flat text). SQLite/MySQL keep `?`.
29
+ *
30
+ * Everything else (SELECT/UPDATE/DELETE skeletons, `col = ?`, `col IN (?, …)` expansion, the
31
+ * fragment tree, RETURNING for SQLite/Postgres) is identical text across dialects, so it is
32
+ * NOT part of the strategy — only the genuine divergences live here.
33
+ *
34
+ * ## DB behavior differences beyond SQL text (spec §13) — closeable-by-convention line
35
+ *
36
+ * SQL-text identity does not guarantee identical DB behavior (NULL ordering, collation,
37
+ * timezone). Per spec §13, those are closed "by dialect-compile-time convention" only within
38
+ * the closeable line. WS6 encodes the ONE convention that is mechanically closeable in the
39
+ * compiled SQL text: deterministic NULL ordering for an `ORDER BY … NULLS` requirement
40
+ * ({@link Dialect.orderByNulls}). Collation / timezone / float semantics are OUT OF SCOPE
41
+ * (documented, not silently defaulted).
42
+ */
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.MYSQL = exports.POSTGRES = exports.SQLITE = void 0;
45
+ exports.toDollarPlaceholders = toDollarPlaceholders;
46
+ exports.dialectFor = dialectFor;
47
+ // ── `?`→`$N` final one-pass (Postgres only) ───────────────────────────────────
48
+ /**
49
+ * Replace each `?` placeholder in a fully-assembled SQL string with `$1, $2, …` left-to-right
50
+ * (Postgres). This is the spec §8 "最終1パス" conversion: it runs ONCE over the final flat SQL
51
+ * text, so placeholder numbering is a plain running counter — the number-reassignment problem
52
+ * cannot reappear (there is no per-fragment renumbering). Every `?` is a bound param position
53
+ * (the render pipeline never emits a literal `?` inside a string literal — values are always
54
+ * parameterized), so a naive scan is correct for the compiled surface.
55
+ */
56
+ function toDollarPlaceholders(sql) {
57
+ let n = 0;
58
+ let out = '';
59
+ for (const ch of sql) {
60
+ if (ch === '?') {
61
+ n += 1;
62
+ out += `$${n}`;
63
+ }
64
+ else {
65
+ out += ch;
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ // ── Shared conflict-clause rendering (parameterized by the SET-assignment form) ─
71
+ /**
72
+ * Render the trailing conflict clause for the "has a column target" upsert/ignore forms shared
73
+ * by SQLite and Postgres (`ON CONFLICT (cols) DO NOTHING | DO UPDATE SET …`). MySQL overrides
74
+ * with its `ON DUPLICATE KEY UPDATE` form. `excludedRef` produces the per-column right-hand side
75
+ * (`excluded.c` for SQLite, `EXCLUDED.c` for Postgres).
76
+ */
77
+ function onConflictClause(conflictColumns, action, excludedRef) {
78
+ const target = conflictColumns.length > 0 ? ` (${conflictColumns.join(', ')})` : '';
79
+ if (action === 'ignore') {
80
+ return ` ON CONFLICT${target} DO NOTHING`;
81
+ }
82
+ const sets = action.updateColumns.map((c) => `${c} = ${excludedRef(c)}`);
83
+ return ` ON CONFLICT${target} DO UPDATE SET ${sets.join(', ')}`;
84
+ }
85
+ /**
86
+ * Render a bare do-nothing guard INSERT. `trailingDoNothing` dialects (SQLite/Postgres) emit
87
+ * `INSERT INTO … VALUES (…) ON CONFLICT DO NOTHING`; MySQL emits `INSERT IGNORE INTO … VALUES (…)`.
88
+ */
89
+ function guardInsertText(table, columns, placeholders, form) {
90
+ const head = form === 'insertIgnore' ? 'INSERT IGNORE INTO' : 'INSERT INTO';
91
+ const tail = form === 'trailingDoNothing' ? ' ON CONFLICT DO NOTHING' : '';
92
+ return `${head} ${table} (${columns.join(', ')}) VALUES (${placeholders})${tail}`;
93
+ }
94
+ // ── The three frozen dialect strategies (SSoT) ─────────────────────────────────
95
+ /**
96
+ * SQLite. `ignore` is an INSERT-verb modifier (`INSERT OR IGNORE INTO`); upsert uses
97
+ * `ON CONFLICT (cols) DO UPDATE SET c = excluded.c`. Placeholders stay `?`. NULL ordering uses
98
+ * the native `NULLS FIRST/LAST` (SQLite 3.30+).
99
+ */
100
+ exports.SQLITE = Object.freeze({
101
+ name: 'sqlite',
102
+ ignoreIsTrailingClause: false,
103
+ insertVerb(action) {
104
+ return action === 'ignore' ? 'INSERT OR IGNORE INTO' : 'INSERT INTO';
105
+ },
106
+ insertConflictClause(conflictColumns, action) {
107
+ // `ignore` is handled by the verb (`INSERT OR IGNORE`), so no trailing clause for it.
108
+ if (action === 'ignore')
109
+ return '';
110
+ return onConflictClause(conflictColumns, action, (c) => `excluded.${c}`);
111
+ },
112
+ guardInsert(table, columns, placeholders) {
113
+ return guardInsertText(table, columns, placeholders, 'trailingDoNothing');
114
+ },
115
+ finalizePlaceholders(sql) {
116
+ return sql;
117
+ },
118
+ orderByNulls(expr, dir, nulls) {
119
+ return `${expr} ${dir} NULLS ${nulls}`;
120
+ },
121
+ });
122
+ /**
123
+ * Postgres. `ignore` is a TRAILING clause (`… ON CONFLICT (cols) DO NOTHING`); upsert uses
124
+ * `ON CONFLICT (cols) DO UPDATE SET c = EXCLUDED.c`. Placeholders are converted `?`→`$N` in the
125
+ * final one-pass. NULL ordering uses native `NULLS FIRST/LAST`.
126
+ */
127
+ exports.POSTGRES = Object.freeze({
128
+ name: 'postgres',
129
+ ignoreIsTrailingClause: true,
130
+ insertVerb(_action) {
131
+ return 'INSERT INTO';
132
+ },
133
+ insertConflictClause(conflictColumns, action) {
134
+ return onConflictClause(conflictColumns, action, (c) => `EXCLUDED.${c}`);
135
+ },
136
+ guardInsert(table, columns, placeholders) {
137
+ return guardInsertText(table, columns, placeholders, 'trailingDoNothing');
138
+ },
139
+ finalizePlaceholders(sql) {
140
+ return toDollarPlaceholders(sql);
141
+ },
142
+ orderByNulls(expr, dir, nulls) {
143
+ return `${expr} ${dir} NULLS ${nulls}`;
144
+ },
145
+ });
146
+ /**
147
+ * MySQL. `ignore` is an INSERT-verb modifier (`INSERT IGNORE INTO`); upsert uses
148
+ * `… ON DUPLICATE KEY UPDATE c = VALUES(c)` (no conflict-column target — MySQL keys off any
149
+ * unique index). Placeholders stay `?`. NULL ordering is emulated with a leading
150
+ * `<expr> IS NULL` sort key (MySQL has no `NULLS FIRST/LAST`).
151
+ */
152
+ exports.MYSQL = Object.freeze({
153
+ name: 'mysql',
154
+ ignoreIsTrailingClause: false,
155
+ insertVerb(action) {
156
+ return action === 'ignore' ? 'INSERT IGNORE INTO' : 'INSERT INTO';
157
+ },
158
+ insertConflictClause(_conflictColumns, action) {
159
+ if (action === 'ignore')
160
+ return ''; // handled by the `INSERT IGNORE` verb
161
+ const sets = action.updateColumns.map((c) => `${c} = VALUES(${c})`);
162
+ return ` ON DUPLICATE KEY UPDATE ${sets.join(', ')}`;
163
+ },
164
+ guardInsert(table, columns, placeholders) {
165
+ return guardInsertText(table, columns, placeholders, 'insertIgnore');
166
+ },
167
+ finalizePlaceholders(sql) {
168
+ return sql;
169
+ },
170
+ orderByNulls(expr, dir, nulls) {
171
+ // MySQL: emulate NULLS FIRST/LAST with a leading `IS NULL` key.
172
+ // NULLS FIRST → `expr IS NULL DESC, expr <dir>` (nulls sort as 1, before 0... wait)
173
+ // In MySQL, NULL sorts LOWEST by default. `expr IS NULL` is 1 for null, 0 otherwise.
174
+ // NULLS FIRST: nulls must come first → order the IS-NULL flag DESC (1 before 0).
175
+ // NULLS LAST: nulls must come last → order the IS-NULL flag ASC (0 before 1).
176
+ const flagDir = nulls === 'FIRST' ? 'DESC' : 'ASC';
177
+ return `${expr} IS NULL ${flagDir}, ${expr} ${dir}`;
178
+ },
179
+ });
180
+ /** Resolve a {@link DialectName} to its frozen {@link Dialect} strategy (fail-closed). */
181
+ const DIALECTS = Object.freeze({
182
+ sqlite: exports.SQLITE,
183
+ postgres: exports.POSTGRES,
184
+ mysql: exports.MYSQL,
185
+ });
186
+ /** Look up a dialect strategy by name (throws on an unknown dialect — no silent default). */
187
+ function dialectFor(name) {
188
+ const d = DIALECTS[name];
189
+ if (d === undefined) {
190
+ throw new Error(`scp dialect: unknown dialect '${String(name)}' (known: ${Object.keys(DIALECTS).join(', ')})`);
191
+ }
192
+ return d;
193
+ }
194
+ //# sourceMappingURL=dialect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialect.js","sourceRoot":"","sources":["../../src/scp/dialect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * litedbmodel v2 SCP — Error Mapping (WS3, #23; spec §11 item 5).
3
+ *
4
+ * Maps a SQLite driver error (better-sqlite3 `SqliteError`, carrying a `SQLITE_*` `code`)
5
+ * to an SCP {@link SqlFailure}: a structured Failure with a stable `kind` and the bc
6
+ * Execution-Plan {@link PolicyKind} the runtime honors (fail / retry / continue). The
7
+ * handler returns `{ error }` to bc's `runBehavior`; a `fail`-policy node re-throws through
8
+ * `runPlan` (`OP_FAILED`), so the driver surfaces this structured failure at the boundary.
9
+ *
10
+ * The mapping is closed and explicit (no silent catch-all that hides a driver error): an
11
+ * unrecognized SQLite code maps to `kind: 'driver_error'` with `policy: 'fail'` — loud, and
12
+ * carrying the original code + message for diagnosis.
13
+ */
14
+ import type { PolicyKind } from 'behavior-contracts';
15
+ /** The SCP failure kinds litedbmodel maps SQL driver errors to (spec §11 item 5). */
16
+ export type SqlFailureKind = 'constraint_violation' | 'foreign_key_violation' | 'retryable' | 'driver_error';
17
+ /**
18
+ * A mapped SCP failure: the SCP `kind`, the honored bc `PolicyKind`, the original SQLite
19
+ * `code`, and a human message. Thrown at the runtime boundary (or carried as `{error}`
20
+ * into `runBehavior` for Policy-Kind interpretation by `runPlan`).
21
+ */
22
+ export declare class SqlFailure extends Error {
23
+ readonly kind: SqlFailureKind;
24
+ readonly policy: PolicyKind;
25
+ readonly sqliteCode: string | undefined;
26
+ constructor(kind: SqlFailureKind, policy: PolicyKind, sqliteCode: string | undefined, message: string);
27
+ }
28
+ /**
29
+ * Map a caught driver error to an {@link SqlFailure}. The `SQLITE_CONSTRAINT_*` family maps
30
+ * to constraint / FK kinds (Policy `fail` — a data conflict is not retryable); the
31
+ * `SQLITE_BUSY` / `SQLITE_LOCKED` family maps to `retryable` (Policy `retry`). Anything else
32
+ * (including a non-SQLite error) maps to `driver_error` (Policy `fail`), preserving the
33
+ * original message.
34
+ */
35
+ export declare function mapSqliteError(e: unknown): SqlFailure;
36
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/scp/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,qFAAqF;AACrF,MAAM,MAAM,cAAc,GACtB,sBAAsB,GACtB,uBAAuB,GACvB,WAAW,GACX,cAAc,CAAC;AAEnB;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,KAAK;IAEjC,QAAQ,CAAC,IAAI,EAAE,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,UAAU;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS;gBAF9B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,GAAG,SAAS,EACvC,OAAO,EAAE,MAAM;CAKlB;AAYD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,UAAU,CAqBrD"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * litedbmodel v2 SCP — Error Mapping (WS3, #23; spec §11 item 5).
4
+ *
5
+ * Maps a SQLite driver error (better-sqlite3 `SqliteError`, carrying a `SQLITE_*` `code`)
6
+ * to an SCP {@link SqlFailure}: a structured Failure with a stable `kind` and the bc
7
+ * Execution-Plan {@link PolicyKind} the runtime honors (fail / retry / continue). The
8
+ * handler returns `{ error }` to bc's `runBehavior`; a `fail`-policy node re-throws through
9
+ * `runPlan` (`OP_FAILED`), so the driver surfaces this structured failure at the boundary.
10
+ *
11
+ * The mapping is closed and explicit (no silent catch-all that hides a driver error): an
12
+ * unrecognized SQLite code maps to `kind: 'driver_error'` with `policy: 'fail'` — loud, and
13
+ * carrying the original code + message for diagnosis.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.SqlFailure = void 0;
17
+ exports.mapSqliteError = mapSqliteError;
18
+ /**
19
+ * A mapped SCP failure: the SCP `kind`, the honored bc `PolicyKind`, the original SQLite
20
+ * `code`, and a human message. Thrown at the runtime boundary (or carried as `{error}`
21
+ * into `runBehavior` for Policy-Kind interpretation by `runPlan`).
22
+ */
23
+ class SqlFailure extends Error {
24
+ kind;
25
+ policy;
26
+ sqliteCode;
27
+ constructor(kind, policy, sqliteCode, message) {
28
+ super(message);
29
+ this.kind = kind;
30
+ this.policy = policy;
31
+ this.sqliteCode = sqliteCode;
32
+ this.name = 'SqlFailure';
33
+ }
34
+ }
35
+ exports.SqlFailure = SqlFailure;
36
+ /** True if `e` looks like a better-sqlite3 `SqliteError` (has a `SQLITE_*` string `code`). */
37
+ function isSqliteError(e) {
38
+ return (typeof e === 'object' &&
39
+ e !== null &&
40
+ typeof e.code === 'string' &&
41
+ e.code.startsWith('SQLITE_'));
42
+ }
43
+ /**
44
+ * Map a caught driver error to an {@link SqlFailure}. The `SQLITE_CONSTRAINT_*` family maps
45
+ * to constraint / FK kinds (Policy `fail` — a data conflict is not retryable); the
46
+ * `SQLITE_BUSY` / `SQLITE_LOCKED` family maps to `retryable` (Policy `retry`). Anything else
47
+ * (including a non-SQLite error) maps to `driver_error` (Policy `fail`), preserving the
48
+ * original message.
49
+ */
50
+ function mapSqliteError(e) {
51
+ if (!isSqliteError(e)) {
52
+ const message = e instanceof Error ? e.message : String(e);
53
+ return new SqlFailure('driver_error', 'fail', undefined, `non-SQLite driver error: ${message}`);
54
+ }
55
+ const { code, message } = e;
56
+ // The message embeds the `SQLITE_*` code so it survives being wrapped by bc's `runPlan`
57
+ // (`OP_FAILED: <handler message>`); the runtime re-maps from it to re-surface the
58
+ // structured SqlFailure (kind / policy / code) at the boundary.
59
+ const tagged = `[${code}] ${message}`;
60
+ if (code === 'SQLITE_CONSTRAINT_FOREIGNKEY') {
61
+ return new SqlFailure('foreign_key_violation', 'fail', code, tagged);
62
+ }
63
+ if (code.startsWith('SQLITE_CONSTRAINT')) {
64
+ // UNIQUE / PRIMARYKEY / CHECK / NOTNULL / … — a durable constraint conflict.
65
+ return new SqlFailure('constraint_violation', 'fail', code, tagged);
66
+ }
67
+ if (code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED') {
68
+ return new SqlFailure('retryable', 'retry', code, tagged);
69
+ }
70
+ return new SqlFailure('driver_error', 'fail', code, tagged);
71
+ }
72
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/scp/errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AA6CH,wCAqBC;AAvDD;;;;GAIG;AACH,MAAa,UAAW,SAAQ,KAAK;IAExB;IACA;IACA;IAHX,YACW,IAAoB,EACpB,MAAkB,EAClB,UAA8B,EACvC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QALN,SAAI,GAAJ,IAAI,CAAgB;QACpB,WAAM,GAAN,MAAM,CAAY;QAClB,eAAU,GAAV,UAAU,CAAoB;QAIvC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAVD,gCAUC;AAED,8FAA8F;AAC9F,SAAS,aAAa,CAAC,CAAU;IAC/B,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,KAAK,IAAI;QACV,OAAQ,CAAwB,CAAC,IAAI,KAAK,QAAQ;QACjD,CAAsB,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CACnD,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,CAAU;IACvC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;QACtB,MAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3D,OAAO,IAAI,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,4BAA4B,OAAO,EAAE,CAAC,CAAC;IAClG,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAC5B,wFAAwF;IACxF,kFAAkF;IAClF,gEAAgE;IAChE,MAAM,MAAM,GAAG,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;IACtC,IAAI,IAAI,KAAK,8BAA8B,EAAE,CAAC;QAC5C,OAAO,IAAI,UAAU,CAAC,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACzC,6EAA6E;QAC7E,OAAO,IAAI,UAAU,CAAC,sBAAsB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtE,CAAC;IACD,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;QACvD,OAAO,IAAI,UAAU,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * litedbmodel v2 SCP — FIND_FILTER fail-closed authoring guard (#47 Finding B / plan R8).
3
+ *
4
+ * `FIND_FILTER` is a v1 PER-MODEL implicit predicate (a soft-delete / tenant scope) that
5
+ * v1 MERGES into the WHERE of every internal `find()`/`count()` before compiling
6
+ * (`DBModel._buildSelectSQL` / `_count`: `normalizedCond.add(condsToRecord(FIND_FILTER))`).
7
+ * It is not a distinct SQL construct — it manifests purely as extra WHERE fragments.
8
+ *
9
+ * The SCP compile path (`L.Select({ where })` → `compileReadGraph` → `renderReadPrimary`)
10
+ * is AUTHORED from an explicit `where`; it has NO `DBModel` class context, so it CANNOT
11
+ * auto-apply a model's `FIND_FILTER` (there is no model reference at compile time, and no
12
+ * production seam routes a `DBModel` into the SCP compile — grep: `FIND_FILTER` appears only
13
+ * in `DBModel`/`decorators`/`types`, never under `src/scp/`). Auto-apply is therefore
14
+ * impossible without model context the SCP compile does not have.
15
+ *
16
+ * Resolution = the plan's variant chosen fail-closed: a model that DECLARES a `FIND_FILTER`
17
+ * MUST have its scope predicates folded into the authored `L.Select({ where })`; if the
18
+ * author routes such a model through SCP WITHOUT the scope keys, this guard throws loudly
19
+ * ({@link FindFilterLeakError}) rather than silently dropping the filter (which would leak
20
+ * soft-deleted / cross-tenant rows — a correctness bug, not a cosmetic one). When the scope
21
+ * keys ARE present the guard is a no-op (byte-identical to v1's merged WHERE — proven in
22
+ * `test/scp/find-filter-noop.test.ts`).
23
+ */
24
+ import { type Conds } from '../Column';
25
+ /** A `DBModel`-shaped source: the only field this guard reads is the optional `FIND_FILTER`. */
26
+ export interface FindFilterSource {
27
+ /** The per-model implicit scope predicate (v1 `DBModel.FIND_FILTER`), or null/undefined. */
28
+ readonly FIND_FILTER?: Conds | null;
29
+ /** Class name for diagnostics (present on every `DBModel` subclass). */
30
+ readonly name?: string;
31
+ }
32
+ /**
33
+ * A model declaring a `FIND_FILTER` was compiled via the SCP path without its scope
34
+ * predicates present in the authored WHERE — the filter would silently leak. Thrown
35
+ * fail-closed at compile time; never swallowed.
36
+ */
37
+ export declare class FindFilterLeakError extends Error {
38
+ /** The declaring model's class name (diagnostics). */
39
+ readonly modelName: string;
40
+ /** The FIND_FILTER scope keys that are MISSING from the authored WHERE. */
41
+ readonly missingKeys: readonly string[];
42
+ constructor(
43
+ /** The declaring model's class name (diagnostics). */
44
+ modelName: string,
45
+ /** The FIND_FILTER scope keys that are MISSING from the authored WHERE. */
46
+ missingKeys: readonly string[]);
47
+ }
48
+ /**
49
+ * Extract the top-level predicate KEYS a `FIND_FILTER` folds into a WHERE. Uses the SAME
50
+ * `condsToRecord` v1 runs when merging (`DBModel._buildSelectSQL`:597) so the key set is
51
+ * exactly what v1 would add to `normalizedCond`. `__or__` (grouped OR) is kept as a key so a
52
+ * scoped OR-group also demands explicit authoring (fail-closed; never silently satisfied).
53
+ */
54
+ export declare function findFilterKeys(filter: Conds | null | undefined): string[];
55
+ /**
56
+ * Fail-closed guard: assert a model's `FIND_FILTER` scope keys are all present in the
57
+ * authored WHERE key set BEFORE an SCP compile of that model. No FIND_FILTER → no-op. When
58
+ * present, EVERY scope key must appear in `authoredWhereKeys`; any missing key throws
59
+ * {@link FindFilterLeakError}. The caller passes the top-level keys of the object it hands to
60
+ * `L.Select({ where })` (the same shape `condsToRecord` yields), so the check compares
61
+ * like-for-like against v1's merged predicate.
62
+ *
63
+ * @param model a `DBModel`-shaped source that MAY declare `FIND_FILTER`.
64
+ * @param authoredWhereKeys the top-level WHERE keys the SCP author expressed for this model.
65
+ * @throws FindFilterLeakError when a declared scope key is absent from the authored WHERE.
66
+ */
67
+ export declare function assertFindFilterFolded(model: FindFilterSource, authoredWhereKeys: Iterable<string>): void;
68
+ //# sourceMappingURL=find-filter-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-filter-guard.d.ts","sourceRoot":"","sources":["../../src/scp/find-filter-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AAEtD,gGAAgG;AAChG,MAAM,WAAW,gBAAgB;IAC/B,4FAA4F;IAC5F,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,sDAAsD;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE;;IAHvC,sDAAsD;IAC7C,SAAS,EAAE,MAAM;IAC1B,2EAA2E;IAClE,WAAW,EAAE,SAAS,MAAM,EAAE;CAY1C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAGzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,gBAAgB,EACvB,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,GAClC,IAAI,CAQN"}
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * litedbmodel v2 SCP — FIND_FILTER fail-closed authoring guard (#47 Finding B / plan R8).
4
+ *
5
+ * `FIND_FILTER` is a v1 PER-MODEL implicit predicate (a soft-delete / tenant scope) that
6
+ * v1 MERGES into the WHERE of every internal `find()`/`count()` before compiling
7
+ * (`DBModel._buildSelectSQL` / `_count`: `normalizedCond.add(condsToRecord(FIND_FILTER))`).
8
+ * It is not a distinct SQL construct — it manifests purely as extra WHERE fragments.
9
+ *
10
+ * The SCP compile path (`L.Select({ where })` → `compileReadGraph` → `renderReadPrimary`)
11
+ * is AUTHORED from an explicit `where`; it has NO `DBModel` class context, so it CANNOT
12
+ * auto-apply a model's `FIND_FILTER` (there is no model reference at compile time, and no
13
+ * production seam routes a `DBModel` into the SCP compile — grep: `FIND_FILTER` appears only
14
+ * in `DBModel`/`decorators`/`types`, never under `src/scp/`). Auto-apply is therefore
15
+ * impossible without model context the SCP compile does not have.
16
+ *
17
+ * Resolution = the plan's variant chosen fail-closed: a model that DECLARES a `FIND_FILTER`
18
+ * MUST have its scope predicates folded into the authored `L.Select({ where })`; if the
19
+ * author routes such a model through SCP WITHOUT the scope keys, this guard throws loudly
20
+ * ({@link FindFilterLeakError}) rather than silently dropping the filter (which would leak
21
+ * soft-deleted / cross-tenant rows — a correctness bug, not a cosmetic one). When the scope
22
+ * keys ARE present the guard is a no-op (byte-identical to v1's merged WHERE — proven in
23
+ * `test/scp/find-filter-noop.test.ts`).
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.FindFilterLeakError = void 0;
27
+ exports.findFilterKeys = findFilterKeys;
28
+ exports.assertFindFilterFolded = assertFindFilterFolded;
29
+ const Column_1 = require("../Column");
30
+ /**
31
+ * A model declaring a `FIND_FILTER` was compiled via the SCP path without its scope
32
+ * predicates present in the authored WHERE — the filter would silently leak. Thrown
33
+ * fail-closed at compile time; never swallowed.
34
+ */
35
+ class FindFilterLeakError extends Error {
36
+ modelName;
37
+ missingKeys;
38
+ constructor(
39
+ /** The declaring model's class name (diagnostics). */
40
+ modelName,
41
+ /** The FIND_FILTER scope keys that are MISSING from the authored WHERE. */
42
+ missingKeys) {
43
+ super(`FIND_FILTER leak: model '${modelName}' declares an implicit scope predicate on ` +
44
+ `[${missingKeys.join(', ')}] that is NOT present in the SCP-authored WHERE. The SCP ` +
45
+ `compile cannot auto-apply FIND_FILTER (no model context at compile time), so the ` +
46
+ `scope predicate would be silently dropped (soft-delete / tenant scope leak). Fold ` +
47
+ `the FIND_FILTER predicates into the authored L.Select({ where }) explicitly ` +
48
+ `(e.g. whereEq/whereIsNull on ${missingKeys.join(', ')}).`);
49
+ this.modelName = modelName;
50
+ this.missingKeys = missingKeys;
51
+ this.name = 'FindFilterLeakError';
52
+ }
53
+ }
54
+ exports.FindFilterLeakError = FindFilterLeakError;
55
+ /**
56
+ * Extract the top-level predicate KEYS a `FIND_FILTER` folds into a WHERE. Uses the SAME
57
+ * `condsToRecord` v1 runs when merging (`DBModel._buildSelectSQL`:597) so the key set is
58
+ * exactly what v1 would add to `normalizedCond`. `__or__` (grouped OR) is kept as a key so a
59
+ * scoped OR-group also demands explicit authoring (fail-closed; never silently satisfied).
60
+ */
61
+ function findFilterKeys(filter) {
62
+ if (!filter || filter.length === 0)
63
+ return [];
64
+ return Object.keys((0, Column_1.condsToRecord)(filter));
65
+ }
66
+ /**
67
+ * Fail-closed guard: assert a model's `FIND_FILTER` scope keys are all present in the
68
+ * authored WHERE key set BEFORE an SCP compile of that model. No FIND_FILTER → no-op. When
69
+ * present, EVERY scope key must appear in `authoredWhereKeys`; any missing key throws
70
+ * {@link FindFilterLeakError}. The caller passes the top-level keys of the object it hands to
71
+ * `L.Select({ where })` (the same shape `condsToRecord` yields), so the check compares
72
+ * like-for-like against v1's merged predicate.
73
+ *
74
+ * @param model a `DBModel`-shaped source that MAY declare `FIND_FILTER`.
75
+ * @param authoredWhereKeys the top-level WHERE keys the SCP author expressed for this model.
76
+ * @throws FindFilterLeakError when a declared scope key is absent from the authored WHERE.
77
+ */
78
+ function assertFindFilterFolded(model, authoredWhereKeys) {
79
+ const required = findFilterKeys(model.FIND_FILTER);
80
+ if (required.length === 0)
81
+ return; // No implicit scope → nothing to fold → no-op.
82
+ const present = new Set(authoredWhereKeys);
83
+ const missing = required.filter((k) => !present.has(k));
84
+ if (missing.length > 0) {
85
+ throw new FindFilterLeakError(model.name ?? '<anonymous model>', missing);
86
+ }
87
+ }
88
+ //# sourceMappingURL=find-filter-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-filter-guard.js","sourceRoot":"","sources":["../../src/scp/find-filter-guard.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AA0CH,wCAGC;AAcD,wDAWC;AApED,sCAAsD;AAUtD;;;;GAIG;AACH,MAAa,mBAAoB,SAAQ,KAAK;IAGjC;IAEA;IAJX;IACE,sDAAsD;IAC7C,SAAiB;IAC1B,2EAA2E;IAClE,WAA8B;QAEvC,KAAK,CACH,4BAA4B,SAAS,4CAA4C;YAC/E,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,2DAA2D;YACrF,mFAAmF;YACnF,oFAAoF;YACpF,8EAA8E;YAC9E,gCAAgC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC7D,CAAC;QAXO,cAAS,GAAT,SAAS,CAAQ;QAEjB,gBAAW,GAAX,WAAW,CAAmB;QAUvC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAjBD,kDAiBC;AAED;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,MAAgC;IAC7D,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9C,OAAO,MAAM,CAAC,IAAI,CAAC,IAAA,sBAAa,EAAC,MAAM,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,sBAAsB,CACpC,KAAuB,EACvB,iBAAmC;IAEnC,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACnD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,+CAA+C;IAClF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,mBAAmB,CAAC,KAAK,CAAC,IAAI,IAAI,mBAAmB,EAAE,OAAO,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * litedbmodel v2 SCP — fragment RUNTIME (assemble + bind).
3
+ *
4
+ * This is the thin runtime a bundle drives. It does exactly three things:
5
+ *
6
+ * 1. ASSEMBLE — walk the ordered fragment list; drop any fragment whose `skip`
7
+ * is satisfied; concatenate the present fragments' `sql`, and their `params`
8
+ * in order. Nested-fragment params splice their own `sql` at the `?` and
9
+ * contribute their own params in order.
10
+ * 2. EVALUATE — turn each value-spec param into a concrete bound value using the
11
+ * runtime `input` (and upstream `wire` results).
12
+ * 3. RENDER — the produced `{ sql, params }` uses `?`; a dialect step turns `?`
13
+ * into `$N` for PostgreSQL (identical to the original driver's naive replace).
14
+ *
15
+ * NO SQL structure is generated here. The `sql` text is already complete; the
16
+ * runtime only splices nested-fragment text and fills `?`.
17
+ */
18
+ import { type Fragment, type Node, type WhereGroup, type ValueSpec, type PresenceCondition } from './model';
19
+ /** Runtime binding scope: FLAT — input keys and wire results are top-level roots. */
20
+ export interface Scope {
21
+ input: Record<string, unknown>;
22
+ wire?: Record<string, Record<string, unknown>>;
23
+ }
24
+ /** Is a fragment's `skip` satisfied (⇒ omit the fragment)? */
25
+ export declare function isSkipped(cond: PresenceCondition | undefined, scope: Scope): boolean;
26
+ /** Evaluate a value-spec to its bound value against the runtime scope. */
27
+ export declare function evalValueSpec(spec: ValueSpec, scope: Scope): unknown;
28
+ /**
29
+ * Assemble a single fragment: interleave its literal `sql` (split on `?`) with the
30
+ * rendered text of each param, concatenating params in order.
31
+ */
32
+ export declare function assembleFragment(frag: Fragment, scope: Scope): {
33
+ sql: string;
34
+ params: unknown[];
35
+ };
36
+ /**
37
+ * Assemble a WHERE group: join present members with `" AND "`, prefix `" WHERE "`.
38
+ * If no member is present, produce the empty string (identical to the ORIGINAL
39
+ * `DBConditions.compile` → `parts.length === 0 ? '' : parts.join(' AND ')`, with the
40
+ * `" WHERE "` prefix owned by the caller/select in the original — here folded in).
41
+ */
42
+ export declare function assembleWhereGroup(group: WhereGroup, scope: Scope): {
43
+ sql: string;
44
+ params: unknown[];
45
+ };
46
+ /**
47
+ * Assemble an ordered fragment/WHERE-group list into a single `{ sql, params }`
48
+ * (with `?`). Fragments whose `skip` is satisfied are omitted entirely (sql AND
49
+ * params). A WHERE group joins its present members with the original's text glue.
50
+ */
51
+ export declare function assemble(nodes: Node[], scope: Scope): {
52
+ sql: string;
53
+ params: unknown[];
54
+ };
55
+ /**
56
+ * PostgreSQL dialect render: `?` → `$1, $2, …`. Byte-identical to the original
57
+ * driver's `convertPlaceholders` (naive left-to-right replace; see
58
+ * `src/drivers/postgres.ts`).
59
+ */
60
+ export declare function toPostgres(sql: string): string;
61
+ //# sourceMappingURL=assemble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../../src/scp/fragment/assemble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,IAAI,EACT,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,iBAAiB,EAOvB,MAAM,SAAS,CAAC;AAEjB,qFAAqF;AACrF,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD;AAWD,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAIpF;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAqBpE;AAYD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAejG;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAWtG;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAgBxF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG9C"}