@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/authoring.d.cts
CHANGED
|
@@ -286,7 +286,7 @@ declare const nonNullFirst: (candidates: readonly string[]) => Comparator;
|
|
|
286
286
|
* wins; `ord` is always appended as the final tie-break.
|
|
287
287
|
*
|
|
288
288
|
* ```ts
|
|
289
|
-
* custom(['edits', '
|
|
289
|
+
* custom(['edits', 'vendor'], "CASE WHEN source = 'vendor' AND value IS NOT NULL THEN 0 ELSE 1 END")
|
|
290
290
|
* ```
|
|
291
291
|
*/
|
|
292
292
|
declare const custom: (candidates: readonly string[], orderBy: string) => Comparator;
|
|
@@ -305,6 +305,14 @@ interface ResolvedTableConfig {
|
|
|
305
305
|
default?: Comparator;
|
|
306
306
|
rows?: {
|
|
307
307
|
deletedBy?: readonly string[];
|
|
308
|
+
/**
|
|
309
|
+
* A row exists only while this etl source has its key; edits and other
|
|
310
|
+
* sources only change values. A column resolved from this source alone
|
|
311
|
+
* keeps the synced column's NOT NULL. Exactly one etl source. A read-only
|
|
312
|
+
* table (no write policy) may then declare a composite key: several
|
|
313
|
+
* `.primaryKey()` columns that are exactly this source's primary key.
|
|
314
|
+
*/
|
|
315
|
+
existsIn?: readonly string[];
|
|
308
316
|
};
|
|
309
317
|
links?: Record<string, SeqLink>;
|
|
310
318
|
/** Reads use `read`; writes are evaluated against the resolved row. */
|
|
@@ -338,6 +346,8 @@ interface ResolvedViewConfig {
|
|
|
338
346
|
orderBy?: string;
|
|
339
347
|
}>;
|
|
340
348
|
readonly deletedBy: readonly string[];
|
|
349
|
+
/** `rows.existsIn`: the etl source whose rows are the table's rows. */
|
|
350
|
+
readonly existsIn?: readonly string[];
|
|
341
351
|
readonly policies: Omit<ResolvedTablePoliciesInput, 'read'>;
|
|
342
352
|
}
|
|
343
353
|
interface ResolvedSourcePlan {
|
|
@@ -547,7 +557,7 @@ interface ForeignKeyInput {
|
|
|
547
557
|
interface TableConfig {
|
|
548
558
|
displayName?: string;
|
|
549
559
|
description?: string;
|
|
550
|
-
/** Table-level migration rename hint
|
|
560
|
+
/** Table-level migration rename hint. */
|
|
551
561
|
renamedFrom?: string;
|
|
552
562
|
/** Composite primary key: property names (or column names). Mutually exclusive with column-level `.primaryKey()`. */
|
|
553
563
|
primaryKey?: readonly string[];
|
package/dist/authoring.d.ts
CHANGED
|
@@ -286,7 +286,7 @@ declare const nonNullFirst: (candidates: readonly string[]) => Comparator;
|
|
|
286
286
|
* wins; `ord` is always appended as the final tie-break.
|
|
287
287
|
*
|
|
288
288
|
* ```ts
|
|
289
|
-
* custom(['edits', '
|
|
289
|
+
* custom(['edits', 'vendor'], "CASE WHEN source = 'vendor' AND value IS NOT NULL THEN 0 ELSE 1 END")
|
|
290
290
|
* ```
|
|
291
291
|
*/
|
|
292
292
|
declare const custom: (candidates: readonly string[], orderBy: string) => Comparator;
|
|
@@ -305,6 +305,14 @@ interface ResolvedTableConfig {
|
|
|
305
305
|
default?: Comparator;
|
|
306
306
|
rows?: {
|
|
307
307
|
deletedBy?: readonly string[];
|
|
308
|
+
/**
|
|
309
|
+
* A row exists only while this etl source has its key; edits and other
|
|
310
|
+
* sources only change values. A column resolved from this source alone
|
|
311
|
+
* keeps the synced column's NOT NULL. Exactly one etl source. A read-only
|
|
312
|
+
* table (no write policy) may then declare a composite key: several
|
|
313
|
+
* `.primaryKey()` columns that are exactly this source's primary key.
|
|
314
|
+
*/
|
|
315
|
+
existsIn?: readonly string[];
|
|
308
316
|
};
|
|
309
317
|
links?: Record<string, SeqLink>;
|
|
310
318
|
/** Reads use `read`; writes are evaluated against the resolved row. */
|
|
@@ -338,6 +346,8 @@ interface ResolvedViewConfig {
|
|
|
338
346
|
orderBy?: string;
|
|
339
347
|
}>;
|
|
340
348
|
readonly deletedBy: readonly string[];
|
|
349
|
+
/** `rows.existsIn`: the etl source whose rows are the table's rows. */
|
|
350
|
+
readonly existsIn?: readonly string[];
|
|
341
351
|
readonly policies: Omit<ResolvedTablePoliciesInput, 'read'>;
|
|
342
352
|
}
|
|
343
353
|
interface ResolvedSourcePlan {
|
|
@@ -547,7 +557,7 @@ interface ForeignKeyInput {
|
|
|
547
557
|
interface TableConfig {
|
|
548
558
|
displayName?: string;
|
|
549
559
|
description?: string;
|
|
550
|
-
/** Table-level migration rename hint
|
|
560
|
+
/** Table-level migration rename hint. */
|
|
551
561
|
renamedFrom?: string;
|
|
552
562
|
/** Composite primary key: property names (or column names). Mutually exclusive with column-level `.primaryKey()`. */
|
|
553
563
|
primaryKey?: readonly string[];
|
package/dist/authoring.js
CHANGED
|
@@ -1123,6 +1123,48 @@ var policyExpressionSchema = z.union([
|
|
|
1123
1123
|
variants: z.array(policyVariantSchema).max(10).default([])
|
|
1124
1124
|
})
|
|
1125
1125
|
]);
|
|
1126
|
+
|
|
1127
|
+
// src/canonical/resolved-rows.ts
|
|
1128
|
+
function logicalKey(table) {
|
|
1129
|
+
return (table.primaryKey ?? []).map((columnName) => table.columns.find((c) => c.columnName === columnName)?.name ?? columnName);
|
|
1130
|
+
}
|
|
1131
|
+
function sameKey(key, expected) {
|
|
1132
|
+
return key.length === expected.length && key.every((name2) => expected.includes(name2));
|
|
1133
|
+
}
|
|
1134
|
+
function validateRowIdentity(input) {
|
|
1135
|
+
validateExistsIn(input);
|
|
1136
|
+
if (input.keys.length > 1) validateCompositeKey(input);
|
|
1137
|
+
}
|
|
1138
|
+
function validateExistsIn({ resolved, sourceByName, fail }) {
|
|
1139
|
+
const name2 = resolved.existsIn?.[0];
|
|
1140
|
+
if (name2 === void 0) return;
|
|
1141
|
+
if (sourceByName.get(name2)?.kind !== "etl") fail(`existsIn '${name2}' must be an etl source`);
|
|
1142
|
+
if (resolved.policies.insert !== void 0 || resolved.policies.write !== void 0) {
|
|
1143
|
+
fail("existsIn allows only an update policy: rows come from the etl source");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function validateCompositeKey({ resolved, keys, tableByName, fail }) {
|
|
1147
|
+
const keyList = `(${keys.join(", ")})`;
|
|
1148
|
+
if (resolved.existsIn === void 0) {
|
|
1149
|
+
fail(`requires exactly one primary-key column: composite key ${keyList} requires existsIn`);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const writes = ["insert", "update", "write"].filter((kind) => resolved.policies[kind] !== void 0);
|
|
1153
|
+
if (writes.length > 0) fail(`composite key ${keyList} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
1154
|
+
if (resolved.materialized) fail(`composite key ${keyList} cannot be materialized`);
|
|
1155
|
+
for (const source of resolved.sources) {
|
|
1156
|
+
if (source.kind === "prefetch") {
|
|
1157
|
+
fail(`composite key ${keyList} cannot have prefetch source '${source.name}'`);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
const table = tableByName.get(source.table);
|
|
1161
|
+
if (table && !sameKey(logicalKey(table), keys)) {
|
|
1162
|
+
fail(`composite key ${keyList} requires ${source.kind} source '${source.name}' to be keyed by it; table '${source.table}' is keyed by (${logicalKey(table).join(", ")})`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/canonical/resolved.ts
|
|
1126
1168
|
function parseDurationMs(input) {
|
|
1127
1169
|
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1128
1170
|
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
@@ -1199,6 +1241,13 @@ var canonicalResolvedSchema = z.object({
|
|
|
1199
1241
|
resolve: z.array(canonicalResolveRuleSchema).max(500),
|
|
1200
1242
|
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1201
1243
|
deletedBy: z.array(identifier).max(1).default([]),
|
|
1244
|
+
/**
|
|
1245
|
+
* `rows.existsIn`: the one etl source whose rows are the table's rows (read
|
|
1246
|
+
* directly as the row source). Only an update policy may write. It is also
|
|
1247
|
+
* what admits a composite primary key: a read-only table keyed exactly like
|
|
1248
|
+
* that source.
|
|
1249
|
+
*/
|
|
1250
|
+
existsIn: z.array(identifier).length(1).optional(),
|
|
1202
1251
|
/**
|
|
1203
1252
|
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1204
1253
|
* data plane keeps in step with `compute`, the internal view holding the
|
|
@@ -1217,8 +1266,8 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1217
1266
|
if (!resolved) return [];
|
|
1218
1267
|
const errors = [];
|
|
1219
1268
|
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1220
|
-
const
|
|
1221
|
-
if (
|
|
1269
|
+
const keyColumns = (view.primaryKey ?? []).map((columnName) => view.columns.find((c) => c.columnName === columnName)).filter((column) => column !== void 0);
|
|
1270
|
+
if (keyColumns.length === 0 || keyColumns.length !== view.primaryKey?.length) {
|
|
1222
1271
|
fail("requires exactly one primary-key column");
|
|
1223
1272
|
return errors;
|
|
1224
1273
|
}
|
|
@@ -1226,7 +1275,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1226
1275
|
const scope = {
|
|
1227
1276
|
view,
|
|
1228
1277
|
resolved,
|
|
1229
|
-
|
|
1278
|
+
keys: new Map(keyColumns.map((column) => [column.name, column])),
|
|
1230
1279
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1231
1280
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
1232
1281
|
fail
|
|
@@ -1240,6 +1289,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1240
1289
|
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1241
1290
|
else validateSource({ scope, source, table });
|
|
1242
1291
|
}
|
|
1292
|
+
validateRowIdentity({ resolved, keys: [...scope.keys.keys()], sourceByName: scope.sourceByName, tableByName, fail });
|
|
1243
1293
|
validateRules(scope);
|
|
1244
1294
|
validateViewCells(scope);
|
|
1245
1295
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
@@ -1269,7 +1319,7 @@ function validateSource({ scope, source, table }) {
|
|
|
1269
1319
|
validatePrefetchFields({ scope, source });
|
|
1270
1320
|
}
|
|
1271
1321
|
function validatePrefetchFields({ scope, source }) {
|
|
1272
|
-
const { fail,
|
|
1322
|
+
const { fail, keys } = scope;
|
|
1273
1323
|
if (source.kind !== "prefetch") {
|
|
1274
1324
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1275
1325
|
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
@@ -1278,7 +1328,7 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1278
1328
|
}
|
|
1279
1329
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1280
1330
|
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1281
|
-
} else if (source.key
|
|
1331
|
+
} else if (!keys.has(source.key) && !source.columns.includes(source.key)) {
|
|
1282
1332
|
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1283
1333
|
}
|
|
1284
1334
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -1286,19 +1336,19 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1286
1336
|
}
|
|
1287
1337
|
}
|
|
1288
1338
|
function validateSourceColumns({ scope, source, byName }) {
|
|
1289
|
-
const { fail,
|
|
1290
|
-
for (const column of [
|
|
1291
|
-
const own = column
|
|
1339
|
+
const { fail, keys, dataColumns } = scope;
|
|
1340
|
+
for (const column of [...keys.keys(), ...source.columns]) {
|
|
1341
|
+
const own = keys.get(column) ?? dataColumns.get(column);
|
|
1292
1342
|
const theirs = byName.get(column);
|
|
1293
1343
|
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1294
1344
|
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1295
1345
|
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
1296
1346
|
}
|
|
1297
1347
|
}
|
|
1298
|
-
function validateRules({ resolved,
|
|
1348
|
+
function validateRules({ resolved, keys, dataColumns, sourceByName, fail }) {
|
|
1299
1349
|
const ruled = /* @__PURE__ */ new Set();
|
|
1300
1350
|
for (const rule of resolved.resolve) {
|
|
1301
|
-
if (!dataColumns.has(rule.column) || rule.column
|
|
1351
|
+
if (!dataColumns.has(rule.column) || keys.has(rule.column)) {
|
|
1302
1352
|
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
1303
1353
|
continue;
|
|
1304
1354
|
}
|
|
@@ -1315,7 +1365,7 @@ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail })
|
|
|
1315
1365
|
}
|
|
1316
1366
|
}
|
|
1317
1367
|
for (const column of dataColumns.keys()) {
|
|
1318
|
-
if (column
|
|
1368
|
+
if (!keys.has(column) && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
1319
1369
|
}
|
|
1320
1370
|
}
|
|
1321
1371
|
var VIEW_CELL_TYPES = {
|
|
@@ -1324,10 +1374,10 @@ var VIEW_CELL_TYPES = {
|
|
|
1324
1374
|
by: "text",
|
|
1325
1375
|
conflicts: "jsonb"
|
|
1326
1376
|
};
|
|
1327
|
-
function validateViewCells({ view, dataColumns,
|
|
1377
|
+
function validateViewCells({ view, dataColumns, keys, fail }) {
|
|
1328
1378
|
const expected = /* @__PURE__ */ new Map();
|
|
1329
1379
|
for (const column of dataColumns.values()) {
|
|
1330
|
-
if (column.name
|
|
1380
|
+
if (keys.has(column.name)) continue;
|
|
1331
1381
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1332
1382
|
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1333
1383
|
}
|
|
@@ -1695,7 +1745,7 @@ var canonicalTableSchema = z.object({
|
|
|
1695
1745
|
table: identifier2,
|
|
1696
1746
|
displayName: z.string().trim().min(1).max(120).optional(),
|
|
1697
1747
|
description: z.string().max(2e3).optional(),
|
|
1698
|
-
/** Table-level rename hint
|
|
1748
|
+
/** Table-level rename hint. */
|
|
1699
1749
|
renamedFrom: identifier2.optional(),
|
|
1700
1750
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
1701
1751
|
source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
@@ -2200,8 +2250,9 @@ function planSource(input) {
|
|
|
2200
2250
|
}
|
|
2201
2251
|
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2202
2252
|
function internalTable(input, supplied, system, options = {}) {
|
|
2203
|
-
const { viewName, sourceName, columns,
|
|
2204
|
-
const tableColumns = {
|
|
2253
|
+
const { viewName, sourceName, columns, keys } = input;
|
|
2254
|
+
const tableColumns = {};
|
|
2255
|
+
for (const key of keys) tableColumns[key] = cloneColumn({ builder: columns[key], property: key, notNull: true, primaryKey: true });
|
|
2205
2256
|
for (const property of supplied) {
|
|
2206
2257
|
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2207
2258
|
}
|
|
@@ -2218,12 +2269,14 @@ function assertColumns(input, list, what) {
|
|
|
2218
2269
|
for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2219
2270
|
}
|
|
2220
2271
|
function planEtl(input, source) {
|
|
2221
|
-
const { sourceName,
|
|
2272
|
+
const { sourceName, keys, dataColumns, fail } = input;
|
|
2222
2273
|
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2223
2274
|
if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2224
2275
|
const synced = table;
|
|
2225
2276
|
if (synced.config.primaryKey?.length === 0) fail(`etl source "${sourceName}" cannot use an unkeyed synced table for resolved row identity`);
|
|
2226
|
-
|
|
2277
|
+
for (const key of keys) {
|
|
2278
|
+
if (!(key in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${key}" column`);
|
|
2279
|
+
}
|
|
2227
2280
|
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2228
2281
|
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2229
2282
|
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
@@ -2232,9 +2285,9 @@ function planEtl(input, source) {
|
|
|
2232
2285
|
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2233
2286
|
}
|
|
2234
2287
|
function planPrefetch(input, source) {
|
|
2235
|
-
const { sourceName,
|
|
2288
|
+
const { sourceName, keys, fail } = input;
|
|
2236
2289
|
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2237
|
-
if (source.key
|
|
2290
|
+
if (!keys.includes(source.key) && !source.columns.includes(source.key)) {
|
|
2238
2291
|
fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2239
2292
|
}
|
|
2240
2293
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -2287,7 +2340,7 @@ var PG_TYPE = {
|
|
|
2287
2340
|
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2288
2341
|
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2289
2342
|
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2290
|
-
function candidateSql({ source, column,
|
|
2343
|
+
function candidateSql({ source, column, key, pgType }) {
|
|
2291
2344
|
const a = ident(`s_${source.name}`);
|
|
2292
2345
|
const name2 = physical({ source, property: column });
|
|
2293
2346
|
const value = `CAST(${a}.${ident(name2)} AS ${pgType})`;
|
|
@@ -2295,7 +2348,7 @@ function candidateSql({ source, column, pk, pgType }) {
|
|
|
2295
2348
|
return {
|
|
2296
2349
|
source: source.name,
|
|
2297
2350
|
value,
|
|
2298
|
-
has: `(${a}.${ident(physical({ source, property:
|
|
2351
|
+
has: `(${a}.${ident(physical({ source, property: key }))} IS NOT NULL)`,
|
|
2299
2352
|
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2300
2353
|
by: "CAST(NULL AS text)"
|
|
2301
2354
|
};
|
|
@@ -2359,6 +2412,12 @@ function builtinCell({ comparator, candidates }) {
|
|
|
2359
2412
|
}
|
|
2360
2413
|
};
|
|
2361
2414
|
}
|
|
2415
|
+
function existsInCell(candidate) {
|
|
2416
|
+
return {
|
|
2417
|
+
value: candidate.value,
|
|
2418
|
+
parts: { source: literal(candidate.source), at: candidate.at, by: candidate.by, conflicts: "CAST(NULL AS jsonb)" }
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2362
2421
|
function customCell({ candidates, orderBy, index }) {
|
|
2363
2422
|
const w = ident(`w_${index}`);
|
|
2364
2423
|
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
@@ -2377,36 +2436,44 @@ function customCell({ candidates, orderBy, index }) {
|
|
|
2377
2436
|
}
|
|
2378
2437
|
function buildResolvedQuery({ context, plan }) {
|
|
2379
2438
|
const { qb, relation } = context;
|
|
2380
|
-
const
|
|
2439
|
+
const keyColumns = plan.keys.map((key) => plan.columns[key].columnNameFor(key));
|
|
2381
2440
|
const alias = (source) => ident(`s_${source}`);
|
|
2382
2441
|
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2383
2442
|
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2384
2443
|
const schema = getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2444
|
+
const keySource = plan.existsIn === void 0 ? void 0 : sourceByName.get(plan.existsIn);
|
|
2445
|
+
const keyRefs = plan.keys.map((key, index) => keySource ? `${alias(keySource.name)}.${ident(physical({ source: keySource, property: key }))}` : `"k".${ident(keyColumns[index])}`);
|
|
2385
2446
|
const keyBranches = plan.plans.map((source) => {
|
|
2386
2447
|
const where = source.kind === "prefetch" ? sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : sql.raw("");
|
|
2387
|
-
const
|
|
2388
|
-
|
|
2389
|
-
|
|
2448
|
+
const selectedKey = plan.keys.map((key, index) => {
|
|
2449
|
+
const sourceKey = ident(physical({ source, property: key }));
|
|
2450
|
+
return sourceKey === ident(keyColumns[index]) ? sourceKey : `${sourceKey} AS ${ident(keyColumns[index])}`;
|
|
2451
|
+
});
|
|
2452
|
+
return sql`SELECT ${sql.raw(selectedKey.join(", "))} FROM ${relation(source.table)}${where}`;
|
|
2453
|
+
});
|
|
2454
|
+
const base2 = keySource ? sql`${relation(keySource.table)} AS ${sql.raw(alias(keySource.name))}` : sql`(${sql.join(keyBranches, sql.raw(" UNION "))}) AS "k"`;
|
|
2455
|
+
const joins = plan.plans.filter((source) => source !== keySource).map((source) => {
|
|
2456
|
+
const on = plan.keys.map((key, index) => `${alias(source.name)}.${ident(physical({ source, property: key }))} = ${keyRefs[index]}`);
|
|
2457
|
+
return sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(on.join(" AND "))}`;
|
|
2390
2458
|
});
|
|
2391
|
-
const joins = plan.plans.map(
|
|
2392
|
-
(source) => sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2393
|
-
);
|
|
2394
2459
|
const cells = /* @__PURE__ */ new Map();
|
|
2395
2460
|
plan.rules.forEach((rule, index) => {
|
|
2396
2461
|
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2397
2462
|
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2398
|
-
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column,
|
|
2399
|
-
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2463
|
+
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, key: plan.keys[0], pgType }));
|
|
2464
|
+
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 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2400
2465
|
});
|
|
2401
2466
|
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [sql.raw(cell.lateral)] : []);
|
|
2402
|
-
const from = sql.join([
|
|
2403
|
-
const selected = {
|
|
2404
|
-
[plan.pk]: sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2405
|
-
};
|
|
2467
|
+
const from = sql.join([base2, ...joins, ...laterals], sql.raw(""));
|
|
2468
|
+
const selected = {};
|
|
2406
2469
|
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2407
|
-
if (property === plan.pk) continue;
|
|
2408
|
-
const cell = cells.get(property);
|
|
2409
2470
|
const columnName = builder.columnNameFor(property);
|
|
2471
|
+
const keyIndex = plan.keys.indexOf(property);
|
|
2472
|
+
if (keyIndex !== -1) {
|
|
2473
|
+
selected[property] = sql.raw(keyRefs[keyIndex]).as(columnName);
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
const cell = cells.get(property);
|
|
2410
2477
|
selected[property] = sql.raw(cell.value).as(columnName);
|
|
2411
2478
|
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2412
2479
|
selected[cellColumnName(property, part)] = sql.raw(expression).as(cellColumnName(columnName, part));
|
|
@@ -2420,6 +2487,44 @@ function buildResolvedQuery({ context, plan }) {
|
|
|
2420
2487
|
const query = qb.select(selected).from(from);
|
|
2421
2488
|
return hidden.length > 0 ? query.where(sql.raw(hidden.join(" AND "))) : query;
|
|
2422
2489
|
}
|
|
2490
|
+
|
|
2491
|
+
// src/authoring/resolved-rows.ts
|
|
2492
|
+
function planKeys({ columns, config, fail }) {
|
|
2493
|
+
const keys = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey).map(([property]) => property);
|
|
2494
|
+
if (keys.length === 0) fail("declare exactly one .primaryKey() column");
|
|
2495
|
+
if (keys.length === 1) return keys;
|
|
2496
|
+
const key = `(${keys.join(", ")})`;
|
|
2497
|
+
if (config.rows?.existsIn === void 0) fail(`declare exactly one .primaryKey() column: composite key ${key} requires rows.existsIn`);
|
|
2498
|
+
const writes = ["insert", "update", "write"].filter((kind) => config.policies[kind] !== void 0);
|
|
2499
|
+
if (writes.length > 0) fail(`composite key ${key} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
2500
|
+
if (config.materialize) fail(`composite key ${key} cannot be materialized`);
|
|
2501
|
+
const prefetch = Object.entries(config.sources).find(([, source]) => source.kind === "prefetch");
|
|
2502
|
+
if (prefetch) fail(`composite key ${key} cannot have prefetch source "${prefetch[0]}"`);
|
|
2503
|
+
return keys;
|
|
2504
|
+
}
|
|
2505
|
+
function assertCompositeSourceKeys({ keys, plans, fail }) {
|
|
2506
|
+
if (keys.length === 1) return;
|
|
2507
|
+
for (const plan of plans) {
|
|
2508
|
+
if (plan.kind !== "etl") continue;
|
|
2509
|
+
const sourceKey = declaredPrimaryKeyProperties(plan.table);
|
|
2510
|
+
if (sourceKey.length !== keys.length || !keys.every((key) => sourceKey.includes(key))) {
|
|
2511
|
+
fail(`composite key (${keys.join(", ")}) requires etl source "${plan.name}" to be keyed by it; table "${plan.table.tableName}" is keyed by (${sourceKey.join(", ")})`);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function planExistsIn({ config, plans, fail }) {
|
|
2516
|
+
const names = config.rows?.existsIn;
|
|
2517
|
+
if (names === void 0) return void 0;
|
|
2518
|
+
if (names.length !== 1) fail("rows.existsIn names exactly one etl source");
|
|
2519
|
+
const source = plans.find((plan) => plan.name === names[0]);
|
|
2520
|
+
if (source?.kind !== "etl") return fail(`rows.existsIn "${names[0]}" must be an etl source`);
|
|
2521
|
+
if (config.policies.insert !== void 0 || config.policies.write !== void 0) fail("rows.existsIn allows only an update policy: rows come from the etl source");
|
|
2522
|
+
return source;
|
|
2523
|
+
}
|
|
2524
|
+
function inheritedNotNullColumns({ existsIn, rules }) {
|
|
2525
|
+
if (!existsIn) return /* @__PURE__ */ new Set();
|
|
2526
|
+
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));
|
|
2527
|
+
}
|
|
2423
2528
|
function managedFn(id) {
|
|
2424
2529
|
return { kind: "managed", id };
|
|
2425
2530
|
}
|
|
@@ -2458,26 +2563,28 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2458
2563
|
fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2459
2564
|
}
|
|
2460
2565
|
}
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
const [pk, pkBuilder] = pkEntries[0];
|
|
2464
|
-
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2566
|
+
const keys = planKeys({ columns, config, fail });
|
|
2567
|
+
const dataColumns = Object.keys(columns).filter((property) => !keys.includes(property));
|
|
2465
2568
|
if (dataColumns.length === 0) fail("declare at least one non-key column");
|
|
2466
2569
|
const sourceEntries = Object.entries(config.sources);
|
|
2467
2570
|
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
|
|
2468
2571
|
const plans = sourceEntries.map(
|
|
2469
|
-
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns,
|
|
2572
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, keys, dataColumns, fail })
|
|
2470
2573
|
);
|
|
2471
2574
|
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2472
2575
|
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2473
2576
|
for (const source of deletedBy) {
|
|
2474
2577
|
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
|
|
2475
2578
|
}
|
|
2579
|
+
assertCompositeSourceKeys({ keys, plans, fail });
|
|
2580
|
+
const existsIn = planExistsIn({ config, plans, fail });
|
|
2581
|
+
const notNullColumns = inheritedNotNullColumns({ existsIn, rules });
|
|
2476
2582
|
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2477
2583
|
const viewColumns = {};
|
|
2478
2584
|
for (const [property, builder] of Object.entries(columns)) {
|
|
2479
|
-
|
|
2480
|
-
|
|
2585
|
+
const isKey = keys.includes(property);
|
|
2586
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: isKey || notNullColumns.has(property), primaryKey: isKey });
|
|
2587
|
+
if (isKey) continue;
|
|
2481
2588
|
const columnName = builder.columnNameFor(property);
|
|
2482
2589
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2483
2590
|
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
@@ -2490,9 +2597,10 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2490
2597
|
sources: plans,
|
|
2491
2598
|
rules,
|
|
2492
2599
|
deletedBy,
|
|
2600
|
+
...existsIn ? { existsIn: [existsIn.name] } : {},
|
|
2493
2601
|
policies: writePolicies
|
|
2494
2602
|
};
|
|
2495
|
-
const queryPlan = { columns,
|
|
2603
|
+
const queryPlan = { columns, keys, plans, rules, deletedBy, ...existsIn ? { existsIn: existsIn.name } : {} };
|
|
2496
2604
|
const typedColumns = viewColumns;
|
|
2497
2605
|
const surface = {
|
|
2498
2606
|
...config.description !== void 0 ? { description: config.description } : {},
|
|
@@ -3055,6 +3163,7 @@ function buildCanonicalResolved(resolved) {
|
|
|
3055
3163
|
})),
|
|
3056
3164
|
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
3057
3165
|
deletedBy: [...resolved.deletedBy],
|
|
3166
|
+
...resolved.existsIn !== void 0 ? { existsIn: [...resolved.existsIn] } : {},
|
|
3058
3167
|
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3059
3168
|
policies
|
|
3060
3169
|
};
|