@prisma-next/cli 0.5.0-dev.26 → 0.5.0-dev.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +11 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-enZIahga.mjs → client-keSCAgjW.mjs} +25 -16
- package/dist/client-keSCAgjW.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +5 -0
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +7 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +6 -4
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +5 -2
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +3 -2
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.mjs +5 -4
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +3 -2
- package/dist/commands/db-verify.mjs.map +1 -1
- package/dist/commands/migration-apply.d.mts +1 -1
- package/dist/commands/migration-apply.mjs +3 -2
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +1 -3
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +8 -2
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +10 -12
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-show.d.mts +10 -4
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +12 -6
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.mjs +6 -1
- package/dist/{contract-emit-LjzCoicC.mjs → contract-emit-DS5NzZh2.mjs} +2 -0
- package/dist/{contract-infer-BjzkcwQt.mjs → contract-infer-GztVCOCJ.mjs} +8 -16
- package/dist/contract-infer-GztVCOCJ.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +37 -5
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -1
- package/dist/exports/index.mjs +5 -0
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{init-BKgjxw6r.mjs → init-DAbQMxIR.mjs} +3 -3
- package/dist/{init-BKgjxw6r.mjs.map → init-DAbQMxIR.mjs.map} +1 -1
- package/dist/{inspect-live-schema-QklSDLt_.mjs → inspect-live-schema-BaR9ISwa.mjs} +5 -5
- package/dist/inspect-live-schema-BaR9ISwa.mjs.map +1 -0
- package/dist/{migration-command-scaffold-BfloSWPZ.mjs → migration-command-scaffold-D1dWuEWQ.mjs} +2 -2
- package/dist/{migration-command-scaffold-BfloSWPZ.mjs.map → migration-command-scaffold-D1dWuEWQ.mjs.map} +1 -1
- package/dist/{migration-status-C5VYA5r9.mjs → migration-status-CP5k8O5i.mjs} +2 -2
- package/dist/{migration-status-C5VYA5r9.mjs.map → migration-status-CP5k8O5i.mjs.map} +1 -1
- package/dist/{migrations-CSaDHNpB.mjs → migrations-MEoKMiV5.mjs} +40 -19
- package/dist/migrations-MEoKMiV5.mjs.map +1 -0
- package/dist/{output-BiO7kt87.mjs → output-BpcQrnnq.mjs} +1 -1
- package/dist/{output-BiO7kt87.mjs.map → output-BpcQrnnq.mjs.map} +1 -1
- package/dist/{verify-BumcH6Ry.mjs → verify-BT9tgCOH.mjs} +1 -1
- package/dist/{verify-BumcH6Ry.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
- package/package.json +14 -14
- package/src/commands/contract-infer.ts +7 -20
- package/src/commands/db-init.ts +1 -0
- package/src/commands/db-update.ts +1 -1
- package/src/commands/inspect-live-schema.ts +10 -5
- package/src/commands/migration-apply.ts +1 -1
- package/src/commands/migration-new.ts +2 -4
- package/src/commands/migration-plan.ts +22 -13
- package/src/commands/migration-show.ts +27 -9
- package/src/control-api/client.ts +21 -0
- package/src/control-api/operations/db-init.ts +8 -5
- package/src/control-api/operations/db-update.ts +8 -5
- package/src/control-api/operations/migration-apply.ts +14 -9
- package/src/control-api/types.ts +39 -4
- package/src/utils/formatters/migrations.ts +60 -24
- package/dist/client-enZIahga.mjs.map +0 -1
- package/dist/contract-infer-BjzkcwQt.mjs.map +0 -1
- package/dist/extract-operation-statements-CU-Pp4-N.mjs +0 -13
- package/dist/extract-operation-statements-CU-Pp4-N.mjs.map +0 -1
- package/dist/extract-sql-ddl-Bm0Mm0IT.mjs +0 -26
- package/dist/extract-sql-ddl-Bm0Mm0IT.mjs.map +0 -1
- package/dist/inspect-live-schema-QklSDLt_.mjs.map +0 -1
- package/dist/migrations-CSaDHNpB.mjs.map +0 -1
- package/src/control-api/operations/extract-operation-statements.ts +0 -14
- package/src/control-api/operations/extract-sql-ddl.ts +0 -47
- /package/dist/{cli-errors-BJLUczXT.d.mts → cli-errors-DDeVsP2Y.d.mts} +0 -0
|
@@ -3,9 +3,10 @@ import type { Contract } from '@prisma-next/contract/types';
|
|
|
3
3
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import {
|
|
5
5
|
createControlStack,
|
|
6
|
+
hasOperationPreview,
|
|
6
7
|
type MigrationPlanOperation,
|
|
8
|
+
type OperationPreview,
|
|
7
9
|
} from '@prisma-next/framework-components/control';
|
|
8
|
-
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
9
10
|
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
10
11
|
import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
11
12
|
import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
|
|
@@ -21,7 +22,6 @@ import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
|
21
22
|
import { Command } from 'commander';
|
|
22
23
|
import { join, relative } from 'pathe';
|
|
23
24
|
import { loadConfig } from '../config-loader';
|
|
24
|
-
import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
|
|
25
25
|
import {
|
|
26
26
|
type CliErrorConflict,
|
|
27
27
|
CliStructuredError,
|
|
@@ -58,7 +58,7 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
58
58
|
export interface MigrationPlanResult {
|
|
59
59
|
readonly ok: boolean;
|
|
60
60
|
readonly noOp: boolean;
|
|
61
|
-
readonly from: string;
|
|
61
|
+
readonly from: string | null;
|
|
62
62
|
readonly to: string;
|
|
63
63
|
readonly dir?: string;
|
|
64
64
|
readonly operations: readonly {
|
|
@@ -66,7 +66,12 @@ export interface MigrationPlanResult {
|
|
|
66
66
|
readonly label: string;
|
|
67
67
|
readonly operationClass: string;
|
|
68
68
|
}[];
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Family-agnostic textual preview of the migration plan operations.
|
|
71
|
+
* Replaces the previous `sql?: readonly string[]` field; consumers should
|
|
72
|
+
* read `result.preview?.statements`.
|
|
73
|
+
*/
|
|
74
|
+
readonly preview?: OperationPreview;
|
|
70
75
|
readonly summary: string;
|
|
71
76
|
/**
|
|
72
77
|
* When true, `migration.ts` was written but contains unfilled
|
|
@@ -160,7 +165,7 @@ async function executeMigrationPlanCommand(
|
|
|
160
165
|
|
|
161
166
|
// Read existing migrations and determine "from" contract
|
|
162
167
|
let fromContract: Contract | null = null;
|
|
163
|
-
let fromHash: string =
|
|
168
|
+
let fromHash: string | null = null;
|
|
164
169
|
let fromContractSourceDir: string | null = null;
|
|
165
170
|
|
|
166
171
|
try {
|
|
@@ -252,7 +257,6 @@ async function executeMigrationPlanCommand(
|
|
|
252
257
|
const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
|
|
253
258
|
from: fromHash,
|
|
254
259
|
to: toStorageHash,
|
|
255
|
-
kind: 'regular',
|
|
256
260
|
fromContract,
|
|
257
261
|
toContract: toContractJson,
|
|
258
262
|
hints: {
|
|
@@ -366,7 +370,9 @@ async function executeMigrationPlanCommand(
|
|
|
366
370
|
return ok(result);
|
|
367
371
|
}
|
|
368
372
|
|
|
369
|
-
const
|
|
373
|
+
const preview = hasOperationPreview(familyInstance)
|
|
374
|
+
? familyInstance.toOperationPreview(plannedOps)
|
|
375
|
+
: undefined;
|
|
370
376
|
const result: MigrationPlanResult = {
|
|
371
377
|
ok: true,
|
|
372
378
|
noOp: false,
|
|
@@ -378,7 +384,7 @@ async function executeMigrationPlanCommand(
|
|
|
378
384
|
label: op.label,
|
|
379
385
|
operationClass: op.operationClass,
|
|
380
386
|
})),
|
|
381
|
-
|
|
387
|
+
...(preview !== undefined ? { preview } : {}),
|
|
382
388
|
summary: `Planned ${plannedOps.length} operation(s)`,
|
|
383
389
|
timings: { total: Date.now() - startTime },
|
|
384
390
|
};
|
|
@@ -505,14 +511,17 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
505
511
|
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
|
|
506
512
|
);
|
|
507
513
|
|
|
508
|
-
if (result.
|
|
514
|
+
if (result.preview && result.preview.statements.length > 0) {
|
|
515
|
+
// The non-empty length is already guaranteed by the surrounding check, so
|
|
516
|
+
// a plain `every` here is equivalent to the helper in formatters/migrations.ts.
|
|
517
|
+
const allSql = result.preview.statements.every((s) => s.language === 'sql');
|
|
509
518
|
lines.push('');
|
|
510
|
-
lines.push(dim_('DDL preview'));
|
|
519
|
+
lines.push(dim_(allSql ? 'DDL preview' : 'Operation preview'));
|
|
511
520
|
lines.push('');
|
|
512
|
-
for (const statement of result.
|
|
513
|
-
const trimmed = statement.trim();
|
|
521
|
+
for (const statement of result.preview.statements) {
|
|
522
|
+
const trimmed = statement.text.trim();
|
|
514
523
|
if (!trimmed) continue;
|
|
515
|
-
const line = trimmed.endsWith(';') ? trimmed :
|
|
524
|
+
const line = statement.language === 'sql' && !trimmed.endsWith(';') ? `${trimmed};` : trimmed;
|
|
516
525
|
lines.push(line);
|
|
517
526
|
}
|
|
518
527
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
MigrationPlanOperation,
|
|
3
|
+
OperationPreview,
|
|
4
|
+
} from '@prisma-next/framework-components/control';
|
|
2
5
|
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
3
6
|
import { readMigrationPackage, readMigrationsDir } from '@prisma-next/migration-tools/io';
|
|
4
7
|
import {
|
|
@@ -10,7 +13,7 @@ import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
|
10
13
|
import { Command } from 'commander';
|
|
11
14
|
import { relative, resolve } from 'pathe';
|
|
12
15
|
import { loadConfig } from '../config-loader';
|
|
13
|
-
import {
|
|
16
|
+
import { createControlClient } from '../control-api/client';
|
|
14
17
|
import {
|
|
15
18
|
type CliStructuredError,
|
|
16
19
|
errorRuntime,
|
|
@@ -37,17 +40,22 @@ export interface MigrationShowResult {
|
|
|
37
40
|
readonly ok: true;
|
|
38
41
|
readonly dirName: string;
|
|
39
42
|
readonly dirPath: string;
|
|
40
|
-
readonly from: string;
|
|
43
|
+
readonly from: string | null;
|
|
41
44
|
readonly to: string;
|
|
42
45
|
readonly migrationHash: string;
|
|
43
|
-
readonly kind: string;
|
|
44
46
|
readonly createdAt: string;
|
|
45
47
|
readonly operations: readonly {
|
|
46
48
|
readonly id: string;
|
|
47
49
|
readonly label: string;
|
|
48
50
|
readonly operationClass: string;
|
|
49
51
|
}[];
|
|
50
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Family-agnostic textual preview of the migration's operations. Replaces
|
|
54
|
+
* the previous string-array DDL field. Always defined; statements is empty
|
|
55
|
+
* for a no-op migration or a family that does not implement the
|
|
56
|
+
* `OperationPreviewCapable` capability.
|
|
57
|
+
*/
|
|
58
|
+
readonly preview: OperationPreview;
|
|
51
59
|
readonly summary: string;
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -175,7 +183,18 @@ async function executeMigrationShowCommand(
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
const ops = pkg.ops as readonly MigrationPlanOperation[];
|
|
178
|
-
|
|
186
|
+
|
|
187
|
+
// `migration show` is an offline command; the control client is constructed
|
|
188
|
+
// purely to dispatch the family-specific `toOperationPreview` capability and
|
|
189
|
+
// is not connected to a database.
|
|
190
|
+
const client = createControlClient({
|
|
191
|
+
family: config.family,
|
|
192
|
+
target: config.target,
|
|
193
|
+
adapter: config.adapter,
|
|
194
|
+
...(config.driver ? { driver: config.driver } : {}),
|
|
195
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
196
|
+
});
|
|
197
|
+
const preview: OperationPreview = client.toOperationPreview(ops) ?? { statements: [] };
|
|
179
198
|
|
|
180
199
|
const result: MigrationShowResult = {
|
|
181
200
|
ok: true,
|
|
@@ -184,14 +203,13 @@ async function executeMigrationShowCommand(
|
|
|
184
203
|
from: pkg.metadata.from,
|
|
185
204
|
to: pkg.metadata.to,
|
|
186
205
|
migrationHash: pkg.metadata.migrationHash,
|
|
187
|
-
kind: pkg.metadata.kind,
|
|
188
206
|
createdAt: pkg.metadata.createdAt,
|
|
189
207
|
operations: ops.map((op) => ({
|
|
190
208
|
id: op.id,
|
|
191
209
|
label: op.label,
|
|
192
210
|
operationClass: op.operationClass,
|
|
193
211
|
})),
|
|
194
|
-
|
|
212
|
+
preview,
|
|
195
213
|
summary: `${ops.length} operation(s)`,
|
|
196
214
|
};
|
|
197
215
|
return ok(result);
|
|
@@ -202,7 +220,7 @@ export function createMigrationShowCommand(): Command {
|
|
|
202
220
|
setCommandDescriptions(
|
|
203
221
|
command,
|
|
204
222
|
'Display migration package contents',
|
|
205
|
-
'Shows the operations,
|
|
223
|
+
'Shows the operations, statement preview, and metadata for a migration package.\n' +
|
|
206
224
|
'Accepts a directory path, a hash prefix (git-style), or defaults to the\n' +
|
|
207
225
|
'latest migration.',
|
|
208
226
|
);
|
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
ControlFamilyInstance,
|
|
7
7
|
ControlStack,
|
|
8
8
|
CoreSchemaView,
|
|
9
|
+
MigrationPlanOperation,
|
|
10
|
+
OperationPreview,
|
|
9
11
|
SignDatabaseResult,
|
|
10
12
|
VerifyDatabaseResult,
|
|
11
13
|
VerifyDatabaseSchemaResult,
|
|
@@ -13,8 +15,11 @@ import type {
|
|
|
13
15
|
import {
|
|
14
16
|
createControlStack,
|
|
15
17
|
hasMigrations,
|
|
18
|
+
hasOperationPreview,
|
|
19
|
+
hasPslContractInfer,
|
|
16
20
|
hasSchemaView,
|
|
17
21
|
} from '@prisma-next/framework-components/control';
|
|
22
|
+
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
18
23
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
19
24
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
20
25
|
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
@@ -469,6 +474,22 @@ class ControlClientImpl implements ControlClient {
|
|
|
469
474
|
return undefined;
|
|
470
475
|
}
|
|
471
476
|
|
|
477
|
+
inferPslContract(schemaIR: unknown): PslDocumentAst | undefined {
|
|
478
|
+
this.init();
|
|
479
|
+
if (this.familyInstance && hasPslContractInfer(this.familyInstance)) {
|
|
480
|
+
return this.familyInstance.inferPslContract(schemaIR);
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview | undefined {
|
|
486
|
+
this.init();
|
|
487
|
+
if (this.familyInstance && hasOperationPreview(this.familyInstance)) {
|
|
488
|
+
return this.familyInstance.toOperationPreview(operations);
|
|
489
|
+
}
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
472
493
|
async emit(options: EmitOptions): Promise<EmitResult> {
|
|
473
494
|
const { onProgress, contractConfig } = options;
|
|
474
495
|
|
|
@@ -8,10 +8,10 @@ import type {
|
|
|
8
8
|
MigrationRunnerResult,
|
|
9
9
|
TargetMigrationsCapability,
|
|
10
10
|
} from '@prisma-next/framework-components/control';
|
|
11
|
+
import { hasOperationPreview } from '@prisma-next/framework-components/control';
|
|
11
12
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
12
13
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
13
14
|
import type { DbInitResult, DbInitSuccess, OnControlProgress } from '../types';
|
|
14
|
-
import { extractOperationStatements } from './extract-operation-statements';
|
|
15
15
|
import { createOperationCallbacks, stripOperations } from './migration-helpers';
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -84,8 +84,9 @@ export async function executeDbInit<TFamilyId extends string, TTargetId extends
|
|
|
84
84
|
schema: schemaIR,
|
|
85
85
|
policy,
|
|
86
86
|
// `db init` does not produce a `migration.ts`, so the from-hash on the
|
|
87
|
-
// resulting plan is never surfaced to authoring — pass
|
|
88
|
-
fromHash
|
|
87
|
+
// resulting plan is never surfaced to authoring — pass null (the
|
|
88
|
+
// baseline encoding for `MigrationPlanner.plan({ fromHash })`).
|
|
89
|
+
fromHash: null,
|
|
89
90
|
frameworkComponents,
|
|
90
91
|
});
|
|
91
92
|
|
|
@@ -194,12 +195,14 @@ export async function executeDbInit<TFamilyId extends string, TTargetId extends
|
|
|
194
195
|
|
|
195
196
|
// Plan mode - don't execute
|
|
196
197
|
if (mode === 'plan') {
|
|
197
|
-
const
|
|
198
|
+
const preview = hasOperationPreview(familyInstance)
|
|
199
|
+
? familyInstance.toOperationPreview(migrationPlan.operations)
|
|
200
|
+
: undefined;
|
|
198
201
|
const result: DbInitSuccess = {
|
|
199
202
|
mode: 'plan',
|
|
200
203
|
plan: {
|
|
201
204
|
operations: stripOperations(migrationPlan.operations),
|
|
202
|
-
...ifDefined('
|
|
205
|
+
...ifDefined('preview', preview),
|
|
203
206
|
},
|
|
204
207
|
destination: {
|
|
205
208
|
storageHash: migrationPlan.destination.storageHash,
|
|
@@ -7,10 +7,10 @@ import type {
|
|
|
7
7
|
MigrationRunnerResult,
|
|
8
8
|
TargetMigrationsCapability,
|
|
9
9
|
} from '@prisma-next/framework-components/control';
|
|
10
|
+
import { hasOperationPreview } from '@prisma-next/framework-components/control';
|
|
10
11
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
11
12
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
12
13
|
import type { DbUpdateResult, DbUpdateSuccess, OnControlProgress } from '../types';
|
|
13
|
-
import { extractOperationStatements } from './extract-operation-statements';
|
|
14
14
|
import { createOperationCallbacks, stripOperations } from './migration-helpers';
|
|
15
15
|
|
|
16
16
|
// F12: db update allows additive, widening, and destructive operations.
|
|
@@ -84,8 +84,9 @@ export async function executeDbUpdate<TFamilyId extends string, TTargetId extend
|
|
|
84
84
|
schema: schemaIR,
|
|
85
85
|
policy,
|
|
86
86
|
// `db update` does not produce a `migration.ts`, so the from-hash on the
|
|
87
|
-
// resulting plan is never surfaced to authoring — pass
|
|
88
|
-
fromHash
|
|
87
|
+
// resulting plan is never surfaced to authoring — pass null (the
|
|
88
|
+
// baseline encoding for `MigrationPlanner.plan({ fromHash })`).
|
|
89
|
+
fromHash: null,
|
|
89
90
|
frameworkComponents,
|
|
90
91
|
});
|
|
91
92
|
if (plannerResult.kind === 'failure') {
|
|
@@ -113,12 +114,14 @@ export async function executeDbUpdate<TFamilyId extends string, TTargetId extend
|
|
|
113
114
|
const migrationPlan = plannerResult.plan;
|
|
114
115
|
|
|
115
116
|
if (mode === 'plan') {
|
|
116
|
-
const
|
|
117
|
+
const preview = hasOperationPreview(familyInstance)
|
|
118
|
+
? familyInstance.toOperationPreview(migrationPlan.operations)
|
|
119
|
+
: undefined;
|
|
117
120
|
const result: DbUpdateSuccess = {
|
|
118
121
|
mode: 'plan',
|
|
119
122
|
plan: {
|
|
120
123
|
operations: stripOperations(migrationPlan.operations),
|
|
121
|
-
...(
|
|
124
|
+
...ifDefined('preview', preview),
|
|
122
125
|
},
|
|
123
126
|
destination: {
|
|
124
127
|
storageHash: migrationPlan.destination.storageHash,
|
|
@@ -79,15 +79,19 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
|
|
|
79
79
|
|
|
80
80
|
const firstMigration = pendingMigrations[0]!;
|
|
81
81
|
const lastMigration = pendingMigrations[pendingMigrations.length - 1]!;
|
|
82
|
-
|
|
82
|
+
// Manifest `from` is `string | null` (null = baseline). The live-marker
|
|
83
|
+
// layer encodes "no prior state" as EMPTY_CONTRACT_HASH; bridge here so the
|
|
84
|
+
// string comparisons below work uniformly.
|
|
85
|
+
const firstFromMarker = firstMigration.from ?? EMPTY_CONTRACT_HASH;
|
|
86
|
+
if (firstFromMarker !== originHash || lastMigration.to !== destinationHash) {
|
|
83
87
|
return notOk({
|
|
84
88
|
code: 'MIGRATION_PATH_NOT_FOUND' as const,
|
|
85
89
|
summary: 'Migration apply path does not match requested origin and destination',
|
|
86
|
-
why: `Path resolved as ${
|
|
90
|
+
why: `Path resolved as ${firstFromMarker} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
|
|
87
91
|
meta: {
|
|
88
92
|
originHash,
|
|
89
93
|
destinationHash,
|
|
90
|
-
pathOrigin:
|
|
94
|
+
pathOrigin: firstFromMarker,
|
|
91
95
|
pathDestination: lastMigration.to,
|
|
92
96
|
},
|
|
93
97
|
});
|
|
@@ -96,18 +100,19 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
|
|
|
96
100
|
for (let i = 1; i < pendingMigrations.length; i++) {
|
|
97
101
|
const previous = pendingMigrations[i - 1]!;
|
|
98
102
|
const current = pendingMigrations[i]!;
|
|
99
|
-
|
|
103
|
+
const currentFromMarker = current.from ?? EMPTY_CONTRACT_HASH;
|
|
104
|
+
if (previous.to !== currentFromMarker) {
|
|
100
105
|
return notOk({
|
|
101
106
|
code: 'MIGRATION_PATH_NOT_FOUND' as const,
|
|
102
107
|
summary: 'Migration apply path contains a discontinuity between adjacent migrations',
|
|
103
|
-
why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${
|
|
108
|
+
why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${currentFromMarker}`,
|
|
104
109
|
meta: {
|
|
105
110
|
originHash,
|
|
106
111
|
destinationHash,
|
|
107
112
|
previousDirName: previous.dirName,
|
|
108
113
|
previousTo: previous.to,
|
|
109
114
|
currentDirName: current.dirName,
|
|
110
|
-
currentFrom:
|
|
115
|
+
currentFrom: currentFromMarker,
|
|
111
116
|
discontinuityIndex: i,
|
|
112
117
|
},
|
|
113
118
|
});
|
|
@@ -135,11 +140,11 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
|
|
|
135
140
|
allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] as const,
|
|
136
141
|
};
|
|
137
142
|
|
|
138
|
-
//
|
|
139
|
-
// for a fresh database (no marker present).
|
|
143
|
+
// Manifest `from === null` means "no prior state" — the runner expects
|
|
144
|
+
// `origin: null` for a fresh database (no marker present).
|
|
140
145
|
const plan = {
|
|
141
146
|
targetId,
|
|
142
|
-
origin: migration.from ===
|
|
147
|
+
origin: migration.from === null ? null : { storageHash: migration.from },
|
|
143
148
|
destination: { storageHash: migration.to },
|
|
144
149
|
operations,
|
|
145
150
|
};
|
package/src/control-api/types.ts
CHANGED
|
@@ -12,10 +12,12 @@ import type {
|
|
|
12
12
|
CoreSchemaView,
|
|
13
13
|
MigrationPlannerConflict,
|
|
14
14
|
MigrationPlanOperation,
|
|
15
|
+
OperationPreview,
|
|
15
16
|
SignDatabaseResult,
|
|
16
17
|
VerifyDatabaseResult,
|
|
17
18
|
VerifyDatabaseSchemaResult,
|
|
18
19
|
} from '@prisma-next/framework-components/control';
|
|
20
|
+
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
19
21
|
import type { Result } from '@prisma-next/utils/result';
|
|
20
22
|
|
|
21
23
|
// ============================================================================
|
|
@@ -283,7 +285,14 @@ export interface DbInitSuccess {
|
|
|
283
285
|
readonly label: string;
|
|
284
286
|
readonly operationClass: string;
|
|
285
287
|
}>;
|
|
286
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Family-agnostic textual preview of the planned operations, e.g.
|
|
290
|
+
* `language: 'sql'` for SQL families and `language: 'mongodb-shell'`
|
|
291
|
+
* for the Mongo family. Replaces the previous `sql?: readonly string[]`
|
|
292
|
+
* field; consumers that previously read `plan.sql` should read
|
|
293
|
+
* `plan.preview?.statements.map((s) => s.text)`.
|
|
294
|
+
*/
|
|
295
|
+
readonly preview?: OperationPreview;
|
|
287
296
|
};
|
|
288
297
|
readonly destination: {
|
|
289
298
|
readonly storageHash: string;
|
|
@@ -341,7 +350,14 @@ export interface DbUpdateSuccess {
|
|
|
341
350
|
readonly label: string;
|
|
342
351
|
readonly operationClass: string;
|
|
343
352
|
}>;
|
|
344
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Family-agnostic textual preview of the planned operations, e.g.
|
|
355
|
+
* `language: 'sql'` for SQL families and `language: 'mongodb-shell'`
|
|
356
|
+
* for the Mongo family. Replaces the previous `sql?: readonly string[]`
|
|
357
|
+
* field; consumers that previously read `plan.sql` should read
|
|
358
|
+
* `plan.preview?.statements.map((s) => s.text)`.
|
|
359
|
+
*/
|
|
360
|
+
readonly preview?: OperationPreview;
|
|
345
361
|
};
|
|
346
362
|
readonly destination: {
|
|
347
363
|
readonly storageHash: string;
|
|
@@ -432,7 +448,7 @@ export type EmitResult = Result<EmitSuccess, EmitFailure>;
|
|
|
432
448
|
*/
|
|
433
449
|
export interface MigrationApplyStep {
|
|
434
450
|
readonly dirName: string;
|
|
435
|
-
readonly from: string;
|
|
451
|
+
readonly from: string | null;
|
|
436
452
|
readonly to: string;
|
|
437
453
|
readonly toContract: Contract;
|
|
438
454
|
readonly operations: readonly MigrationPlanOperation[];
|
|
@@ -471,7 +487,7 @@ export interface MigrationApplyOptions {
|
|
|
471
487
|
*/
|
|
472
488
|
export interface MigrationApplyAppliedEntry {
|
|
473
489
|
readonly dirName: string;
|
|
474
|
-
readonly from: string;
|
|
490
|
+
readonly from: string | null;
|
|
475
491
|
readonly to: string;
|
|
476
492
|
readonly operationsExecuted: number;
|
|
477
493
|
}
|
|
@@ -691,6 +707,25 @@ export interface ControlClient {
|
|
|
691
707
|
*/
|
|
692
708
|
toSchemaView(schemaIR: unknown): CoreSchemaView | undefined;
|
|
693
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Infers a PSL contract AST from an introspected schema IR.
|
|
712
|
+
* Delegates to the family instance's inferPslContract method.
|
|
713
|
+
*
|
|
714
|
+
* @param schemaIR - The schema IR from introspect()
|
|
715
|
+
* @returns PslDocumentAst if the family supports the capability, undefined otherwise
|
|
716
|
+
*/
|
|
717
|
+
inferPslContract(schemaIR: unknown): PslDocumentAst | undefined;
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Renders a textual preview of a migration plan's operations for the CLI's
|
|
721
|
+
* "DDL preview" output. Delegates to the family instance's
|
|
722
|
+
* `toOperationPreview` method.
|
|
723
|
+
*
|
|
724
|
+
* @param operations - The migration plan operations to render
|
|
725
|
+
* @returns OperationPreview if the family supports the capability, undefined otherwise
|
|
726
|
+
*/
|
|
727
|
+
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview | undefined;
|
|
728
|
+
|
|
694
729
|
/**
|
|
695
730
|
* Emits the contract to JSON and TypeScript declarations.
|
|
696
731
|
* This is an offline operation that does NOT require a database connection.
|
|
@@ -1,8 +1,41 @@
|
|
|
1
|
+
import type { OperationPreview } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { green, yellow } from 'colorette';
|
|
2
3
|
|
|
3
4
|
import type { GlobalFlags } from '../global-flags';
|
|
4
5
|
import { createColorFormatter, formatDim, isVerbose } from './helpers';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Render a single statement of an `OperationPreview` for the human-readable
|
|
9
|
+
* preview block. SQL statements get a trailing `;` if missing — matches the
|
|
10
|
+
* legacy `string[]`-based renderer byte-for-byte (per spec OQ-4). Other
|
|
11
|
+
* languages (`'mongodb-shell'`) render verbatim.
|
|
12
|
+
*/
|
|
13
|
+
function renderPreviewStatement(text: string, language: string): string | undefined {
|
|
14
|
+
const trimmed = text.trim();
|
|
15
|
+
if (!trimmed) return undefined;
|
|
16
|
+
if (language === 'sql') {
|
|
17
|
+
return trimmed.endsWith(';') ? trimmed : `${trimmed};`;
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Choose the header label for a preview block. SQL-only previews keep the
|
|
24
|
+
* legacy `DDL preview` label (preserves CLI byte-identity for SQL targets per
|
|
25
|
+
* spec OQ-4); previews from any other family — or a mix that includes any
|
|
26
|
+
* non-SQL language — use the family-agnostic `Operation preview` label.
|
|
27
|
+
*
|
|
28
|
+
* An empty `statements` array deliberately renders as `Operation preview`
|
|
29
|
+
* rather than `DDL preview`: `Array.prototype.every` is vacuously true for
|
|
30
|
+
* empty arrays, but we have no evidence the preview is SQL-only when no
|
|
31
|
+
* statements are present, so the family-agnostic label is the safer default.
|
|
32
|
+
*/
|
|
33
|
+
export function previewBlockHeader(preview: OperationPreview): string {
|
|
34
|
+
const allSql =
|
|
35
|
+
preview.statements.length > 0 && preview.statements.every((s) => s.language === 'sql');
|
|
36
|
+
return allSql ? 'DDL preview' : 'Operation preview';
|
|
37
|
+
}
|
|
38
|
+
|
|
6
39
|
// ============================================================================
|
|
7
40
|
// Migration Command Output Formatters (shared by db init and db update)
|
|
8
41
|
// ============================================================================
|
|
@@ -24,7 +57,12 @@ export interface MigrationCommandResult {
|
|
|
24
57
|
readonly label: string;
|
|
25
58
|
readonly operationClass: string;
|
|
26
59
|
}[];
|
|
27
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Family-agnostic textual preview of the planned operations. Replaces the
|
|
62
|
+
* previous `sql?: readonly string[]`. Consumers should read
|
|
63
|
+
* `plan.preview?.statements`.
|
|
64
|
+
*/
|
|
65
|
+
readonly preview?: OperationPreview;
|
|
28
66
|
};
|
|
29
67
|
readonly execution?: {
|
|
30
68
|
readonly operationsPlanned: number;
|
|
@@ -92,20 +130,20 @@ export function formatMigrationPlanOutput(
|
|
|
92
130
|
lines.push(`${formatDimText(`Destination hash: ${result.plan.destination.storageHash}`)}`);
|
|
93
131
|
}
|
|
94
132
|
|
|
95
|
-
//
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
133
|
+
// Statement preview (any family that implements OperationPreviewCapable)
|
|
134
|
+
const preview = result.plan?.preview;
|
|
135
|
+
if (preview) {
|
|
98
136
|
lines.push('');
|
|
99
|
-
lines.push(`${formatDimText(
|
|
100
|
-
if (
|
|
101
|
-
lines.push(`${formatDimText('No
|
|
137
|
+
lines.push(`${formatDimText(previewBlockHeader(preview))}`);
|
|
138
|
+
if (preview.statements.length === 0) {
|
|
139
|
+
lines.push(`${formatDimText('No operations.')}`);
|
|
102
140
|
} else {
|
|
103
141
|
lines.push('');
|
|
104
|
-
for (const statement of
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
for (const statement of preview.statements) {
|
|
143
|
+
const rendered = renderPreviewStatement(statement.text, statement.language);
|
|
144
|
+
if (rendered) {
|
|
145
|
+
lines.push(rendered);
|
|
146
|
+
}
|
|
109
147
|
}
|
|
110
148
|
}
|
|
111
149
|
}
|
|
@@ -181,17 +219,16 @@ export function formatMigrationApplyCommandOutput(
|
|
|
181
219
|
interface MigrationShowResult {
|
|
182
220
|
readonly dirName: string;
|
|
183
221
|
readonly dirPath: string;
|
|
184
|
-
readonly from: string;
|
|
222
|
+
readonly from: string | null;
|
|
185
223
|
readonly to: string;
|
|
186
224
|
readonly migrationHash: string;
|
|
187
|
-
readonly kind: string;
|
|
188
225
|
readonly createdAt: string;
|
|
189
226
|
readonly operations: readonly {
|
|
190
227
|
readonly id: string;
|
|
191
228
|
readonly label: string;
|
|
192
229
|
readonly operationClass: string;
|
|
193
230
|
}[];
|
|
194
|
-
readonly
|
|
231
|
+
readonly preview: OperationPreview;
|
|
195
232
|
readonly summary: string;
|
|
196
233
|
}
|
|
197
234
|
|
|
@@ -208,8 +245,7 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
|
|
|
208
245
|
const formatDimText = (text: string) => formatDim(useColor, text);
|
|
209
246
|
|
|
210
247
|
lines.push(`${formatGreen('✔')} ${result.dirName}`);
|
|
211
|
-
lines.push(`${formatDimText(`
|
|
212
|
-
lines.push(`${formatDimText(` from: ${result.from}`)}`);
|
|
248
|
+
lines.push(`${formatDimText(` from: ${result.from ?? '(baseline)'}`)}`);
|
|
213
249
|
lines.push(`${formatDimText(` to: ${result.to}`)}`);
|
|
214
250
|
lines.push(`${formatDimText(` migrationHash: ${result.migrationHash}`)}`);
|
|
215
251
|
lines.push(`${formatDimText(` created: ${result.createdAt}`)}`);
|
|
@@ -239,15 +275,15 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
|
|
|
239
275
|
}
|
|
240
276
|
}
|
|
241
277
|
|
|
242
|
-
if (result.
|
|
278
|
+
if (result.preview.statements.length > 0) {
|
|
243
279
|
lines.push('');
|
|
244
|
-
lines.push(`${formatDimText(
|
|
280
|
+
lines.push(`${formatDimText(previewBlockHeader(result.preview))}`);
|
|
245
281
|
lines.push('');
|
|
246
|
-
for (const statement of result.
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
for (const statement of result.preview.statements) {
|
|
283
|
+
const rendered = renderPreviewStatement(statement.text, statement.language);
|
|
284
|
+
if (rendered) {
|
|
285
|
+
lines.push(rendered);
|
|
286
|
+
}
|
|
251
287
|
}
|
|
252
288
|
}
|
|
253
289
|
|