@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/sdl.js CHANGED
@@ -238,6 +238,48 @@ var policyExpressionSchema = z.union([
238
238
  variants: z.array(policyVariantSchema).max(10).default([])
239
239
  })
240
240
  ]);
241
+
242
+ // src/canonical/resolved-rows.ts
243
+ function logicalKey(table) {
244
+ return (table.primaryKey ?? []).map((columnName) => table.columns.find((c) => c.columnName === columnName)?.name ?? columnName);
245
+ }
246
+ function sameKey(key, expected) {
247
+ return key.length === expected.length && key.every((name) => expected.includes(name));
248
+ }
249
+ function validateRowIdentity(input) {
250
+ validateExistsIn(input);
251
+ if (input.keys.length > 1) validateCompositeKey(input);
252
+ }
253
+ function validateExistsIn({ resolved, sourceByName, fail }) {
254
+ const name = resolved.existsIn?.[0];
255
+ if (name === void 0) return;
256
+ if (sourceByName.get(name)?.kind !== "etl") fail(`existsIn '${name}' must be an etl source`);
257
+ if (resolved.policies.insert !== void 0 || resolved.policies.write !== void 0) {
258
+ fail("existsIn allows only an update policy: rows come from the etl source");
259
+ }
260
+ }
261
+ function validateCompositeKey({ resolved, keys, tableByName, fail }) {
262
+ const keyList = `(${keys.join(", ")})`;
263
+ if (resolved.existsIn === void 0) {
264
+ fail(`requires exactly one primary-key column: composite key ${keyList} requires existsIn`);
265
+ return;
266
+ }
267
+ const writes = ["insert", "update", "write"].filter((kind) => resolved.policies[kind] !== void 0);
268
+ if (writes.length > 0) fail(`composite key ${keyList} requires a read-only table: remove the ${writes.join("/")} policy`);
269
+ if (resolved.materialized) fail(`composite key ${keyList} cannot be materialized`);
270
+ for (const source of resolved.sources) {
271
+ if (source.kind === "prefetch") {
272
+ fail(`composite key ${keyList} cannot have prefetch source '${source.name}'`);
273
+ continue;
274
+ }
275
+ const table = tableByName.get(source.table);
276
+ if (table && !sameKey(logicalKey(table), keys)) {
277
+ fail(`composite key ${keyList} requires ${source.kind} source '${source.name}' to be keyed by it; table '${source.table}' is keyed by (${logicalKey(table).join(", ")})`);
278
+ }
279
+ }
280
+ }
281
+
282
+ // src/canonical/resolved.ts
241
283
  var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
242
284
  var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
243
285
  var RESOLVED_VIEW_SYSTEM_COLUMNS = {
@@ -306,6 +348,13 @@ var canonicalResolvedSchema = z.object({
306
348
  resolve: z.array(canonicalResolveRuleSchema).max(500),
307
349
  /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
308
350
  deletedBy: z.array(identifier).max(1).default([]),
351
+ /**
352
+ * `rows.existsIn`: the one etl source whose rows are the table's rows (read
353
+ * directly as the row source). Only an update policy may write. It is also
354
+ * what admits a composite primary key: a read-only table keyed exactly like
355
+ * that source.
356
+ */
357
+ existsIn: z.array(identifier).length(1).optional(),
309
358
  /**
310
359
  * Materialized: the view reads an indexed internal table (`table`) that the
311
360
  * data plane keeps in step with `compute`, the internal view holding the
@@ -324,8 +373,8 @@ function validateResolvedView({ view, tables, views = [] }) {
324
373
  if (!resolved) return [];
325
374
  const errors = [];
326
375
  const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
327
- const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
328
- if (!pkColumn) {
376
+ const keyColumns = (view.primaryKey ?? []).map((columnName) => view.columns.find((c) => c.columnName === columnName)).filter((column) => column !== void 0);
377
+ if (keyColumns.length === 0 || keyColumns.length !== view.primaryKey?.length) {
329
378
  fail("requires exactly one primary-key column");
330
379
  return errors;
331
380
  }
@@ -333,7 +382,7 @@ function validateResolvedView({ view, tables, views = [] }) {
333
382
  const scope = {
334
383
  view,
335
384
  resolved,
336
- pkColumn,
385
+ keys: new Map(keyColumns.map((column) => [column.name, column])),
337
386
  dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
338
387
  sourceByName: /* @__PURE__ */ new Map(),
339
388
  fail
@@ -347,6 +396,7 @@ function validateResolvedView({ view, tables, views = [] }) {
347
396
  if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
348
397
  else validateSource({ scope, source, table });
349
398
  }
399
+ validateRowIdentity({ resolved, keys: [...scope.keys.keys()], sourceByName: scope.sourceByName, tableByName, fail });
350
400
  validateRules(scope);
351
401
  validateViewCells(scope);
352
402
  if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
@@ -376,7 +426,7 @@ function validateSource({ scope, source, table }) {
376
426
  validatePrefetchFields({ scope, source });
377
427
  }
378
428
  function validatePrefetchFields({ scope, source }) {
379
- const { fail, pkColumn } = scope;
429
+ const { fail, keys } = scope;
380
430
  if (source.kind !== "prefetch") {
381
431
  if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
382
432
  fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
@@ -385,7 +435,7 @@ function validatePrefetchFields({ scope, source }) {
385
435
  }
386
436
  if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
387
437
  fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
388
- } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
438
+ } else if (!keys.has(source.key) && !source.columns.includes(source.key)) {
389
439
  fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
390
440
  }
391
441
  for (const column of source.writeback?.columns ?? []) {
@@ -393,19 +443,19 @@ function validatePrefetchFields({ scope, source }) {
393
443
  }
394
444
  }
395
445
  function validateSourceColumns({ scope, source, byName }) {
396
- const { fail, pkColumn, dataColumns } = scope;
397
- for (const column of [pkColumn.name, ...source.columns]) {
398
- const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
446
+ const { fail, keys, dataColumns } = scope;
447
+ for (const column of [...keys.keys(), ...source.columns]) {
448
+ const own = keys.get(column) ?? dataColumns.get(column);
399
449
  const theirs = byName.get(column);
400
450
  if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
401
451
  else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
402
452
  else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
403
453
  }
404
454
  }
405
- function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
455
+ function validateRules({ resolved, keys, dataColumns, sourceByName, fail }) {
406
456
  const ruled = /* @__PURE__ */ new Set();
407
457
  for (const rule of resolved.resolve) {
408
- if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
458
+ if (!dataColumns.has(rule.column) || keys.has(rule.column)) {
409
459
  fail(`resolve rule targets unknown column '${rule.column}'`);
410
460
  continue;
411
461
  }
@@ -422,7 +472,7 @@ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail })
422
472
  }
423
473
  }
424
474
  for (const column of dataColumns.keys()) {
425
- if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
475
+ if (!keys.has(column) && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
426
476
  }
427
477
  }
428
478
  var VIEW_CELL_TYPES = {
@@ -431,10 +481,10 @@ var VIEW_CELL_TYPES = {
431
481
  by: "text",
432
482
  conflicts: "jsonb"
433
483
  };
434
- function validateViewCells({ view, dataColumns, pkColumn, fail }) {
484
+ function validateViewCells({ view, dataColumns, keys, fail }) {
435
485
  const expected = /* @__PURE__ */ new Map();
436
486
  for (const column of dataColumns.values()) {
437
- if (column.name === pkColumn.name) continue;
487
+ if (keys.has(column.name)) continue;
438
488
  for (const part of RESOLVED_VIEW_CELL_PARTS) {
439
489
  expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
440
490
  }
@@ -1611,8 +1661,13 @@ var resolvedSourceIrSchema = z.object({
1611
1661
  }).optional()
1612
1662
  });
1613
1663
  var resolvedMetaIrSchema = z.object({
1614
- /** Logical pk column of the resolved view (single-column). */
1615
- pk: z.string(),
1664
+ /**
1665
+ * Logical pk column of the resolved view. Absent for a composite key, which
1666
+ * only a read-only `existsIn` table may declare: it has no writes, prefetch
1667
+ * or materialization (the paths that address one key column), and its reads
1668
+ * use the view's `primaryKey` like any keyed view.
1669
+ */
1670
+ pk: z.string().optional(),
1616
1671
  /** Source name of the manual source. */
1617
1672
  manual: z.string(),
1618
1673
  sources: z.array(resolvedSourceIrSchema),