@prisma-next/family-sql 0.5.0-dev.7 → 0.5.0-dev.71

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 (69) hide show
  1. package/README.md +2 -2
  2. package/dist/{authoring-type-constructors-BAR65pSK.mjs → authoring-type-constructors-F4JpCJl7.mjs} +14 -15
  3. package/dist/authoring-type-constructors-F4JpCJl7.mjs.map +1 -0
  4. package/dist/control-adapter.d.mts +26 -2
  5. package/dist/control-adapter.d.mts.map +1 -1
  6. package/dist/control-adapter.mjs +1 -1
  7. package/dist/control.d.mts +122 -40
  8. package/dist/control.d.mts.map +1 -1
  9. package/dist/control.mjs +1168 -22
  10. package/dist/control.mjs.map +1 -1
  11. package/dist/migration.d.mts +22 -24
  12. package/dist/migration.d.mts.map +1 -1
  13. package/dist/migration.mjs +25 -24
  14. package/dist/migration.mjs.map +1 -1
  15. package/dist/pack.d.mts +35 -23
  16. package/dist/pack.d.mts.map +1 -1
  17. package/dist/pack.mjs +3 -5
  18. package/dist/pack.mjs.map +1 -1
  19. package/dist/runtime.d.mts +19 -2
  20. package/dist/runtime.d.mts.map +1 -1
  21. package/dist/runtime.mjs +26 -4
  22. package/dist/runtime.mjs.map +1 -1
  23. package/dist/schema-verify.d.mts +2 -4
  24. package/dist/schema-verify.d.mts.map +1 -1
  25. package/dist/schema-verify.mjs +2 -3
  26. package/dist/test-utils.mjs +1 -2
  27. package/dist/timestamp-now-generator-BWp8S2sa.mjs +86 -0
  28. package/dist/timestamp-now-generator-BWp8S2sa.mjs.map +1 -0
  29. package/dist/{types-C6K4mxDM.d.mts → types-CIdiS2QK.d.mts} +202 -23
  30. package/dist/types-CIdiS2QK.d.mts.map +1 -0
  31. package/dist/verify-pRYxnpiG.mjs +81 -0
  32. package/dist/verify-pRYxnpiG.mjs.map +1 -0
  33. package/dist/{verify-sql-schema-Ovz7RXR5.mjs → verify-sql-schema-C84vejAP.mjs} +2 -6
  34. package/dist/verify-sql-schema-C84vejAP.mjs.map +1 -0
  35. package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-CPHiuYHR.d.mts} +2 -3
  36. package/dist/verify-sql-schema-CPHiuYHR.d.mts.map +1 -0
  37. package/dist/verify.d.mts +16 -21
  38. package/dist/verify.d.mts.map +1 -1
  39. package/dist/verify.mjs +2 -3
  40. package/package.json +24 -23
  41. package/src/core/authoring-field-presets.ts +35 -23
  42. package/src/core/control-adapter.ts +32 -0
  43. package/src/core/control-descriptor.ts +2 -1
  44. package/src/core/control-instance.ts +115 -15
  45. package/src/core/migrations/field-event-planner.ts +196 -0
  46. package/src/core/migrations/plan-helpers.ts +4 -0
  47. package/src/core/migrations/types.ts +198 -21
  48. package/src/core/operation-preview.ts +62 -0
  49. package/src/core/psl-contract-infer/default-mapping.ts +56 -0
  50. package/src/core/psl-contract-infer/name-transforms.ts +178 -0
  51. package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
  52. package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
  53. package/src/core/psl-contract-infer/printer-config.ts +55 -0
  54. package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
  55. package/src/core/psl-contract-infer/relation-inference.ts +196 -0
  56. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
  57. package/src/core/sql-migration.ts +25 -23
  58. package/src/core/timestamp-now-generator.ts +74 -0
  59. package/src/core/timestamp-now-runtime-generator.ts +24 -0
  60. package/src/core/verify.ts +46 -108
  61. package/src/exports/control.ts +11 -1
  62. package/src/exports/runtime.ts +2 -0
  63. package/src/exports/verify.ts +1 -1
  64. package/dist/authoring-type-constructors-BAR65pSK.mjs.map +0 -1
  65. package/dist/types-C6K4mxDM.d.mts.map +0 -1
  66. package/dist/verify-4GshvY4p.mjs +0 -122
  67. package/dist/verify-4GshvY4p.mjs.map +0 -1
  68. package/dist/verify-sql-schema-BBhkqEDo.d.mts.map +0 -1
  69. package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +0 -1
@@ -9,19 +9,26 @@ import type {
9
9
  ControlFamilyInstance,
10
10
  ControlStack,
11
11
  CoreSchemaView,
12
+ MigrationPlanOperation,
12
13
  OperationContext,
14
+ OperationPreview,
15
+ OperationPreviewCapable,
16
+ PslContractInferCapable,
13
17
  SchemaViewCapable,
14
18
  SignDatabaseResult,
15
19
  VerifyDatabaseResult,
16
20
  VerifyDatabaseSchemaResult,
17
21
  } from '@prisma-next/framework-components/control';
18
22
  import {
23
+ APP_SPACE_ID,
19
24
  SchemaTreeNode,
20
25
  VERIFY_CODE_HASH_MISMATCH,
21
26
  VERIFY_CODE_MARKER_MISSING,
22
27
  VERIFY_CODE_TARGET_MISMATCH,
23
28
  } from '@prisma-next/framework-components/control';
24
29
  import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
30
+ import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
31
+ import { assertDescriptorSelfConsistency } from '@prisma-next/migration-tools/spaces';
25
32
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
26
33
  import { validateContract as sqlValidateContract } from '@prisma-next/sql-contract/validate';
27
34
  import {
@@ -37,8 +44,10 @@ import type {
37
44
  SqlControlAdapterDescriptor,
38
45
  SqlControlExtensionDescriptor,
39
46
  } from './migrations/types';
47
+ import { sqlOperationsToPreview } from './operation-preview';
48
+ import { sqlSchemaIrToPslAst } from './psl-contract-infer/sql-schema-ir-to-psl-ast';
40
49
  import { verifySqlSchema } from './schema-verify/verify-sql-schema';
41
- import { collectSupportedCodecTypeIds, readMarker } from './verify';
50
+ import { collectSupportedCodecTypeIds } from './verify';
42
51
 
43
52
  function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
44
53
  const typeIds = new Set<string>();
@@ -182,6 +191,8 @@ export interface SchemaVerifyOptions {
182
191
  export interface SqlControlFamilyInstance
183
192
  extends ControlFamilyInstance<'sql', SqlSchemaIR>,
184
193
  SchemaViewCapable<SqlSchemaIR>,
194
+ PslContractInferCapable<SqlSchemaIR>,
195
+ OperationPreviewCapable,
185
196
  SqlFamilyInstanceState {
186
197
  validateContract(contractJson: unknown): Contract;
187
198
 
@@ -195,6 +206,22 @@ export interface SqlControlFamilyInstance
195
206
 
196
207
  schemaVerify(options: SchemaVerifyOptions): Promise<VerifyDatabaseSchemaResult>;
197
208
 
209
+ /**
210
+ * Verify a contract against an already-introspected schema slice.
211
+ *
212
+ * Used by the aggregate verifier to invoke per-member verification
213
+ * with the live schema pre-projected to the member's claimed slice
214
+ * via `projectSchemaToSpace`. Closes F23 — without per-member
215
+ * pre-projection, single-contract verifiers see other-space tables
216
+ * as `extras`.
217
+ */
218
+ schemaVerifyAgainstSchema(options: {
219
+ readonly contract: unknown;
220
+ readonly schema: SqlSchemaIR;
221
+ readonly strict: boolean;
222
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
223
+ }): VerifyDatabaseSchemaResult;
224
+
198
225
  sign(options: {
199
226
  readonly driver: ControlDriverInstance<'sql', string>;
200
227
  readonly contract: unknown;
@@ -206,6 +233,10 @@ export interface SqlControlFamilyInstance
206
233
  readonly driver: ControlDriverInstance<'sql', string>;
207
234
  readonly contract?: unknown;
208
235
  }): Promise<SqlSchemaIR>;
236
+
237
+ inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst;
238
+
239
+ toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview;
209
240
  }
210
241
 
211
242
  export type SqlFamilyInstance = SqlControlFamilyInstance;
@@ -217,7 +248,11 @@ function isSqlControlAdapter<TTargetId extends string>(
217
248
  typeof value === 'object' &&
218
249
  value !== null &&
219
250
  'introspect' in value &&
220
- typeof (value as { introspect: unknown }).introspect === 'function'
251
+ typeof (value as { introspect: unknown }).introspect === 'function' &&
252
+ 'readMarker' in value &&
253
+ typeof (value as { readMarker: unknown }).readMarker === 'function' &&
254
+ 'readAllMarkers' in value &&
255
+ typeof (value as { readAllMarkers: unknown }).readAllMarkers === 'function'
221
256
  );
222
257
  }
223
258
 
@@ -285,6 +320,29 @@ export function createSqlFamilyInstance<TTargetId extends string>(
285
320
  stack.extensionPacks as unknown as readonly (SqlControlExtensionDescriptor<TTargetId> &
286
321
  DescriptorWithStorageTypes)[];
287
322
 
323
+ // Descriptor self-consistency check.
324
+ // Each extension that exposes a `contractSpace` must publish a
325
+ // `headRef.hash` that matches the canonical hash recomputed from its
326
+ // `contractJson`. A stale value would silently corrupt every downstream
327
+ // boundary that trusts `headRef.hash` as the canonical identity (drift
328
+ // detection, on-disk artefact emission, runner marker writes). Failing
329
+ // fast at descriptor-load time turns "extension author shipped an
330
+ // inconsistent descriptor" into an explicit, actionable error
331
+ // (`MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH`) rather than a confusing
332
+ // mismatch surfacing several layers downstream.
333
+ for (const extension of extensions) {
334
+ if (extension.contractSpace) {
335
+ const { contractJson, headRef } = extension.contractSpace;
336
+ assertDescriptorSelfConsistency({
337
+ extensionId: extension.id,
338
+ target: contractJson.target,
339
+ targetFamily: contractJson.targetFamily,
340
+ storage: contractJson.storage,
341
+ headRefHash: headRef.hash,
342
+ });
343
+ }
344
+ }
345
+
288
346
  const { codecTypeImports, operationTypeImports, extensionIds } = stack;
289
347
 
290
348
  const typeMetadataRegistry = buildSqlTypeMetadataRegistry({
@@ -293,6 +351,20 @@ export function createSqlFamilyInstance<TTargetId extends string>(
293
351
  extensionPacks: extensions,
294
352
  });
295
353
 
354
+ // Family-instance methods accept `ControlDriverInstance<'sql', string>` —
355
+ // the family API isn't generic on the target id. Letting `isSqlControlAdapter`
356
+ // default its type parameter narrows the adapter to `SqlControlAdapter<string>`,
357
+ // which matches the family-level driver type without any cast at call sites.
358
+ const getControlAdapter = () => {
359
+ const controlAdapter = adapter.create(stack);
360
+ if (!isSqlControlAdapter(controlAdapter)) {
361
+ throw new Error(
362
+ 'Adapter does not implement SqlControlAdapter (missing introspect, readMarker, or readAllMarkers)',
363
+ );
364
+ }
365
+ return controlAdapter;
366
+ };
367
+
296
368
  return {
297
369
  familyId: 'sql',
298
370
  codecTypeImports,
@@ -326,7 +398,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
326
398
  const contractProfileHash = contract.profileHash;
327
399
  const contractTarget = contract.target;
328
400
 
329
- const marker = await readMarker(driver);
401
+ const marker = await getControlAdapter().readMarker(driver, APP_SPACE_ID);
330
402
 
331
403
  let missingCodecs: readonly string[] | undefined;
332
404
  let codecCoverageSkipped = false;
@@ -435,10 +507,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
435
507
 
436
508
  const contract = sqlValidateContract<Contract<SqlStorage>>(contractInput, emptyCodecLookup);
437
509
 
438
- const controlAdapter = adapter.create(stack);
439
- if (!isSqlControlAdapter(controlAdapter)) {
440
- throw new Error('Adapter does not implement SqlControlAdapter.introspect()');
441
- }
510
+ const controlAdapter = getControlAdapter();
442
511
  const schemaIR = await controlAdapter.introspect(driver, contractInput);
443
512
 
444
513
  return verifySqlSchema({
@@ -453,6 +522,27 @@ export function createSqlFamilyInstance<TTargetId extends string>(
453
522
  ...ifDefined('normalizeNativeType', controlAdapter.normalizeNativeType),
454
523
  });
455
524
  },
525
+ schemaVerifyAgainstSchema(options: {
526
+ readonly contract: unknown;
527
+ readonly schema: SqlSchemaIR;
528
+ readonly strict: boolean;
529
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
530
+ }): VerifyDatabaseSchemaResult {
531
+ const contract = sqlValidateContract<Contract<SqlStorage>>(
532
+ options.contract,
533
+ emptyCodecLookup,
534
+ );
535
+ const controlAdapter = getControlAdapter();
536
+ return verifySqlSchema({
537
+ contract,
538
+ schema: options.schema,
539
+ strict: options.strict,
540
+ typeMetadataRegistry,
541
+ frameworkComponents: options.frameworkComponents,
542
+ ...ifDefined('normalizeDefault', controlAdapter.normalizeDefault),
543
+ ...ifDefined('normalizeNativeType', controlAdapter.normalizeNativeType),
544
+ });
545
+ },
456
546
  async sign(options: {
457
547
  readonly driver: ControlDriverInstance<'sql', string>;
458
548
  readonly contract: unknown;
@@ -474,7 +564,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
474
564
  await driver.query(ensureSchemaStatement.sql, ensureSchemaStatement.params);
475
565
  await driver.query(ensureTableStatement.sql, ensureTableStatement.params);
476
566
 
477
- const existingMarker = await readMarker(driver);
567
+ const existingMarker = await getControlAdapter().readMarker(driver, APP_SPACE_ID);
478
568
 
479
569
  let markerCreated = false;
480
570
  let markerUpdated = false;
@@ -482,6 +572,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
482
572
 
483
573
  if (!existingMarker) {
484
574
  const write = writeContractMarker({
575
+ space: APP_SPACE_ID,
485
576
  storageHash: contractStorageHash,
486
577
  profileHash: contractProfileHash,
487
578
  contractJson: contractInput,
@@ -502,6 +593,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
502
593
  profileHash: existingProfileHash,
503
594
  };
504
595
  const write = writeContractMarker({
596
+ space: APP_SPACE_ID,
505
597
  storageHash: contractStorageHash,
506
598
  profileHash: contractProfileHash,
507
599
  contractJson: contractInput,
@@ -550,20 +642,28 @@ export function createSqlFamilyInstance<TTargetId extends string>(
550
642
  },
551
643
  async readMarker(options: {
552
644
  readonly driver: ControlDriverInstance<'sql', string>;
645
+ readonly space: string;
553
646
  }): Promise<ContractMarkerRecord | null> {
554
- return readMarker(options.driver);
647
+ return getControlAdapter().readMarker(options.driver, options.space);
648
+ },
649
+ async readAllMarkers(options: {
650
+ readonly driver: ControlDriverInstance<'sql', string>;
651
+ }): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
652
+ return getControlAdapter().readAllMarkers(options.driver);
555
653
  },
556
654
  async introspect(options: {
557
655
  readonly driver: ControlDriverInstance<'sql', string>;
558
656
  readonly contract?: unknown;
559
657
  }): Promise<SqlSchemaIR> {
560
- const { driver, contract } = options;
658
+ return getControlAdapter().introspect(options.driver, options.contract);
659
+ },
561
660
 
562
- const controlAdapter = adapter.create(stack);
563
- if (!isSqlControlAdapter(controlAdapter)) {
564
- throw new Error('Adapter does not implement SqlControlAdapter.introspect()');
565
- }
566
- return controlAdapter.introspect(driver, contract);
661
+ inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst {
662
+ return sqlSchemaIrToPslAst(schemaIR);
663
+ },
664
+
665
+ toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
666
+ return sqlOperationsToPreview(operations);
567
667
  },
568
668
 
569
669
  toSchemaView(schema: SqlSchemaIR): CoreSchemaView {
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Codec lifecycle hook planner — runs `onFieldEvent` for every per-field
3
+ * delta between two contracts and concatenates the returned ops in a
4
+ * deterministic order.
5
+ *
6
+ * Wired by each target's planner (`PostgresMigrationPlanner`,
7
+ * `SqliteMigrationPlanner`) so codec-emitted ops are inlined alongside
8
+ * structural DDL in the app-space migration's `ops.json`. Pure, target-
9
+ * agnostic, and only ever invoked at the app-space emitter; extension-space
10
+ * planning never reaches this helper.
11
+ *
12
+ * Ordering rules:
13
+ *
14
+ * - Events are grouped by phase: `'added'` → `'dropped'` → `'altered'`.
15
+ * - Within each phase, entries are sorted alphabetically by
16
+ * `(tableName, fieldName)`.
17
+ * - The hook's returned ops are appended in the order the hook returned them.
18
+ *
19
+ * `'altered'` is suppressed when only `codecId` differs (codec rotation is a
20
+ * v1 non-goal).
21
+ */
22
+
23
+ import type { Contract } from '@prisma-next/contract/types';
24
+ import type { SqlStorage, StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
25
+ import type {
26
+ CodecControlHooks,
27
+ FieldEvent,
28
+ FieldEventContext,
29
+ SqlMigrationPlanOperation,
30
+ } from './types';
31
+
32
+ export interface PlanFieldEventOperationsOptions {
33
+ /**
34
+ * Prior contract the planner is diffing against. `null` for first emits
35
+ * (every field is treated as added).
36
+ */
37
+ readonly priorContract: Contract<SqlStorage> | null;
38
+ /**
39
+ * New contract the user just authored.
40
+ */
41
+ readonly newContract: Contract<SqlStorage>;
42
+ /**
43
+ * Codec-id keyed map of control hooks, as produced by
44
+ * {@link import('./assembly').extractCodecControlHooks}. Hooks carry
45
+ * `unknown` target details after extraction; the caller casts the
46
+ * helper's returned ops to its target's `SqlMigrationPlanOperation`
47
+ * specialisation at the integration boundary, mirroring how
48
+ * `storageTypePlanCallStrategy` lifts `planTypeOperations` results into
49
+ * `RawSqlCall`.
50
+ */
51
+ readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;
52
+ }
53
+
54
+ interface FieldEntry {
55
+ readonly tableName: string;
56
+ readonly fieldName: string;
57
+ readonly priorTable: StorageTable | undefined;
58
+ readonly newTable: StorageTable | undefined;
59
+ readonly priorField: StorageColumn | undefined;
60
+ readonly newField: StorageColumn | undefined;
61
+ }
62
+
63
+ export function planFieldEventOperations(
64
+ options: PlanFieldEventOperationsOptions,
65
+ ): readonly SqlMigrationPlanOperation<unknown>[] {
66
+ const priorTables = options.priorContract?.storage.tables ?? {};
67
+ const newTables = options.newContract.storage.tables;
68
+
69
+ const added: FieldEntry[] = [];
70
+ const dropped: FieldEntry[] = [];
71
+ const altered: FieldEntry[] = [];
72
+
73
+ const tableNames = unionSorted(Object.keys(priorTables), Object.keys(newTables));
74
+ for (const tableName of tableNames) {
75
+ const priorTable = priorTables[tableName];
76
+ const newTable = newTables[tableName];
77
+ const fieldNames = unionSorted(
78
+ priorTable ? Object.keys(priorTable.columns) : [],
79
+ newTable ? Object.keys(newTable.columns) : [],
80
+ );
81
+ for (const fieldName of fieldNames) {
82
+ const priorField = priorTable?.columns[fieldName];
83
+ const newField = newTable?.columns[fieldName];
84
+ const entry: FieldEntry = {
85
+ tableName,
86
+ fieldName,
87
+ priorTable,
88
+ newTable,
89
+ priorField,
90
+ newField,
91
+ };
92
+ if (priorField === undefined && newField !== undefined) {
93
+ added.push(entry);
94
+ } else if (priorField !== undefined && newField === undefined) {
95
+ dropped.push(entry);
96
+ } else if (priorField !== undefined && newField !== undefined) {
97
+ if (isAlteration(priorField, newField)) altered.push(entry);
98
+ }
99
+ }
100
+ }
101
+
102
+ const ops: SqlMigrationPlanOperation<unknown>[] = [];
103
+ appendOps('added', added, options.codecHooks, ops, (e) => e.newField?.codecId);
104
+ appendOps('dropped', dropped, options.codecHooks, ops, (e) => e.priorField?.codecId);
105
+ appendOps('altered', altered, options.codecHooks, ops, (e) => e.newField?.codecId);
106
+ return ops;
107
+ }
108
+
109
+ function appendOps(
110
+ event: FieldEvent,
111
+ entries: readonly FieldEntry[],
112
+ codecHooks: ReadonlyMap<string, CodecControlHooks>,
113
+ ops: SqlMigrationPlanOperation<unknown>[],
114
+ pickCodecId: (entry: FieldEntry) => string | undefined,
115
+ ): void {
116
+ for (const entry of entries) {
117
+ const codecId = pickCodecId(entry);
118
+ if (codecId === undefined) continue;
119
+ const hook = codecHooks.get(codecId);
120
+ if (!hook?.onFieldEvent) continue;
121
+ const ctx = buildContext(event, entry);
122
+ const emitted = hook.onFieldEvent(event, ctx);
123
+ for (const op of emitted) ops.push(op);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * The context's prior/new sides are scoped to the event:
129
+ *
130
+ * - `'added'` — only `newTable` / `newField` populated.
131
+ * - `'dropped'` — only `priorTable` / `priorField` populated.
132
+ * - `'altered'` — both sides populated.
133
+ */
134
+ function buildContext(event: FieldEvent, entry: FieldEntry): FieldEventContext {
135
+ const base = { tableName: entry.tableName, fieldName: entry.fieldName };
136
+ if (event === 'added') {
137
+ return {
138
+ ...base,
139
+ ...(entry.newTable !== undefined ? { newTable: entry.newTable } : {}),
140
+ ...(entry.newField !== undefined ? { newField: entry.newField } : {}),
141
+ };
142
+ }
143
+ if (event === 'dropped') {
144
+ return {
145
+ ...base,
146
+ ...(entry.priorTable !== undefined ? { priorTable: entry.priorTable } : {}),
147
+ ...(entry.priorField !== undefined ? { priorField: entry.priorField } : {}),
148
+ };
149
+ }
150
+ return {
151
+ ...base,
152
+ ...(entry.priorTable !== undefined ? { priorTable: entry.priorTable } : {}),
153
+ ...(entry.newTable !== undefined ? { newTable: entry.newTable } : {}),
154
+ ...(entry.priorField !== undefined ? { priorField: entry.priorField } : {}),
155
+ ...(entry.newField !== undefined ? { newField: entry.newField } : {}),
156
+ };
157
+ }
158
+
159
+ /**
160
+ * `'altered'` predicate. Returns `false` whenever `codecId` differs —
161
+ * any codec change suppresses the `altered` event entirely, including
162
+ * cases where another property also differs in the same diff. Codec
163
+ * rotation is a v1 non-goal (project spec § Non-goals); avoiding the
164
+ * mixed event keeps the migration semantics for codec changes explicit
165
+ * (out of scope) rather than smuggling them through as `altered`.
166
+ *
167
+ * For non-`codecId` diffs, returns `true` iff any other column property
168
+ * differs.
169
+ */
170
+ function isAlteration(prior: StorageColumn, current: StorageColumn): boolean {
171
+ if (prior.codecId !== current.codecId) return false;
172
+ return !sameStorageColumn(prior, current);
173
+ }
174
+
175
+ function sameStorageColumn(a: StorageColumn, b: StorageColumn): boolean {
176
+ if (a === b) return true;
177
+ if (a.nativeType !== b.nativeType) return false;
178
+ if (a.nullable !== b.nullable) return false;
179
+ if (a.typeRef !== b.typeRef) return false;
180
+ if (!sameJson(a.typeParams, b.typeParams)) return false;
181
+ if (!sameJson(a.default, b.default)) return false;
182
+ return true;
183
+ }
184
+
185
+ function sameJson(a: unknown, b: unknown): boolean {
186
+ if (a === b) return true;
187
+ if (a === undefined || b === undefined) return false;
188
+ return JSON.stringify(a) === JSON.stringify(b);
189
+ }
190
+
191
+ function unionSorted(a: readonly string[], b: readonly string[]): readonly string[] {
192
+ const set = new Set<string>();
193
+ for (const name of a) set.add(name);
194
+ for (const name of b) set.add(name);
195
+ return [...set].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0));
196
+ }
@@ -35,6 +35,7 @@ function freezeSteps(
35
35
  Object.freeze({
36
36
  description: step.description,
37
37
  sql: step.sql,
38
+ ...(step.params ? { params: Object.freeze([...step.params]) } : {}),
38
39
  ...(step.meta ? { meta: cloneRecord(step.meta) } : {}),
39
40
  }),
40
41
  ),
@@ -74,6 +75,7 @@ function freezeOperation<TTargetDetails>(
74
75
  label: operation.label,
75
76
  ...(operation.summary ? { summary: operation.summary } : {}),
76
77
  operationClass: operation.operationClass,
78
+ ...(operation.invariantId ? { invariantId: operation.invariantId } : {}),
77
79
  target: freezeTargetDetails(operation.target),
78
80
  precheck: freezeSteps(operation.precheck),
79
81
  execute: freezeSteps(operation.execute),
@@ -96,11 +98,13 @@ export function createMigrationPlan<TTargetDetails>(
96
98
  ): SqlMigrationPlan<TTargetDetails> {
97
99
  return Object.freeze({
98
100
  targetId: options.targetId,
101
+ spaceId: options.spaceId,
99
102
  ...(options.origin !== undefined
100
103
  ? { origin: options.origin ? Object.freeze({ ...options.origin }) : null }
101
104
  : {}),
102
105
  destination: Object.freeze({ ...options.destination }),
103
106
  operations: freezeOperations(options.operations),
107
+ providedInvariants: Object.freeze([...options.providedInvariants]),
104
108
  ...(options.meta ? { meta: cloneRecord(options.meta) } : {}),
105
109
  });
106
110
  }