@prisma-next/family-sql 0.5.0-dev.7 → 0.5.0-dev.70
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/README.md +2 -2
- package/dist/{authoring-type-constructors-BAR65pSK.mjs → authoring-type-constructors-F4JpCJl7.mjs} +14 -15
- package/dist/authoring-type-constructors-F4JpCJl7.mjs.map +1 -0
- package/dist/control-adapter.d.mts +26 -2
- package/dist/control-adapter.d.mts.map +1 -1
- package/dist/control-adapter.mjs +1 -1
- package/dist/control.d.mts +122 -40
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +1168 -22
- package/dist/control.mjs.map +1 -1
- package/dist/migration.d.mts +22 -24
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +25 -24
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.d.mts +35 -23
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +3 -5
- package/dist/pack.mjs.map +1 -1
- package/dist/runtime.d.mts +19 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +26 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-verify.d.mts +2 -4
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +2 -3
- package/dist/test-utils.mjs +1 -2
- package/dist/timestamp-now-generator-BWp8S2sa.mjs +86 -0
- package/dist/timestamp-now-generator-BWp8S2sa.mjs.map +1 -0
- package/dist/{types-C6K4mxDM.d.mts → types-CIdiS2QK.d.mts} +202 -23
- package/dist/types-CIdiS2QK.d.mts.map +1 -0
- package/dist/verify-pRYxnpiG.mjs +81 -0
- package/dist/verify-pRYxnpiG.mjs.map +1 -0
- package/dist/{verify-sql-schema-Ovz7RXR5.mjs → verify-sql-schema-C84vejAP.mjs} +2 -6
- package/dist/verify-sql-schema-C84vejAP.mjs.map +1 -0
- package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-CPHiuYHR.d.mts} +2 -3
- package/dist/verify-sql-schema-CPHiuYHR.d.mts.map +1 -0
- package/dist/verify.d.mts +16 -21
- package/dist/verify.d.mts.map +1 -1
- package/dist/verify.mjs +2 -3
- package/package.json +22 -21
- package/src/core/authoring-field-presets.ts +35 -23
- package/src/core/control-adapter.ts +32 -0
- package/src/core/control-descriptor.ts +2 -1
- package/src/core/control-instance.ts +115 -15
- package/src/core/migrations/field-event-planner.ts +196 -0
- package/src/core/migrations/plan-helpers.ts +4 -0
- package/src/core/migrations/types.ts +198 -21
- package/src/core/operation-preview.ts +62 -0
- package/src/core/psl-contract-infer/default-mapping.ts +56 -0
- package/src/core/psl-contract-infer/name-transforms.ts +178 -0
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
- package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
- package/src/core/psl-contract-infer/printer-config.ts +55 -0
- package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
- package/src/core/psl-contract-infer/relation-inference.ts +196 -0
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
- package/src/core/sql-migration.ts +25 -23
- package/src/core/timestamp-now-generator.ts +74 -0
- package/src/core/timestamp-now-runtime-generator.ts +24 -0
- package/src/core/verify.ts +46 -108
- package/src/exports/control.ts +11 -1
- package/src/exports/runtime.ts +2 -0
- package/src/exports/verify.ts +1 -1
- package/dist/authoring-type-constructors-BAR65pSK.mjs.map +0 -1
- package/dist/types-C6K4mxDM.d.mts.map +0 -1
- package/dist/verify-4GshvY4p.mjs +0 -122
- package/dist/verify-4GshvY4p.mjs.map +0 -1
- package/dist/verify-sql-schema-BBhkqEDo.d.mts.map +0 -1
- 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
|
|
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 =
|
|
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
|
-
|
|
658
|
+
return getControlAdapter().introspect(options.driver, options.contract);
|
|
659
|
+
},
|
|
561
660
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
}
|