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.
- package/README.md +1 -1
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +6 -0
- package/dist/DBModel.js.map +1 -1
- package/dist/scp/authoring-sql.d.ts +134 -0
- package/dist/scp/authoring-sql.d.ts.map +1 -0
- package/dist/scp/authoring-sql.js +191 -0
- package/dist/scp/authoring-sql.js.map +1 -0
- package/dist/scp/authoring.d.ts +155 -0
- package/dist/scp/authoring.d.ts.map +1 -0
- package/dist/scp/authoring.js +162 -0
- package/dist/scp/authoring.js.map +1 -0
- package/dist/scp/catalog.d.ts +91 -0
- package/dist/scp/catalog.d.ts.map +1 -0
- package/dist/scp/catalog.js +230 -0
- package/dist/scp/catalog.js.map +1 -0
- package/dist/scp/codegen.d.ts +95 -0
- package/dist/scp/codegen.d.ts.map +1 -0
- package/dist/scp/codegen.js +94 -0
- package/dist/scp/codegen.js.map +1 -0
- package/dist/scp/dialect.d.ts +141 -0
- package/dist/scp/dialect.d.ts.map +1 -0
- package/dist/scp/dialect.js +194 -0
- package/dist/scp/dialect.js.map +1 -0
- package/dist/scp/errors.d.ts +36 -0
- package/dist/scp/errors.d.ts.map +1 -0
- package/dist/scp/errors.js +72 -0
- package/dist/scp/errors.js.map +1 -0
- package/dist/scp/find-filter-guard.d.ts +68 -0
- package/dist/scp/find-filter-guard.d.ts.map +1 -0
- package/dist/scp/find-filter-guard.js +88 -0
- package/dist/scp/find-filter-guard.js.map +1 -0
- package/dist/scp/fragment/assemble.d.ts +61 -0
- package/dist/scp/fragment/assemble.d.ts.map +1 -0
- package/dist/scp/fragment/assemble.js +148 -0
- package/dist/scp/fragment/assemble.js.map +1 -0
- package/dist/scp/fragment/compile-pg.d.ts +107 -0
- package/dist/scp/fragment/compile-pg.d.ts.map +1 -0
- package/dist/scp/fragment/compile-pg.js +142 -0
- package/dist/scp/fragment/compile-pg.js.map +1 -0
- package/dist/scp/fragment/index.d.ts +16 -0
- package/dist/scp/fragment/index.d.ts.map +1 -0
- package/dist/scp/fragment/index.js +34 -0
- package/dist/scp/fragment/index.js.map +1 -0
- package/dist/scp/fragment/model.d.ts +119 -0
- package/dist/scp/fragment/model.d.ts.map +1 -0
- package/dist/scp/fragment/model.js +66 -0
- package/dist/scp/fragment/model.js.map +1 -0
- package/dist/scp/fragment/pg-array-type.d.ts +13 -0
- package/dist/scp/fragment/pg-array-type.d.ts.map +1 -0
- package/dist/scp/fragment/pg-array-type.js +34 -0
- package/dist/scp/fragment/pg-array-type.js.map +1 -0
- package/dist/scp/guard.d.ts +43 -0
- package/dist/scp/guard.d.ts.map +1 -0
- package/dist/scp/guard.js +134 -0
- package/dist/scp/guard.js.map +1 -0
- package/dist/scp/index.cjs +12202 -0
- package/dist/scp/index.d.ts +64 -0
- package/dist/scp/index.d.ts.map +1 -0
- package/dist/scp/index.js +238 -0
- package/dist/scp/index.js.map +1 -0
- package/dist/scp/index.mjs +4819 -0
- package/dist/scp/makesql/authoring-compile.d.ts +90 -0
- package/dist/scp/makesql/authoring-compile.d.ts.map +1 -0
- package/dist/scp/makesql/authoring-compile.js +494 -0
- package/dist/scp/makesql/authoring-compile.js.map +1 -0
- package/dist/scp/makesql/compile-crud.d.ts +133 -0
- package/dist/scp/makesql/compile-crud.d.ts.map +1 -0
- package/dist/scp/makesql/compile-crud.js +288 -0
- package/dist/scp/makesql/compile-crud.js.map +1 -0
- package/dist/scp/makesql/compile-relation.d.ts +157 -0
- package/dist/scp/makesql/compile-relation.d.ts.map +1 -0
- package/dist/scp/makesql/compile-relation.js +425 -0
- package/dist/scp/makesql/compile-relation.js.map +1 -0
- package/dist/scp/makesql/compile-select.d.ts +45 -0
- package/dist/scp/makesql/compile-select.d.ts.map +1 -0
- package/dist/scp/makesql/compile-select.js +58 -0
- package/dist/scp/makesql/compile-select.js.map +1 -0
- package/dist/scp/makesql/compile.d.ts +63 -0
- package/dist/scp/makesql/compile.d.ts.map +1 -0
- package/dist/scp/makesql/compile.js +99 -0
- package/dist/scp/makesql/compile.js.map +1 -0
- package/dist/scp/makesql/handler.d.ts +58 -0
- package/dist/scp/makesql/handler.d.ts.map +1 -0
- package/dist/scp/makesql/handler.js +113 -0
- package/dist/scp/makesql/handler.js.map +1 -0
- package/dist/scp/makesql/index.d.ts +33 -0
- package/dist/scp/makesql/index.d.ts.map +1 -0
- package/dist/scp/makesql/index.js +114 -0
- package/dist/scp/makesql/index.js.map +1 -0
- package/dist/scp/makesql/ir.d.ts +42 -0
- package/dist/scp/makesql/ir.d.ts.map +1 -0
- package/dist/scp/makesql/ir.js +66 -0
- package/dist/scp/makesql/ir.js.map +1 -0
- package/dist/scp/makesql/json-array.d.ts +84 -0
- package/dist/scp/makesql/json-array.d.ts.map +1 -0
- package/dist/scp/makesql/json-array.js +118 -0
- package/dist/scp/makesql/json-array.js.map +1 -0
- package/dist/scp/makesql/json-batch.d.ts +102 -0
- package/dist/scp/makesql/json-batch.d.ts.map +1 -0
- package/dist/scp/makesql/json-batch.js +269 -0
- package/dist/scp/makesql/json-batch.js.map +1 -0
- package/dist/scp/makesql/makesql.d.ts +99 -0
- package/dist/scp/makesql/makesql.d.ts.map +1 -0
- package/dist/scp/makesql/makesql.js +122 -0
- package/dist/scp/makesql/makesql.js.map +1 -0
- package/dist/scp/makesql/pool-executor.d.ts +47 -0
- package/dist/scp/makesql/pool-executor.d.ts.map +1 -0
- package/dist/scp/makesql/pool-executor.js +50 -0
- package/dist/scp/makesql/pool-executor.js.map +1 -0
- package/dist/scp/makesql/static-bundle.d.ts +185 -0
- package/dist/scp/makesql/static-bundle.d.ts.map +1 -0
- package/dist/scp/makesql/static-bundle.js +1024 -0
- package/dist/scp/makesql/static-bundle.js.map +1 -0
- package/dist/scp/makesql/tx.d.ts +216 -0
- package/dist/scp/makesql/tx.d.ts.map +1 -0
- package/dist/scp/makesql/tx.js +743 -0
- package/dist/scp/makesql/tx.js.map +1 -0
- package/dist/scp/relation.d.ts +148 -0
- package/dist/scp/relation.d.ts.map +1 -0
- package/dist/scp/relation.js +257 -0
- package/dist/scp/relation.js.map +1 -0
- package/dist/scp/runtime.d.ts +231 -0
- package/dist/scp/runtime.d.ts.map +1 -0
- package/dist/scp/runtime.js +381 -0
- package/dist/scp/runtime.js.map +1 -0
- package/dist/scp/typed-object.d.ts +99 -0
- package/dist/scp/typed-object.d.ts.map +1 -0
- package/dist/scp/typed-object.js +184 -0
- package/dist/scp/typed-object.js.map +1 -0
- package/dist/scp/writes.d.ts +251 -0
- package/dist/scp/writes.d.ts.map +1 -0
- package/dist/scp/writes.js +176 -0
- package/dist/scp/writes.js.map +1 -0
- package/docs/proposal/contracts-architecture-feasibility.md +328 -0
- package/docs/proposal/contracts-architecture.md +579 -0
- package/docs/proposal/litedbmodel-v2-scp-architecture.md +382 -0
- package/docs/proposal/sql-dynamic-expansion-spec.md +175 -0
- package/docs/proposal/v0-coverage-matrix.md +157 -0
- package/docs/proposal/v2-completeness-plan.md +90 -0
- package/docs/proposal/v2-sql-parity-checklist.md +162 -0
- package/package.json +27 -5
- 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"}
|