@sequenceholdings/orm 2.8.3 → 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 +154 -45
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +12 -2
- package/dist/authoring.d.ts +12 -2
- package/dist/authoring.js +154 -45
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +64 -14
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +12 -7
- package/dist/canonical.d.ts +12 -7
- package/dist/canonical.js +64 -14
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +78 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +78 -20
- package/dist/cli.js.map +1 -1
- package/dist/compiler.cjs +77 -20
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +4 -4
- package/dist/compiler.d.ts +4 -4
- package/dist/compiler.js +77 -20
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +161 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +161 -47
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +71 -16
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +9 -9
- package/dist/ir.d.ts +9 -9
- package/dist/ir.js +71 -16
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CU9ThP_E.d.cts → naming-BMZKFO0W.d.cts} +1 -2
- package/dist/{naming-CPfx7EfF.d.ts → naming-DUk7AcXs.d.ts} +1 -2
- package/dist/predicate-v2.cjs +71 -16
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +3 -3
- package/dist/predicate-v2.d.ts +3 -3
- package/dist/predicate-v2.js +71 -16
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +71 -16
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +2 -2
- package/dist/sdl.d.ts +2 -2
- package/dist/sdl.js +71 -16
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen-gql/apollo-hooks.cjs +1 -1
- package/src/codegen-gql/server-fns.cjs +1 -1
- package/templates/orm-namespace/graphql/example/queries.ts +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
|
}
|
|
@@ -1761,7 +1811,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1761
1811
|
table: identifier2,
|
|
1762
1812
|
displayName: zod.z.string().trim().min(1).max(120).optional(),
|
|
1763
1813
|
description: zod.z.string().max(2e3).optional(),
|
|
1764
|
-
/** Table-level rename hint
|
|
1814
|
+
/** Table-level rename hint. */
|
|
1765
1815
|
renamedFrom: identifier2.optional(),
|
|
1766
1816
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
1767
1817
|
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
@@ -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
|
};
|
|
@@ -2425,6 +2478,12 @@ function builtinCell({ comparator, candidates }) {
|
|
|
2425
2478
|
}
|
|
2426
2479
|
};
|
|
2427
2480
|
}
|
|
2481
|
+
function existsInCell(candidate) {
|
|
2482
|
+
return {
|
|
2483
|
+
value: candidate.value,
|
|
2484
|
+
parts: { source: literal(candidate.source), at: candidate.at, by: candidate.by, conflicts: "CAST(NULL AS jsonb)" }
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2428
2487
|
function customCell({ candidates, orderBy, index: index3 }) {
|
|
2429
2488
|
const w = ident(`w_${index3}`);
|
|
2430
2489
|
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
@@ -2443,36 +2502,44 @@ function customCell({ candidates, orderBy, index: index3 }) {
|
|
|
2443
2502
|
}
|
|
2444
2503
|
function buildResolvedQuery({ context, plan }) {
|
|
2445
2504
|
const { qb, relation } = context;
|
|
2446
|
-
const
|
|
2505
|
+
const keyColumns = plan.keys.map((key) => plan.columns[key].columnNameFor(key));
|
|
2447
2506
|
const alias = (source) => ident(`s_${source}`);
|
|
2448
2507
|
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2449
2508
|
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2450
2509
|
const schema = pgCore.getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2510
|
+
const keySource = plan.existsIn === void 0 ? void 0 : sourceByName.get(plan.existsIn);
|
|
2511
|
+
const keyRefs = plan.keys.map((key, index3) => keySource ? `${alias(keySource.name)}.${ident(physical({ source: keySource, property: key }))}` : `"k".${ident(keyColumns[index3])}`);
|
|
2451
2512
|
const keyBranches = plan.plans.map((source) => {
|
|
2452
2513
|
const where = source.kind === "prefetch" ? drizzleOrm.sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : drizzleOrm.sql.raw("");
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
|
|
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}`;
|
|
2519
|
+
});
|
|
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"`;
|
|
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 "))}`;
|
|
2456
2524
|
});
|
|
2457
|
-
const joins = plan.plans.map(
|
|
2458
|
-
(source) => drizzleOrm.sql` LEFT JOIN ${relation(source.table)} AS ${drizzleOrm.sql.raw(alias(source.name))} ON ${drizzleOrm.sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2459
|
-
);
|
|
2460
2525
|
const cells = /* @__PURE__ */ new Map();
|
|
2461
2526
|
plan.rules.forEach((rule, index3) => {
|
|
2462
2527
|
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2463
2528
|
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2464
|
-
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column,
|
|
2465
|
-
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2529
|
+
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, key: plan.keys[0], pgType }));
|
|
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 }));
|
|
2466
2531
|
});
|
|
2467
2532
|
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [drizzleOrm.sql.raw(cell.lateral)] : []);
|
|
2468
|
-
const from = drizzleOrm.sql.join([
|
|
2469
|
-
const selected = {
|
|
2470
|
-
[plan.pk]: drizzleOrm.sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2471
|
-
};
|
|
2533
|
+
const from = drizzleOrm.sql.join([base2, ...joins, ...laterals], drizzleOrm.sql.raw(""));
|
|
2534
|
+
const selected = {};
|
|
2472
2535
|
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2473
|
-
if (property === plan.pk) continue;
|
|
2474
|
-
const cell = cells.get(property);
|
|
2475
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);
|
|
2476
2543
|
selected[property] = drizzleOrm.sql.raw(cell.value).as(columnName);
|
|
2477
2544
|
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2478
2545
|
selected[cellColumnName(property, part)] = drizzleOrm.sql.raw(expression).as(cellColumnName(columnName, part));
|
|
@@ -2486,6 +2553,44 @@ function buildResolvedQuery({ context, plan }) {
|
|
|
2486
2553
|
const query = qb.select(selected).from(from);
|
|
2487
2554
|
return hidden.length > 0 ? query.where(drizzleOrm.sql.raw(hidden.join(" AND "))) : query;
|
|
2488
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
|
+
}
|
|
2489
2594
|
function managedFn(id) {
|
|
2490
2595
|
return { kind: "managed", id };
|
|
2491
2596
|
}
|
|
@@ -2524,26 +2629,28 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2524
2629
|
fail3(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2525
2630
|
}
|
|
2526
2631
|
}
|
|
2527
|
-
const
|
|
2528
|
-
|
|
2529
|
-
const [pk, pkBuilder] = pkEntries[0];
|
|
2530
|
-
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));
|
|
2531
2634
|
if (dataColumns.length === 0) fail3("declare at least one non-key column");
|
|
2532
2635
|
const sourceEntries = Object.entries(config.sources);
|
|
2533
2636
|
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail3("declare exactly one manualSource()");
|
|
2534
2637
|
const plans = sourceEntries.map(
|
|
2535
|
-
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns,
|
|
2638
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, keys, dataColumns, fail: fail3 })
|
|
2536
2639
|
);
|
|
2537
2640
|
const rules = planRules({ config, plans, dataColumns, fail: fail3 });
|
|
2538
2641
|
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2539
2642
|
for (const source of deletedBy) {
|
|
2540
2643
|
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail3(`rows.deletedBy "${source}" must be the manual source`);
|
|
2541
2644
|
}
|
|
2645
|
+
assertCompositeSourceKeys({ keys, plans, fail: fail3 });
|
|
2646
|
+
const existsIn = planExistsIn({ config, plans, fail: fail3 });
|
|
2647
|
+
const notNullColumns = inheritedNotNullColumns({ existsIn, rules });
|
|
2542
2648
|
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2543
2649
|
const viewColumns = {};
|
|
2544
2650
|
for (const [property, builder] of Object.entries(columns)) {
|
|
2545
|
-
|
|
2546
|
-
|
|
2651
|
+
const isKey = keys.includes(property);
|
|
2652
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: isKey || notNullColumns.has(property), primaryKey: isKey });
|
|
2653
|
+
if (isKey) continue;
|
|
2547
2654
|
const columnName = builder.columnNameFor(property);
|
|
2548
2655
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2549
2656
|
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
@@ -2556,9 +2663,10 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2556
2663
|
sources: plans,
|
|
2557
2664
|
rules,
|
|
2558
2665
|
deletedBy,
|
|
2666
|
+
...existsIn ? { existsIn: [existsIn.name] } : {},
|
|
2559
2667
|
policies: writePolicies
|
|
2560
2668
|
};
|
|
2561
|
-
const queryPlan = { columns,
|
|
2669
|
+
const queryPlan = { columns, keys, plans, rules, deletedBy, ...existsIn ? { existsIn: existsIn.name } : {} };
|
|
2562
2670
|
const typedColumns = viewColumns;
|
|
2563
2671
|
const surface = {
|
|
2564
2672
|
...config.description !== void 0 ? { description: config.description } : {},
|
|
@@ -3199,6 +3307,7 @@ function buildCanonicalResolved(resolved) {
|
|
|
3199
3307
|
})),
|
|
3200
3308
|
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
3201
3309
|
deletedBy: [...resolved.deletedBy],
|
|
3310
|
+
...resolved.existsIn !== void 0 ? { existsIn: [...resolved.existsIn] } : {},
|
|
3202
3311
|
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3203
3312
|
policies
|
|
3204
3313
|
};
|
|
@@ -7124,8 +7233,13 @@ var resolvedSourceIrSchema = zod.z.object({
|
|
|
7124
7233
|
}).optional()
|
|
7125
7234
|
});
|
|
7126
7235
|
var resolvedMetaIrSchema = zod.z.object({
|
|
7127
|
-
/**
|
|
7128
|
-
|
|
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(),
|
|
7129
7243
|
/** Source name of the manual source. */
|
|
7130
7244
|
manual: zod.z.string(),
|
|
7131
7245
|
sources: zod.z.array(resolvedSourceIrSchema),
|