litedbmodel 2.0.1 → 2.0.2
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/dist/scp/catalog.d.ts +4 -1
- package/dist/scp/catalog.d.ts.map +1 -1
- package/dist/scp/catalog.js +5 -1
- package/dist/scp/catalog.js.map +1 -1
- package/dist/scp/codegen.d.ts +118 -24
- package/dist/scp/codegen.d.ts.map +1 -1
- package/dist/scp/codegen.js +360 -33
- package/dist/scp/codegen.js.map +1 -1
- package/dist/scp/coltype.d.ts +41 -0
- package/dist/scp/coltype.d.ts.map +1 -0
- package/dist/scp/coltype.js +188 -0
- package/dist/scp/coltype.js.map +1 -0
- package/dist/scp/dialect.d.ts +12 -3
- package/dist/scp/dialect.d.ts.map +1 -1
- package/dist/scp/dialect.js +12 -3
- package/dist/scp/dialect.js.map +1 -1
- package/dist/scp/fragment/assemble.d.ts.map +1 -1
- package/dist/scp/fragment/assemble.js +10 -2
- package/dist/scp/fragment/assemble.js.map +1 -1
- package/dist/scp/fragment/compile-pg.js +4 -4
- package/dist/scp/index.cjs +8132 -816
- package/dist/scp/index.d.ts +3 -1
- package/dist/scp/index.d.ts.map +1 -1
- package/dist/scp/index.js +17 -6
- package/dist/scp/index.js.map +1 -1
- package/dist/scp/index.mjs +660 -81
- package/dist/scp/makesql/compile-select.d.ts +8 -1
- package/dist/scp/makesql/compile-select.d.ts.map +1 -1
- package/dist/scp/makesql/compile-select.js +1 -1
- package/dist/scp/makesql/compile-select.js.map +1 -1
- package/dist/scp/makesql/outtype.d.ts +44 -0
- package/dist/scp/makesql/outtype.d.ts.map +1 -0
- package/dist/scp/makesql/outtype.js +197 -0
- package/dist/scp/makesql/outtype.js.map +1 -0
- package/dist/scp/makesql/static-bundle.d.ts +3 -1
- package/dist/scp/makesql/static-bundle.d.ts.map +1 -1
- package/dist/scp/makesql/static-bundle.js +95 -6
- package/dist/scp/makesql/static-bundle.js.map +1 -1
- package/dist/scp/makesql/tx.d.ts +1 -1
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +48 -3
- package/dist/scp/makesql/tx.js.map +1 -1
- package/dist/scp/makesql/writeouttype.d.ts +66 -0
- package/dist/scp/makesql/writeouttype.d.ts.map +1 -0
- package/dist/scp/makesql/writeouttype.js +195 -0
- package/dist/scp/makesql/writeouttype.js.map +1 -0
- package/dist/scp/runtime.d.ts +14 -3
- package/dist/scp/runtime.d.ts.map +1 -1
- package/dist/scp/runtime.js +28 -9
- package/dist/scp/runtime.js.map +1 -1
- package/docs/proposal/litedbmodel-v2-scp-architecture.md +51 -0
- package/package.json +2 -2
package/dist/scp/index.mjs
CHANGED
|
@@ -801,7 +801,7 @@ function condsToRecord(conditions) {
|
|
|
801
801
|
// src/scp/makesql/compile-select.ts
|
|
802
802
|
function compileSelect(desc) {
|
|
803
803
|
const params = [];
|
|
804
|
-
const selectCols = desc.select ?? "*";
|
|
804
|
+
const selectCols = desc.select || (desc.selectColumn ?? "*");
|
|
805
805
|
const formatter = formatterFor(desc.dialect);
|
|
806
806
|
if (desc.cte?.params && desc.cte.params.length > 0) params.push(...desc.cte.params);
|
|
807
807
|
if (desc.joinParams && desc.joinParams.length > 0) params.push(...desc.joinParams);
|
|
@@ -2384,6 +2384,23 @@ function collectFamily(ports, prefix) {
|
|
|
2384
2384
|
}
|
|
2385
2385
|
return out;
|
|
2386
2386
|
}
|
|
2387
|
+
function collectSqlCast(ports) {
|
|
2388
|
+
const map = /* @__PURE__ */ new Map();
|
|
2389
|
+
for (const k of Object.keys(ports)) {
|
|
2390
|
+
if (!k.startsWith("sqlCast.")) continue;
|
|
2391
|
+
const v = ports[k];
|
|
2392
|
+
if (typeof v !== "string") {
|
|
2393
|
+
throw new Error(`compileWriteNode: port '${k}' (a sqlCast type) must be a literal string in the IR (got ${JSON.stringify(v)})`);
|
|
2394
|
+
}
|
|
2395
|
+
map.set(k.slice("sqlCast.".length), v);
|
|
2396
|
+
}
|
|
2397
|
+
return map;
|
|
2398
|
+
}
|
|
2399
|
+
function castPlaceholder(dialect, sqlCastMap, column) {
|
|
2400
|
+
const sqlCast = sqlCastMap.get(column);
|
|
2401
|
+
if (dialect !== "postgres" || sqlCast === void 0 || sqlCast === "timestamp" || sqlCast === "date") return "?";
|
|
2402
|
+
return formatterFor("postgres")("?", sqlCast);
|
|
2403
|
+
}
|
|
2387
2404
|
function opKey(node) {
|
|
2388
2405
|
if (node === null || typeof node !== "object" || Array.isArray(node)) return void 0;
|
|
2389
2406
|
const keys = Object.keys(node);
|
|
@@ -2469,17 +2486,18 @@ function mysqlPkHint(op) {
|
|
|
2469
2486
|
function stripMysqlPkHint(sql2) {
|
|
2470
2487
|
return sql2.replace(MYSQL_PK_HINT_RE, "");
|
|
2471
2488
|
}
|
|
2472
|
-
function compileWriteNode(node) {
|
|
2489
|
+
function compileWriteNode(node, dialect = "sqlite") {
|
|
2473
2490
|
const { component, ports } = node;
|
|
2474
2491
|
const table = stringPort(ports, "table");
|
|
2475
2492
|
if (table === void 0) throw new Error(`compileWriteNode: ${component} node requires a literal 'table' port`);
|
|
2493
|
+
const sqlCastMap = collectSqlCast(ports);
|
|
2476
2494
|
switch (component) {
|
|
2477
2495
|
case "Insert": {
|
|
2478
2496
|
const values = collectFamily(ports, "values");
|
|
2479
2497
|
const cols = Object.keys(values);
|
|
2480
2498
|
if (cols.length === 0) throw new Error(`compileWriteNode: Insert requires at least one 'values.<field>' port`);
|
|
2481
2499
|
const sorted = [...cols].sort();
|
|
2482
|
-
const placeholders = sorted.map(() =>
|
|
2500
|
+
const placeholders = sorted.map((c) => castPlaceholder(dialect, sqlCastMap, c)).join(", ");
|
|
2483
2501
|
const sql2 = `INSERT INTO ${table} (${sorted.join(", ")}) VALUES (${placeholders})${returningTail(ports)}`;
|
|
2484
2502
|
const pk = pkPort(ports);
|
|
2485
2503
|
return { sql: sql2, params: sorted.map((c) => values[c]), ...pk !== void 0 ? { pk } : {} };
|
|
@@ -2490,7 +2508,7 @@ function compileWriteNode(node) {
|
|
|
2490
2508
|
if (setCols.length === 0) throw new Error(`compileWriteNode: Update requires at least one 'set.<field>' port`);
|
|
2491
2509
|
const where = lowerWherePort(ports, "Update");
|
|
2492
2510
|
if (where.sql === "") throw new Error(`compileWriteNode: Update requires a 'where' port`);
|
|
2493
|
-
const setClauses = setCols.map((c) => `${c} =
|
|
2511
|
+
const setClauses = setCols.map((c) => `${c} = ${castPlaceholder(dialect, sqlCastMap, c)}`).join(", ");
|
|
2494
2512
|
const sql2 = `UPDATE ${table} SET ${setClauses} WHERE ${where.sql}${returningTail(ports)}`;
|
|
2495
2513
|
return { sql: sql2, params: [...setCols.map((c) => set[c]), ...where.params] };
|
|
2496
2514
|
}
|
|
@@ -2724,6 +2742,8 @@ function gateShortCircuit(gate, result) {
|
|
|
2724
2742
|
return result.changes === 0 ? "unique_collision" : null;
|
|
2725
2743
|
case "insertedElseNoop":
|
|
2726
2744
|
return result.changes === 0 ? "idempotent_duplicate" : null;
|
|
2745
|
+
default:
|
|
2746
|
+
throw new Error(`scp write: unknown gate rule '${String(gate)}'`);
|
|
2727
2747
|
}
|
|
2728
2748
|
}
|
|
2729
2749
|
function executeTransaction(db, plan, input, dialect = "sqlite") {
|
|
@@ -3084,6 +3104,277 @@ import {
|
|
|
3084
3104
|
runBehaviorAsync
|
|
3085
3105
|
} from "behavior-contracts";
|
|
3086
3106
|
|
|
3107
|
+
// src/scp/find-filter-guard.ts
|
|
3108
|
+
var FindFilterLeakError = class extends Error {
|
|
3109
|
+
constructor(modelName, missingKeys) {
|
|
3110
|
+
super(
|
|
3111
|
+
`FIND_FILTER leak: model '${modelName}' declares an implicit scope predicate on [${missingKeys.join(", ")}] that is NOT present in the SCP-authored WHERE. The SCP compile cannot auto-apply FIND_FILTER (no model context at compile time), so the scope predicate would be silently dropped (soft-delete / tenant scope leak). Fold the FIND_FILTER predicates into the authored L.Select({ where }) explicitly (e.g. whereEq/whereIsNull on ${missingKeys.join(", ")}).`
|
|
3112
|
+
);
|
|
3113
|
+
this.modelName = modelName;
|
|
3114
|
+
this.missingKeys = missingKeys;
|
|
3115
|
+
this.name = "FindFilterLeakError";
|
|
3116
|
+
}
|
|
3117
|
+
modelName;
|
|
3118
|
+
missingKeys;
|
|
3119
|
+
};
|
|
3120
|
+
function findFilterKeys(filter) {
|
|
3121
|
+
if (!filter || filter.length === 0) return [];
|
|
3122
|
+
return Object.keys(condsToRecord(filter));
|
|
3123
|
+
}
|
|
3124
|
+
function assertFindFilterFolded(model, authoredWhereKeys) {
|
|
3125
|
+
const required = findFilterKeys(model.FIND_FILTER);
|
|
3126
|
+
if (required.length === 0) return;
|
|
3127
|
+
const present = new Set(authoredWhereKeys);
|
|
3128
|
+
const missing = required.filter((k) => !present.has(k));
|
|
3129
|
+
if (missing.length > 0) {
|
|
3130
|
+
throw new FindFilterLeakError(model.name ?? "<anonymous model>", missing);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// src/scp/coltype.ts
|
|
3135
|
+
function sqlTypeToBcScalar(sqlType) {
|
|
3136
|
+
const t = sqlType.trim().toUpperCase().replace(/\(.*\)/, "").replace(/\b(UNSIGNED|ZEROFILL|PRECISION)\b/g, "").replace(/\s+/g, " ").trim();
|
|
3137
|
+
switch (t) {
|
|
3138
|
+
// int は既定 64bit(i64)。INTEGER も BIGINT も int(bigint を別型にしない — サイズ制限は列制約で表現)。
|
|
3139
|
+
case "INTEGER":
|
|
3140
|
+
case "INT":
|
|
3141
|
+
case "BIGINT":
|
|
3142
|
+
case "SMALLINT":
|
|
3143
|
+
case "TINYINT":
|
|
3144
|
+
case "MEDIUMINT":
|
|
3145
|
+
case "INT2":
|
|
3146
|
+
case "INT4":
|
|
3147
|
+
case "INT8":
|
|
3148
|
+
return "int";
|
|
3149
|
+
// int と real は明確に分離。
|
|
3150
|
+
case "REAL":
|
|
3151
|
+
case "FLOAT":
|
|
3152
|
+
case "DOUBLE":
|
|
3153
|
+
case "FLOAT4":
|
|
3154
|
+
case "FLOAT8":
|
|
3155
|
+
return "float";
|
|
3156
|
+
// decimal は精度保持のため文字列表現。
|
|
3157
|
+
case "DECIMAL":
|
|
3158
|
+
case "NUMERIC":
|
|
3159
|
+
case "MONEY":
|
|
3160
|
+
return "string";
|
|
3161
|
+
case "TEXT":
|
|
3162
|
+
case "VARCHAR":
|
|
3163
|
+
case "CHAR":
|
|
3164
|
+
case "CHARACTER":
|
|
3165
|
+
case "CHARACTER VARYING":
|
|
3166
|
+
case "CLOB":
|
|
3167
|
+
case "UUID":
|
|
3168
|
+
return "string";
|
|
3169
|
+
case "BOOLEAN":
|
|
3170
|
+
case "BOOL":
|
|
3171
|
+
return "bool";
|
|
3172
|
+
// date/timestamp は一旦 string(spec §4.1 owner 決定)。bc に date scalar が無く(behavior-contracts#84
|
|
3173
|
+
// は defer)、DB は行ごとに別 TZ を保存しないので、文字列表現で往復させる。将来 bc date 型導入時に最適化。
|
|
3174
|
+
case "DATE":
|
|
3175
|
+
case "TIMESTAMP":
|
|
3176
|
+
case "TIMESTAMPTZ":
|
|
3177
|
+
case "DATETIME":
|
|
3178
|
+
case "TIME":
|
|
3179
|
+
return "string";
|
|
3180
|
+
// JSON 列の表現は JSON テキスト=文字列(TS のみ利便で object へ de/serialize。列を typed obj 化しない)。
|
|
3181
|
+
case "JSON":
|
|
3182
|
+
case "JSONB":
|
|
3183
|
+
return "string";
|
|
3184
|
+
default:
|
|
3185
|
+
throw new Error(
|
|
3186
|
+
`litedbmodel type system (spec \xA74.1): no bc outType mapping for SQL type '${sqlType}' (normalized '${t}'). Add it to the \xA74.1 table or fix the column \u2014 ambiguous/unknown types are a hard error (no-assume, no-fallback), never defaulted.`
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
function parseSchemaColumnTypes(ddl) {
|
|
3191
|
+
const tables = /* @__PURE__ */ new Map();
|
|
3192
|
+
const CREATE = /^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z0-9_."`[\]]+)\s*\(([\s\S]*)\)\s*;?\s*$/i;
|
|
3193
|
+
const unquote = (s) => s.replace(/^["`[]|["`\]]$/g, "");
|
|
3194
|
+
const TABLE_CONSTRAINT = /^(PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT|KEY|INDEX)\b/i;
|
|
3195
|
+
for (const stmt of ddl) {
|
|
3196
|
+
const m = CREATE.exec(stmt);
|
|
3197
|
+
if (m === null) continue;
|
|
3198
|
+
const table = unquote(m[1].trim());
|
|
3199
|
+
const cols = /* @__PURE__ */ new Map();
|
|
3200
|
+
for (const entry2 of splitTopLevel(m[2])) {
|
|
3201
|
+
const line = entry2.trim();
|
|
3202
|
+
if (line.length === 0) continue;
|
|
3203
|
+
if (TABLE_CONSTRAINT.test(line)) continue;
|
|
3204
|
+
const parsed = parseColumnDef(line);
|
|
3205
|
+
if (parsed === void 0) continue;
|
|
3206
|
+
cols.set(parsed.name, parsed.sqlType);
|
|
3207
|
+
}
|
|
3208
|
+
tables.set(table, cols);
|
|
3209
|
+
}
|
|
3210
|
+
return tables;
|
|
3211
|
+
}
|
|
3212
|
+
function splitTopLevel(body) {
|
|
3213
|
+
const parts = [];
|
|
3214
|
+
let depth = 0;
|
|
3215
|
+
let cur = "";
|
|
3216
|
+
for (const ch of body) {
|
|
3217
|
+
if (ch === "(") depth++;
|
|
3218
|
+
else if (ch === ")") depth--;
|
|
3219
|
+
if (ch === "," && depth === 0) {
|
|
3220
|
+
parts.push(cur);
|
|
3221
|
+
cur = "";
|
|
3222
|
+
} else cur += ch;
|
|
3223
|
+
}
|
|
3224
|
+
if (cur.trim().length > 0) parts.push(cur);
|
|
3225
|
+
return parts;
|
|
3226
|
+
}
|
|
3227
|
+
var TYPE_CONTINUATION = /^(VARYING|PRECISION)$/i;
|
|
3228
|
+
function parseColumnDef(line) {
|
|
3229
|
+
const unquote = (s) => s.replace(/^["`[]|["`\]]$/g, "");
|
|
3230
|
+
const nameMatch = /^([A-Za-z0-9_."`[\]]+)\s+(.*)$/.exec(line);
|
|
3231
|
+
if (nameMatch === null) return void 0;
|
|
3232
|
+
const name = unquote(nameMatch[1]);
|
|
3233
|
+
const rest = nameMatch[2].trim();
|
|
3234
|
+
const typeMatch = /^([A-Za-z0-9_]+(?:\s*\([^)]*\))?)\s*(.*)$/.exec(rest);
|
|
3235
|
+
if (typeMatch === null) return void 0;
|
|
3236
|
+
let sqlType = typeMatch[1].trim();
|
|
3237
|
+
const after = typeMatch[2].trim();
|
|
3238
|
+
const nextWord = /^([A-Za-z]+)/.exec(after);
|
|
3239
|
+
if (nextWord !== null && TYPE_CONTINUATION.test(nextWord[1])) sqlType = `${sqlType} ${nextWord[1]}`;
|
|
3240
|
+
if (sqlType.length === 0) return void 0;
|
|
3241
|
+
return { name, sqlType };
|
|
3242
|
+
}
|
|
3243
|
+
function schemaColumnTypeResolver(ddl) {
|
|
3244
|
+
const tables = parseSchemaColumnTypes(ddl);
|
|
3245
|
+
return (table, column) => {
|
|
3246
|
+
const cols = tables.get(table);
|
|
3247
|
+
if (cols === void 0) {
|
|
3248
|
+
throw new Error(
|
|
3249
|
+
`litedbmodel type system (spec \xA74.1): no schema for table '${table}' \u2014 cannot type its SELECT projection for typed codegen. The DDL (schema.sql SoT) must declare the table (known: ${[...tables.keys()].join(", ") || "<none>"}). No-assume, no-fallback.`
|
|
3250
|
+
);
|
|
3251
|
+
}
|
|
3252
|
+
const sqlType = cols.get(column);
|
|
3253
|
+
if (sqlType === void 0) {
|
|
3254
|
+
throw new Error(
|
|
3255
|
+
`litedbmodel type system (spec \xA74.1): column '${column}' not declared on table '${table}' (known columns: ${[...cols.keys()].join(", ")}). A projected column with no schema type cannot be typed for de-boxed codegen. No-assume, no-fallback.`
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
return sqlType;
|
|
3259
|
+
};
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// src/scp/makesql/outtype.ts
|
|
3263
|
+
function toPortableScalar(scalar, _table, _column) {
|
|
3264
|
+
return scalar;
|
|
3265
|
+
}
|
|
3266
|
+
function isMap2(n) {
|
|
3267
|
+
return "map" in n;
|
|
3268
|
+
}
|
|
3269
|
+
function nodeRef2(n) {
|
|
3270
|
+
return isMap2(n) ? { component: n.map.component, ports: n.map.ports } : { component: n.component, ports: n.ports };
|
|
3271
|
+
}
|
|
3272
|
+
function opKey3(node) {
|
|
3273
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) return void 0;
|
|
3274
|
+
const keys = Object.keys(node);
|
|
3275
|
+
return keys.length === 1 ? keys[0] : void 0;
|
|
3276
|
+
}
|
|
3277
|
+
function stringPort3(ports, name) {
|
|
3278
|
+
const v = ports[name];
|
|
3279
|
+
if (v === void 0) return void 0;
|
|
3280
|
+
if (typeof v !== "string") {
|
|
3281
|
+
throw new Error(`outtype: port '${name}' must be a literal string in the IR (got ${JSON.stringify(v)})`);
|
|
3282
|
+
}
|
|
3283
|
+
return v;
|
|
3284
|
+
}
|
|
3285
|
+
function stringArrayPort2(ports, name) {
|
|
3286
|
+
const v = ports[name];
|
|
3287
|
+
if (v === void 0) return void 0;
|
|
3288
|
+
if (typeof v === "object" && v !== null && "arr" in v && Array.isArray(v.arr)) {
|
|
3289
|
+
return v.arr.map((e) => {
|
|
3290
|
+
if (typeof e !== "string") throw new Error(`outtype: '${name}' entries must be literal strings (got ${JSON.stringify(e)})`);
|
|
3291
|
+
return e;
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
throw new Error(`outtype: port '${name}' must be an {arr:[\u2026]} literal in the IR (got ${JSON.stringify(v)})`);
|
|
3295
|
+
}
|
|
3296
|
+
function rowObjType(table, projection, resolveColumnType, at) {
|
|
3297
|
+
if (projection === void 0 || projection.length === 0) {
|
|
3298
|
+
throw new Error(
|
|
3299
|
+
`outtype: ${at}: SELECT on '${table}' has no explicit column projection ('*' / empty). A typed de-box needs the concrete column list to build the row struct \u2014 spec \xA74.1 is column-typed. Project explicit columns (no-assume, no-fallback).`
|
|
3300
|
+
);
|
|
3301
|
+
}
|
|
3302
|
+
const obj = {};
|
|
3303
|
+
for (const col of projection) {
|
|
3304
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(col)) {
|
|
3305
|
+
throw new Error(
|
|
3306
|
+
`outtype: ${at}: projected column '${col}' on '${table}' is not a bare column name (alias / qualified / computed). Typed de-box (spec \xA74.1) requires bare schema columns so each maps to a SQL type. No-assume, no-fallback.`
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
if (col in obj) throw new Error(`outtype: ${at}: duplicate projected column '${col}' on '${table}'`);
|
|
3310
|
+
obj[col] = toPortableScalar(sqlTypeToBcScalar(resolveColumnType(table, col)), table, col);
|
|
3311
|
+
}
|
|
3312
|
+
return { obj };
|
|
3313
|
+
}
|
|
3314
|
+
function nodeOutType(node, resolveColumnType) {
|
|
3315
|
+
const { component, ports } = nodeRef2(node);
|
|
3316
|
+
const at = `node '${node.id}'`;
|
|
3317
|
+
if (component === "Count") return "int";
|
|
3318
|
+
if (component === "Select") {
|
|
3319
|
+
const table = stringPort3(ports, "table");
|
|
3320
|
+
if (table === void 0) throw new Error(`outtype: ${at}: Select node requires a literal 'table' port`);
|
|
3321
|
+
const projection = stringArrayPort2(ports, "select");
|
|
3322
|
+
const row = rowObjType(table, projection, resolveColumnType, at);
|
|
3323
|
+
if (isMap2(node)) return { arr: { arr: row } };
|
|
3324
|
+
return { arr: row };
|
|
3325
|
+
}
|
|
3326
|
+
throw new Error(
|
|
3327
|
+
`outtype: ${at}: component '${component}' has no typed outType (typed read codegen covers Select/Count only). A write / unknown node cannot be de-boxed here \u2014 no-assume, no-fallback.`
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
function outputType(output, byNode, at) {
|
|
3331
|
+
const op = opKey3(output);
|
|
3332
|
+
if (op === "ref" || op === "refOpt") {
|
|
3333
|
+
const path = output[op];
|
|
3334
|
+
if (!Array.isArray(path) || path.length === 0 || typeof path[0] !== "string") {
|
|
3335
|
+
throw new Error(`outtype: ${at}: a '${op}' output must reference a node id`);
|
|
3336
|
+
}
|
|
3337
|
+
if (path.length !== 1 || path[0].startsWith("$") || path[0] === IN_SENTINEL) {
|
|
3338
|
+
throw new Error(
|
|
3339
|
+
`outtype: ${at}: output ref path ${JSON.stringify(path)} is not a bare node-result reference; field access / input refs at the output position are not supported by the typed de-box path. No-assume, no-fallback.`
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
const t = byNode.get(path[0]);
|
|
3343
|
+
if (t === void 0) throw new Error(`outtype: ${at}: output references node '${path[0]}' which has no derived outType`);
|
|
3344
|
+
return t;
|
|
3345
|
+
}
|
|
3346
|
+
if (op === "obj") {
|
|
3347
|
+
const obj = output.obj;
|
|
3348
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) throw new Error(`outtype: ${at}: 'obj' output must be a record`);
|
|
3349
|
+
const out = {};
|
|
3350
|
+
for (const [k, expr] of Object.entries(obj)) {
|
|
3351
|
+
if (k === "__proto__") throw new Error(`outtype: ${at}: '__proto__' key forbidden in output obj`);
|
|
3352
|
+
out[k] = outputType(expr, byNode, `${at}.obj.${k}`);
|
|
3353
|
+
}
|
|
3354
|
+
return { obj: out };
|
|
3355
|
+
}
|
|
3356
|
+
if (op === "arr") {
|
|
3357
|
+
const arr = output.arr;
|
|
3358
|
+
if (Array.isArray(arr)) throw new Error(`outtype: ${at}: an '{arr:[\u2026]}' literal list output cannot be typed as a homogeneous array. No-assume, no-fallback.`);
|
|
3359
|
+
return { arr: outputType(arr, byNode, `${at}.arr`) };
|
|
3360
|
+
}
|
|
3361
|
+
if (op === "opt") {
|
|
3362
|
+
const inner = output.opt;
|
|
3363
|
+
return { opt: outputType(inner, byNode, `${at}.opt`) };
|
|
3364
|
+
}
|
|
3365
|
+
throw new Error(
|
|
3366
|
+
`outtype: ${at}: output expression ${JSON.stringify(output)} is not a typed composition (ref / obj / arr / opt of node results). The typed de-box path does not build a dynamic Value output tree \u2014 no-assume, no-fallback.`
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
function deriveReadOutTypes(component, resolveColumnType) {
|
|
3370
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
3371
|
+
for (const n of component.body) {
|
|
3372
|
+
if ("cond" in n) continue;
|
|
3373
|
+
byNode.set(n.id, nodeOutType(n, resolveColumnType));
|
|
3374
|
+
}
|
|
3375
|
+
return { byNode, outputType: outputType(component.output, byNode, `component '${component.name}' output`) };
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3087
3378
|
// src/scp/authoring-sql.ts
|
|
3088
3379
|
import { eq, ne, lt, le, gt, ge } from "behavior-contracts";
|
|
3089
3380
|
var BETWEEN_SENTINEL = "@between";
|
|
@@ -3155,13 +3446,13 @@ function whereExists($, sub2, not2 = false) {
|
|
|
3155
3446
|
}
|
|
3156
3447
|
|
|
3157
3448
|
// src/scp/makesql/static-bundle.ts
|
|
3158
|
-
function
|
|
3449
|
+
function isMap3(n) {
|
|
3159
3450
|
return "map" in n;
|
|
3160
3451
|
}
|
|
3161
|
-
function
|
|
3162
|
-
return
|
|
3452
|
+
function nodeRef3(n) {
|
|
3453
|
+
return isMap3(n) ? { component: n.map.component, ports: n.map.ports } : { component: n.component, ports: n.ports };
|
|
3163
3454
|
}
|
|
3164
|
-
function
|
|
3455
|
+
function stringPort4(ports, name) {
|
|
3165
3456
|
const v = ports[name];
|
|
3166
3457
|
if (v === void 0) return void 0;
|
|
3167
3458
|
if (typeof v !== "string") throw new Error(`static-bundle: port '${name}' must be a literal string in the IR (got ${JSON.stringify(v)})`);
|
|
@@ -3175,7 +3466,7 @@ function arrPort2(ports, name) {
|
|
|
3175
3466
|
}
|
|
3176
3467
|
throw new Error(`static-bundle: port '${name}' must be an {arr:[...]} literal in the IR (got ${JSON.stringify(v)})`);
|
|
3177
3468
|
}
|
|
3178
|
-
function
|
|
3469
|
+
function stringArrayPort3(ports, name) {
|
|
3179
3470
|
const arr = arrPort2(ports, name);
|
|
3180
3471
|
if (arr === void 0) return void 0;
|
|
3181
3472
|
return arr.map((e) => {
|
|
@@ -3200,13 +3491,13 @@ function exprArrayPort(ports, name) {
|
|
|
3200
3491
|
if (arr === void 0) return void 0;
|
|
3201
3492
|
return arr;
|
|
3202
3493
|
}
|
|
3203
|
-
function
|
|
3494
|
+
function opKey4(node) {
|
|
3204
3495
|
if (node === null || typeof node !== "object" || Array.isArray(node)) return void 0;
|
|
3205
3496
|
const keys = Object.keys(node);
|
|
3206
3497
|
return keys.length === 1 ? keys[0] : void 0;
|
|
3207
3498
|
}
|
|
3208
3499
|
function columnOf3(node, ctx) {
|
|
3209
|
-
const op =
|
|
3500
|
+
const op = opKey4(node);
|
|
3210
3501
|
if (op !== "ref" && op !== "refOpt") throw new Error(`static-bundle: ${ctx}: the column operand must be a {ref:[\u2026]} / {refOpt:[\u2026]} path`);
|
|
3211
3502
|
const path = node[op];
|
|
3212
3503
|
if (!Array.isArray(path) || path.length === 0 || typeof path[path.length - 1] !== "string") {
|
|
@@ -3215,7 +3506,7 @@ function columnOf3(node, ctx) {
|
|
|
3215
3506
|
return path[path.length - 1];
|
|
3216
3507
|
}
|
|
3217
3508
|
function inSentinelColumn2(node) {
|
|
3218
|
-
const op =
|
|
3509
|
+
const op = opKey4(node);
|
|
3219
3510
|
if (op !== "ref" && op !== "refOpt") return void 0;
|
|
3220
3511
|
const path = node[op];
|
|
3221
3512
|
if (Array.isArray(path) && path.length >= 2 && path[0] === IN_SENTINEL && typeof path[path.length - 1] === "string") {
|
|
@@ -3229,7 +3520,7 @@ function binOperands3(node, op, at) {
|
|
|
3229
3520
|
return [args[0], args[1]];
|
|
3230
3521
|
}
|
|
3231
3522
|
function sentinelPath(node) {
|
|
3232
|
-
const op =
|
|
3523
|
+
const op = opKey4(node);
|
|
3233
3524
|
if (op !== "ref" && op !== "refOpt") return void 0;
|
|
3234
3525
|
const path = node[op];
|
|
3235
3526
|
if (!Array.isArray(path) || path.length === 0) return void 0;
|
|
@@ -3344,7 +3635,7 @@ function inListFragment(dialect, column, valueSpec) {
|
|
|
3344
3635
|
return { sql: v1ConditionText2(conditions, dialect), params: [jsonArraySpec(dialect, valueSpec)] };
|
|
3345
3636
|
}
|
|
3346
3637
|
function lowerWhereMember2(node, dialect, at) {
|
|
3347
|
-
const op =
|
|
3638
|
+
const op = opKey4(node);
|
|
3348
3639
|
if (op === void 0) throw new Error(`static-bundle: ${at}: a where member must be a single-operator Expression node`);
|
|
3349
3640
|
if (op === "and" || op === "or") {
|
|
3350
3641
|
const args = node[op];
|
|
@@ -3384,7 +3675,7 @@ function lowerWherePort2(ports, dialect, at) {
|
|
|
3384
3675
|
const fragments = [];
|
|
3385
3676
|
arr.forEach((m, i) => {
|
|
3386
3677
|
const memberAt = `${at}.where[${i}]`;
|
|
3387
|
-
if (
|
|
3678
|
+
if (opKey4(m) === "cond") {
|
|
3388
3679
|
const cargs = m.cond;
|
|
3389
3680
|
if (!Array.isArray(cargs) || cargs.length !== 3 || cargs[2] !== null) {
|
|
3390
3681
|
throw new Error(`static-bundle: ${memberAt}: a SKIP-optional condition must be {cond:[c, <member>, null]} (else = null)`);
|
|
@@ -3413,18 +3704,18 @@ function limitOffsetTail(kind, dialect, table) {
|
|
|
3413
3704
|
return tail.replace(String(SENTINEL), "?");
|
|
3414
3705
|
}
|
|
3415
3706
|
function compileSelectNode(node, dialect) {
|
|
3416
|
-
const { component, ports } =
|
|
3707
|
+
const { component, ports } = nodeRef3(node);
|
|
3417
3708
|
if (component !== "Select" && component !== "Count") {
|
|
3418
3709
|
throw new Error(`static-bundle: compileSelectNode only compiles Select/Count nodes (got '${component}')`);
|
|
3419
3710
|
}
|
|
3420
|
-
const table =
|
|
3711
|
+
const table = stringPort4(ports, "table");
|
|
3421
3712
|
if (table === void 0) throw new Error(`static-bundle: ${component} node requires a literal 'table' port`);
|
|
3422
3713
|
const isCount = component === "Count";
|
|
3423
|
-
const select =
|
|
3714
|
+
const select = stringArrayPort3(ports, "select");
|
|
3424
3715
|
const cols = isCount ? "COUNT(*) as count" : select && select.length > 0 ? select.join(", ") : "*";
|
|
3425
3716
|
const statements = [];
|
|
3426
3717
|
const cte = mapPort(ports, "cte");
|
|
3427
|
-
const join =
|
|
3718
|
+
const join = stringPort4(ports, "join");
|
|
3428
3719
|
const cteParams = exprArrayPort(ports, "cteParams");
|
|
3429
3720
|
const joinParams = exprArrayPort(ports, "joinParams");
|
|
3430
3721
|
const headDesc = { dialect, tableName: table, select: cols };
|
|
@@ -3437,15 +3728,15 @@ function compileSelectNode(node, dialect) {
|
|
|
3437
3728
|
statements.push({ sql: frag.sql, params: frag.params, whereFragment: true, ...frag.skip !== void 0 ? { skip: frag.skip } : {} });
|
|
3438
3729
|
}
|
|
3439
3730
|
if (isCount) return statements;
|
|
3440
|
-
const group =
|
|
3731
|
+
const group = stringPort4(ports, "group");
|
|
3441
3732
|
if (group !== void 0) statements.push({ sql: selectTail({ dialect, tableName: table, group }, table), params: [] });
|
|
3442
|
-
const order =
|
|
3733
|
+
const order = stringPort4(ports, "order");
|
|
3443
3734
|
if (order !== void 0) statements.push({ sql: selectTail({ dialect, tableName: table, order }, table), params: [] });
|
|
3444
3735
|
if (ports.limit !== void 0) statements.push({ sql: limitOffsetTail("limit", dialect, table), params: [ports.limit] });
|
|
3445
3736
|
if (ports.offset !== void 0) statements.push({ sql: limitOffsetTail("offset", dialect, table), params: [ports.offset] });
|
|
3446
|
-
const forUpdate =
|
|
3737
|
+
const forUpdate = stringPort4(ports, "forUpdate");
|
|
3447
3738
|
if (forUpdate === "true") statements.push({ sql: selectTail({ dialect, tableName: table, forUpdate: true }, table), params: [] });
|
|
3448
|
-
const append =
|
|
3739
|
+
const append = stringPort4(ports, "append");
|
|
3449
3740
|
if (append !== void 0) statements.push({ sql: selectTail({ dialect, tableName: table, append }, table), params: [] });
|
|
3450
3741
|
return statements;
|
|
3451
3742
|
}
|
|
@@ -3453,7 +3744,7 @@ function compilePrimaryNode(node, dialect) {
|
|
|
3453
3744
|
const component = "map" in node ? node.map.component : node.component;
|
|
3454
3745
|
if (component === "Select" || component === "Count") return compileSelectNode(node, dialect);
|
|
3455
3746
|
if (component === "Insert" || component === "Update" || component === "Delete") {
|
|
3456
|
-
const op = compileWriteNode(node);
|
|
3747
|
+
const op = compileWriteNode(node, dialect);
|
|
3457
3748
|
return [{ sql: op.sql, params: op.params }];
|
|
3458
3749
|
}
|
|
3459
3750
|
throw new Error(`static-bundle: catalog component '${component}' has no makeSQL compile (SQL CRUD only: Select/Count/Insert/Update/Delete)`);
|
|
@@ -3464,7 +3755,7 @@ function compileStaticBundle(contract, dialect, entry2) {
|
|
|
3464
3755
|
let primary;
|
|
3465
3756
|
for (const n of component.body) {
|
|
3466
3757
|
if ("cond" in n) continue;
|
|
3467
|
-
if (
|
|
3758
|
+
if (isMap3(n)) continue;
|
|
3468
3759
|
if (primary === void 0) primary = n;
|
|
3469
3760
|
}
|
|
3470
3761
|
if (primary === void 0) throw new Error(`static-bundle: behavior '${component.name}' has no primary catalog node`);
|
|
@@ -3504,13 +3795,13 @@ function collectRefOptHeads(node, into) {
|
|
|
3504
3795
|
}
|
|
3505
3796
|
function skipGuardHeads2(n, into) {
|
|
3506
3797
|
if ("cond" in n) return;
|
|
3507
|
-
const ports =
|
|
3798
|
+
const ports = isMap3(n) ? n.map.ports : n.ports;
|
|
3508
3799
|
const where = ports.where;
|
|
3509
3800
|
if (typeof where !== "object" || where === null || !("arr" in where)) return;
|
|
3510
3801
|
const arr = where.arr;
|
|
3511
3802
|
if (!Array.isArray(arr)) return;
|
|
3512
3803
|
for (const m of arr) {
|
|
3513
|
-
if (
|
|
3804
|
+
if (opKey4(m) === "cond") {
|
|
3514
3805
|
const cond = m.cond;
|
|
3515
3806
|
if (Array.isArray(cond) && cond.length === 3) collectRefHeads3(cond[0], into);
|
|
3516
3807
|
}
|
|
@@ -3522,7 +3813,7 @@ function collectRefHeads3(node, into) {
|
|
|
3522
3813
|
for (const e of node) collectRefHeads3(e, into);
|
|
3523
3814
|
return;
|
|
3524
3815
|
}
|
|
3525
|
-
const op =
|
|
3816
|
+
const op = opKey4(node);
|
|
3526
3817
|
if (op === "ref" || op === "refOpt") {
|
|
3527
3818
|
const path = node[op];
|
|
3528
3819
|
if (Array.isArray(path) && path.length > 0 && typeof path[0] === "string") into.add(path[0]);
|
|
@@ -3530,6 +3821,43 @@ function collectRefHeads3(node, into) {
|
|
|
3530
3821
|
}
|
|
3531
3822
|
for (const v of Object.values(node)) collectRefHeads3(v, into);
|
|
3532
3823
|
}
|
|
3824
|
+
function whereMemberColumns(node, into) {
|
|
3825
|
+
const op = opKey4(node);
|
|
3826
|
+
if (op === void 0) return;
|
|
3827
|
+
if (op === "and" || op === "or") {
|
|
3828
|
+
const args = node[op];
|
|
3829
|
+
if (Array.isArray(args)) for (const m of args) whereMemberColumns(m, into);
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
if (op === "cond") {
|
|
3833
|
+
const cargs = node.cond;
|
|
3834
|
+
if (Array.isArray(cargs) && cargs.length === 3) whereMemberColumns(cargs[1], into);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
if (op === "eq" || op in CMP_OPS3) {
|
|
3838
|
+
const args = node[op];
|
|
3839
|
+
if (!Array.isArray(args) || args.length !== 2) return;
|
|
3840
|
+
const inCol = inSentinelColumn2(args[0]);
|
|
3841
|
+
if (inCol !== void 0) {
|
|
3842
|
+
into.add(inCol);
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
const lhsOp = opKey4(args[0]);
|
|
3846
|
+
if (lhsOp === "ref" || lhsOp === "refOpt") {
|
|
3847
|
+
const path = args[0][lhsOp];
|
|
3848
|
+
if (Array.isArray(path) && path.length > 0 && typeof path[path.length - 1] === "string" && path[0] !== IN_SENTINEL) {
|
|
3849
|
+
into.add(path[path.length - 1]);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
function authoredWhereKeysOf(ports) {
|
|
3855
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3856
|
+
const arr = arrPort2(ports, "where");
|
|
3857
|
+
if (arr === void 0) return keys;
|
|
3858
|
+
for (const m of arr) whereMemberColumns(m, keys);
|
|
3859
|
+
return keys;
|
|
3860
|
+
}
|
|
3533
3861
|
function toDriverParam2(v) {
|
|
3534
3862
|
if (typeof v === "bigint") {
|
|
3535
3863
|
if (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(v);
|
|
@@ -3636,26 +3964,41 @@ function scopePort(statements) {
|
|
|
3636
3964
|
function compileNodeStatements(node, dialect) {
|
|
3637
3965
|
const component = "map" in node ? node.map.component : node.component;
|
|
3638
3966
|
if (component === "Select" || component === "Count") return compileSelectNode(node, dialect);
|
|
3639
|
-
const op = compileWriteNode(node);
|
|
3967
|
+
const op = compileWriteNode(node, dialect);
|
|
3640
3968
|
return [{ sql: op.sql, params: op.params }];
|
|
3641
3969
|
}
|
|
3642
|
-
function compileReadGraph(contract, dialect, entry2) {
|
|
3970
|
+
function compileReadGraph(contract, dialect, entry2, findFilterModel, resolveColumnType) {
|
|
3643
3971
|
const component = entry2 ? contract.components.find((c) => c.name === entry2) : contract.components[0];
|
|
3644
3972
|
if (component === void 0) throw new Error(`static-bundle: entry component '${entry2 ?? "<first>"}' not found in contract`);
|
|
3973
|
+
if (findFilterModel !== void 0) {
|
|
3974
|
+
for (const n of component.body) {
|
|
3975
|
+
if ("cond" in n) continue;
|
|
3976
|
+
const ref = "map" in n ? n.map : n;
|
|
3977
|
+
if (ref.component !== "Select" && ref.component !== "Count") continue;
|
|
3978
|
+
assertFindFilterFolded(findFilterModel, authoredWhereKeysOf(ref.ports));
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3645
3981
|
const statementsById = {};
|
|
3646
3982
|
for (const n of component.body) {
|
|
3647
3983
|
if ("cond" in n) continue;
|
|
3648
3984
|
statementsById[n.id] = compileNodeStatements(n, dialect);
|
|
3649
3985
|
}
|
|
3986
|
+
const outTypes = resolveColumnType !== void 0 ? deriveReadOutTypes(component, resolveColumnType) : void 0;
|
|
3650
3987
|
const surrogateBody = component.body.map((n) => {
|
|
3651
3988
|
if ("cond" in n) return n;
|
|
3652
3989
|
const stmts = statementsById[n.id];
|
|
3990
|
+
const outType = outTypes?.byNode.get(n.id);
|
|
3991
|
+
const outAnn = outType !== void 0 ? { outType } : {};
|
|
3653
3992
|
if ("map" in n) {
|
|
3654
|
-
return { ...n, map: { ...n.map, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } } };
|
|
3993
|
+
return { ...n, ...outAnn, map: { ...n.map, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } } };
|
|
3655
3994
|
}
|
|
3656
|
-
return { ...n, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } };
|
|
3995
|
+
return { ...n, ...outAnn, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } };
|
|
3657
3996
|
});
|
|
3658
|
-
const surrogate = {
|
|
3997
|
+
const surrogate = {
|
|
3998
|
+
...component,
|
|
3999
|
+
body: surrogateBody,
|
|
4000
|
+
...outTypes !== void 0 ? { outputType: outTypes.outputType } : {}
|
|
4001
|
+
};
|
|
3659
4002
|
const ir = { irVersion: 1, exprVersion: 2, components: [surrogate] };
|
|
3660
4003
|
return {
|
|
3661
4004
|
dialect,
|
|
@@ -3860,7 +4203,8 @@ var WRITE_CATALOG_NAMES = [
|
|
|
3860
4203
|
var WRITE_SET = new Set(WRITE_CATALOG_NAMES);
|
|
3861
4204
|
var WRITE_PORT_FAMILIES = [
|
|
3862
4205
|
"values.",
|
|
3863
|
-
"set."
|
|
4206
|
+
"set.",
|
|
4207
|
+
"sqlCast."
|
|
3864
4208
|
];
|
|
3865
4209
|
function assertComponentsInCatalog(components2) {
|
|
3866
4210
|
const errs = [];
|
|
@@ -4123,33 +4467,6 @@ function makeEagerClass(name, fn) {
|
|
|
4123
4467
|
return cls;
|
|
4124
4468
|
}
|
|
4125
4469
|
|
|
4126
|
-
// src/scp/find-filter-guard.ts
|
|
4127
|
-
var FindFilterLeakError = class extends Error {
|
|
4128
|
-
constructor(modelName, missingKeys) {
|
|
4129
|
-
super(
|
|
4130
|
-
`FIND_FILTER leak: model '${modelName}' declares an implicit scope predicate on [${missingKeys.join(", ")}] that is NOT present in the SCP-authored WHERE. The SCP compile cannot auto-apply FIND_FILTER (no model context at compile time), so the scope predicate would be silently dropped (soft-delete / tenant scope leak). Fold the FIND_FILTER predicates into the authored L.Select({ where }) explicitly (e.g. whereEq/whereIsNull on ${missingKeys.join(", ")}).`
|
|
4131
|
-
);
|
|
4132
|
-
this.modelName = modelName;
|
|
4133
|
-
this.missingKeys = missingKeys;
|
|
4134
|
-
this.name = "FindFilterLeakError";
|
|
4135
|
-
}
|
|
4136
|
-
modelName;
|
|
4137
|
-
missingKeys;
|
|
4138
|
-
};
|
|
4139
|
-
function findFilterKeys(filter) {
|
|
4140
|
-
if (!filter || filter.length === 0) return [];
|
|
4141
|
-
return Object.keys(condsToRecord(filter));
|
|
4142
|
-
}
|
|
4143
|
-
function assertFindFilterFolded(model, authoredWhereKeys) {
|
|
4144
|
-
const required = findFilterKeys(model.FIND_FILTER);
|
|
4145
|
-
if (required.length === 0) return;
|
|
4146
|
-
const present = new Set(authoredWhereKeys);
|
|
4147
|
-
const missing = required.filter((k) => !present.has(k));
|
|
4148
|
-
if (missing.length > 0) {
|
|
4149
|
-
throw new FindFilterLeakError(model.name ?? "<anonymous model>", missing);
|
|
4150
|
-
}
|
|
4151
|
-
}
|
|
4152
|
-
|
|
4153
4470
|
// src/scp/relation.ts
|
|
4154
4471
|
var RELATION_KEYS_HEAD = "__keys";
|
|
4155
4472
|
function compileRelationOp(decl) {
|
|
@@ -4402,6 +4719,100 @@ function buildResultSet(rawRows, ops, db, options = {}) {
|
|
|
4402
4719
|
return rows.map((o) => hydrate(o));
|
|
4403
4720
|
}
|
|
4404
4721
|
|
|
4722
|
+
// src/scp/makesql/writeouttype.ts
|
|
4723
|
+
function toPortableScalar2(scalar) {
|
|
4724
|
+
return scalar;
|
|
4725
|
+
}
|
|
4726
|
+
var SHORT_CIRCUIT_TYPE = { obj: { statementId: "string", reason: "string" } };
|
|
4727
|
+
function targetAndReturning(sql2) {
|
|
4728
|
+
const s = sql2.trim();
|
|
4729
|
+
const ins = /^INSERT\s+(?:IGNORE\s+)?INTO\s+([A-Za-z_][A-Za-z0-9_]*)/i.exec(s);
|
|
4730
|
+
const upd = /^UPDATE\s+([A-Za-z_][A-Za-z0-9_]*)/i.exec(s);
|
|
4731
|
+
const del = /^DELETE\s+FROM\s+([A-Za-z_][A-Za-z0-9_]*)/i.exec(s);
|
|
4732
|
+
const m = ins ?? upd ?? del;
|
|
4733
|
+
if (m === null) {
|
|
4734
|
+
throw new Error(
|
|
4735
|
+
`writeouttype: a write body statement's SQL is not a recognized INSERT/UPDATE/DELETE (${JSON.stringify(sql2)}). The typed de-box must read the target table from the base write. No-assume, no-fallback.`
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4738
|
+
const table = m[1];
|
|
4739
|
+
const clean = s.replace(/\s*\/\*scp:pk=[^*]*\*\//i, "");
|
|
4740
|
+
const ret = /\bRETURNING\s+(.+?)\s*$/i.exec(clean);
|
|
4741
|
+
if (ret === null) return { table, returning: null };
|
|
4742
|
+
const cols = ret[1].split(",").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
4743
|
+
return { table, returning: cols };
|
|
4744
|
+
}
|
|
4745
|
+
function rowObjType2(table, returning, resolve) {
|
|
4746
|
+
const obj = {};
|
|
4747
|
+
for (const col of returning) {
|
|
4748
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(col)) {
|
|
4749
|
+
throw new Error(
|
|
4750
|
+
`writeouttype: RETURNING column '${col}' on '${table}' is not a bare column name (alias / qualified / computed). The typed de-box needs bare schema columns so each maps to a SQL type. No-assume, no-fallback.`
|
|
4751
|
+
);
|
|
4752
|
+
}
|
|
4753
|
+
if (col in obj) throw new Error(`writeouttype: duplicate RETURNING column '${col}' on '${table}'`);
|
|
4754
|
+
obj[col] = toPortableScalar2(sqlTypeToBcScalar(resolve(table, col)));
|
|
4755
|
+
}
|
|
4756
|
+
return { obj };
|
|
4757
|
+
}
|
|
4758
|
+
function bodyStatements(plan) {
|
|
4759
|
+
return plan.statements.filter((s) => s.role === "body");
|
|
4760
|
+
}
|
|
4761
|
+
function isBatchPlan(plan) {
|
|
4762
|
+
return plan.entityFrom === null && plan.statements.length > 0 && plan.statements.every((s) => s.gate === void 0 && s.binds === void 0 && s.role === "body");
|
|
4763
|
+
}
|
|
4764
|
+
function writeTargetReturning(plan) {
|
|
4765
|
+
const bodies = bodyStatements(plan);
|
|
4766
|
+
if (bodies.length === 0) {
|
|
4767
|
+
throw new Error(
|
|
4768
|
+
`writeouttype: the transaction plan has no 'body' write statement \u2014 a write bundle must carry at least one base write. Cannot derive its output row type. No-assume, no-fallback.`
|
|
4769
|
+
);
|
|
4770
|
+
}
|
|
4771
|
+
let table;
|
|
4772
|
+
let returning;
|
|
4773
|
+
for (const b of bodies) {
|
|
4774
|
+
const { table: t, returning: r } = targetAndReturning(b.op.sql);
|
|
4775
|
+
if (table === void 0) {
|
|
4776
|
+
table = t;
|
|
4777
|
+
returning = r;
|
|
4778
|
+
continue;
|
|
4779
|
+
}
|
|
4780
|
+
if (t !== table) {
|
|
4781
|
+
throw new Error(
|
|
4782
|
+
`writeouttype: batch write body statements target DIFFERENT tables ('${table}' vs '${t}'); the typed de-box requires a homogeneous RETURNING row shape across a batch. No-assume, no-fallback.`
|
|
4783
|
+
);
|
|
4784
|
+
}
|
|
4785
|
+
if (JSON.stringify(returning) !== JSON.stringify(r)) {
|
|
4786
|
+
throw new Error(
|
|
4787
|
+
`writeouttype: batch write body statements on '${table}' have DIFFERENT RETURNING projections (${JSON.stringify(returning)} vs ${JSON.stringify(r)}); the de-boxed row struct must be identical across a batch. No-assume, no-fallback.`
|
|
4788
|
+
);
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
return { table, returning: returning ?? null };
|
|
4792
|
+
}
|
|
4793
|
+
function deriveWriteOutputType(plan, resolve) {
|
|
4794
|
+
const { table, returning } = writeTargetReturning(plan);
|
|
4795
|
+
const hasReturning = returning !== null;
|
|
4796
|
+
const row = hasReturning ? rowObjType2(table, returning, resolve) : "null";
|
|
4797
|
+
const fields = {
|
|
4798
|
+
committed: "bool",
|
|
4799
|
+
executed: { arr: "string" },
|
|
4800
|
+
shortCircuit: { opt: SHORT_CIRCUIT_TYPE },
|
|
4801
|
+
entity: { opt: row }
|
|
4802
|
+
};
|
|
4803
|
+
if (isBatchPlan(plan) && hasReturning) fields.returnedRows = { opt: { arr: { arr: row } } };
|
|
4804
|
+
return { obj: fields };
|
|
4805
|
+
}
|
|
4806
|
+
function annotateWriteBundleOutType(bundle, resolve) {
|
|
4807
|
+
if (bundle.transaction === void 0) {
|
|
4808
|
+
throw new Error(
|
|
4809
|
+
`writeouttype: bundle '${bundle.name}' carries no transaction plan \u2014 the write de-box types the TransactionResult of a write-Command / batch bundle. No-assume, no-fallback.`
|
|
4810
|
+
);
|
|
4811
|
+
}
|
|
4812
|
+
const outputType2 = deriveWriteOutputType(bundle.transaction, resolve);
|
|
4813
|
+
return { ...bundle, outputType: outputType2 };
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4405
4816
|
// src/scp/runtime.ts
|
|
4406
4817
|
function primaryNodeOf(component) {
|
|
4407
4818
|
for (const n of component.body) {
|
|
@@ -4420,7 +4831,7 @@ function isWriteComponent(component) {
|
|
|
4420
4831
|
function executeBehavior(contract, input, options) {
|
|
4421
4832
|
return executeBundle(compileBundle(contract, options.entry, options.relations, options.dialect), input, options);
|
|
4422
4833
|
}
|
|
4423
|
-
function compileBundle(contract, entry2, relations = [], dialectName = "sqlite") {
|
|
4834
|
+
function compileBundle(contract, entry2, relations = [], dialectName = "sqlite", findFilterModel, resolveColumnType) {
|
|
4424
4835
|
const component = entry2 ? contract.components.find((c) => c.name === entry2) : contract.components[0];
|
|
4425
4836
|
if (component === void 0) throw new Error(`scp runtime: entry component '${entry2 ?? "<first>"}' not found in contract`);
|
|
4426
4837
|
const relationOps = {};
|
|
@@ -4432,7 +4843,7 @@ function compileBundle(contract, entry2, relations = [], dialectName = "sqlite")
|
|
|
4432
4843
|
}
|
|
4433
4844
|
if (isWriteComponent(component)) {
|
|
4434
4845
|
const writeNode = primaryNodeOf(component);
|
|
4435
|
-
const op = compileWriteNode(writeNode);
|
|
4846
|
+
const op = compileWriteNode(writeNode, dialectName);
|
|
4436
4847
|
return {
|
|
4437
4848
|
dialect: dialectName,
|
|
4438
4849
|
name: component.name,
|
|
@@ -4441,7 +4852,7 @@ function compileBundle(contract, entry2, relations = [], dialectName = "sqlite")
|
|
|
4441
4852
|
relations: relationOps
|
|
4442
4853
|
};
|
|
4443
4854
|
}
|
|
4444
|
-
const readGraph = compileReadGraph(contract, dialectName, entry2);
|
|
4855
|
+
const readGraph = compileReadGraph(contract, dialectName, entry2, findFilterModel, resolveColumnType);
|
|
4445
4856
|
return {
|
|
4446
4857
|
dialect: dialectName,
|
|
4447
4858
|
name: readGraph.name,
|
|
@@ -4490,7 +4901,7 @@ function baseWriteNodeOf(component) {
|
|
|
4490
4901
|
}
|
|
4491
4902
|
return writes[0];
|
|
4492
4903
|
}
|
|
4493
|
-
function compileWriteBundle(contract, entry2, writes, phase, dialectName = "sqlite") {
|
|
4904
|
+
function compileWriteBundle(contract, entry2, writes, phase, dialectName = "sqlite", resolveColumnType) {
|
|
4494
4905
|
const component = contract.components.find((c) => c.name === entry2);
|
|
4495
4906
|
if (component === void 0) throw new Error(`scp write: entry component '${entry2}' not found in contract`);
|
|
4496
4907
|
const lifecycle = lifecycleFor(writes, phase);
|
|
@@ -4498,10 +4909,10 @@ function compileWriteBundle(contract, entry2, writes, phase, dialectName = "sqli
|
|
|
4498
4909
|
throw new Error(`scp write: the '${phase}' lifecycle is not declared in the entityWrites save contract`);
|
|
4499
4910
|
}
|
|
4500
4911
|
const writeNode = baseWriteNodeOf(component);
|
|
4501
|
-
const baseOp = dialectName === "mysql" ? mysqlPkHint(compileWriteNode(writeNode)) : compileWriteNode(writeNode);
|
|
4912
|
+
const baseOp = dialectName === "mysql" ? mysqlPkHint(compileWriteNode(writeNode, dialectName)) : compileWriteNode(writeNode, dialectName);
|
|
4502
4913
|
const base = { op: baseOp, label: `${writeNode.component}` };
|
|
4503
4914
|
const plan = deriveTransactionPlan(phase, [base], lifecycle, dialectName);
|
|
4504
|
-
|
|
4915
|
+
const bundle = {
|
|
4505
4916
|
dialect: dialectName,
|
|
4506
4917
|
name: component.name,
|
|
4507
4918
|
statement: { sql: baseOp.sql, params: baseOp.params },
|
|
@@ -4509,6 +4920,7 @@ function compileWriteBundle(contract, entry2, writes, phase, dialectName = "sqli
|
|
|
4509
4920
|
relations: {},
|
|
4510
4921
|
transaction: plan
|
|
4511
4922
|
};
|
|
4923
|
+
return resolveColumnType === void 0 ? bundle : annotateWriteBundleOutType(bundle, resolveColumnType);
|
|
4512
4924
|
}
|
|
4513
4925
|
function compileCompositeWriteBundle(contract, entries, phase, dialectName = "sqlite") {
|
|
4514
4926
|
if (entries.length < 2) {
|
|
@@ -4522,7 +4934,7 @@ function compileCompositeWriteBundle(contract, entries, phase, dialectName = "sq
|
|
|
4522
4934
|
if (component === void 0) throw new Error(`scp write: entry component '${e.entry}' not found in contract`);
|
|
4523
4935
|
if (firstBaseOp === void 0) firstName = component.name;
|
|
4524
4936
|
const writeNode = baseWriteNodeOf(component);
|
|
4525
|
-
const baseOp = compileWriteNode(writeNode);
|
|
4937
|
+
const baseOp = compileWriteNode(writeNode, dialectName);
|
|
4526
4938
|
if (firstBaseOp === void 0) firstBaseOp = baseOp;
|
|
4527
4939
|
bases.push({ op: baseOp, label: `${writeNode.component} ${e.name}`, name: e.name, effects: e.lifecycle.effects });
|
|
4528
4940
|
}
|
|
@@ -4540,7 +4952,7 @@ function flattenBatchOp(node, label) {
|
|
|
4540
4952
|
const assembled = assembleMakeSQL(node);
|
|
4541
4953
|
return { sql: assembled.sql, params: assembled.params, label };
|
|
4542
4954
|
}
|
|
4543
|
-
function compileCreateManyBundle(name, options, dialectName = "sqlite") {
|
|
4955
|
+
function compileCreateManyBundle(name, options, dialectName = "sqlite", resolveColumnType) {
|
|
4544
4956
|
const components2 = compileInsertMany(dialectName, options);
|
|
4545
4957
|
const ops = components2.map((c, i) => {
|
|
4546
4958
|
const flat = flattenBatchOp(c, `createMany group ${i}`);
|
|
@@ -4550,7 +4962,12 @@ function compileCreateManyBundle(name, options, dialectName = "sqlite") {
|
|
|
4550
4962
|
return flat;
|
|
4551
4963
|
});
|
|
4552
4964
|
const plan = deriveBatchPlan("create", ops);
|
|
4553
|
-
|
|
4965
|
+
const head = ops[0];
|
|
4966
|
+
if (head === void 0) {
|
|
4967
|
+
throw new Error(`scp write: compileCreateManyBundle('${name}') produced no INSERT statements (empty createMany grouping) \u2014 an empty-ops batch bundle is a compile bug, not a silent empty-SQL default.`);
|
|
4968
|
+
}
|
|
4969
|
+
const bundle = { dialect: dialectName, name, statement: { sql: head.sql, params: head.params }, optionalHeads: [], relations: {}, transaction: plan };
|
|
4970
|
+
return resolveColumnType === void 0 ? bundle : annotateWriteBundleOutType(bundle, resolveColumnType);
|
|
4554
4971
|
}
|
|
4555
4972
|
function compileUpdateManyBundle(name, options, dialectName = "sqlite") {
|
|
4556
4973
|
const op = flattenBatchOp(compileUpdateMany(dialectName, options), "updateMany");
|
|
@@ -4604,9 +5021,19 @@ function readBundle(bundle, input, options) {
|
|
|
4604
5021
|
// src/scp/codegen.ts
|
|
4605
5022
|
import { generateModule } from "behavior-contracts";
|
|
4606
5023
|
var CODEGEN_LANGUAGES = ["typescript", "python", "go", "rust", "php"];
|
|
5024
|
+
var CODEGEN_EMITTER = {
|
|
5025
|
+
typescript: "typescript-typed",
|
|
5026
|
+
go: "go-typed-native",
|
|
5027
|
+
rust: "rust-typed-native"
|
|
5028
|
+
};
|
|
4607
5029
|
function bundleToPortableIR(bundle) {
|
|
4608
5030
|
if (bundle.readGraph !== void 0) return bundle.readGraph.ir;
|
|
4609
|
-
|
|
5031
|
+
const ir = makeSqlComponentIR(bundle.name);
|
|
5032
|
+
if (bundle.outputType === void 0) return ir;
|
|
5033
|
+
const c = ir.components[0];
|
|
5034
|
+
const body = c.body.map((n, i) => i === 0 ? { ...n, outType: bundle.outputType } : n);
|
|
5035
|
+
const component = { ...c, body, outputType: bundle.outputType };
|
|
5036
|
+
return { ...ir, components: [component] };
|
|
4610
5037
|
}
|
|
4611
5038
|
function companionOf(bundle) {
|
|
4612
5039
|
const base = {
|
|
@@ -4614,27 +5041,172 @@ function companionOf(bundle) {
|
|
|
4614
5041
|
optionalHeads: bundle.optionalHeads,
|
|
4615
5042
|
relations: bundle.relations,
|
|
4616
5043
|
...bundle.readGraph !== void 0 ? { readGraph: bundle.readGraph } : {},
|
|
4617
|
-
...bundle.statement !== void 0 ? { statement: bundle.statement } : {}
|
|
5044
|
+
...bundle.statement !== void 0 ? { statement: bundle.statement } : {},
|
|
5045
|
+
...bundle.outputType !== void 0 ? { outputType: bundle.outputType } : {}
|
|
4618
5046
|
};
|
|
4619
5047
|
return bundle.transaction === void 0 ? base : { ...base, transaction: bundle.transaction };
|
|
4620
5048
|
}
|
|
4621
|
-
|
|
5049
|
+
var SCOPE_PORT2 = "__scope";
|
|
5050
|
+
var TypedNativeCoverageError = class extends Error {
|
|
5051
|
+
constructor(component, reasons) {
|
|
5052
|
+
super(
|
|
5053
|
+
`litedbmodel codegen (#60 m1): component '${component}' is not typed-native-coverable:
|
|
5054
|
+
` + reasons.map((r) => ` - ${r}`).join("\n") + `
|
|
5055
|
+
|
|
5056
|
+
bc's typed-native endpoint (bc#77/#90) fails closed on an uncovered shape \u2014 this is NOT regenerated on the boxed '-typed'/'-typed-raw' emitter (retired from litedbmodel's codegen surface, #60 milestone 1). Report this as a bc#86 coverage gap (array-typed / non-scalar input heads have no native port type yet), or exclude this case from native-codegen benching.`
|
|
5057
|
+
);
|
|
5058
|
+
this.component = component;
|
|
5059
|
+
this.reasons = reasons;
|
|
5060
|
+
this.name = "TypedNativeCoverageError";
|
|
5061
|
+
}
|
|
5062
|
+
component;
|
|
5063
|
+
reasons;
|
|
5064
|
+
};
|
|
5065
|
+
var WHERE_FRAGMENT_COLUMN = /^([A-Za-z_][A-Za-z0-9_]*)\s+(?:=|<>|<=|>=|<|>|LIKE|ILIKE)\s*\?$/;
|
|
5066
|
+
var SELECT_FROM_TABLE = /\bFROM\s+([A-Za-z_][A-Za-z0-9_]*)\b/i;
|
|
5067
|
+
function deriveHeadTypesFromStatements(statements, resolveColumnType) {
|
|
5068
|
+
const byHead = /* @__PURE__ */ new Map();
|
|
5069
|
+
const unresolved = /* @__PURE__ */ new Map();
|
|
5070
|
+
const head = statements[0];
|
|
5071
|
+
const tableMatch = head === void 0 ? null : SELECT_FROM_TABLE.exec(head.sql);
|
|
5072
|
+
const table = tableMatch?.[1];
|
|
5073
|
+
for (const stmt of statements) {
|
|
5074
|
+
if (stmt.whereFragment !== true) continue;
|
|
5075
|
+
for (const param of stmt.params) {
|
|
5076
|
+
const path = refPathOf(param);
|
|
5077
|
+
if (path === void 0 || path.length !== 1) {
|
|
5078
|
+
const named = headNameOf(param);
|
|
5079
|
+
if (named !== void 0) {
|
|
5080
|
+
unresolved.set(
|
|
5081
|
+
named,
|
|
5082
|
+
`input head '${named}' is bound by a non-scalar param shape (${JSON.stringify(param)}) \u2014 bc's typed-native emitter has no native port type for it (string/int/float/bool scalars only).`
|
|
5083
|
+
);
|
|
5084
|
+
}
|
|
5085
|
+
continue;
|
|
5086
|
+
}
|
|
5087
|
+
const headName = path[0];
|
|
5088
|
+
if (table === void 0) {
|
|
5089
|
+
unresolved.set(headName, `input head '${headName}': could not resolve the owning table from the SELECT head SQL ('${head?.sql ?? "<none>"}')`);
|
|
5090
|
+
continue;
|
|
5091
|
+
}
|
|
5092
|
+
const m = WHERE_FRAGMENT_COLUMN.exec(stmt.sql.trim());
|
|
5093
|
+
if (m === null) {
|
|
5094
|
+
unresolved.set(
|
|
5095
|
+
headName,
|
|
5096
|
+
`input head '${headName}': WHERE fragment SQL '${stmt.sql}' is not the closed '<col> <op> ?' shape this codegen-only deriver covers`
|
|
5097
|
+
);
|
|
5098
|
+
continue;
|
|
5099
|
+
}
|
|
5100
|
+
const column = m[1];
|
|
5101
|
+
const sqlType = resolveColumnType(table, column);
|
|
5102
|
+
const scalar = sqlTypeToBcScalar(sqlType);
|
|
5103
|
+
const prior = byHead.get(headName);
|
|
5104
|
+
if (prior !== void 0 && prior !== scalar) {
|
|
5105
|
+
unresolved.set(headName, `input head '${headName}' resolves to conflicting scalar types ('${prior}' vs '${scalar}') across statements`);
|
|
5106
|
+
continue;
|
|
5107
|
+
}
|
|
5108
|
+
byHead.set(headName, scalar);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
return { byHead, unresolved };
|
|
5112
|
+
}
|
|
5113
|
+
function refPathOf(param) {
|
|
5114
|
+
if (param === null || typeof param !== "object" || Array.isArray(param)) return void 0;
|
|
5115
|
+
const keys = Object.keys(param);
|
|
5116
|
+
if (keys.length !== 1 || keys[0] !== "ref" && keys[0] !== "refOpt") return void 0;
|
|
5117
|
+
const path = param[keys[0]];
|
|
5118
|
+
if (!Array.isArray(path) || path.length === 0 || !path.every((s) => typeof s === "string")) return void 0;
|
|
5119
|
+
return path;
|
|
5120
|
+
}
|
|
5121
|
+
function headNameOf(param) {
|
|
5122
|
+
if (param === null || typeof param !== "object" || Array.isArray(param)) return void 0;
|
|
5123
|
+
const obj = param;
|
|
5124
|
+
if ("__jsonArray" in obj) {
|
|
5125
|
+
const inner = refPathOf(obj.__jsonArray);
|
|
5126
|
+
return inner?.[0];
|
|
5127
|
+
}
|
|
5128
|
+
return void 0;
|
|
5129
|
+
}
|
|
5130
|
+
function lowerReadGraphForTypedNative(readGraph, resolveColumnType) {
|
|
5131
|
+
const ir = readGraph.ir;
|
|
5132
|
+
const reasons = [];
|
|
5133
|
+
const components2 = ir.components.map((c) => {
|
|
5134
|
+
const inputPortTypes = /* @__PURE__ */ new Map();
|
|
5135
|
+
const body = c.body.map((n) => {
|
|
5136
|
+
if ("cond" in n) return n;
|
|
5137
|
+
const isMapNode = "map" in n;
|
|
5138
|
+
const ref = isMapNode ? n.map : n;
|
|
5139
|
+
const scopeVal = ref.ports[SCOPE_PORT2];
|
|
5140
|
+
if (scopeVal === void 0 || typeof scopeVal !== "object" || scopeVal === null || !("obj" in scopeVal)) {
|
|
5141
|
+
return n;
|
|
5142
|
+
}
|
|
5143
|
+
const obj = scopeVal.obj;
|
|
5144
|
+
const stmts = readGraph.statementsById[n.id] ?? [];
|
|
5145
|
+
const { byHead, unresolved } = deriveHeadTypesFromStatements(stmts, resolveColumnType);
|
|
5146
|
+
for (const [, reason] of unresolved) reasons.push(`node '${n.id}': ${reason}`);
|
|
5147
|
+
for (const [head, scalar] of byHead) {
|
|
5148
|
+
const prior = inputPortTypes.get(head);
|
|
5149
|
+
if (prior !== void 0 && prior !== scalar) {
|
|
5150
|
+
reasons.push(`input head '${head}' resolves to conflicting scalar types ('${prior}' vs '${scalar}') across nodes`);
|
|
5151
|
+
continue;
|
|
5152
|
+
}
|
|
5153
|
+
inputPortTypes.set(head, scalar);
|
|
5154
|
+
}
|
|
5155
|
+
const newPorts = {};
|
|
5156
|
+
for (const head of Object.keys(obj)) {
|
|
5157
|
+
if (!byHead.has(head) && !unresolved.has(head)) {
|
|
5158
|
+
reasons.push(`node '${n.id}': input head '${head}' referenced by the surrogate scope but not resolved by any WHERE fragment \u2014 cannot type it for native codegen`);
|
|
5159
|
+
continue;
|
|
5160
|
+
}
|
|
5161
|
+
newPorts[head] = { ref: [head] };
|
|
5162
|
+
}
|
|
5163
|
+
return isMapNode ? { ...n, map: { ...n.map, ports: newPorts } } : { ...n, ports: newPorts };
|
|
5164
|
+
});
|
|
5165
|
+
if (reasons.length > 0) throw new TypedNativeCoverageError(c.name, reasons);
|
|
5166
|
+
const inputPorts = {};
|
|
5167
|
+
for (const [name, scalar] of inputPortTypes) {
|
|
5168
|
+
const schema = (c.inputPorts ?? {})[name];
|
|
5169
|
+
inputPorts[name] = schema === void 0 ? { required: true, type: scalar } : { ...schema, type: scalar };
|
|
5170
|
+
}
|
|
5171
|
+
return { ...c, body, inputPorts };
|
|
5172
|
+
});
|
|
5173
|
+
return { ...ir, components: components2 };
|
|
5174
|
+
}
|
|
5175
|
+
function typedEmitterFor(language, registered) {
|
|
4622
5176
|
if (!CODEGEN_LANGUAGES.includes(language)) {
|
|
4623
5177
|
throw new Error(
|
|
4624
5178
|
`litedbmodel codegen: language '${language}' is not a supported target (supported: ${CODEGEN_LANGUAGES.join(", ")})`
|
|
4625
5179
|
);
|
|
4626
5180
|
}
|
|
4627
|
-
|
|
5181
|
+
const emitter = CODEGEN_EMITTER[language];
|
|
5182
|
+
if (emitter === void 0) {
|
|
4628
5183
|
throw new Error(
|
|
4629
|
-
`litedbmodel codegen:
|
|
5184
|
+
`litedbmodel codegen: no READ codegen endpoint for '${language}'. litedbmodel codegen is STATIC de-interpreted codegen (spec \xA79) with NO literal/interpreter/boxed fallback (an unspec'd fallback is invalid). bc registers a READ endpoint only for ${Object.keys(CODEGEN_EMITTER).join("/")} \u2014 a '${language}' endpoint is a bc capability gap. ESCALATE to bc (bc#22 pattern); never substitute the literal emitter.`
|
|
4630
5185
|
);
|
|
4631
5186
|
}
|
|
5187
|
+
if (!registered.includes(emitter)) {
|
|
5188
|
+
throw new Error(
|
|
5189
|
+
`litedbmodel codegen: bc's shared generator does not register the '${emitter}' emitter for '${language}' (bc registered: ${[...registered].sort().join(", ")}). ESCALATE to bc (bc#22 pattern), never fork locally.`
|
|
5190
|
+
);
|
|
5191
|
+
}
|
|
5192
|
+
return emitter;
|
|
5193
|
+
}
|
|
5194
|
+
function codegenEmitterFor(language, registered) {
|
|
5195
|
+
return typedEmitterFor(language, registered);
|
|
4632
5196
|
}
|
|
4633
|
-
function
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
5197
|
+
function isTypedNativeEmitter(emitter) {
|
|
5198
|
+
return emitter.endsWith("-typed-native");
|
|
5199
|
+
}
|
|
5200
|
+
function generateCodegenArtifact(bundle, language, registeredLanguages, resolveColumnType, runtimeImport) {
|
|
5201
|
+
if (bundle.readGraph === void 0) {
|
|
5202
|
+
throw new Error(
|
|
5203
|
+
`litedbmodel codegen (#60 m1): bundle '${bundle.name}' has no readGraph \u2014 WRITE bundles are not codegen-module cases (they stay on the existing write/tx execution path). Only READ bundles are generated through generateCodegenArtifact.`
|
|
5204
|
+
);
|
|
5205
|
+
}
|
|
5206
|
+
const emitter = typedEmitterFor(language, registeredLanguages);
|
|
5207
|
+
const ir = isTypedNativeEmitter(emitter) ? lowerReadGraphForTypedNative(bundle.readGraph, resolveColumnType) : bundleToPortableIR(bundle);
|
|
5208
|
+
const module = generateModule(ir, runtimeImport === void 0 ? { language: emitter } : { language: emitter, runtimeImport });
|
|
5209
|
+
return { language, module, companion: companionOf(bundle), bundle };
|
|
4638
5210
|
}
|
|
4639
5211
|
function codegenExecuteBundleForTest(artifact, input, db) {
|
|
4640
5212
|
return executeBundle(artifact.bundle, input, { db });
|
|
@@ -4667,6 +5239,7 @@ import {
|
|
|
4667
5239
|
} from "behavior-contracts";
|
|
4668
5240
|
export {
|
|
4669
5241
|
CATALOG_NAMES,
|
|
5242
|
+
CODEGEN_EMITTER,
|
|
4670
5243
|
CODEGEN_LANGUAGES,
|
|
4671
5244
|
ENTITY_ROOT,
|
|
4672
5245
|
FindFilterLeakError,
|
|
@@ -4682,6 +5255,7 @@ export {
|
|
|
4682
5255
|
SQLITE,
|
|
4683
5256
|
SemanticBehavior2 as SemanticBehavior,
|
|
4684
5257
|
SqlFailure,
|
|
5258
|
+
TypedNativeCoverageError,
|
|
4685
5259
|
WRITE_CATALOG_NAMES,
|
|
4686
5260
|
WRITE_PORT_FAMILIES,
|
|
4687
5261
|
add,
|
|
@@ -4693,13 +5267,13 @@ export {
|
|
|
4693
5267
|
assertComponentsInCatalog,
|
|
4694
5268
|
assertExprPortable,
|
|
4695
5269
|
assertFindFilterFolded,
|
|
4696
|
-
assertLanguageSupported,
|
|
4697
5270
|
behavior,
|
|
4698
5271
|
buildResultSet,
|
|
4699
5272
|
builderFor,
|
|
4700
5273
|
bundleToPortableIR,
|
|
4701
5274
|
catalogEntry,
|
|
4702
5275
|
coalesce,
|
|
5276
|
+
codegenEmitterFor,
|
|
4703
5277
|
codegenExecuteBundleForTest,
|
|
4704
5278
|
compileAuthoredBehavior,
|
|
4705
5279
|
compileAuthoredNode,
|
|
@@ -4764,6 +5338,7 @@ export {
|
|
|
4764
5338
|
le2 as le,
|
|
4765
5339
|
len,
|
|
4766
5340
|
lifecycleFor,
|
|
5341
|
+
lowerReadGraphForTypedNative,
|
|
4767
5342
|
lt2 as lt,
|
|
4768
5343
|
makeSqlCatalogEntry,
|
|
4769
5344
|
makeSqlComponentIR,
|
|
@@ -4782,6 +5357,7 @@ export {
|
|
|
4782
5357
|
opt,
|
|
4783
5358
|
or,
|
|
4784
5359
|
parseEffectPath,
|
|
5360
|
+
parseSchemaColumnTypes,
|
|
4785
5361
|
pgCastFormatter,
|
|
4786
5362
|
pgPoolExecutor,
|
|
4787
5363
|
publishBehaviors,
|
|
@@ -4794,9 +5370,12 @@ export {
|
|
|
4794
5370
|
renderTxStatement,
|
|
4795
5371
|
resolvePgArrayCast,
|
|
4796
5372
|
runRelationOp,
|
|
5373
|
+
schemaColumnTypeResolver,
|
|
5374
|
+
sqlTypeToBcScalar,
|
|
4797
5375
|
stripMysqlPkHint,
|
|
4798
5376
|
sub,
|
|
4799
5377
|
toDollarPlaceholders,
|
|
5378
|
+
typedEmitterFor,
|
|
4800
5379
|
when,
|
|
4801
5380
|
whereBetween,
|
|
4802
5381
|
whereCast,
|