@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.
Files changed (83) hide show
  1. package/dist/cli.mjs +11 -3
  2. package/dist/cli.mjs.map +1 -1
  3. package/dist/{client-enZIahga.mjs → client-keSCAgjW.mjs} +25 -16
  4. package/dist/client-keSCAgjW.mjs.map +1 -0
  5. package/dist/commands/contract-emit.mjs +5 -0
  6. package/dist/commands/contract-infer.d.mts.map +1 -1
  7. package/dist/commands/contract-infer.mjs +7 -1
  8. package/dist/commands/db-init.d.mts.map +1 -1
  9. package/dist/commands/db-init.mjs +6 -4
  10. package/dist/commands/db-init.mjs.map +1 -1
  11. package/dist/commands/db-schema.mjs +5 -2
  12. package/dist/commands/db-schema.mjs.map +1 -1
  13. package/dist/commands/db-sign.mjs +3 -2
  14. package/dist/commands/db-sign.mjs.map +1 -1
  15. package/dist/commands/db-update.mjs +5 -4
  16. package/dist/commands/db-update.mjs.map +1 -1
  17. package/dist/commands/db-verify.mjs +3 -2
  18. package/dist/commands/db-verify.mjs.map +1 -1
  19. package/dist/commands/migration-apply.d.mts +1 -1
  20. package/dist/commands/migration-apply.mjs +3 -2
  21. package/dist/commands/migration-apply.mjs.map +1 -1
  22. package/dist/commands/migration-new.d.mts.map +1 -1
  23. package/dist/commands/migration-new.mjs +1 -3
  24. package/dist/commands/migration-new.mjs.map +1 -1
  25. package/dist/commands/migration-plan.d.mts +8 -2
  26. package/dist/commands/migration-plan.d.mts.map +1 -1
  27. package/dist/commands/migration-plan.mjs +10 -12
  28. package/dist/commands/migration-plan.mjs.map +1 -1
  29. package/dist/commands/migration-ref.d.mts +1 -1
  30. package/dist/commands/migration-show.d.mts +10 -4
  31. package/dist/commands/migration-show.d.mts.map +1 -1
  32. package/dist/commands/migration-show.mjs +12 -6
  33. package/dist/commands/migration-show.mjs.map +1 -1
  34. package/dist/commands/migration-status.mjs +6 -1
  35. package/dist/{contract-emit-LjzCoicC.mjs → contract-emit-DS5NzZh2.mjs} +2 -0
  36. package/dist/{contract-infer-BjzkcwQt.mjs → contract-infer-GztVCOCJ.mjs} +8 -16
  37. package/dist/contract-infer-GztVCOCJ.mjs.map +1 -0
  38. package/dist/exports/control-api.d.mts +37 -5
  39. package/dist/exports/control-api.d.mts.map +1 -1
  40. package/dist/exports/control-api.mjs +3 -1
  41. package/dist/exports/index.mjs +5 -0
  42. package/dist/exports/index.mjs.map +1 -1
  43. package/dist/exports/init-output.mjs +1 -1
  44. package/dist/{init-BKgjxw6r.mjs → init-DAbQMxIR.mjs} +3 -3
  45. package/dist/{init-BKgjxw6r.mjs.map → init-DAbQMxIR.mjs.map} +1 -1
  46. package/dist/{inspect-live-schema-QklSDLt_.mjs → inspect-live-schema-BaR9ISwa.mjs} +5 -5
  47. package/dist/inspect-live-schema-BaR9ISwa.mjs.map +1 -0
  48. package/dist/{migration-command-scaffold-BfloSWPZ.mjs → migration-command-scaffold-D1dWuEWQ.mjs} +2 -2
  49. package/dist/{migration-command-scaffold-BfloSWPZ.mjs.map → migration-command-scaffold-D1dWuEWQ.mjs.map} +1 -1
  50. package/dist/{migration-status-C5VYA5r9.mjs → migration-status-CP5k8O5i.mjs} +2 -2
  51. package/dist/{migration-status-C5VYA5r9.mjs.map → migration-status-CP5k8O5i.mjs.map} +1 -1
  52. package/dist/{migrations-CSaDHNpB.mjs → migrations-MEoKMiV5.mjs} +40 -19
  53. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  54. package/dist/{output-BiO7kt87.mjs → output-BpcQrnnq.mjs} +1 -1
  55. package/dist/{output-BiO7kt87.mjs.map → output-BpcQrnnq.mjs.map} +1 -1
  56. package/dist/{verify-BumcH6Ry.mjs → verify-BT9tgCOH.mjs} +1 -1
  57. package/dist/{verify-BumcH6Ry.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  58. package/package.json +14 -14
  59. package/src/commands/contract-infer.ts +7 -20
  60. package/src/commands/db-init.ts +1 -0
  61. package/src/commands/db-update.ts +1 -1
  62. package/src/commands/inspect-live-schema.ts +10 -5
  63. package/src/commands/migration-apply.ts +1 -1
  64. package/src/commands/migration-new.ts +2 -4
  65. package/src/commands/migration-plan.ts +22 -13
  66. package/src/commands/migration-show.ts +27 -9
  67. package/src/control-api/client.ts +21 -0
  68. package/src/control-api/operations/db-init.ts +8 -5
  69. package/src/control-api/operations/db-update.ts +8 -5
  70. package/src/control-api/operations/migration-apply.ts +14 -9
  71. package/src/control-api/types.ts +39 -4
  72. package/src/utils/formatters/migrations.ts +60 -24
  73. package/dist/client-enZIahga.mjs.map +0 -1
  74. package/dist/contract-infer-BjzkcwQt.mjs.map +0 -1
  75. package/dist/extract-operation-statements-CU-Pp4-N.mjs +0 -13
  76. package/dist/extract-operation-statements-CU-Pp4-N.mjs.map +0 -1
  77. package/dist/extract-sql-ddl-Bm0Mm0IT.mjs +0 -26
  78. package/dist/extract-sql-ddl-Bm0Mm0IT.mjs.map +0 -1
  79. package/dist/inspect-live-schema-QklSDLt_.mjs.map +0 -1
  80. package/dist/migrations-CSaDHNpB.mjs.map +0 -1
  81. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  82. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
  83. /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
- readonly sql?: readonly string[];
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 = EMPTY_CONTRACT_HASH;
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 sql = extractSqlDdl(plannedOps);
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
- sql,
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.sql && result.sql.length > 0) {
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.sql) {
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 : `${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 { MigrationPlanOperation } from '@prisma-next/framework-components/control';
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 { extractOperationStatements } from '../control-api/operations/extract-operation-statements';
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
- readonly sql: readonly string[];
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
- const sql = extractOperationStatements(config.family.familyId, ops) ?? [];
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
- sql,
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, DDL preview, and metadata for a migration package.\n' +
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 empty string.
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 planSql = extractOperationStatements(familyInstance.familyId, migrationPlan.operations);
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('sql', planSql),
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 empty string.
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 planSql = extractOperationStatements(familyInstance.familyId, migrationPlan.operations);
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
- ...(planSql !== undefined ? { sql: planSql } : {}),
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
- if (firstMigration.from !== originHash || lastMigration.to !== destinationHash) {
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 ${firstMigration.from} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
90
+ why: `Path resolved as ${firstFromMarker} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
87
91
  meta: {
88
92
  originHash,
89
93
  destinationHash,
90
- pathOrigin: firstMigration.from,
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
- if (previous.to !== current.from) {
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 ${current.from}`,
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: current.from,
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
- // EMPTY_CONTRACT_HASH means "no prior state" — the runner expects origin: null
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 === EMPTY_CONTRACT_HASH ? null : { storageHash: migration.from },
147
+ origin: migration.from === null ? null : { storageHash: migration.from },
143
148
  destination: { storageHash: migration.to },
144
149
  operations,
145
150
  };
@@ -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
- readonly sql?: ReadonlyArray<string>;
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
- readonly sql?: ReadonlyArray<string>;
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
- readonly sql?: readonly string[];
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
- // SQL DDL preview (SQL family only)
96
- const planSql = result.plan?.sql;
97
- if (planSql) {
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('DDL preview')}`);
100
- if (planSql.length === 0) {
101
- lines.push(`${formatDimText('No DDL operations.')}`);
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 planSql) {
105
- const trimmed = statement.trim();
106
- if (!trimmed) continue;
107
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
108
- lines.push(`${line}`);
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 sql: readonly string[];
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(` kind: ${result.kind}`)}`);
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.sql.length > 0) {
278
+ if (result.preview.statements.length > 0) {
243
279
  lines.push('');
244
- lines.push(`${formatDimText('DDL preview')}`);
280
+ lines.push(`${formatDimText(previewBlockHeader(result.preview))}`);
245
281
  lines.push('');
246
- for (const statement of result.sql) {
247
- const trimmed = statement.trim();
248
- if (!trimmed) continue;
249
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
250
- lines.push(`${line}`);
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