@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.cjs
CHANGED
|
@@ -1126,6 +1126,48 @@ var policyExpressionSchema = zod.z.union([
|
|
|
1126
1126
|
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
1127
1127
|
})
|
|
1128
1128
|
]);
|
|
1129
|
+
|
|
1130
|
+
// src/canonical/resolved-rows.ts
|
|
1131
|
+
function logicalKey(table) {
|
|
1132
|
+
return (table.primaryKey ?? []).map((columnName) => table.columns.find((c) => c.columnName === columnName)?.name ?? columnName);
|
|
1133
|
+
}
|
|
1134
|
+
function sameKey(key, expected) {
|
|
1135
|
+
return key.length === expected.length && key.every((name2) => expected.includes(name2));
|
|
1136
|
+
}
|
|
1137
|
+
function validateRowIdentity(input) {
|
|
1138
|
+
validateExistsIn(input);
|
|
1139
|
+
if (input.keys.length > 1) validateCompositeKey(input);
|
|
1140
|
+
}
|
|
1141
|
+
function validateExistsIn({ resolved, sourceByName, fail }) {
|
|
1142
|
+
const name2 = resolved.existsIn?.[0];
|
|
1143
|
+
if (name2 === void 0) return;
|
|
1144
|
+
if (sourceByName.get(name2)?.kind !== "etl") fail(`existsIn '${name2}' must be an etl source`);
|
|
1145
|
+
if (resolved.policies.insert !== void 0 || resolved.policies.write !== void 0) {
|
|
1146
|
+
fail("existsIn allows only an update policy: rows come from the etl source");
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function validateCompositeKey({ resolved, keys, tableByName, fail }) {
|
|
1150
|
+
const keyList = `(${keys.join(", ")})`;
|
|
1151
|
+
if (resolved.existsIn === void 0) {
|
|
1152
|
+
fail(`requires exactly one primary-key column: composite key ${keyList} requires existsIn`);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const writes = ["insert", "update", "write"].filter((kind) => resolved.policies[kind] !== void 0);
|
|
1156
|
+
if (writes.length > 0) fail(`composite key ${keyList} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
1157
|
+
if (resolved.materialized) fail(`composite key ${keyList} cannot be materialized`);
|
|
1158
|
+
for (const source of resolved.sources) {
|
|
1159
|
+
if (source.kind === "prefetch") {
|
|
1160
|
+
fail(`composite key ${keyList} cannot have prefetch source '${source.name}'`);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
const table = tableByName.get(source.table);
|
|
1164
|
+
if (table && !sameKey(logicalKey(table), keys)) {
|
|
1165
|
+
fail(`composite key ${keyList} requires ${source.kind} source '${source.name}' to be keyed by it; table '${source.table}' is keyed by (${logicalKey(table).join(", ")})`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/canonical/resolved.ts
|
|
1129
1171
|
function parseDurationMs(input) {
|
|
1130
1172
|
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1131
1173
|
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
@@ -1202,6 +1244,13 @@ var canonicalResolvedSchema = zod.z.object({
|
|
|
1202
1244
|
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
1203
1245
|
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1204
1246
|
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
1247
|
+
/**
|
|
1248
|
+
* `rows.existsIn`: the one etl source whose rows are the table's rows (read
|
|
1249
|
+
* directly as the row source). Only an update policy may write. It is also
|
|
1250
|
+
* what admits a composite primary key: a read-only table keyed exactly like
|
|
1251
|
+
* that source.
|
|
1252
|
+
*/
|
|
1253
|
+
existsIn: zod.z.array(identifier).length(1).optional(),
|
|
1205
1254
|
/**
|
|
1206
1255
|
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1207
1256
|
* data plane keeps in step with `compute`, the internal view holding the
|
|
@@ -1220,8 +1269,8 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1220
1269
|
if (!resolved) return [];
|
|
1221
1270
|
const errors = [];
|
|
1222
1271
|
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1223
|
-
const
|
|
1224
|
-
if (
|
|
1272
|
+
const keyColumns = (view.primaryKey ?? []).map((columnName) => view.columns.find((c) => c.columnName === columnName)).filter((column) => column !== void 0);
|
|
1273
|
+
if (keyColumns.length === 0 || keyColumns.length !== view.primaryKey?.length) {
|
|
1225
1274
|
fail("requires exactly one primary-key column");
|
|
1226
1275
|
return errors;
|
|
1227
1276
|
}
|
|
@@ -1229,7 +1278,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1229
1278
|
const scope = {
|
|
1230
1279
|
view,
|
|
1231
1280
|
resolved,
|
|
1232
|
-
|
|
1281
|
+
keys: new Map(keyColumns.map((column) => [column.name, column])),
|
|
1233
1282
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1234
1283
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
1235
1284
|
fail
|
|
@@ -1243,6 +1292,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1243
1292
|
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1244
1293
|
else validateSource({ scope, source, table });
|
|
1245
1294
|
}
|
|
1295
|
+
validateRowIdentity({ resolved, keys: [...scope.keys.keys()], sourceByName: scope.sourceByName, tableByName, fail });
|
|
1246
1296
|
validateRules(scope);
|
|
1247
1297
|
validateViewCells(scope);
|
|
1248
1298
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
@@ -1272,7 +1322,7 @@ function validateSource({ scope, source, table }) {
|
|
|
1272
1322
|
validatePrefetchFields({ scope, source });
|
|
1273
1323
|
}
|
|
1274
1324
|
function validatePrefetchFields({ scope, source }) {
|
|
1275
|
-
const { fail,
|
|
1325
|
+
const { fail, keys } = scope;
|
|
1276
1326
|
if (source.kind !== "prefetch") {
|
|
1277
1327
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1278
1328
|
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
@@ -1281,7 +1331,7 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1281
1331
|
}
|
|
1282
1332
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1283
1333
|
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1284
|
-
} else if (source.key
|
|
1334
|
+
} else if (!keys.has(source.key) && !source.columns.includes(source.key)) {
|
|
1285
1335
|
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1286
1336
|
}
|
|
1287
1337
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -1289,19 +1339,19 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1289
1339
|
}
|
|
1290
1340
|
}
|
|
1291
1341
|
function validateSourceColumns({ scope, source, byName }) {
|
|
1292
|
-
const { fail,
|
|
1293
|
-
for (const column of [
|
|
1294
|
-
const own = column
|
|
1342
|
+
const { fail, keys, dataColumns } = scope;
|
|
1343
|
+
for (const column of [...keys.keys(), ...source.columns]) {
|
|
1344
|
+
const own = keys.get(column) ?? dataColumns.get(column);
|
|
1295
1345
|
const theirs = byName.get(column);
|
|
1296
1346
|
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1297
1347
|
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1298
1348
|
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
1299
1349
|
}
|
|
1300
1350
|
}
|
|
1301
|
-
function validateRules({ resolved,
|
|
1351
|
+
function validateRules({ resolved, keys, dataColumns, sourceByName, fail }) {
|
|
1302
1352
|
const ruled = /* @__PURE__ */ new Set();
|
|
1303
1353
|
for (const rule of resolved.resolve) {
|
|
1304
|
-
if (!dataColumns.has(rule.column) || rule.column
|
|
1354
|
+
if (!dataColumns.has(rule.column) || keys.has(rule.column)) {
|
|
1305
1355
|
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
1306
1356
|
continue;
|
|
1307
1357
|
}
|
|
@@ -1318,7 +1368,7 @@ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail })
|
|
|
1318
1368
|
}
|
|
1319
1369
|
}
|
|
1320
1370
|
for (const column of dataColumns.keys()) {
|
|
1321
|
-
if (column
|
|
1371
|
+
if (!keys.has(column) && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
1322
1372
|
}
|
|
1323
1373
|
}
|
|
1324
1374
|
var VIEW_CELL_TYPES = {
|
|
@@ -1327,10 +1377,10 @@ var VIEW_CELL_TYPES = {
|
|
|
1327
1377
|
by: "text",
|
|
1328
1378
|
conflicts: "jsonb"
|
|
1329
1379
|
};
|
|
1330
|
-
function validateViewCells({ view, dataColumns,
|
|
1380
|
+
function validateViewCells({ view, dataColumns, keys, fail }) {
|
|
1331
1381
|
const expected = /* @__PURE__ */ new Map();
|
|
1332
1382
|
for (const column of dataColumns.values()) {
|
|
1333
|
-
if (column.name
|
|
1383
|
+
if (keys.has(column.name)) continue;
|
|
1334
1384
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1335
1385
|
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1336
1386
|
}
|
|
@@ -1698,7 +1748,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1698
1748
|
table: identifier2,
|
|
1699
1749
|
displayName: zod.z.string().trim().min(1).max(120).optional(),
|
|
1700
1750
|
description: zod.z.string().max(2e3).optional(),
|
|
1701
|
-
/** Table-level rename hint
|
|
1751
|
+
/** Table-level rename hint. */
|
|
1702
1752
|
renamedFrom: identifier2.optional(),
|
|
1703
1753
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
1704
1754
|
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
@@ -2203,8 +2253,9 @@ function planSource(input) {
|
|
|
2203
2253
|
}
|
|
2204
2254
|
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2205
2255
|
function internalTable(input, supplied, system, options = {}) {
|
|
2206
|
-
const { viewName, sourceName, columns,
|
|
2207
|
-
const tableColumns = {
|
|
2256
|
+
const { viewName, sourceName, columns, keys } = input;
|
|
2257
|
+
const tableColumns = {};
|
|
2258
|
+
for (const key of keys) tableColumns[key] = cloneColumn({ builder: columns[key], property: key, notNull: true, primaryKey: true });
|
|
2208
2259
|
for (const property of supplied) {
|
|
2209
2260
|
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2210
2261
|
}
|
|
@@ -2221,12 +2272,14 @@ function assertColumns(input, list, what) {
|
|
|
2221
2272
|
for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2222
2273
|
}
|
|
2223
2274
|
function planEtl(input, source) {
|
|
2224
|
-
const { sourceName,
|
|
2275
|
+
const { sourceName, keys, dataColumns, fail } = input;
|
|
2225
2276
|
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2226
2277
|
if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2227
2278
|
const synced = table;
|
|
2228
2279
|
if (synced.config.primaryKey?.length === 0) fail(`etl source "${sourceName}" cannot use an unkeyed synced table for resolved row identity`);
|
|
2229
|
-
|
|
2280
|
+
for (const key of keys) {
|
|
2281
|
+
if (!(key in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${key}" column`);
|
|
2282
|
+
}
|
|
2230
2283
|
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2231
2284
|
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2232
2285
|
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
@@ -2235,9 +2288,9 @@ function planEtl(input, source) {
|
|
|
2235
2288
|
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2236
2289
|
}
|
|
2237
2290
|
function planPrefetch(input, source) {
|
|
2238
|
-
const { sourceName,
|
|
2291
|
+
const { sourceName, keys, fail } = input;
|
|
2239
2292
|
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2240
|
-
if (source.key
|
|
2293
|
+
if (!keys.includes(source.key) && !source.columns.includes(source.key)) {
|
|
2241
2294
|
fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2242
2295
|
}
|
|
2243
2296
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -2290,7 +2343,7 @@ var PG_TYPE = {
|
|
|
2290
2343
|
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2291
2344
|
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2292
2345
|
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2293
|
-
function candidateSql({ source, column,
|
|
2346
|
+
function candidateSql({ source, column, key, pgType }) {
|
|
2294
2347
|
const a = ident(`s_${source.name}`);
|
|
2295
2348
|
const name2 = physical({ source, property: column });
|
|
2296
2349
|
const value = `CAST(${a}.${ident(name2)} AS ${pgType})`;
|
|
@@ -2298,7 +2351,7 @@ function candidateSql({ source, column, pk, pgType }) {
|
|
|
2298
2351
|
return {
|
|
2299
2352
|
source: source.name,
|
|
2300
2353
|
value,
|
|
2301
|
-
has: `(${a}.${ident(physical({ source, property:
|
|
2354
|
+
has: `(${a}.${ident(physical({ source, property: key }))} IS NOT NULL)`,
|
|
2302
2355
|
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2303
2356
|
by: "CAST(NULL AS text)"
|
|
2304
2357
|
};
|
|
@@ -2362,6 +2415,12 @@ function builtinCell({ comparator, candidates }) {
|
|
|
2362
2415
|
}
|
|
2363
2416
|
};
|
|
2364
2417
|
}
|
|
2418
|
+
function existsInCell(candidate) {
|
|
2419
|
+
return {
|
|
2420
|
+
value: candidate.value,
|
|
2421
|
+
parts: { source: literal(candidate.source), at: candidate.at, by: candidate.by, conflicts: "CAST(NULL AS jsonb)" }
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2365
2424
|
function customCell({ candidates, orderBy, index }) {
|
|
2366
2425
|
const w = ident(`w_${index}`);
|
|
2367
2426
|
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
@@ -2380,36 +2439,44 @@ function customCell({ candidates, orderBy, index }) {
|
|
|
2380
2439
|
}
|
|
2381
2440
|
function buildResolvedQuery({ context, plan }) {
|
|
2382
2441
|
const { qb, relation } = context;
|
|
2383
|
-
const
|
|
2442
|
+
const keyColumns = plan.keys.map((key) => plan.columns[key].columnNameFor(key));
|
|
2384
2443
|
const alias = (source) => ident(`s_${source}`);
|
|
2385
2444
|
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2386
2445
|
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2387
2446
|
const schema = pgCore.getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2447
|
+
const keySource = plan.existsIn === void 0 ? void 0 : sourceByName.get(plan.existsIn);
|
|
2448
|
+
const keyRefs = plan.keys.map((key, index) => keySource ? `${alias(keySource.name)}.${ident(physical({ source: keySource, property: key }))}` : `"k".${ident(keyColumns[index])}`);
|
|
2388
2449
|
const keyBranches = plan.plans.map((source) => {
|
|
2389
2450
|
const where = source.kind === "prefetch" ? drizzleOrm.sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : drizzleOrm.sql.raw("");
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2451
|
+
const selectedKey = plan.keys.map((key, index) => {
|
|
2452
|
+
const sourceKey = ident(physical({ source, property: key }));
|
|
2453
|
+
return sourceKey === ident(keyColumns[index]) ? sourceKey : `${sourceKey} AS ${ident(keyColumns[index])}`;
|
|
2454
|
+
});
|
|
2455
|
+
return drizzleOrm.sql`SELECT ${drizzleOrm.sql.raw(selectedKey.join(", "))} FROM ${relation(source.table)}${where}`;
|
|
2456
|
+
});
|
|
2457
|
+
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"`;
|
|
2458
|
+
const joins = plan.plans.filter((source) => source !== keySource).map((source) => {
|
|
2459
|
+
const on = plan.keys.map((key, index) => `${alias(source.name)}.${ident(physical({ source, property: key }))} = ${keyRefs[index]}`);
|
|
2460
|
+
return drizzleOrm.sql` LEFT JOIN ${relation(source.table)} AS ${drizzleOrm.sql.raw(alias(source.name))} ON ${drizzleOrm.sql.raw(on.join(" AND "))}`;
|
|
2393
2461
|
});
|
|
2394
|
-
const joins = plan.plans.map(
|
|
2395
|
-
(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)}`)}`
|
|
2396
|
-
);
|
|
2397
2462
|
const cells = /* @__PURE__ */ new Map();
|
|
2398
2463
|
plan.rules.forEach((rule, index) => {
|
|
2399
2464
|
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2400
2465
|
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2401
|
-
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column,
|
|
2402
|
-
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2466
|
+
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, key: plan.keys[0], pgType }));
|
|
2467
|
+
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 }));
|
|
2403
2468
|
});
|
|
2404
2469
|
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [drizzleOrm.sql.raw(cell.lateral)] : []);
|
|
2405
|
-
const from = drizzleOrm.sql.join([
|
|
2406
|
-
const selected = {
|
|
2407
|
-
[plan.pk]: drizzleOrm.sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2408
|
-
};
|
|
2470
|
+
const from = drizzleOrm.sql.join([base2, ...joins, ...laterals], drizzleOrm.sql.raw(""));
|
|
2471
|
+
const selected = {};
|
|
2409
2472
|
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2410
|
-
if (property === plan.pk) continue;
|
|
2411
|
-
const cell = cells.get(property);
|
|
2412
2473
|
const columnName = builder.columnNameFor(property);
|
|
2474
|
+
const keyIndex = plan.keys.indexOf(property);
|
|
2475
|
+
if (keyIndex !== -1) {
|
|
2476
|
+
selected[property] = drizzleOrm.sql.raw(keyRefs[keyIndex]).as(columnName);
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
const cell = cells.get(property);
|
|
2413
2480
|
selected[property] = drizzleOrm.sql.raw(cell.value).as(columnName);
|
|
2414
2481
|
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2415
2482
|
selected[cellColumnName(property, part)] = drizzleOrm.sql.raw(expression).as(cellColumnName(columnName, part));
|
|
@@ -2423,6 +2490,44 @@ function buildResolvedQuery({ context, plan }) {
|
|
|
2423
2490
|
const query = qb.select(selected).from(from);
|
|
2424
2491
|
return hidden.length > 0 ? query.where(drizzleOrm.sql.raw(hidden.join(" AND "))) : query;
|
|
2425
2492
|
}
|
|
2493
|
+
|
|
2494
|
+
// src/authoring/resolved-rows.ts
|
|
2495
|
+
function planKeys({ columns, config, fail }) {
|
|
2496
|
+
const keys = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey).map(([property]) => property);
|
|
2497
|
+
if (keys.length === 0) fail("declare exactly one .primaryKey() column");
|
|
2498
|
+
if (keys.length === 1) return keys;
|
|
2499
|
+
const key = `(${keys.join(", ")})`;
|
|
2500
|
+
if (config.rows?.existsIn === void 0) fail(`declare exactly one .primaryKey() column: composite key ${key} requires rows.existsIn`);
|
|
2501
|
+
const writes = ["insert", "update", "write"].filter((kind) => config.policies[kind] !== void 0);
|
|
2502
|
+
if (writes.length > 0) fail(`composite key ${key} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
2503
|
+
if (config.materialize) fail(`composite key ${key} cannot be materialized`);
|
|
2504
|
+
const prefetch = Object.entries(config.sources).find(([, source]) => source.kind === "prefetch");
|
|
2505
|
+
if (prefetch) fail(`composite key ${key} cannot have prefetch source "${prefetch[0]}"`);
|
|
2506
|
+
return keys;
|
|
2507
|
+
}
|
|
2508
|
+
function assertCompositeSourceKeys({ keys, plans, fail }) {
|
|
2509
|
+
if (keys.length === 1) return;
|
|
2510
|
+
for (const plan of plans) {
|
|
2511
|
+
if (plan.kind !== "etl") continue;
|
|
2512
|
+
const sourceKey = declaredPrimaryKeyProperties(plan.table);
|
|
2513
|
+
if (sourceKey.length !== keys.length || !keys.every((key) => sourceKey.includes(key))) {
|
|
2514
|
+
fail(`composite key (${keys.join(", ")}) requires etl source "${plan.name}" to be keyed by it; table "${plan.table.tableName}" is keyed by (${sourceKey.join(", ")})`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
function planExistsIn({ config, plans, fail }) {
|
|
2519
|
+
const names = config.rows?.existsIn;
|
|
2520
|
+
if (names === void 0) return void 0;
|
|
2521
|
+
if (names.length !== 1) fail("rows.existsIn names exactly one etl source");
|
|
2522
|
+
const source = plans.find((plan) => plan.name === names[0]);
|
|
2523
|
+
if (source?.kind !== "etl") return fail(`rows.existsIn "${names[0]}" must be an etl source`);
|
|
2524
|
+
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");
|
|
2525
|
+
return source;
|
|
2526
|
+
}
|
|
2527
|
+
function inheritedNotNullColumns({ existsIn, rules }) {
|
|
2528
|
+
if (!existsIn) return /* @__PURE__ */ new Set();
|
|
2529
|
+
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));
|
|
2530
|
+
}
|
|
2426
2531
|
function managedFn(id) {
|
|
2427
2532
|
return { kind: "managed", id };
|
|
2428
2533
|
}
|
|
@@ -2461,26 +2566,28 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2461
2566
|
fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2462
2567
|
}
|
|
2463
2568
|
}
|
|
2464
|
-
const
|
|
2465
|
-
|
|
2466
|
-
const [pk, pkBuilder] = pkEntries[0];
|
|
2467
|
-
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2569
|
+
const keys = planKeys({ columns, config, fail });
|
|
2570
|
+
const dataColumns = Object.keys(columns).filter((property) => !keys.includes(property));
|
|
2468
2571
|
if (dataColumns.length === 0) fail("declare at least one non-key column");
|
|
2469
2572
|
const sourceEntries = Object.entries(config.sources);
|
|
2470
2573
|
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
|
|
2471
2574
|
const plans = sourceEntries.map(
|
|
2472
|
-
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns,
|
|
2575
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, keys, dataColumns, fail })
|
|
2473
2576
|
);
|
|
2474
2577
|
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2475
2578
|
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2476
2579
|
for (const source of deletedBy) {
|
|
2477
2580
|
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
|
|
2478
2581
|
}
|
|
2582
|
+
assertCompositeSourceKeys({ keys, plans, fail });
|
|
2583
|
+
const existsIn = planExistsIn({ config, plans, fail });
|
|
2584
|
+
const notNullColumns = inheritedNotNullColumns({ existsIn, rules });
|
|
2479
2585
|
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2480
2586
|
const viewColumns = {};
|
|
2481
2587
|
for (const [property, builder] of Object.entries(columns)) {
|
|
2482
|
-
|
|
2483
|
-
|
|
2588
|
+
const isKey = keys.includes(property);
|
|
2589
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: isKey || notNullColumns.has(property), primaryKey: isKey });
|
|
2590
|
+
if (isKey) continue;
|
|
2484
2591
|
const columnName = builder.columnNameFor(property);
|
|
2485
2592
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2486
2593
|
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
@@ -2493,9 +2600,10 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2493
2600
|
sources: plans,
|
|
2494
2601
|
rules,
|
|
2495
2602
|
deletedBy,
|
|
2603
|
+
...existsIn ? { existsIn: [existsIn.name] } : {},
|
|
2496
2604
|
policies: writePolicies
|
|
2497
2605
|
};
|
|
2498
|
-
const queryPlan = { columns,
|
|
2606
|
+
const queryPlan = { columns, keys, plans, rules, deletedBy, ...existsIn ? { existsIn: existsIn.name } : {} };
|
|
2499
2607
|
const typedColumns = viewColumns;
|
|
2500
2608
|
const surface = {
|
|
2501
2609
|
...config.description !== void 0 ? { description: config.description } : {},
|
|
@@ -3058,6 +3166,7 @@ function buildCanonicalResolved(resolved) {
|
|
|
3058
3166
|
})),
|
|
3059
3167
|
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
3060
3168
|
deletedBy: [...resolved.deletedBy],
|
|
3169
|
+
...resolved.existsIn !== void 0 ? { existsIn: [...resolved.existsIn] } : {},
|
|
3061
3170
|
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3062
3171
|
policies
|
|
3063
3172
|
};
|