@sequenceholdings/orm 2.8.4 → 2.8.5
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/authoring.cjs +142 -56
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +5 -1
- package/dist/authoring.d.ts +5 -1
- package/dist/authoring.js +142 -56
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +63 -13
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +11 -6
- package/dist/canonical.d.ts +11 -6
- package/dist/canonical.js +63 -13
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +77 -19
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +77 -19
- package/dist/cli.js.map +1 -1
- package/dist/compiler.cjs +76 -19
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.js +76 -19
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +149 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +149 -58
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +70 -15
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +7 -7
- package/dist/ir.d.ts +7 -7
- package/dist/ir.js +70 -15
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +70 -15
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.js +70 -15
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +70 -15
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.js +70 -15
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1189,6 +1189,48 @@ var policyExpressionSchema = zod.z.union([
|
|
|
1189
1189
|
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
1190
1190
|
})
|
|
1191
1191
|
]);
|
|
1192
|
+
|
|
1193
|
+
// src/canonical/resolved-rows.ts
|
|
1194
|
+
function logicalKey(table) {
|
|
1195
|
+
return (table.primaryKey ?? []).map((columnName) => table.columns.find((c) => c.columnName === columnName)?.name ?? columnName);
|
|
1196
|
+
}
|
|
1197
|
+
function sameKey(key, expected) {
|
|
1198
|
+
return key.length === expected.length && key.every((name2) => expected.includes(name2));
|
|
1199
|
+
}
|
|
1200
|
+
function validateRowIdentity(input) {
|
|
1201
|
+
validateExistsIn(input);
|
|
1202
|
+
if (input.keys.length > 1) validateCompositeKey(input);
|
|
1203
|
+
}
|
|
1204
|
+
function validateExistsIn({ resolved, sourceByName, fail: fail3 }) {
|
|
1205
|
+
const name2 = resolved.existsIn?.[0];
|
|
1206
|
+
if (name2 === void 0) return;
|
|
1207
|
+
if (sourceByName.get(name2)?.kind !== "etl") fail3(`existsIn '${name2}' must be an etl source`);
|
|
1208
|
+
if (resolved.policies.insert !== void 0 || resolved.policies.write !== void 0) {
|
|
1209
|
+
fail3("existsIn allows only an update policy: rows come from the etl source");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function validateCompositeKey({ resolved, keys, tableByName, fail: fail3 }) {
|
|
1213
|
+
const keyList = `(${keys.join(", ")})`;
|
|
1214
|
+
if (resolved.existsIn === void 0) {
|
|
1215
|
+
fail3(`requires exactly one primary-key column: composite key ${keyList} requires existsIn`);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const writes = ["insert", "update", "write"].filter((kind) => resolved.policies[kind] !== void 0);
|
|
1219
|
+
if (writes.length > 0) fail3(`composite key ${keyList} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
1220
|
+
if (resolved.materialized) fail3(`composite key ${keyList} cannot be materialized`);
|
|
1221
|
+
for (const source of resolved.sources) {
|
|
1222
|
+
if (source.kind === "prefetch") {
|
|
1223
|
+
fail3(`composite key ${keyList} cannot have prefetch source '${source.name}'`);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const table = tableByName.get(source.table);
|
|
1227
|
+
if (table && !sameKey(logicalKey(table), keys)) {
|
|
1228
|
+
fail3(`composite key ${keyList} requires ${source.kind} source '${source.name}' to be keyed by it; table '${source.table}' is keyed by (${logicalKey(table).join(", ")})`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/canonical/resolved.ts
|
|
1192
1234
|
function parseDurationMs(input) {
|
|
1193
1235
|
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1194
1236
|
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
@@ -1265,6 +1307,13 @@ var canonicalResolvedSchema = zod.z.object({
|
|
|
1265
1307
|
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
1266
1308
|
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1267
1309
|
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
1310
|
+
/**
|
|
1311
|
+
* `rows.existsIn`: the one etl source whose rows are the table's rows (read
|
|
1312
|
+
* directly as the row source). Only an update policy may write. It is also
|
|
1313
|
+
* what admits a composite primary key: a read-only table keyed exactly like
|
|
1314
|
+
* that source.
|
|
1315
|
+
*/
|
|
1316
|
+
existsIn: zod.z.array(identifier).length(1).optional(),
|
|
1268
1317
|
/**
|
|
1269
1318
|
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1270
1319
|
* data plane keeps in step with `compute`, the internal view holding the
|
|
@@ -1283,8 +1332,8 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1283
1332
|
if (!resolved) return [];
|
|
1284
1333
|
const errors = [];
|
|
1285
1334
|
const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1286
|
-
const
|
|
1287
|
-
if (
|
|
1335
|
+
const keyColumns = (view.primaryKey ?? []).map((columnName) => view.columns.find((c) => c.columnName === columnName)).filter((column) => column !== void 0);
|
|
1336
|
+
if (keyColumns.length === 0 || keyColumns.length !== view.primaryKey?.length) {
|
|
1288
1337
|
fail3("requires exactly one primary-key column");
|
|
1289
1338
|
return errors;
|
|
1290
1339
|
}
|
|
@@ -1292,7 +1341,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1292
1341
|
const scope = {
|
|
1293
1342
|
view,
|
|
1294
1343
|
resolved,
|
|
1295
|
-
|
|
1344
|
+
keys: new Map(keyColumns.map((column) => [column.name, column])),
|
|
1296
1345
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1297
1346
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
1298
1347
|
fail: fail3
|
|
@@ -1306,6 +1355,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1306
1355
|
if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1307
1356
|
else validateSource({ scope, source, table });
|
|
1308
1357
|
}
|
|
1358
|
+
validateRowIdentity({ resolved, keys: [...scope.keys.keys()], sourceByName: scope.sourceByName, tableByName, fail: fail3 });
|
|
1309
1359
|
validateRules(scope);
|
|
1310
1360
|
validateViewCells(scope);
|
|
1311
1361
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
@@ -1335,7 +1385,7 @@ function validateSource({ scope, source, table }) {
|
|
|
1335
1385
|
validatePrefetchFields({ scope, source });
|
|
1336
1386
|
}
|
|
1337
1387
|
function validatePrefetchFields({ scope, source }) {
|
|
1338
|
-
const { fail: fail3,
|
|
1388
|
+
const { fail: fail3, keys } = scope;
|
|
1339
1389
|
if (source.kind !== "prefetch") {
|
|
1340
1390
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1341
1391
|
fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
@@ -1344,7 +1394,7 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1344
1394
|
}
|
|
1345
1395
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1346
1396
|
fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1347
|
-
} else if (source.key
|
|
1397
|
+
} else if (!keys.has(source.key) && !source.columns.includes(source.key)) {
|
|
1348
1398
|
fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1349
1399
|
}
|
|
1350
1400
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -1352,19 +1402,19 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1352
1402
|
}
|
|
1353
1403
|
}
|
|
1354
1404
|
function validateSourceColumns({ scope, source, byName }) {
|
|
1355
|
-
const { fail: fail3,
|
|
1356
|
-
for (const column of [
|
|
1357
|
-
const own = column
|
|
1405
|
+
const { fail: fail3, keys, dataColumns } = scope;
|
|
1406
|
+
for (const column of [...keys.keys(), ...source.columns]) {
|
|
1407
|
+
const own = keys.get(column) ?? dataColumns.get(column);
|
|
1358
1408
|
const theirs = byName.get(column);
|
|
1359
1409
|
if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1360
1410
|
else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1361
1411
|
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
|
|
1362
1412
|
}
|
|
1363
1413
|
}
|
|
1364
|
-
function validateRules({ resolved,
|
|
1414
|
+
function validateRules({ resolved, keys, dataColumns, sourceByName, fail: fail3 }) {
|
|
1365
1415
|
const ruled = /* @__PURE__ */ new Set();
|
|
1366
1416
|
for (const rule of resolved.resolve) {
|
|
1367
|
-
if (!dataColumns.has(rule.column) || rule.column
|
|
1417
|
+
if (!dataColumns.has(rule.column) || keys.has(rule.column)) {
|
|
1368
1418
|
fail3(`resolve rule targets unknown column '${rule.column}'`);
|
|
1369
1419
|
continue;
|
|
1370
1420
|
}
|
|
@@ -1381,7 +1431,7 @@ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fa
|
|
|
1381
1431
|
}
|
|
1382
1432
|
}
|
|
1383
1433
|
for (const column of dataColumns.keys()) {
|
|
1384
|
-
if (column
|
|
1434
|
+
if (!keys.has(column) && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
|
|
1385
1435
|
}
|
|
1386
1436
|
}
|
|
1387
1437
|
var VIEW_CELL_TYPES = {
|
|
@@ -1390,10 +1440,10 @@ var VIEW_CELL_TYPES = {
|
|
|
1390
1440
|
by: "text",
|
|
1391
1441
|
conflicts: "jsonb"
|
|
1392
1442
|
};
|
|
1393
|
-
function validateViewCells({ view, dataColumns,
|
|
1443
|
+
function validateViewCells({ view, dataColumns, keys, fail: fail3 }) {
|
|
1394
1444
|
const expected = /* @__PURE__ */ new Map();
|
|
1395
1445
|
for (const column of dataColumns.values()) {
|
|
1396
|
-
if (column.name
|
|
1446
|
+
if (keys.has(column.name)) continue;
|
|
1397
1447
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1398
1448
|
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1399
1449
|
}
|
|
@@ -2266,8 +2316,9 @@ function planSource(input) {
|
|
|
2266
2316
|
}
|
|
2267
2317
|
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2268
2318
|
function internalTable(input, supplied, system, options = {}) {
|
|
2269
|
-
const { viewName, sourceName, columns,
|
|
2270
|
-
const tableColumns = {
|
|
2319
|
+
const { viewName, sourceName, columns, keys } = input;
|
|
2320
|
+
const tableColumns = {};
|
|
2321
|
+
for (const key of keys) tableColumns[key] = cloneColumn({ builder: columns[key], property: key, notNull: true, primaryKey: true });
|
|
2271
2322
|
for (const property of supplied) {
|
|
2272
2323
|
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2273
2324
|
}
|
|
@@ -2284,12 +2335,14 @@ function assertColumns(input, list2, what) {
|
|
|
2284
2335
|
for (const column of list2) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2285
2336
|
}
|
|
2286
2337
|
function planEtl(input, source) {
|
|
2287
|
-
const { sourceName,
|
|
2338
|
+
const { sourceName, keys, dataColumns, fail: fail3 } = input;
|
|
2288
2339
|
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2289
2340
|
if (!isSeqTable(table) || table.source === void 0) fail3(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2290
2341
|
const synced = table;
|
|
2291
2342
|
if (synced.config.primaryKey?.length === 0) fail3(`etl source "${sourceName}" cannot use an unkeyed synced table for resolved row identity`);
|
|
2292
|
-
|
|
2343
|
+
for (const key of keys) {
|
|
2344
|
+
if (!(key in synced.columns)) fail3(`etl source "${sourceName}" table "${synced.tableName}" has no "${key}" column`);
|
|
2345
|
+
}
|
|
2293
2346
|
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2294
2347
|
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2295
2348
|
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
@@ -2298,9 +2351,9 @@ function planEtl(input, source) {
|
|
|
2298
2351
|
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2299
2352
|
}
|
|
2300
2353
|
function planPrefetch(input, source) {
|
|
2301
|
-
const { sourceName,
|
|
2354
|
+
const { sourceName, keys, fail: fail3 } = input;
|
|
2302
2355
|
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2303
|
-
if (source.key
|
|
2356
|
+
if (!keys.includes(source.key) && !source.columns.includes(source.key)) {
|
|
2304
2357
|
fail3(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2305
2358
|
}
|
|
2306
2359
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -2353,7 +2406,7 @@ var PG_TYPE = {
|
|
|
2353
2406
|
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2354
2407
|
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2355
2408
|
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2356
|
-
function candidateSql({ source, column,
|
|
2409
|
+
function candidateSql({ source, column, key, pgType }) {
|
|
2357
2410
|
const a = ident(`s_${source.name}`);
|
|
2358
2411
|
const name2 = physical({ source, property: column });
|
|
2359
2412
|
const value = `CAST(${a}.${ident(name2)} AS ${pgType})`;
|
|
@@ -2361,7 +2414,7 @@ function candidateSql({ source, column, pk, pgType }) {
|
|
|
2361
2414
|
return {
|
|
2362
2415
|
source: source.name,
|
|
2363
2416
|
value,
|
|
2364
|
-
has: `(${a}.${ident(physical({ source, property:
|
|
2417
|
+
has: `(${a}.${ident(physical({ source, property: key }))} IS NOT NULL)`,
|
|
2365
2418
|
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2366
2419
|
by: "CAST(NULL AS text)"
|
|
2367
2420
|
};
|
|
@@ -2449,39 +2502,44 @@ function customCell({ candidates, orderBy, index: index3 }) {
|
|
|
2449
2502
|
}
|
|
2450
2503
|
function buildResolvedQuery({ context, plan }) {
|
|
2451
2504
|
const { qb, relation } = context;
|
|
2452
|
-
const
|
|
2505
|
+
const keyColumns = plan.keys.map((key) => plan.columns[key].columnNameFor(key));
|
|
2453
2506
|
const alias = (source) => ident(`s_${source}`);
|
|
2454
2507
|
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2455
2508
|
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2456
2509
|
const schema = pgCore.getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2457
2510
|
const keySource = plan.existsIn === void 0 ? void 0 : sourceByName.get(plan.existsIn);
|
|
2458
|
-
const
|
|
2511
|
+
const keyRefs = plan.keys.map((key, index3) => keySource ? `${alias(keySource.name)}.${ident(physical({ source: keySource, property: key }))}` : `"k".${ident(keyColumns[index3])}`);
|
|
2459
2512
|
const keyBranches = plan.plans.map((source) => {
|
|
2460
2513
|
const where = source.kind === "prefetch" ? drizzleOrm.sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : drizzleOrm.sql.raw("");
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
|
|
2514
|
+
const selectedKey = plan.keys.map((key, index3) => {
|
|
2515
|
+
const sourceKey = ident(physical({ source, property: key }));
|
|
2516
|
+
return sourceKey === ident(keyColumns[index3]) ? sourceKey : `${sourceKey} AS ${ident(keyColumns[index3])}`;
|
|
2517
|
+
});
|
|
2518
|
+
return drizzleOrm.sql`SELECT ${drizzleOrm.sql.raw(selectedKey.join(", "))} FROM ${relation(source.table)}${where}`;
|
|
2464
2519
|
});
|
|
2465
2520
|
const base2 = keySource ? drizzleOrm.sql`${relation(keySource.table)} AS ${drizzleOrm.sql.raw(alias(keySource.name))}` : drizzleOrm.sql`(${drizzleOrm.sql.join(keyBranches, drizzleOrm.sql.raw(" UNION "))}) AS "k"`;
|
|
2466
|
-
const joins = plan.plans.filter((source) => source !== keySource).map(
|
|
2467
|
-
|
|
2468
|
-
|
|
2521
|
+
const joins = plan.plans.filter((source) => source !== keySource).map((source) => {
|
|
2522
|
+
const on = plan.keys.map((key, index3) => `${alias(source.name)}.${ident(physical({ source, property: key }))} = ${keyRefs[index3]}`);
|
|
2523
|
+
return drizzleOrm.sql` LEFT JOIN ${relation(source.table)} AS ${drizzleOrm.sql.raw(alias(source.name))} ON ${drizzleOrm.sql.raw(on.join(" AND "))}`;
|
|
2524
|
+
});
|
|
2469
2525
|
const cells = /* @__PURE__ */ new Map();
|
|
2470
2526
|
plan.rules.forEach((rule, index3) => {
|
|
2471
2527
|
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2472
2528
|
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2473
|
-
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column,
|
|
2529
|
+
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, key: plan.keys[0], pgType }));
|
|
2474
2530
|
cells.set(rule.column, keySource && rule.candidates.length === 1 && rule.candidates[0] === keySource.name ? existsInCell(candidates[0]) : rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2475
2531
|
});
|
|
2476
2532
|
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [drizzleOrm.sql.raw(cell.lateral)] : []);
|
|
2477
2533
|
const from = drizzleOrm.sql.join([base2, ...joins, ...laterals], drizzleOrm.sql.raw(""));
|
|
2478
|
-
const selected = {
|
|
2479
|
-
[plan.pk]: drizzleOrm.sql.raw(keyRef).as(pkColumn)
|
|
2480
|
-
};
|
|
2534
|
+
const selected = {};
|
|
2481
2535
|
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2482
|
-
if (property === plan.pk) continue;
|
|
2483
|
-
const cell = cells.get(property);
|
|
2484
2536
|
const columnName = builder.columnNameFor(property);
|
|
2537
|
+
const keyIndex = plan.keys.indexOf(property);
|
|
2538
|
+
if (keyIndex !== -1) {
|
|
2539
|
+
selected[property] = drizzleOrm.sql.raw(keyRefs[keyIndex]).as(columnName);
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
const cell = cells.get(property);
|
|
2485
2543
|
selected[property] = drizzleOrm.sql.raw(cell.value).as(columnName);
|
|
2486
2544
|
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2487
2545
|
selected[cellColumnName(property, part)] = drizzleOrm.sql.raw(expression).as(cellColumnName(columnName, part));
|
|
@@ -2495,6 +2553,44 @@ function buildResolvedQuery({ context, plan }) {
|
|
|
2495
2553
|
const query = qb.select(selected).from(from);
|
|
2496
2554
|
return hidden.length > 0 ? query.where(drizzleOrm.sql.raw(hidden.join(" AND "))) : query;
|
|
2497
2555
|
}
|
|
2556
|
+
|
|
2557
|
+
// src/authoring/resolved-rows.ts
|
|
2558
|
+
function planKeys({ columns, config, fail: fail3 }) {
|
|
2559
|
+
const keys = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey).map(([property]) => property);
|
|
2560
|
+
if (keys.length === 0) fail3("declare exactly one .primaryKey() column");
|
|
2561
|
+
if (keys.length === 1) return keys;
|
|
2562
|
+
const key = `(${keys.join(", ")})`;
|
|
2563
|
+
if (config.rows?.existsIn === void 0) fail3(`declare exactly one .primaryKey() column: composite key ${key} requires rows.existsIn`);
|
|
2564
|
+
const writes = ["insert", "update", "write"].filter((kind) => config.policies[kind] !== void 0);
|
|
2565
|
+
if (writes.length > 0) fail3(`composite key ${key} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
2566
|
+
if (config.materialize) fail3(`composite key ${key} cannot be materialized`);
|
|
2567
|
+
const prefetch = Object.entries(config.sources).find(([, source]) => source.kind === "prefetch");
|
|
2568
|
+
if (prefetch) fail3(`composite key ${key} cannot have prefetch source "${prefetch[0]}"`);
|
|
2569
|
+
return keys;
|
|
2570
|
+
}
|
|
2571
|
+
function assertCompositeSourceKeys({ keys, plans, fail: fail3 }) {
|
|
2572
|
+
if (keys.length === 1) return;
|
|
2573
|
+
for (const plan of plans) {
|
|
2574
|
+
if (plan.kind !== "etl") continue;
|
|
2575
|
+
const sourceKey = declaredPrimaryKeyProperties(plan.table);
|
|
2576
|
+
if (sourceKey.length !== keys.length || !keys.every((key) => sourceKey.includes(key))) {
|
|
2577
|
+
fail3(`composite key (${keys.join(", ")}) requires etl source "${plan.name}" to be keyed by it; table "${plan.table.tableName}" is keyed by (${sourceKey.join(", ")})`);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
function planExistsIn({ config, plans, fail: fail3 }) {
|
|
2582
|
+
const names = config.rows?.existsIn;
|
|
2583
|
+
if (names === void 0) return void 0;
|
|
2584
|
+
if (names.length !== 1) fail3("rows.existsIn names exactly one etl source");
|
|
2585
|
+
const source = plans.find((plan) => plan.name === names[0]);
|
|
2586
|
+
if (source?.kind !== "etl") return fail3(`rows.existsIn "${names[0]}" must be an etl source`);
|
|
2587
|
+
if (config.policies.insert !== void 0 || config.policies.write !== void 0) fail3("rows.existsIn allows only an update policy: rows come from the etl source");
|
|
2588
|
+
return source;
|
|
2589
|
+
}
|
|
2590
|
+
function inheritedNotNullColumns({ existsIn, rules }) {
|
|
2591
|
+
if (!existsIn) return /* @__PURE__ */ new Set();
|
|
2592
|
+
return new Set(rules.filter((rule) => rule.candidates.length === 1 && rule.candidates[0] === existsIn.name).map((rule) => rule.column).filter((column) => existsIn.table.columns[column]?.toCanonicalColumn(column).nullable === false));
|
|
2593
|
+
}
|
|
2498
2594
|
function managedFn(id) {
|
|
2499
2595
|
return { kind: "managed", id };
|
|
2500
2596
|
}
|
|
@@ -2533,28 +2629,28 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2533
2629
|
fail3(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2534
2630
|
}
|
|
2535
2631
|
}
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2538
|
-
const [pk, pkBuilder] = pkEntries[0];
|
|
2539
|
-
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2632
|
+
const keys = planKeys({ columns, config, fail: fail3 });
|
|
2633
|
+
const dataColumns = Object.keys(columns).filter((property) => !keys.includes(property));
|
|
2540
2634
|
if (dataColumns.length === 0) fail3("declare at least one non-key column");
|
|
2541
2635
|
const sourceEntries = Object.entries(config.sources);
|
|
2542
2636
|
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail3("declare exactly one manualSource()");
|
|
2543
2637
|
const plans = sourceEntries.map(
|
|
2544
|
-
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns,
|
|
2638
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, keys, dataColumns, fail: fail3 })
|
|
2545
2639
|
);
|
|
2546
2640
|
const rules = planRules({ config, plans, dataColumns, fail: fail3 });
|
|
2547
2641
|
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2548
2642
|
for (const source of deletedBy) {
|
|
2549
2643
|
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail3(`rows.deletedBy "${source}" must be the manual source`);
|
|
2550
2644
|
}
|
|
2645
|
+
assertCompositeSourceKeys({ keys, plans, fail: fail3 });
|
|
2551
2646
|
const existsIn = planExistsIn({ config, plans, fail: fail3 });
|
|
2552
|
-
const notNullColumns =
|
|
2647
|
+
const notNullColumns = inheritedNotNullColumns({ existsIn, rules });
|
|
2553
2648
|
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2554
2649
|
const viewColumns = {};
|
|
2555
2650
|
for (const [property, builder] of Object.entries(columns)) {
|
|
2556
|
-
|
|
2557
|
-
|
|
2651
|
+
const isKey = keys.includes(property);
|
|
2652
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: isKey || notNullColumns.has(property), primaryKey: isKey });
|
|
2653
|
+
if (isKey) continue;
|
|
2558
2654
|
const columnName = builder.columnNameFor(property);
|
|
2559
2655
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2560
2656
|
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
@@ -2567,9 +2663,10 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2567
2663
|
sources: plans,
|
|
2568
2664
|
rules,
|
|
2569
2665
|
deletedBy,
|
|
2666
|
+
...existsIn ? { existsIn: [existsIn.name] } : {},
|
|
2570
2667
|
policies: writePolicies
|
|
2571
2668
|
};
|
|
2572
|
-
const queryPlan = { columns,
|
|
2669
|
+
const queryPlan = { columns, keys, plans, rules, deletedBy, ...existsIn ? { existsIn: existsIn.name } : {} };
|
|
2573
2670
|
const typedColumns = viewColumns;
|
|
2574
2671
|
const surface = {
|
|
2575
2672
|
...config.description !== void 0 ? { description: config.description } : {},
|
|
@@ -2611,18 +2708,6 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2611
2708
|
}
|
|
2612
2709
|
});
|
|
2613
2710
|
}
|
|
2614
|
-
function planExistsIn({ config, plans, fail: fail3 }) {
|
|
2615
|
-
const names = config.rows?.existsIn;
|
|
2616
|
-
if (names === void 0) return void 0;
|
|
2617
|
-
if (names.length !== 1) fail3("rows.existsIn names exactly one etl source");
|
|
2618
|
-
const source = plans.find((plan) => plan.name === names[0]);
|
|
2619
|
-
if (source?.kind !== "etl") fail3(`rows.existsIn "${names[0]}" must be an etl source`);
|
|
2620
|
-
if (config.policies.insert !== void 0 || config.policies.write !== void 0) fail3("rows.existsIn allows only an update policy: rows come from the etl source");
|
|
2621
|
-
return source;
|
|
2622
|
-
}
|
|
2623
|
-
function inheritedNotNullColumns({ existsIn, rules }) {
|
|
2624
|
-
return new Set(rules.filter((rule) => rule.candidates.length === 1 && rule.candidates[0] === existsIn.name).map((rule) => rule.column).filter((column) => existsIn.table.columns[column]?.toCanonicalColumn(column).nullable === false));
|
|
2625
|
-
}
|
|
2626
2711
|
function planRules({ config, plans, dataColumns, fail: fail3 }) {
|
|
2627
2712
|
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2628
2713
|
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
@@ -3222,6 +3307,7 @@ function buildCanonicalResolved(resolved) {
|
|
|
3222
3307
|
})),
|
|
3223
3308
|
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
3224
3309
|
deletedBy: [...resolved.deletedBy],
|
|
3310
|
+
...resolved.existsIn !== void 0 ? { existsIn: [...resolved.existsIn] } : {},
|
|
3225
3311
|
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3226
3312
|
policies
|
|
3227
3313
|
};
|
|
@@ -7147,8 +7233,13 @@ var resolvedSourceIrSchema = zod.z.object({
|
|
|
7147
7233
|
}).optional()
|
|
7148
7234
|
});
|
|
7149
7235
|
var resolvedMetaIrSchema = zod.z.object({
|
|
7150
|
-
/**
|
|
7151
|
-
|
|
7236
|
+
/**
|
|
7237
|
+
* Logical pk column of the resolved view. Absent for a composite key, which
|
|
7238
|
+
* only a read-only `existsIn` table may declare: it has no writes, prefetch
|
|
7239
|
+
* or materialization (the paths that address one key column), and its reads
|
|
7240
|
+
* use the view's `primaryKey` like any keyed view.
|
|
7241
|
+
*/
|
|
7242
|
+
pk: zod.z.string().optional(),
|
|
7152
7243
|
/** Source name of the manual source. */
|
|
7153
7244
|
manual: zod.z.string(),
|
|
7154
7245
|
sources: zod.z.array(resolvedSourceIrSchema),
|