@prisma-next/cli 0.3.0-dev.12 → 0.3.0-dev.122

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 (215) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +470 -134
  3. package/dist/cli-errors-ByGuoqNj.mjs +3 -0
  4. package/dist/cli-errors-D6HxRn3A.d.mts +2 -0
  5. package/dist/cli.d.mts +1 -0
  6. package/dist/cli.js +1 -2350
  7. package/dist/cli.mjs +242 -0
  8. package/dist/cli.mjs.map +1 -0
  9. package/dist/client-612RJJD_.mjs +1069 -0
  10. package/dist/client-612RJJD_.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts +7 -0
  12. package/dist/commands/contract-emit.d.mts.map +1 -0
  13. package/dist/commands/contract-emit.mjs +8 -0
  14. package/dist/commands/contract-infer.d.mts +7 -0
  15. package/dist/commands/contract-infer.d.mts.map +1 -0
  16. package/dist/commands/contract-infer.mjs +9 -0
  17. package/dist/commands/db-init.d.mts +7 -0
  18. package/dist/commands/db-init.d.mts.map +1 -0
  19. package/dist/commands/db-init.mjs +125 -0
  20. package/dist/commands/db-init.mjs.map +1 -0
  21. package/dist/commands/db-schema.d.mts +7 -0
  22. package/dist/commands/db-schema.d.mts.map +1 -0
  23. package/dist/commands/db-schema.mjs +55 -0
  24. package/dist/commands/db-schema.mjs.map +1 -0
  25. package/dist/commands/db-sign.d.mts +7 -0
  26. package/dist/commands/db-sign.d.mts.map +1 -0
  27. package/dist/commands/db-sign.mjs +136 -0
  28. package/dist/commands/db-sign.mjs.map +1 -0
  29. package/dist/commands/db-update.d.mts +7 -0
  30. package/dist/commands/db-update.d.mts.map +1 -0
  31. package/dist/commands/db-update.mjs +122 -0
  32. package/dist/commands/db-update.mjs.map +1 -0
  33. package/dist/commands/db-verify.d.mts +7 -0
  34. package/dist/commands/db-verify.d.mts.map +1 -0
  35. package/dist/commands/db-verify.mjs +311 -0
  36. package/dist/commands/db-verify.mjs.map +1 -0
  37. package/dist/commands/migration-apply.d.mts +36 -0
  38. package/dist/commands/migration-apply.d.mts.map +1 -0
  39. package/dist/commands/migration-apply.mjs +241 -0
  40. package/dist/commands/migration-apply.mjs.map +1 -0
  41. package/dist/commands/migration-plan.d.mts +47 -0
  42. package/dist/commands/migration-plan.d.mts.map +1 -0
  43. package/dist/commands/migration-plan.mjs +288 -0
  44. package/dist/commands/migration-plan.mjs.map +1 -0
  45. package/dist/commands/migration-ref.d.mts +43 -0
  46. package/dist/commands/migration-ref.d.mts.map +1 -0
  47. package/dist/commands/migration-ref.mjs +194 -0
  48. package/dist/commands/migration-ref.mjs.map +1 -0
  49. package/dist/commands/migration-show.d.mts +28 -0
  50. package/dist/commands/migration-show.d.mts.map +1 -0
  51. package/dist/commands/migration-show.mjs +139 -0
  52. package/dist/commands/migration-show.mjs.map +1 -0
  53. package/dist/commands/migration-status.d.mts +85 -0
  54. package/dist/commands/migration-status.d.mts.map +1 -0
  55. package/dist/commands/migration-status.mjs +8 -0
  56. package/dist/commands/migration-verify.d.mts +16 -0
  57. package/dist/commands/migration-verify.d.mts.map +1 -0
  58. package/dist/commands/migration-verify.mjs +87 -0
  59. package/dist/commands/migration-verify.mjs.map +1 -0
  60. package/dist/config-loader-d_KF19Tw.mjs +43 -0
  61. package/dist/config-loader-d_KF19Tw.mjs.map +1 -0
  62. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  63. package/dist/config-loader.d.mts.map +1 -0
  64. package/dist/config-loader.mjs +3 -0
  65. package/dist/contract-emit-CVv7dbQ9.mjs +187 -0
  66. package/dist/contract-emit-CVv7dbQ9.mjs.map +1 -0
  67. package/dist/contract-infer-Bvw8u8Eu.mjs +83 -0
  68. package/dist/contract-infer-Bvw8u8Eu.mjs.map +1 -0
  69. package/dist/exports/config-types.d.mts +2 -0
  70. package/dist/exports/config-types.mjs +3 -0
  71. package/dist/exports/control-api.d.mts +626 -0
  72. package/dist/exports/control-api.d.mts.map +1 -0
  73. package/dist/exports/control-api.mjs +107 -0
  74. package/dist/exports/control-api.mjs.map +1 -0
  75. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +10 -5
  76. package/dist/exports/index.d.mts.map +1 -0
  77. package/dist/exports/index.mjs +134 -0
  78. package/dist/exports/index.mjs.map +1 -0
  79. package/dist/extract-sql-ddl-Jf5blEO0.mjs +26 -0
  80. package/dist/extract-sql-ddl-Jf5blEO0.mjs.map +1 -0
  81. package/dist/framework-components-M2j-qPfr.mjs +59 -0
  82. package/dist/framework-components-M2j-qPfr.mjs.map +1 -0
  83. package/dist/inspect-live-schema-BQe5i4YE.mjs +90 -0
  84. package/dist/inspect-live-schema-BQe5i4YE.mjs.map +1 -0
  85. package/dist/migration-command-scaffold-SLrjcKXS.mjs +104 -0
  86. package/dist/migration-command-scaffold-SLrjcKXS.mjs.map +1 -0
  87. package/dist/migration-status-B7OVZ-Ka.mjs +1576 -0
  88. package/dist/migration-status-B7OVZ-Ka.mjs.map +1 -0
  89. package/dist/migrations-Db_ea9eE.mjs +173 -0
  90. package/dist/migrations-Db_ea9eE.mjs.map +1 -0
  91. package/dist/progress-adapter-DRNe2idZ.mjs +43 -0
  92. package/dist/progress-adapter-DRNe2idZ.mjs.map +1 -0
  93. package/dist/terminal-ui-DAcMBRKf.mjs +980 -0
  94. package/dist/terminal-ui-DAcMBRKf.mjs.map +1 -0
  95. package/dist/verify-DXKxBFvU.mjs +385 -0
  96. package/dist/verify-DXKxBFvU.mjs.map +1 -0
  97. package/package.json +85 -40
  98. package/src/cli.ts +109 -58
  99. package/src/commands/contract-emit.ts +236 -143
  100. package/src/commands/contract-infer-paths.ts +32 -0
  101. package/src/commands/contract-infer.ts +131 -0
  102. package/src/commands/db-init.ts +211 -425
  103. package/src/commands/db-schema.ts +77 -0
  104. package/src/commands/db-sign.ts +207 -228
  105. package/src/commands/db-update.ts +236 -0
  106. package/src/commands/db-verify.ts +484 -186
  107. package/src/commands/inspect-live-schema.ts +171 -0
  108. package/src/commands/migration-apply.ts +416 -0
  109. package/src/commands/migration-plan.ts +451 -0
  110. package/src/commands/migration-ref.ts +305 -0
  111. package/src/commands/migration-show.ts +246 -0
  112. package/src/commands/migration-status.ts +838 -0
  113. package/src/commands/migration-verify.ts +134 -0
  114. package/src/config-loader.ts +13 -3
  115. package/src/control-api/client.ts +614 -0
  116. package/src/control-api/contract-enrichment.ts +135 -0
  117. package/src/control-api/errors.ts +9 -0
  118. package/src/control-api/operations/contract-emit.ts +173 -0
  119. package/src/control-api/operations/db-init.ts +286 -0
  120. package/src/control-api/operations/db-update.ts +221 -0
  121. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  122. package/src/control-api/operations/migration-apply.ts +194 -0
  123. package/src/control-api/operations/migration-helpers.ts +49 -0
  124. package/src/control-api/types.ts +683 -0
  125. package/src/exports/config-types.ts +4 -3
  126. package/src/exports/control-api.ts +56 -0
  127. package/src/load-ts-contract.ts +16 -11
  128. package/src/utils/cli-errors.ts +5 -2
  129. package/src/utils/command-helpers.ts +293 -3
  130. package/src/utils/formatters/emit.ts +67 -0
  131. package/src/utils/formatters/errors.ts +82 -0
  132. package/src/utils/formatters/graph-migration-mapper.ts +220 -0
  133. package/src/utils/formatters/graph-render.ts +1317 -0
  134. package/src/utils/formatters/graph-types.ts +114 -0
  135. package/src/utils/formatters/help.ts +380 -0
  136. package/src/utils/formatters/helpers.ts +28 -0
  137. package/src/utils/formatters/migrations.ts +346 -0
  138. package/src/utils/formatters/styled.ts +212 -0
  139. package/src/utils/formatters/verify.ts +620 -0
  140. package/src/utils/global-flags.ts +41 -23
  141. package/src/utils/migration-command-scaffold.ts +187 -0
  142. package/src/utils/migration-types.ts +12 -0
  143. package/src/utils/progress-adapter.ts +75 -0
  144. package/src/utils/result-handler.ts +12 -13
  145. package/src/utils/shutdown.ts +92 -0
  146. package/src/utils/suggest-command.ts +31 -0
  147. package/src/utils/terminal-ui.ts +276 -0
  148. package/dist/chunk-BZMBKEEQ.js +0 -997
  149. package/dist/chunk-BZMBKEEQ.js.map +0 -1
  150. package/dist/chunk-CVNWLFXO.js +0 -91
  151. package/dist/chunk-CVNWLFXO.js.map +0 -1
  152. package/dist/chunk-HWYQOCAJ.js +0 -47
  153. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  154. package/dist/chunk-QUPBU4KV.js +0 -131
  155. package/dist/chunk-QUPBU4KV.js.map +0 -1
  156. package/dist/cli.d.ts +0 -2
  157. package/dist/cli.d.ts.map +0 -1
  158. package/dist/cli.js.map +0 -1
  159. package/dist/commands/contract-emit.d.ts +0 -3
  160. package/dist/commands/contract-emit.d.ts.map +0 -1
  161. package/dist/commands/contract-emit.js +0 -9
  162. package/dist/commands/contract-emit.js.map +0 -1
  163. package/dist/commands/db-init.d.ts +0 -3
  164. package/dist/commands/db-init.d.ts.map +0 -1
  165. package/dist/commands/db-init.js +0 -337
  166. package/dist/commands/db-init.js.map +0 -1
  167. package/dist/commands/db-introspect.d.ts +0 -3
  168. package/dist/commands/db-introspect.d.ts.map +0 -1
  169. package/dist/commands/db-introspect.js +0 -186
  170. package/dist/commands/db-introspect.js.map +0 -1
  171. package/dist/commands/db-schema-verify.d.ts +0 -3
  172. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  173. package/dist/commands/db-schema-verify.js +0 -160
  174. package/dist/commands/db-schema-verify.js.map +0 -1
  175. package/dist/commands/db-sign.d.ts +0 -3
  176. package/dist/commands/db-sign.d.ts.map +0 -1
  177. package/dist/commands/db-sign.js +0 -195
  178. package/dist/commands/db-sign.js.map +0 -1
  179. package/dist/commands/db-verify.d.ts +0 -3
  180. package/dist/commands/db-verify.d.ts.map +0 -1
  181. package/dist/commands/db-verify.js +0 -169
  182. package/dist/commands/db-verify.js.map +0 -1
  183. package/dist/config-loader.d.ts.map +0 -1
  184. package/dist/config-loader.js +0 -7
  185. package/dist/config-loader.js.map +0 -1
  186. package/dist/exports/config-types.d.ts +0 -3
  187. package/dist/exports/config-types.d.ts.map +0 -1
  188. package/dist/exports/config-types.js +0 -6
  189. package/dist/exports/config-types.js.map +0 -1
  190. package/dist/exports/index.d.ts +0 -4
  191. package/dist/exports/index.d.ts.map +0 -1
  192. package/dist/exports/index.js +0 -175
  193. package/dist/exports/index.js.map +0 -1
  194. package/dist/load-ts-contract.d.ts.map +0 -1
  195. package/dist/utils/action.d.ts +0 -16
  196. package/dist/utils/action.d.ts.map +0 -1
  197. package/dist/utils/cli-errors.d.ts +0 -7
  198. package/dist/utils/cli-errors.d.ts.map +0 -1
  199. package/dist/utils/command-helpers.d.ts +0 -12
  200. package/dist/utils/command-helpers.d.ts.map +0 -1
  201. package/dist/utils/framework-components.d.ts +0 -70
  202. package/dist/utils/framework-components.d.ts.map +0 -1
  203. package/dist/utils/global-flags.d.ts +0 -25
  204. package/dist/utils/global-flags.d.ts.map +0 -1
  205. package/dist/utils/output.d.ts +0 -142
  206. package/dist/utils/output.d.ts.map +0 -1
  207. package/dist/utils/result-handler.d.ts +0 -15
  208. package/dist/utils/result-handler.d.ts.map +0 -1
  209. package/dist/utils/spinner.d.ts +0 -29
  210. package/dist/utils/spinner.d.ts.map +0 -1
  211. package/src/commands/db-introspect.ts +0 -256
  212. package/src/commands/db-schema-verify.ts +0 -232
  213. package/src/utils/action.ts +0 -43
  214. package/src/utils/output.ts +0 -1471
  215. package/src/utils/spinner.ts +0 -67
@@ -0,0 +1,221 @@
1
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
2
+ import type { ContractIR } from '@prisma-next/contract/ir';
3
+ import type {
4
+ ControlDriverInstance,
5
+ ControlFamilyInstance,
6
+ MigrationPlannerResult,
7
+ MigrationRunnerResult,
8
+ TargetMigrationsCapability,
9
+ } from '@prisma-next/core-control-plane/types';
10
+ import { ifDefined } from '@prisma-next/utils/defined';
11
+ import { notOk, ok } from '@prisma-next/utils/result';
12
+ import type { DbUpdateResult, DbUpdateSuccess, OnControlProgress } from '../types';
13
+ import { extractSqlDdl } from './extract-sql-ddl';
14
+ import { createOperationCallbacks, stripOperations } from './migration-helpers';
15
+
16
+ // F12: db update allows additive, widening, and destructive operations.
17
+ const DB_UPDATE_POLICY = {
18
+ allowedOperationClasses: ['additive', 'widening', 'destructive'] as const,
19
+ } as const;
20
+
21
+ /**
22
+ * Options for the executeDbUpdate operation.
23
+ * Config-agnostic: receives pre-resolved driver, family, contract, and migrations capability.
24
+ */
25
+ export interface ExecuteDbUpdateOptions<TFamilyId extends string, TTargetId extends string> {
26
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
27
+ readonly familyInstance: ControlFamilyInstance<TFamilyId>;
28
+ readonly contractIR: ContractIR;
29
+ readonly mode: 'plan' | 'apply';
30
+ readonly migrations: TargetMigrationsCapability<
31
+ TFamilyId,
32
+ TTargetId,
33
+ ControlFamilyInstance<TFamilyId>
34
+ >;
35
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
36
+ readonly acceptDataLoss?: boolean;
37
+ /** Optional progress callback for observing operation progress. */
38
+ readonly onProgress?: OnControlProgress;
39
+ }
40
+
41
+ /**
42
+ * Executes the db update operation: introspect → plan → (optionally) apply → marker.
43
+ *
44
+ * db update is a pure reconciliation command: it introspects the live schema, plans the diff
45
+ * to the destination contract, and applies operations. The marker is bookkeeping only — written
46
+ * after apply so that `verify` and `db init` can reference it, but never read or validated
47
+ * by db update itself. The runner creates the marker table if it does not exist.
48
+ */
49
+ export async function executeDbUpdate<TFamilyId extends string, TTargetId extends string>(
50
+ options: ExecuteDbUpdateOptions<TFamilyId, TTargetId>,
51
+ ): Promise<DbUpdateResult> {
52
+ const { driver, familyInstance, contractIR, mode, migrations, frameworkComponents, onProgress } =
53
+ options;
54
+
55
+ const planner = migrations.createPlanner(familyInstance);
56
+ const runner = migrations.createRunner(familyInstance);
57
+
58
+ const introspectSpanId = 'introspect';
59
+ onProgress?.({
60
+ action: 'dbUpdate',
61
+ kind: 'spanStart',
62
+ spanId: introspectSpanId,
63
+ label: 'Introspecting database schema',
64
+ });
65
+ const schemaIR = await familyInstance.introspect({ driver });
66
+ onProgress?.({
67
+ action: 'dbUpdate',
68
+ kind: 'spanEnd',
69
+ spanId: introspectSpanId,
70
+ outcome: 'ok',
71
+ });
72
+
73
+ const policy = DB_UPDATE_POLICY;
74
+
75
+ const planSpanId = 'plan';
76
+ onProgress?.({
77
+ action: 'dbUpdate',
78
+ kind: 'spanStart',
79
+ spanId: planSpanId,
80
+ label: 'Planning migration',
81
+ });
82
+ const plannerResult: MigrationPlannerResult = await planner.plan({
83
+ contract: contractIR,
84
+ schema: schemaIR,
85
+ policy,
86
+ frameworkComponents,
87
+ });
88
+ if (plannerResult.kind === 'failure') {
89
+ onProgress?.({
90
+ action: 'dbUpdate',
91
+ kind: 'spanEnd',
92
+ spanId: planSpanId,
93
+ outcome: 'error',
94
+ });
95
+ return notOk({
96
+ code: 'PLANNING_FAILED',
97
+ summary: 'Migration planning failed due to conflicts',
98
+ conflicts: plannerResult.conflicts,
99
+ why: undefined,
100
+ meta: undefined,
101
+ });
102
+ }
103
+ onProgress?.({
104
+ action: 'dbUpdate',
105
+ kind: 'spanEnd',
106
+ spanId: planSpanId,
107
+ outcome: 'ok',
108
+ });
109
+
110
+ const migrationPlan = plannerResult.plan;
111
+
112
+ if (mode === 'plan') {
113
+ const planSql =
114
+ familyInstance.familyId === 'sql' ? extractSqlDdl(migrationPlan.operations) : undefined;
115
+ const result: DbUpdateSuccess = {
116
+ mode: 'plan',
117
+ plan: {
118
+ operations: stripOperations(migrationPlan.operations),
119
+ ...(planSql !== undefined ? { sql: planSql } : {}),
120
+ },
121
+ destination: {
122
+ storageHash: migrationPlan.destination.storageHash,
123
+ ...ifDefined('profileHash', migrationPlan.destination.profileHash),
124
+ },
125
+ summary: `Planned ${migrationPlan.operations.length} operation(s)`,
126
+ };
127
+ return ok(result);
128
+ }
129
+
130
+ // When applying, require explicit acceptance for destructive operations
131
+ if (!options.acceptDataLoss) {
132
+ const destructiveOps = migrationPlan.operations
133
+ .filter((op) => op.operationClass === 'destructive')
134
+ .map((op) => ({ id: op.id, label: op.label }));
135
+ if (destructiveOps.length > 0) {
136
+ return notOk({
137
+ code: 'DESTRUCTIVE_CHANGES',
138
+ summary: `Planned ${destructiveOps.length} destructive operation(s) that require confirmation`,
139
+ why: 'Destructive operations require confirmation — re-run with -y to accept',
140
+ conflicts: undefined,
141
+ meta: { destructiveOperations: destructiveOps },
142
+ });
143
+ }
144
+ }
145
+
146
+ const applySpanId = 'apply';
147
+ onProgress?.({
148
+ action: 'dbUpdate',
149
+ kind: 'spanStart',
150
+ spanId: applySpanId,
151
+ label: 'Applying migration plan',
152
+ });
153
+
154
+ const callbacks = createOperationCallbacks(onProgress, 'dbUpdate', applySpanId);
155
+
156
+ const runnerResult: MigrationRunnerResult = await runner.execute({
157
+ plan: migrationPlan,
158
+ driver,
159
+ destinationContract: contractIR,
160
+ policy,
161
+ ...(callbacks ? { callbacks } : {}),
162
+ // db update plans and applies from a single introspection pass, so per-operation pre/postchecks
163
+ // and idempotency probes are intentionally disabled to avoid redundant roundtrips.
164
+ executionChecks: {
165
+ prechecks: false,
166
+ postchecks: false,
167
+ idempotencyChecks: false,
168
+ },
169
+ frameworkComponents,
170
+ });
171
+
172
+ if (!runnerResult.ok) {
173
+ onProgress?.({
174
+ action: 'dbUpdate',
175
+ kind: 'spanEnd',
176
+ spanId: applySpanId,
177
+ outcome: 'error',
178
+ });
179
+ return notOk({
180
+ code: 'RUNNER_FAILED',
181
+ summary: runnerResult.failure.summary,
182
+ why: runnerResult.failure.why,
183
+ meta: runnerResult.failure.meta,
184
+ conflicts: undefined,
185
+ });
186
+ }
187
+
188
+ const execution = runnerResult.value;
189
+ onProgress?.({
190
+ action: 'dbUpdate',
191
+ kind: 'spanEnd',
192
+ spanId: applySpanId,
193
+ outcome: 'ok',
194
+ });
195
+
196
+ const result: DbUpdateSuccess = {
197
+ mode: 'apply',
198
+ plan: {
199
+ operations: stripOperations(migrationPlan.operations),
200
+ },
201
+ destination: {
202
+ storageHash: migrationPlan.destination.storageHash,
203
+ ...ifDefined('profileHash', migrationPlan.destination.profileHash),
204
+ },
205
+ execution: {
206
+ operationsPlanned: execution.operationsPlanned,
207
+ operationsExecuted: execution.operationsExecuted,
208
+ },
209
+ marker: migrationPlan.destination.profileHash
210
+ ? {
211
+ storageHash: migrationPlan.destination.storageHash,
212
+ profileHash: migrationPlan.destination.profileHash,
213
+ }
214
+ : { storageHash: migrationPlan.destination.storageHash },
215
+ summary:
216
+ execution.operationsExecuted === 0
217
+ ? 'Database already matches contract, signature updated'
218
+ : `Applied ${execution.operationsExecuted} operation(s), signature updated`,
219
+ };
220
+ return ok(result);
221
+ }
@@ -0,0 +1,47 @@
1
+ import type { MigrationPlanOperation } from '@prisma-next/core-control-plane/types';
2
+
3
+ /**
4
+ * Shape of an SQL execute step on SqlMigrationPlanOperation.
5
+ * Used for runtime type narrowing without importing the concrete SQL type.
6
+ */
7
+ interface SqlExecuteStep {
8
+ readonly sql: string;
9
+ }
10
+
11
+ function isDdlStatement(sqlStatement: string): boolean {
12
+ const trimmed = sqlStatement.trim().toLowerCase();
13
+ return (
14
+ trimmed.startsWith('create ') || trimmed.startsWith('alter ') || trimmed.startsWith('drop ')
15
+ );
16
+ }
17
+
18
+ function hasExecuteSteps(
19
+ operation: MigrationPlanOperation,
20
+ ): operation is MigrationPlanOperation & { readonly execute: readonly SqlExecuteStep[] } {
21
+ const candidate = operation as unknown as Record<string, unknown>;
22
+ if (!('execute' in candidate) || !Array.isArray(candidate['execute'])) {
23
+ return false;
24
+ }
25
+ return candidate['execute'].every(
26
+ (step: unknown) => typeof step === 'object' && step !== null && 'sql' in step,
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Extracts a best-effort SQL DDL preview for CLI plan output.
32
+ * This helper is presentation-only and is never used to decide migration correctness.
33
+ */
34
+ export function extractSqlDdl(operations: readonly MigrationPlanOperation[]): string[] {
35
+ const statements: string[] = [];
36
+ for (const operation of operations) {
37
+ if (!hasExecuteSteps(operation)) {
38
+ continue;
39
+ }
40
+ for (const step of operation.execute) {
41
+ if (typeof step.sql === 'string' && isDdlStatement(step.sql)) {
42
+ statements.push(step.sql.trim());
43
+ }
44
+ }
45
+ }
46
+ return statements;
47
+ }
@@ -0,0 +1,194 @@
1
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
2
+ import type { ContractIR } from '@prisma-next/contract/ir';
3
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';
4
+ import type {
5
+ ControlDriverInstance,
6
+ ControlFamilyInstance,
7
+ MigrationRunnerResult,
8
+ TargetMigrationsCapability,
9
+ } from '@prisma-next/core-control-plane/types';
10
+ import { notOk, ok } from '@prisma-next/utils/result';
11
+ import type {
12
+ MigrationApplyAppliedEntry,
13
+ MigrationApplyResult,
14
+ MigrationApplyStep,
15
+ OnControlProgress,
16
+ } from '../types';
17
+
18
+ export interface ExecuteMigrationApplyOptions<TFamilyId extends string, TTargetId extends string> {
19
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
20
+ readonly familyInstance: ControlFamilyInstance<TFamilyId>;
21
+ readonly originHash: string;
22
+ readonly destinationHash: string;
23
+ readonly pendingMigrations: readonly MigrationApplyStep[];
24
+ readonly migrations: TargetMigrationsCapability<
25
+ TFamilyId,
26
+ TTargetId,
27
+ ControlFamilyInstance<TFamilyId>
28
+ >;
29
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
30
+ readonly targetId: string;
31
+ readonly onProgress?: OnControlProgress;
32
+ }
33
+
34
+ export async function executeMigrationApply<TFamilyId extends string, TTargetId extends string>(
35
+ options: ExecuteMigrationApplyOptions<TFamilyId, TTargetId>,
36
+ ): Promise<MigrationApplyResult> {
37
+ const {
38
+ driver,
39
+ familyInstance,
40
+ originHash,
41
+ destinationHash,
42
+ pendingMigrations,
43
+ migrations,
44
+ frameworkComponents,
45
+ targetId,
46
+ onProgress,
47
+ } = options;
48
+
49
+ if (pendingMigrations.length === 0) {
50
+ if (originHash !== destinationHash) {
51
+ return notOk({
52
+ code: 'MIGRATION_PATH_NOT_FOUND' as const,
53
+ summary: 'No migrations provided for requested origin and destination',
54
+ why: `Requested ${originHash} -> ${destinationHash} but pendingMigrations is empty`,
55
+ meta: { originHash, destinationHash },
56
+ });
57
+ }
58
+ return ok({
59
+ migrationsApplied: 0,
60
+ markerHash: originHash,
61
+ applied: [],
62
+ summary: 'Already up to date',
63
+ });
64
+ }
65
+
66
+ const firstMigration = pendingMigrations[0]!;
67
+ const lastMigration = pendingMigrations[pendingMigrations.length - 1]!;
68
+ if (firstMigration.from !== originHash || lastMigration.to !== destinationHash) {
69
+ return notOk({
70
+ code: 'MIGRATION_PATH_NOT_FOUND' as const,
71
+ summary: 'Migration apply path does not match requested origin and destination',
72
+ why: `Path resolved as ${firstMigration.from} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
73
+ meta: {
74
+ originHash,
75
+ destinationHash,
76
+ pathOrigin: firstMigration.from,
77
+ pathDestination: lastMigration.to,
78
+ },
79
+ });
80
+ }
81
+
82
+ for (let i = 1; i < pendingMigrations.length; i++) {
83
+ const previous = pendingMigrations[i - 1]!;
84
+ const current = pendingMigrations[i]!;
85
+ if (previous.to !== current.from) {
86
+ return notOk({
87
+ code: 'MIGRATION_PATH_NOT_FOUND' as const,
88
+ summary: 'Migration apply path contains a discontinuity between adjacent migrations',
89
+ why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${current.from}`,
90
+ meta: {
91
+ originHash,
92
+ destinationHash,
93
+ previousDirName: previous.dirName,
94
+ previousTo: previous.to,
95
+ currentDirName: current.dirName,
96
+ currentFrom: current.from,
97
+ discontinuityIndex: i,
98
+ },
99
+ });
100
+ }
101
+ }
102
+
103
+ const runner = migrations.createRunner(familyInstance);
104
+ const applied: MigrationApplyAppliedEntry[] = [];
105
+
106
+ for (const migration of pendingMigrations) {
107
+ const migrationSpanId = `migration:${migration.dirName}`;
108
+ onProgress?.({
109
+ action: 'migrationApply',
110
+ kind: 'spanStart',
111
+ spanId: migrationSpanId,
112
+ label: `Applying ${migration.dirName}`,
113
+ });
114
+
115
+ const { operations } = migration;
116
+
117
+ // Allow all operation classes. The policy gate belongs at plan time, not
118
+ // apply time — the planner already decided what to emit. Restricting here
119
+ // would be a tautology (the allowed set would just mirror what's in ops).
120
+ const policy = {
121
+ allowedOperationClasses: ['additive', 'widening', 'destructive'] as const,
122
+ };
123
+
124
+ // EMPTY_CONTRACT_HASH means "no prior state" — the runner expects origin: null
125
+ // for a fresh database (no marker present).
126
+ const plan = {
127
+ targetId,
128
+ origin: migration.from === EMPTY_CONTRACT_HASH ? null : { storageHash: migration.from },
129
+ destination: { storageHash: migration.to },
130
+ operations,
131
+ };
132
+
133
+ const destinationContract = familyInstance.validateContractIR(
134
+ migration.toContract as ContractIR,
135
+ );
136
+
137
+ const runnerResult: MigrationRunnerResult = await runner.execute({
138
+ plan,
139
+ driver,
140
+ destinationContract,
141
+ policy,
142
+ executionChecks: {
143
+ prechecks: true,
144
+ postchecks: true,
145
+ idempotencyChecks: true,
146
+ },
147
+ frameworkComponents,
148
+ });
149
+
150
+ if (!runnerResult.ok) {
151
+ onProgress?.({
152
+ action: 'migrationApply',
153
+ kind: 'spanEnd',
154
+ spanId: migrationSpanId,
155
+ outcome: 'error',
156
+ });
157
+ return notOk({
158
+ code: 'RUNNER_FAILED' as const,
159
+ summary: runnerResult.failure.summary,
160
+ why: runnerResult.failure.why,
161
+ meta: {
162
+ migration: migration.dirName,
163
+ from: migration.from,
164
+ to: migration.to,
165
+ ...(runnerResult.failure.meta ?? {}),
166
+ },
167
+ });
168
+ }
169
+
170
+ onProgress?.({
171
+ action: 'migrationApply',
172
+ kind: 'spanEnd',
173
+ spanId: migrationSpanId,
174
+ outcome: 'ok',
175
+ });
176
+
177
+ applied.push({
178
+ dirName: migration.dirName,
179
+ from: migration.from,
180
+ to: migration.to,
181
+ operationsExecuted: runnerResult.value.operationsExecuted,
182
+ });
183
+ }
184
+
185
+ const finalHash = pendingMigrations[pendingMigrations.length - 1]!.to;
186
+ const totalOps = applied.reduce((sum, a) => sum + a.operationsExecuted, 0);
187
+
188
+ return ok({
189
+ migrationsApplied: applied.length,
190
+ markerHash: finalHash,
191
+ applied,
192
+ summary: `Applied ${applied.length} migration(s) (${totalOps} operation(s)), marker at ${finalHash}`,
193
+ });
194
+ }
@@ -0,0 +1,49 @@
1
+ import type { MigrationPlanOperation } from '@prisma-next/core-control-plane/types';
2
+ import type { ControlActionName, OnControlProgress } from '../types';
3
+
4
+ /**
5
+ * Strips operation objects to their public shape (id, label, operationClass).
6
+ * Used at the API boundary to avoid leaking internal fields (precheck, execute, postcheck, etc.).
7
+ */
8
+ export function stripOperations(
9
+ operations: readonly MigrationPlanOperation[],
10
+ ): ReadonlyArray<{ readonly id: string; readonly label: string; readonly operationClass: string }> {
11
+ return operations.map((op) => ({
12
+ id: op.id,
13
+ label: op.label,
14
+ operationClass: op.operationClass,
15
+ }));
16
+ }
17
+
18
+ /**
19
+ * Creates per-operation progress callbacks for the runner.
20
+ * Returns undefined when no onProgress callback is provided.
21
+ */
22
+ export function createOperationCallbacks(
23
+ onProgress: OnControlProgress | undefined,
24
+ action: ControlActionName,
25
+ parentSpanId: string,
26
+ ) {
27
+ if (!onProgress) {
28
+ return undefined;
29
+ }
30
+ return {
31
+ onOperationStart: (op: MigrationPlanOperation) => {
32
+ onProgress({
33
+ action,
34
+ kind: 'spanStart',
35
+ spanId: `operation:${op.id}`,
36
+ parentSpanId,
37
+ label: op.label,
38
+ });
39
+ },
40
+ onOperationComplete: (op: MigrationPlanOperation) => {
41
+ onProgress({
42
+ action,
43
+ kind: 'spanEnd',
44
+ spanId: `operation:${op.id}`,
45
+ outcome: 'ok',
46
+ });
47
+ },
48
+ };
49
+ }