@prisma-next/cli 0.3.0-dev.12 → 0.3.0-dev.123
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/LICENSE +201 -0
- package/README.md +470 -134
- package/dist/cli-errors-ByGuoqNj.mjs +3 -0
- package/dist/cli-errors-D6HxRn3A.d.mts +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.js +1 -2350
- package/dist/cli.mjs +242 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-612RJJD_.mjs +1069 -0
- package/dist/client-612RJJD_.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts +7 -0
- package/dist/commands/contract-emit.d.mts.map +1 -0
- package/dist/commands/contract-emit.mjs +8 -0
- package/dist/commands/contract-infer.d.mts +7 -0
- package/dist/commands/contract-infer.d.mts.map +1 -0
- package/dist/commands/contract-infer.mjs +9 -0
- package/dist/commands/db-init.d.mts +7 -0
- package/dist/commands/db-init.d.mts.map +1 -0
- package/dist/commands/db-init.mjs +125 -0
- package/dist/commands/db-init.mjs.map +1 -0
- package/dist/commands/db-schema.d.mts +7 -0
- package/dist/commands/db-schema.d.mts.map +1 -0
- package/dist/commands/db-schema.mjs +55 -0
- package/dist/commands/db-schema.mjs.map +1 -0
- package/dist/commands/db-sign.d.mts +7 -0
- package/dist/commands/db-sign.d.mts.map +1 -0
- package/dist/commands/db-sign.mjs +136 -0
- package/dist/commands/db-sign.mjs.map +1 -0
- package/dist/commands/db-update.d.mts +7 -0
- package/dist/commands/db-update.d.mts.map +1 -0
- package/dist/commands/db-update.mjs +122 -0
- package/dist/commands/db-update.mjs.map +1 -0
- package/dist/commands/db-verify.d.mts +7 -0
- package/dist/commands/db-verify.d.mts.map +1 -0
- package/dist/commands/db-verify.mjs +311 -0
- package/dist/commands/db-verify.mjs.map +1 -0
- package/dist/commands/migration-apply.d.mts +36 -0
- package/dist/commands/migration-apply.d.mts.map +1 -0
- package/dist/commands/migration-apply.mjs +241 -0
- package/dist/commands/migration-apply.mjs.map +1 -0
- package/dist/commands/migration-plan.d.mts +47 -0
- package/dist/commands/migration-plan.d.mts.map +1 -0
- package/dist/commands/migration-plan.mjs +288 -0
- package/dist/commands/migration-plan.mjs.map +1 -0
- package/dist/commands/migration-ref.d.mts +43 -0
- package/dist/commands/migration-ref.d.mts.map +1 -0
- package/dist/commands/migration-ref.mjs +194 -0
- package/dist/commands/migration-ref.mjs.map +1 -0
- package/dist/commands/migration-show.d.mts +28 -0
- package/dist/commands/migration-show.d.mts.map +1 -0
- package/dist/commands/migration-show.mjs +139 -0
- package/dist/commands/migration-show.mjs.map +1 -0
- package/dist/commands/migration-status.d.mts +85 -0
- package/dist/commands/migration-status.d.mts.map +1 -0
- package/dist/commands/migration-status.mjs +8 -0
- package/dist/commands/migration-verify.d.mts +16 -0
- package/dist/commands/migration-verify.d.mts.map +1 -0
- package/dist/commands/migration-verify.mjs +87 -0
- package/dist/commands/migration-verify.mjs.map +1 -0
- package/dist/config-loader-d_KF19Tw.mjs +43 -0
- package/dist/config-loader-d_KF19Tw.mjs.map +1 -0
- package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
- package/dist/config-loader.d.mts.map +1 -0
- package/dist/config-loader.mjs +3 -0
- package/dist/contract-emit-CVv7dbQ9.mjs +187 -0
- package/dist/contract-emit-CVv7dbQ9.mjs.map +1 -0
- package/dist/contract-infer-Bvw8u8Eu.mjs +83 -0
- package/dist/contract-infer-Bvw8u8Eu.mjs.map +1 -0
- package/dist/exports/config-types.d.mts +2 -0
- package/dist/exports/config-types.mjs +3 -0
- package/dist/exports/control-api.d.mts +626 -0
- package/dist/exports/control-api.d.mts.map +1 -0
- package/dist/exports/control-api.mjs +107 -0
- package/dist/exports/control-api.mjs.map +1 -0
- package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +10 -5
- package/dist/exports/index.d.mts.map +1 -0
- package/dist/exports/index.mjs +134 -0
- package/dist/exports/index.mjs.map +1 -0
- package/dist/extract-sql-ddl-Jf5blEO0.mjs +26 -0
- package/dist/extract-sql-ddl-Jf5blEO0.mjs.map +1 -0
- package/dist/framework-components-M2j-qPfr.mjs +59 -0
- package/dist/framework-components-M2j-qPfr.mjs.map +1 -0
- package/dist/inspect-live-schema-BQe5i4YE.mjs +90 -0
- package/dist/inspect-live-schema-BQe5i4YE.mjs.map +1 -0
- package/dist/migration-command-scaffold-SLrjcKXS.mjs +104 -0
- package/dist/migration-command-scaffold-SLrjcKXS.mjs.map +1 -0
- package/dist/migration-status-B7OVZ-Ka.mjs +1576 -0
- package/dist/migration-status-B7OVZ-Ka.mjs.map +1 -0
- package/dist/migrations-Db_ea9eE.mjs +173 -0
- package/dist/migrations-Db_ea9eE.mjs.map +1 -0
- package/dist/progress-adapter-DRNe2idZ.mjs +43 -0
- package/dist/progress-adapter-DRNe2idZ.mjs.map +1 -0
- package/dist/terminal-ui-DAcMBRKf.mjs +980 -0
- package/dist/terminal-ui-DAcMBRKf.mjs.map +1 -0
- package/dist/verify-DXKxBFvU.mjs +385 -0
- package/dist/verify-DXKxBFvU.mjs.map +1 -0
- package/package.json +85 -40
- package/src/cli.ts +109 -58
- package/src/commands/contract-emit.ts +236 -143
- package/src/commands/contract-infer-paths.ts +32 -0
- package/src/commands/contract-infer.ts +131 -0
- package/src/commands/db-init.ts +211 -425
- package/src/commands/db-schema.ts +77 -0
- package/src/commands/db-sign.ts +207 -228
- package/src/commands/db-update.ts +236 -0
- package/src/commands/db-verify.ts +484 -186
- package/src/commands/inspect-live-schema.ts +171 -0
- package/src/commands/migration-apply.ts +416 -0
- package/src/commands/migration-plan.ts +451 -0
- package/src/commands/migration-ref.ts +305 -0
- package/src/commands/migration-show.ts +246 -0
- package/src/commands/migration-status.ts +838 -0
- package/src/commands/migration-verify.ts +134 -0
- package/src/config-loader.ts +13 -3
- package/src/control-api/client.ts +614 -0
- package/src/control-api/contract-enrichment.ts +135 -0
- package/src/control-api/errors.ts +9 -0
- package/src/control-api/operations/contract-emit.ts +173 -0
- package/src/control-api/operations/db-init.ts +286 -0
- package/src/control-api/operations/db-update.ts +221 -0
- package/src/control-api/operations/extract-sql-ddl.ts +47 -0
- package/src/control-api/operations/migration-apply.ts +194 -0
- package/src/control-api/operations/migration-helpers.ts +49 -0
- package/src/control-api/types.ts +683 -0
- package/src/exports/config-types.ts +4 -3
- package/src/exports/control-api.ts +56 -0
- package/src/load-ts-contract.ts +16 -11
- package/src/utils/cli-errors.ts +5 -2
- package/src/utils/command-helpers.ts +293 -3
- package/src/utils/formatters/emit.ts +67 -0
- package/src/utils/formatters/errors.ts +82 -0
- package/src/utils/formatters/graph-migration-mapper.ts +220 -0
- package/src/utils/formatters/graph-render.ts +1317 -0
- package/src/utils/formatters/graph-types.ts +114 -0
- package/src/utils/formatters/help.ts +380 -0
- package/src/utils/formatters/helpers.ts +28 -0
- package/src/utils/formatters/migrations.ts +346 -0
- package/src/utils/formatters/styled.ts +212 -0
- package/src/utils/formatters/verify.ts +620 -0
- package/src/utils/global-flags.ts +41 -23
- package/src/utils/migration-command-scaffold.ts +187 -0
- package/src/utils/migration-types.ts +12 -0
- package/src/utils/progress-adapter.ts +75 -0
- package/src/utils/result-handler.ts +12 -13
- package/src/utils/shutdown.ts +92 -0
- package/src/utils/suggest-command.ts +31 -0
- package/src/utils/terminal-ui.ts +276 -0
- package/dist/chunk-BZMBKEEQ.js +0 -997
- package/dist/chunk-BZMBKEEQ.js.map +0 -1
- package/dist/chunk-CVNWLFXO.js +0 -91
- package/dist/chunk-CVNWLFXO.js.map +0 -1
- package/dist/chunk-HWYQOCAJ.js +0 -47
- package/dist/chunk-HWYQOCAJ.js.map +0 -1
- package/dist/chunk-QUPBU4KV.js +0 -131
- package/dist/chunk-QUPBU4KV.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/contract-emit.d.ts +0 -3
- package/dist/commands/contract-emit.d.ts.map +0 -1
- package/dist/commands/contract-emit.js +0 -9
- package/dist/commands/contract-emit.js.map +0 -1
- package/dist/commands/db-init.d.ts +0 -3
- package/dist/commands/db-init.d.ts.map +0 -1
- package/dist/commands/db-init.js +0 -337
- package/dist/commands/db-init.js.map +0 -1
- package/dist/commands/db-introspect.d.ts +0 -3
- package/dist/commands/db-introspect.d.ts.map +0 -1
- package/dist/commands/db-introspect.js +0 -186
- package/dist/commands/db-introspect.js.map +0 -1
- package/dist/commands/db-schema-verify.d.ts +0 -3
- package/dist/commands/db-schema-verify.d.ts.map +0 -1
- package/dist/commands/db-schema-verify.js +0 -160
- package/dist/commands/db-schema-verify.js.map +0 -1
- package/dist/commands/db-sign.d.ts +0 -3
- package/dist/commands/db-sign.d.ts.map +0 -1
- package/dist/commands/db-sign.js +0 -195
- package/dist/commands/db-sign.js.map +0 -1
- package/dist/commands/db-verify.d.ts +0 -3
- package/dist/commands/db-verify.d.ts.map +0 -1
- package/dist/commands/db-verify.js +0 -169
- package/dist/commands/db-verify.js.map +0 -1
- package/dist/config-loader.d.ts.map +0 -1
- package/dist/config-loader.js +0 -7
- package/dist/config-loader.js.map +0 -1
- package/dist/exports/config-types.d.ts +0 -3
- package/dist/exports/config-types.d.ts.map +0 -1
- package/dist/exports/config-types.js +0 -6
- package/dist/exports/config-types.js.map +0 -1
- package/dist/exports/index.d.ts +0 -4
- package/dist/exports/index.d.ts.map +0 -1
- package/dist/exports/index.js +0 -175
- package/dist/exports/index.js.map +0 -1
- package/dist/load-ts-contract.d.ts.map +0 -1
- package/dist/utils/action.d.ts +0 -16
- package/dist/utils/action.d.ts.map +0 -1
- package/dist/utils/cli-errors.d.ts +0 -7
- package/dist/utils/cli-errors.d.ts.map +0 -1
- package/dist/utils/command-helpers.d.ts +0 -12
- package/dist/utils/command-helpers.d.ts.map +0 -1
- package/dist/utils/framework-components.d.ts +0 -70
- package/dist/utils/framework-components.d.ts.map +0 -1
- package/dist/utils/global-flags.d.ts +0 -25
- package/dist/utils/global-flags.d.ts.map +0 -1
- package/dist/utils/output.d.ts +0 -142
- package/dist/utils/output.d.ts.map +0 -1
- package/dist/utils/result-handler.d.ts +0 -15
- package/dist/utils/result-handler.d.ts.map +0 -1
- package/dist/utils/spinner.d.ts +0 -29
- package/dist/utils/spinner.d.ts.map +0 -1
- package/src/commands/db-introspect.ts +0 -256
- package/src/commands/db-schema-verify.ts +0 -232
- package/src/utils/action.ts +0 -43
- package/src/utils/output.ts +0 -1471
- 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
|
+
}
|