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