@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/compiler.cjs CHANGED
@@ -266,6 +266,48 @@ var policyExpressionSchema = zod.z.union([
266
266
  variants: zod.z.array(policyVariantSchema).max(10).default([])
267
267
  })
268
268
  ]);
269
+
270
+ // src/canonical/resolved-rows.ts
271
+ function logicalKey(table) {
272
+ return (table.primaryKey ?? []).map((columnName) => table.columns.find((c) => c.columnName === columnName)?.name ?? columnName);
273
+ }
274
+ function sameKey(key, expected) {
275
+ return key.length === expected.length && key.every((name2) => expected.includes(name2));
276
+ }
277
+ function validateRowIdentity(input) {
278
+ validateExistsIn(input);
279
+ if (input.keys.length > 1) validateCompositeKey(input);
280
+ }
281
+ function validateExistsIn({ resolved, sourceByName, fail: fail4 }) {
282
+ const name2 = resolved.existsIn?.[0];
283
+ if (name2 === void 0) return;
284
+ if (sourceByName.get(name2)?.kind !== "etl") fail4(`existsIn '${name2}' must be an etl source`);
285
+ if (resolved.policies.insert !== void 0 || resolved.policies.write !== void 0) {
286
+ fail4("existsIn allows only an update policy: rows come from the etl source");
287
+ }
288
+ }
289
+ function validateCompositeKey({ resolved, keys, tableByName, fail: fail4 }) {
290
+ const keyList = `(${keys.join(", ")})`;
291
+ if (resolved.existsIn === void 0) {
292
+ fail4(`requires exactly one primary-key column: composite key ${keyList} requires existsIn`);
293
+ return;
294
+ }
295
+ const writes = ["insert", "update", "write"].filter((kind) => resolved.policies[kind] !== void 0);
296
+ if (writes.length > 0) fail4(`composite key ${keyList} requires a read-only table: remove the ${writes.join("/")} policy`);
297
+ if (resolved.materialized) fail4(`composite key ${keyList} cannot be materialized`);
298
+ for (const source of resolved.sources) {
299
+ if (source.kind === "prefetch") {
300
+ fail4(`composite key ${keyList} cannot have prefetch source '${source.name}'`);
301
+ continue;
302
+ }
303
+ const table = tableByName.get(source.table);
304
+ if (table && !sameKey(logicalKey(table), keys)) {
305
+ fail4(`composite key ${keyList} requires ${source.kind} source '${source.name}' to be keyed by it; table '${source.table}' is keyed by (${logicalKey(table).join(", ")})`);
306
+ }
307
+ }
308
+ }
309
+
310
+ // src/canonical/resolved.ts
269
311
  function parseDurationMs(input) {
270
312
  if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
271
313
  const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
@@ -342,6 +384,13 @@ var canonicalResolvedSchema = zod.z.object({
342
384
  resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
343
385
  /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
344
386
  deletedBy: zod.z.array(identifier).max(1).default([]),
387
+ /**
388
+ * `rows.existsIn`: the one etl source whose rows are the table's rows (read
389
+ * directly as the row source). Only an update policy may write. It is also
390
+ * what admits a composite primary key: a read-only table keyed exactly like
391
+ * that source.
392
+ */
393
+ existsIn: zod.z.array(identifier).length(1).optional(),
345
394
  /**
346
395
  * Materialized: the view reads an indexed internal table (`table`) that the
347
396
  * data plane keeps in step with `compute`, the internal view holding the
@@ -360,8 +409,8 @@ function validateResolvedView({ view, tables, views = [] }) {
360
409
  if (!resolved) return [];
361
410
  const errors = [];
362
411
  const fail4 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
363
- const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
364
- if (!pkColumn) {
412
+ const keyColumns = (view.primaryKey ?? []).map((columnName) => view.columns.find((c) => c.columnName === columnName)).filter((column) => column !== void 0);
413
+ if (keyColumns.length === 0 || keyColumns.length !== view.primaryKey?.length) {
365
414
  fail4("requires exactly one primary-key column");
366
415
  return errors;
367
416
  }
@@ -369,7 +418,7 @@ function validateResolvedView({ view, tables, views = [] }) {
369
418
  const scope = {
370
419
  view,
371
420
  resolved,
372
- pkColumn,
421
+ keys: new Map(keyColumns.map((column) => [column.name, column])),
373
422
  dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
374
423
  sourceByName: /* @__PURE__ */ new Map(),
375
424
  fail: fail4
@@ -383,6 +432,7 @@ function validateResolvedView({ view, tables, views = [] }) {
383
432
  if (!table) fail4(`source '${source.name}' references unknown table '${source.table}'`);
384
433
  else validateSource({ scope, source, table });
385
434
  }
435
+ validateRowIdentity({ resolved, keys: [...scope.keys.keys()], sourceByName: scope.sourceByName, tableByName, fail: fail4 });
386
436
  validateRules(scope);
387
437
  validateViewCells(scope);
388
438
  if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
@@ -412,7 +462,7 @@ function validateSource({ scope, source, table }) {
412
462
  validatePrefetchFields({ scope, source });
413
463
  }
414
464
  function validatePrefetchFields({ scope, source }) {
415
- const { fail: fail4, pkColumn } = scope;
465
+ const { fail: fail4, keys } = scope;
416
466
  if (source.kind !== "prefetch") {
417
467
  if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
418
468
  fail4(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
@@ -421,7 +471,7 @@ function validatePrefetchFields({ scope, source }) {
421
471
  }
422
472
  if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
423
473
  fail4(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
424
- } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
474
+ } else if (!keys.has(source.key) && !source.columns.includes(source.key)) {
425
475
  fail4(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
426
476
  }
427
477
  for (const column of source.writeback?.columns ?? []) {
@@ -429,19 +479,19 @@ function validatePrefetchFields({ scope, source }) {
429
479
  }
430
480
  }
431
481
  function validateSourceColumns({ scope, source, byName }) {
432
- const { fail: fail4, pkColumn, dataColumns } = scope;
433
- for (const column of [pkColumn.name, ...source.columns]) {
434
- const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
482
+ const { fail: fail4, keys, dataColumns } = scope;
483
+ for (const column of [...keys.keys(), ...source.columns]) {
484
+ const own = keys.get(column) ?? dataColumns.get(column);
435
485
  const theirs = byName.get(column);
436
486
  if (!own) fail4(`source '${source.name}' supplies unknown column '${column}'`);
437
487
  else if (!theirs) fail4(`source '${source.name}' table '${source.table}' has no column '${column}'`);
438
488
  else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail4(`column '${column}' has a different type on source '${source.name}'`);
439
489
  }
440
490
  }
441
- function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail4 }) {
491
+ function validateRules({ resolved, keys, dataColumns, sourceByName, fail: fail4 }) {
442
492
  const ruled = /* @__PURE__ */ new Set();
443
493
  for (const rule of resolved.resolve) {
444
- if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
494
+ if (!dataColumns.has(rule.column) || keys.has(rule.column)) {
445
495
  fail4(`resolve rule targets unknown column '${rule.column}'`);
446
496
  continue;
447
497
  }
@@ -458,7 +508,7 @@ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fa
458
508
  }
459
509
  }
460
510
  for (const column of dataColumns.keys()) {
461
- if (column !== pkColumn.name && !ruled.has(column)) fail4(`column '${column}' has no resolve rule`);
511
+ if (!keys.has(column) && !ruled.has(column)) fail4(`column '${column}' has no resolve rule`);
462
512
  }
463
513
  }
464
514
  var VIEW_CELL_TYPES = {
@@ -467,10 +517,10 @@ var VIEW_CELL_TYPES = {
467
517
  by: "text",
468
518
  conflicts: "jsonb"
469
519
  };
470
- function validateViewCells({ view, dataColumns, pkColumn, fail: fail4 }) {
520
+ function validateViewCells({ view, dataColumns, keys, fail: fail4 }) {
471
521
  const expected = /* @__PURE__ */ new Map();
472
522
  for (const column of dataColumns.values()) {
473
- if (column.name === pkColumn.name) continue;
523
+ if (keys.has(column.name)) continue;
474
524
  for (const part of RESOLVED_VIEW_CELL_PARTS) {
475
525
  expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
476
526
  }
@@ -1696,8 +1746,13 @@ var resolvedSourceIrSchema = zod.z.object({
1696
1746
  }).optional()
1697
1747
  });
1698
1748
  var resolvedMetaIrSchema = zod.z.object({
1699
- /** Logical pk column of the resolved view (single-column). */
1700
- pk: zod.z.string(),
1749
+ /**
1750
+ * Logical pk column of the resolved view. Absent for a composite key, which
1751
+ * only a read-only `existsIn` table may declare: it has no writes, prefetch
1752
+ * or materialization (the paths that address one key column), and its reads
1753
+ * use the view's `primaryKey` like any keyed view.
1754
+ */
1755
+ pk: zod.z.string().optional(),
1701
1756
  /** Source name of the manual source. */
1702
1757
  manual: zod.z.string(),
1703
1758
  sources: zod.z.array(resolvedSourceIrSchema),
@@ -3153,7 +3208,7 @@ function buildCompileContext(rawCanonical) {
3153
3208
  appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
3154
3209
  ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
3155
3210
  ...cells ? { cells } : {},
3156
- ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
3211
+ ...resolved ? { resolved: resolvedMeta({ resolved, primaryKey }) } : {},
3157
3212
  constraints: isView ? [] : constraints,
3158
3213
  dynamicFilterMaxDepth: 6,
3159
3214
  dynamicFilterMaxNodes: 64
@@ -3435,10 +3490,10 @@ function resolvedWritable(entries) {
3435
3490
  const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
3436
3491
  return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
3437
3492
  }
3438
- function resolvedMeta({ resolved, pk }) {
3493
+ function resolvedMeta({ resolved, primaryKey }) {
3439
3494
  const manual = resolved.sources.find((source) => source.kind === "manual");
3440
3495
  return {
3441
- pk,
3496
+ ...primaryKey.length === 1 ? { pk: primaryKey[0] } : {},
3442
3497
  manual: manual?.name ?? "",
3443
3498
  deletedBy: [...resolved.deletedBy],
3444
3499
  ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
@@ -3564,7 +3619,9 @@ function applyPrefetchDirectives({ node, root, meta }) {
3564
3619
  if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
3565
3620
  fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
3566
3621
  }
3567
- const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
3622
+ const pk = meta.resolved?.pk;
3623
+ if (pk === void 0) return fail(`@prefetch on '${root.alias}' needs a single-column primary key`, "invalid-source-directive");
3624
+ const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk, source, alias: root.alias });
3568
3625
  return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
3569
3626
  });
3570
3627
  return { ...root, prefetch: plans };