@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/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 pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
1285
- if (!pkColumn) {
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
- pkColumn,
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, pkColumn } = scope;
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 !== pkColumn.name && !source.columns.includes(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, pkColumn, dataColumns } = scope;
1354
- for (const column of [pkColumn.name, ...source.columns]) {
1355
- const own = column === pkColumn.name ? pkColumn : dataColumns.get(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, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
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 === pkColumn.name) {
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 !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
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, pkColumn, fail: fail3 }) {
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 === pkColumn.name) continue;
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, pk, pkBuilder } = input;
2268
- const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
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, pk, dataColumns, fail: fail3 } = input;
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
- if (!(pk in synced.columns)) fail3(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
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, pk, fail: fail3 } = input;
2352
+ const { sourceName, keys, fail: fail3 } = input;
2300
2353
  assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
2301
- if (source.key !== pk && !source.columns.includes(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, pk, pgType }) {
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: pk }))} IS NOT NULL)`,
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 pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
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 keyRef = keySource ? `${alias(keySource.name)}.${ident(physical({ source: keySource, property: plan.pk }))}` : `"k".${ident(pkColumn)}`;
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 sourcePk = ident(physical({ source, property: plan.pk }));
2460
- const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
2461
- return sql`SELECT ${sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
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
- (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 }))} = ${keyRef}`)}`
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, pk: plan.pk, pgType }));
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 pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
2535
- if (pkEntries.length !== 1) fail3("declare exactly one .primaryKey() column");
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, pk, pkBuilder, dataColumns, fail: fail3 })
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 = existsIn ? inheritedNotNullColumns({ existsIn, rules }) : /* @__PURE__ */ new Set();
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
- viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk || notNullColumns.has(property), primaryKey: property === pk });
2555
- if (property === pk) continue;
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, pk, plans, rules, deletedBy, ...existsIn ? { existsIn: existsIn.name } : {} };
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
- /** Logical pk column of the resolved view (single-column). */
7149
- pk: z.string(),
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),