@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.
- 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 +1169 -24
- 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.d.mts +2 -2
- package/dist/test-utils.mjs +2 -3
- 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-BQBbcXg3.d.mts} +206 -28
- package/dist/types-BQBbcXg3.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-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-sql-schema-Ovz7RXR5.mjs → verify-sql-schema-r1-2apHI.mjs} +18 -9
- package/dist/verify-sql-schema-r1-2apHI.mjs.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 +23 -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 +116 -18
- package/src/core/migrations/field-event-planner.ts +192 -0
- package/src/core/migrations/plan-helpers.ts +4 -0
- package/src/core/migrations/types.ts +200 -25
- 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/schema-verify/verify-helpers.ts +46 -6
- 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/test-utils.ts +0 -1
- 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>();
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
656
|
+
return getControlAdapter().introspect(options.driver, options.contract);
|
|
657
|
+
},
|
|
561
658
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
}
|