@prisma-next/family-sql 0.5.0-dev.9 → 0.5.0

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