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