@prisma-next/family-sql 0.5.0-dev.9 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/{authoring-type-constructors-BAR65pSK.mjs → authoring-type-constructors-F4JpCJl7.mjs} +14 -15
- package/dist/authoring-type-constructors-F4JpCJl7.mjs.map +1 -0
- package/dist/control-adapter.d.mts +26 -2
- package/dist/control-adapter.d.mts.map +1 -1
- package/dist/control-adapter.mjs +1 -1
- package/dist/control.d.mts +125 -45
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +1174 -54
- package/dist/control.mjs.map +1 -1
- package/dist/migration.d.mts +22 -24
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +25 -24
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.d.mts +35 -23
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +3 -5
- package/dist/pack.mjs.map +1 -1
- package/dist/runtime.d.mts +19 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +26 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-verify.d.mts +4 -15
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +2 -3
- package/dist/test-utils.d.mts +2 -2
- package/dist/test-utils.mjs +2 -3
- package/dist/timestamp-now-generator-BWp8S2sa.mjs +86 -0
- package/dist/timestamp-now-generator-BWp8S2sa.mjs.map +1 -0
- package/dist/types-mhjAPuMn.d.mts +470 -0
- package/dist/types-mhjAPuMn.d.mts.map +1 -0
- package/dist/verify-pRYxnpiG.mjs +81 -0
- package/dist/verify-pRYxnpiG.mjs.map +1 -0
- package/dist/{verify-sql-schema-Ovz7RXR5.mjs → verify-sql-schema-1tDh3x5x.mjs} +18 -72
- package/dist/verify-sql-schema-1tDh3x5x.mjs.map +1 -0
- package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-CPHiuYHR.d.mts} +2 -3
- package/dist/verify-sql-schema-CPHiuYHR.d.mts.map +1 -0
- package/dist/verify.d.mts +16 -21
- package/dist/verify.d.mts.map +1 -1
- package/dist/verify.mjs +2 -3
- package/package.json +23 -21
- package/src/core/authoring-field-presets.ts +35 -23
- package/src/core/control-adapter.ts +32 -0
- package/src/core/control-descriptor.ts +2 -1
- package/src/core/control-instance.ts +117 -30
- package/src/core/migrations/contract-to-schema-ir.ts +4 -29
- package/src/core/migrations/field-event-planner.ts +194 -0
- package/src/core/migrations/plan-helpers.ts +4 -0
- package/src/core/migrations/types.ts +207 -59
- package/src/core/operation-preview.ts +62 -0
- package/src/core/psl-contract-infer/default-mapping.ts +56 -0
- package/src/core/psl-contract-infer/name-transforms.ts +178 -0
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
- package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
- package/src/core/psl-contract-infer/printer-config.ts +55 -0
- package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
- package/src/core/psl-contract-infer/relation-inference.ts +196 -0
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
- package/src/core/schema-verify/verify-helpers.ts +47 -70
- package/src/core/schema-verify/verify-sql-schema.ts +1 -6
- package/src/core/sql-migration.ts +25 -23
- package/src/core/timestamp-now-generator.ts +74 -0
- package/src/core/timestamp-now-runtime-generator.ts +24 -0
- package/src/core/verify.ts +46 -108
- package/src/exports/control.ts +11 -4
- package/src/exports/runtime.ts +2 -0
- package/src/exports/schema-verify.ts +0 -1
- package/src/exports/test-utils.ts +0 -1
- package/src/exports/verify.ts +1 -1
- package/dist/authoring-type-constructors-BAR65pSK.mjs.map +0 -1
- package/dist/types-C6K4mxDM.d.mts +0 -301
- package/dist/types-C6K4mxDM.d.mts.map +0 -1
- package/dist/verify-4GshvY4p.mjs +0 -122
- package/dist/verify-4GshvY4p.mjs.map +0 -1
- package/dist/verify-sql-schema-BBhkqEDo.d.mts.map +0 -1
- package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
3
|
import type {
|
|
4
|
+
ContractSpace,
|
|
4
5
|
ControlAdapterDescriptor,
|
|
5
6
|
ControlDriverInstance,
|
|
6
7
|
ControlExtensionDescriptor,
|
|
7
|
-
DataTransformOperation,
|
|
8
8
|
MigratableTargetDescriptor,
|
|
9
9
|
MigrationOperationPolicy,
|
|
10
10
|
MigrationPlan,
|
|
@@ -16,47 +16,22 @@ import type {
|
|
|
16
16
|
MigrationRunnerFailure,
|
|
17
17
|
MigrationRunnerSuccessValue,
|
|
18
18
|
OperationContext,
|
|
19
|
+
OpFactoryCall,
|
|
19
20
|
SchemaIssue,
|
|
20
21
|
} from '@prisma-next/framework-components/control';
|
|
21
|
-
import type {
|
|
22
|
-
|
|
22
|
+
import type {
|
|
23
|
+
SqlStorage,
|
|
24
|
+
StorageColumn,
|
|
25
|
+
StorageTable,
|
|
26
|
+
StorageTypeInstance,
|
|
27
|
+
} from '@prisma-next/sql-contract/types';
|
|
28
|
+
import type { SqlOperationDescriptors } from '@prisma-next/sql-operations';
|
|
23
29
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
24
30
|
import type { Result } from '@prisma-next/utils/result';
|
|
25
31
|
import type { SqlControlFamilyInstance } from '../control-instance';
|
|
26
32
|
|
|
27
33
|
export type AnyRecord = Readonly<Record<string, unknown>>;
|
|
28
34
|
|
|
29
|
-
export interface ComponentDatabaseDependency<TTargetDetails> {
|
|
30
|
-
readonly id: string;
|
|
31
|
-
readonly label: string;
|
|
32
|
-
readonly install: readonly SqlMigrationPlanOperation<TTargetDetails>[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ComponentDatabaseDependencies<TTargetDetails> {
|
|
36
|
-
readonly init?: readonly ComponentDatabaseDependency<TTargetDetails>[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface DatabaseDependencyProvider {
|
|
40
|
-
readonly databaseDependencies?: ComponentDatabaseDependencies<unknown>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function isDatabaseDependencyProvider(value: unknown): value is DatabaseDependencyProvider {
|
|
44
|
-
return typeof value === 'object' && value !== null && 'databaseDependencies' in value;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function collectInitDependencies(
|
|
48
|
-
components: ReadonlyArray<unknown>,
|
|
49
|
-
): readonly ComponentDatabaseDependency<unknown>[] {
|
|
50
|
-
const result: ComponentDatabaseDependency<unknown>[] = [];
|
|
51
|
-
for (const component of components) {
|
|
52
|
-
if (!isDatabaseDependencyProvider(component)) continue;
|
|
53
|
-
const deps = component.databaseDependencies?.init;
|
|
54
|
-
if (!deps) continue;
|
|
55
|
-
result.push(...deps);
|
|
56
|
-
}
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
35
|
export interface StorageTypePlanResult<TTargetDetails> {
|
|
61
36
|
readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
|
|
62
37
|
}
|
|
@@ -83,6 +58,43 @@ export interface ResolveIdentityValueInput {
|
|
|
83
58
|
readonly typeParams?: Record<string, unknown>;
|
|
84
59
|
}
|
|
85
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Per-field lifecycle event a codec hook can react to.
|
|
63
|
+
*
|
|
64
|
+
* Fired during app-space migration emission as the SQL family diffs the
|
|
65
|
+
* prior contract against the new contract. See
|
|
66
|
+
* `docs/architecture docs/adrs/ADR 213 - Codec lifecycle hooks.md`
|
|
67
|
+
* for the wiring contract.
|
|
68
|
+
*
|
|
69
|
+
* - `'added'` — the field is present in the new contract but not the prior.
|
|
70
|
+
* - `'dropped'` — the field is present in the prior contract but not the new.
|
|
71
|
+
* - `'altered'` — the field is present in both and any property other than
|
|
72
|
+
* `codecId` differs. Codec-id changes are a v1 non-goal:
|
|
73
|
+
* when only `codecId` differs, no `'altered'` event fires.
|
|
74
|
+
*/
|
|
75
|
+
export type FieldEvent = 'added' | 'dropped' | 'altered';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Context passed to {@link CodecControlHooks.onFieldEvent}.
|
|
79
|
+
*
|
|
80
|
+
* `tableName` and `fieldName` are always populated; `priorTable` /
|
|
81
|
+
* `priorField` carry the prior contract's view of the table and column
|
|
82
|
+
* (present for `'dropped'` and `'altered'`); `newTable` / `newField`
|
|
83
|
+
* carry the new contract's view (present for `'added'` and `'altered'`).
|
|
84
|
+
*
|
|
85
|
+
* The hook only ever receives app-space contract IR — extension-space
|
|
86
|
+
* fields are scoped out by the API: the hook is wired at the
|
|
87
|
+
* application emitter only.
|
|
88
|
+
*/
|
|
89
|
+
export interface FieldEventContext {
|
|
90
|
+
readonly tableName: string;
|
|
91
|
+
readonly fieldName: string;
|
|
92
|
+
readonly priorTable?: StorageTable;
|
|
93
|
+
readonly newTable?: StorageTable;
|
|
94
|
+
readonly priorField?: StorageColumn;
|
|
95
|
+
readonly newField?: StorageColumn;
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
export interface CodecControlHooks<TTargetDetails = unknown> {
|
|
87
99
|
planTypeOperations?: (options: {
|
|
88
100
|
readonly typeName: string;
|
|
@@ -123,22 +135,57 @@ export interface CodecControlHooks<TTargetDetails = unknown> {
|
|
|
123
135
|
* - undefined: no opinion; planner may use built-in fallbacks
|
|
124
136
|
*/
|
|
125
137
|
resolveIdentityValue?: (input: ResolveIdentityValueInput) => string | null | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Reacts to per-field added / dropped / altered events as the app-space
|
|
140
|
+
* emitter diffs the prior contract against the new contract. Returned
|
|
141
|
+
* ops are inlined into the app-space migration's `ops.json` alongside
|
|
142
|
+
* the user's structural ops.
|
|
143
|
+
*
|
|
144
|
+
* Synchronous. Each returned op must carry its own `invariantId`. Hooks
|
|
145
|
+
* are dispatched per `(table, field)` based on the field's `codecId`
|
|
146
|
+
* (the new field's codec for `'added'` / `'altered'`; the prior field's
|
|
147
|
+
* codec for `'dropped'`).
|
|
148
|
+
*
|
|
149
|
+
* See `docs/architecture docs/adrs/ADR 213 - Codec lifecycle hooks.md`
|
|
150
|
+
* for the wiring contract and the deterministic ordering rule.
|
|
151
|
+
*/
|
|
152
|
+
onFieldEvent?: (event: FieldEvent, ctx: FieldEventContext) => readonly OpFactoryCall[];
|
|
126
153
|
}
|
|
127
154
|
|
|
128
155
|
export interface SqlControlExtensionDescriptor<TTargetId extends string>
|
|
129
156
|
extends ControlExtensionDescriptor<'sql', TTargetId> {
|
|
130
|
-
readonly
|
|
131
|
-
|
|
157
|
+
readonly queryOperations?: () => SqlOperationDescriptors;
|
|
158
|
+
/**
|
|
159
|
+
* Schema-contributing extensions opt into the per-space planner / runner /
|
|
160
|
+
* verifier by setting this field. Extensions without it are codec-only or
|
|
161
|
+
* query-ops-only — today's behaviour preserved.
|
|
162
|
+
*
|
|
163
|
+
* The shape comes from `@prisma-next/framework-components/control`
|
|
164
|
+
* (`ContractSpace`) — contract-space identity is a framework concept,
|
|
165
|
+
* not a SQL-specific one. The SQL family specialises the generic to
|
|
166
|
+
* `Contract<SqlStorage>` so descriptor authors continue to see a
|
|
167
|
+
* typed contract value.
|
|
168
|
+
*/
|
|
169
|
+
readonly contractSpace?: ContractSpace<Contract<SqlStorage>>;
|
|
132
170
|
}
|
|
133
171
|
|
|
134
172
|
export interface SqlControlAdapterDescriptor<TTargetId extends string>
|
|
135
173
|
extends ControlAdapterDescriptor<'sql', TTargetId> {
|
|
136
|
-
readonly queryOperations?: () =>
|
|
174
|
+
readonly queryOperations?: () => SqlOperationDescriptors;
|
|
137
175
|
}
|
|
138
176
|
|
|
139
177
|
export interface SqlMigrationPlanOperationStep {
|
|
140
178
|
readonly description: string;
|
|
141
179
|
readonly sql: string;
|
|
180
|
+
/**
|
|
181
|
+
* Optional parameter values bound at execution time. The runner forwards
|
|
182
|
+
* these to `driver.query(sql, params ?? [])`, so step authors can use
|
|
183
|
+
* placeholder syntax (`$1`, `$2`, …) instead of inlining literals into
|
|
184
|
+
* the SQL string. Reuses the driver's parameter binder rather than
|
|
185
|
+
* rolling per-target literal serialization for every type the planner
|
|
186
|
+
* may emit.
|
|
187
|
+
*/
|
|
188
|
+
readonly params?: readonly unknown[];
|
|
142
189
|
readonly meta?: AnyRecord;
|
|
143
190
|
}
|
|
144
191
|
|
|
@@ -168,25 +215,27 @@ export interface SqlMigrationPlanOperation<TTargetDetails> extends MigrationPlan
|
|
|
168
215
|
readonly meta?: AnyRecord;
|
|
169
216
|
}
|
|
170
217
|
|
|
171
|
-
/**
|
|
172
|
-
* Union of all operation shapes a SQL-family migration may emit: schema-facing
|
|
173
|
-
* `SqlMigrationPlanOperation`s and family-agnostic `DataTransformOperation`s.
|
|
174
|
-
*
|
|
175
|
-
* Mirrors `AnyMongoMigrationOperation` in shape — the runner already handles
|
|
176
|
-
* both branches via `isDataTransformOperation`, and authored `migration.ts`
|
|
177
|
-
* files must be able to intermix `dataTransform(endContract, …)` calls with
|
|
178
|
-
* DDL factory calls (e.g. `setNotNull(…)`) in a single `operations` array.
|
|
179
|
-
*/
|
|
180
|
-
export type AnySqlMigrationOperation<TTargetDetails> =
|
|
181
|
-
| SqlMigrationPlanOperation<TTargetDetails>
|
|
182
|
-
| DataTransformOperation;
|
|
183
|
-
|
|
184
218
|
export interface SqlMigrationPlanContractInfo {
|
|
185
219
|
readonly storageHash: string;
|
|
186
220
|
readonly profileHash?: string;
|
|
187
221
|
}
|
|
188
222
|
|
|
189
223
|
export interface SqlMigrationPlan<TTargetDetails> extends MigrationPlan {
|
|
224
|
+
/**
|
|
225
|
+
* Contract space this plan applies to. The runner uses this to key the
|
|
226
|
+
* `prisma_contract.marker` row it writes/reads (`space = <spaceId>`),
|
|
227
|
+
* so per-extension plans hit per-extension marker rows instead of all
|
|
228
|
+
* collapsing onto the app's row.
|
|
229
|
+
*
|
|
230
|
+
* App-plan callers pass `APP_SPACE_ID` (`'app'`); per-extension plans
|
|
231
|
+
* pass the extension's space id. Required at every call site so the
|
|
232
|
+
* type system surfaces every place that needs to thread the value
|
|
233
|
+
* (rather than letting an `?? APP_SPACE_ID` fall-through silently
|
|
234
|
+
* collapse multi-space markers onto the `'app'` row).
|
|
235
|
+
*
|
|
236
|
+
* @see specs/framework-mechanism.spec.md § 2.
|
|
237
|
+
*/
|
|
238
|
+
readonly spaceId: string;
|
|
190
239
|
/**
|
|
191
240
|
* Origin contract identity that the plan expects the database to currently be at.
|
|
192
241
|
* If omitted or null, the runner skips origin validation entirely.
|
|
@@ -197,6 +246,15 @@ export interface SqlMigrationPlan<TTargetDetails> extends MigrationPlan {
|
|
|
197
246
|
*/
|
|
198
247
|
readonly destination: SqlMigrationPlanContractInfo;
|
|
199
248
|
readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
|
|
249
|
+
/**
|
|
250
|
+
* Sorted, deduplicated invariant ids declared by this plan's data-transform
|
|
251
|
+
* ops. Required at the SQL-family layer (the SQL runners consume this as
|
|
252
|
+
* the source of truth for marker writes and self-edge no-op checks); the
|
|
253
|
+
* framework-level {@link MigrationPlan.providedInvariants} stays optional
|
|
254
|
+
* because `db init` / `db update` plans don't have a corresponding
|
|
255
|
+
* migration manifest.
|
|
256
|
+
*/
|
|
257
|
+
readonly providedInvariants: readonly string[];
|
|
200
258
|
readonly meta?: AnyRecord;
|
|
201
259
|
}
|
|
202
260
|
|
|
@@ -243,17 +301,33 @@ export interface SqlMigrationPlannerPlanOptions {
|
|
|
243
301
|
readonly policy: MigrationOperationPolicy;
|
|
244
302
|
readonly schemaName?: string;
|
|
245
303
|
/**
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
304
|
+
* Contract space the plan applies to. The planner stamps this onto
|
|
305
|
+
* the produced {@link SqlMigrationPlan.spaceId} so the runner keys
|
|
306
|
+
* the marker row by the right space. App-plan callers pass
|
|
307
|
+
* `APP_SPACE_ID`; per-extension callers pass the extension's space
|
|
308
|
+
* id.
|
|
309
|
+
*/
|
|
310
|
+
readonly spaceId: string;
|
|
311
|
+
/**
|
|
312
|
+
* The "from" contract (state the planner assumes the database starts at),
|
|
313
|
+
* or `null` for reconciliation flows that have no prior contract.
|
|
314
|
+
*
|
|
315
|
+
* Required at every call site so the structural fact "I have a prior
|
|
316
|
+
* contract / I don't" is visible in the type. `migration plan` supplies
|
|
317
|
+
* the previous bundle's `metadata.toContract`; `db update` / `db init`
|
|
318
|
+
* reconcile against the live schema and pass `null`. Strategies that
|
|
319
|
+
* need from/to column-shape comparisons (unsafe type change, nullability
|
|
250
320
|
* tightening) use this to decide whether to emit `dataTransform`
|
|
251
|
-
* placeholders
|
|
321
|
+
* placeholders; they short-circuit when it is `null`.
|
|
322
|
+
*
|
|
323
|
+
* Planners also derive the "from" identity they stamp onto the produced
|
|
324
|
+
* plan's `describe()` as `fromContract?.storage.storageHash ?? null`.
|
|
252
325
|
*/
|
|
253
|
-
readonly fromContract
|
|
326
|
+
readonly fromContract: Contract<SqlStorage> | null;
|
|
254
327
|
/**
|
|
255
328
|
* Active framework components participating in this composition.
|
|
256
|
-
*
|
|
329
|
+
* Each component is target-bound so SQL targets can dispatch
|
|
330
|
+
* component-owned planning behaviour from the same descriptor list.
|
|
257
331
|
* All components must have matching familyId ('sql') and targetId.
|
|
258
332
|
*/
|
|
259
333
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
@@ -271,6 +345,14 @@ export interface SqlMigrationRunnerExecuteCallbacks<TTargetDetails> {
|
|
|
271
345
|
export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
|
|
272
346
|
readonly plan: SqlMigrationPlan<TTargetDetails>;
|
|
273
347
|
readonly driver: ControlDriverInstance<'sql', string>;
|
|
348
|
+
/**
|
|
349
|
+
* Logical contract space this plan applies to. When omitted the
|
|
350
|
+
* runner derives the space from {@link SqlMigrationPlan.spaceId};
|
|
351
|
+
* when supplied, the runner asserts it matches `plan.spaceId` so a
|
|
352
|
+
* caller cannot accidentally write the marker row for a different
|
|
353
|
+
* space than the plan was produced for.
|
|
354
|
+
*/
|
|
355
|
+
readonly space?: string;
|
|
274
356
|
/**
|
|
275
357
|
* Destination contract IR.
|
|
276
358
|
* Must correspond to `plan.destination` and is used for schema verification and marker/ledger writes.
|
|
@@ -292,7 +374,8 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
|
|
|
292
374
|
readonly executionChecks?: MigrationRunnerExecutionChecks;
|
|
293
375
|
/**
|
|
294
376
|
* Active framework components participating in this composition.
|
|
295
|
-
*
|
|
377
|
+
* Each component is target-bound so SQL targets can dispatch
|
|
378
|
+
* component-owned execution behaviour from the same descriptor list.
|
|
296
379
|
* All components must have matching familyId ('sql') and targetId.
|
|
297
380
|
*/
|
|
298
381
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
@@ -300,11 +383,13 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
|
|
|
300
383
|
|
|
301
384
|
export type SqlMigrationRunnerErrorCode =
|
|
302
385
|
| 'DESTINATION_CONTRACT_MISMATCH'
|
|
386
|
+
| 'LEGACY_MARKER_SHAPE'
|
|
303
387
|
| 'MARKER_ORIGIN_MISMATCH'
|
|
304
388
|
| 'POLICY_VIOLATION'
|
|
305
389
|
| 'PRECHECK_FAILED'
|
|
306
390
|
| 'POSTCHECK_FAILED'
|
|
307
391
|
| 'SCHEMA_VERIFY_FAILED'
|
|
392
|
+
| 'FOREIGN_KEY_VIOLATION'
|
|
308
393
|
| 'EXECUTION_FAILED';
|
|
309
394
|
|
|
310
395
|
export interface SqlMigrationRunnerFailure extends MigrationRunnerFailure {
|
|
@@ -320,22 +405,85 @@ export type SqlMigrationRunnerResult = Result<
|
|
|
320
405
|
>;
|
|
321
406
|
|
|
322
407
|
export interface SqlMigrationRunner<TTargetDetails> {
|
|
408
|
+
/**
|
|
409
|
+
* Apply a single migration plan, opening and managing its own
|
|
410
|
+
* transaction (and any target-specific connection-level setup, e.g.
|
|
411
|
+
* SQLite's `PRAGMA foreign_keys` toggle). Existing single-space
|
|
412
|
+
* callers route through here.
|
|
413
|
+
*/
|
|
323
414
|
execute(
|
|
324
415
|
options: SqlMigrationRunnerExecuteOptions<TTargetDetails>,
|
|
325
416
|
): Promise<SqlMigrationRunnerResult>;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Apply a single migration plan against an already-open connection
|
|
420
|
+
* **without** opening a transaction. The caller is responsible for
|
|
421
|
+
* wrapping the call (and any siblings) in `BEGIN` / `COMMIT` /
|
|
422
|
+
* `ROLLBACK`. Used by the per-space runner wiring to fan out across
|
|
423
|
+
* contract spaces inside one outer transaction so a mid-apply
|
|
424
|
+
* failure rolls back every space's writes.
|
|
425
|
+
*
|
|
426
|
+
* Idempotent control-table setup (`prisma_contract.*`) and marker
|
|
427
|
+
* writes use `options.space` to address the per-space marker row.
|
|
428
|
+
*/
|
|
429
|
+
executeOnConnection(
|
|
430
|
+
options: SqlMigrationRunnerExecuteOptions<TTargetDetails>,
|
|
431
|
+
): Promise<SqlMigrationRunnerResult>;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Apply per-space plans across multiple contract spaces inside a
|
|
435
|
+
* single outer transaction. The caller orders the input list
|
|
436
|
+
* (typically via the aggregate planner's `applyOrder`: extensions
|
|
437
|
+
* alphabetical, then app); the runner is responsible for opening
|
|
438
|
+
* / committing the outer
|
|
439
|
+
* transaction (and any target-specific connection-level setup such
|
|
440
|
+
* as the SQLite FK pragma toggle). A failure on any space rolls
|
|
441
|
+
* back every space's writes.
|
|
442
|
+
*
|
|
443
|
+
* Each space's `SqlMigrationRunnerExecuteOptions` must reference the
|
|
444
|
+
* same `driver` (the connection the outer transaction is open on).
|
|
445
|
+
* Per-space marker writes use `options.space` to address the row.
|
|
446
|
+
*/
|
|
447
|
+
executeAcrossSpaces(options: {
|
|
448
|
+
readonly driver: ControlDriverInstance<'sql', string>;
|
|
449
|
+
readonly perSpaceOptions: ReadonlyArray<SqlMigrationRunnerExecuteOptions<TTargetDetails>>;
|
|
450
|
+
}): Promise<MultiSpaceRunnerResult>;
|
|
326
451
|
}
|
|
327
452
|
|
|
453
|
+
export interface MultiSpaceRunnerSuccessValue {
|
|
454
|
+
readonly perSpaceResults: ReadonlyArray<{
|
|
455
|
+
readonly space: string;
|
|
456
|
+
readonly value: SqlMigrationRunnerSuccessValue;
|
|
457
|
+
}>;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export interface MultiSpaceRunnerFailure extends SqlMigrationRunnerFailure {
|
|
461
|
+
readonly failingSpace: string;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export type MultiSpaceRunnerResult = Result<MultiSpaceRunnerSuccessValue, MultiSpaceRunnerFailure>;
|
|
465
|
+
|
|
328
466
|
export interface SqlControlTargetDescriptor<TTargetId extends string, TTargetDetails>
|
|
329
467
|
extends MigratableTargetDescriptor<'sql', TTargetId, SqlControlFamilyInstance> {
|
|
330
|
-
readonly queryOperations?: () =>
|
|
468
|
+
readonly queryOperations?: () => SqlOperationDescriptors;
|
|
331
469
|
createPlanner(family: SqlControlFamilyInstance): SqlMigrationPlanner<TTargetDetails>;
|
|
332
470
|
createRunner(family: SqlControlFamilyInstance): SqlMigrationRunner<TTargetDetails>;
|
|
333
471
|
}
|
|
334
472
|
|
|
335
473
|
export interface CreateSqlMigrationPlanOptions<TTargetDetails> {
|
|
336
474
|
readonly targetId: string;
|
|
475
|
+
/**
|
|
476
|
+
* Contract space this plan applies to. Mirrors {@link SqlMigrationPlan.spaceId}.
|
|
477
|
+
*/
|
|
478
|
+
readonly spaceId: string;
|
|
337
479
|
readonly origin?: SqlMigrationPlanContractInfo | null;
|
|
338
480
|
readonly destination: SqlMigrationPlanContractInfo;
|
|
339
481
|
readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
|
|
482
|
+
/**
|
|
483
|
+
* Sorted, deduplicated invariant ids for this plan; mirrors the required
|
|
484
|
+
* field on {@link SqlMigrationPlan}. Callers without a migration manifest
|
|
485
|
+
* (`db init`, `db update`, planner-built plans) pass `[]`.
|
|
486
|
+
*/
|
|
487
|
+
readonly providedInvariants: readonly string[];
|
|
340
488
|
readonly meta?: AnyRecord;
|
|
341
489
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MigrationPlanOperation,
|
|
3
|
+
OperationPreview,
|
|
4
|
+
} from '@prisma-next/framework-components/control';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shape of an SQL execute step on `SqlMigrationPlanOperation`. Used for runtime
|
|
8
|
+
* type narrowing without importing the concrete SQL type.
|
|
9
|
+
*/
|
|
10
|
+
interface SqlExecuteStep {
|
|
11
|
+
readonly sql: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isDdlStatement(sqlStatement: string): boolean {
|
|
15
|
+
const trimmed = sqlStatement.trim().toLowerCase();
|
|
16
|
+
return (
|
|
17
|
+
trimmed.startsWith('create ') || trimmed.startsWith('alter ') || trimmed.startsWith('drop ')
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasExecuteSteps(
|
|
22
|
+
operation: MigrationPlanOperation,
|
|
23
|
+
): operation is MigrationPlanOperation & { readonly execute: readonly SqlExecuteStep[] } {
|
|
24
|
+
const candidate = operation as unknown as Record<string, unknown>;
|
|
25
|
+
if (!('execute' in candidate) || !Array.isArray(candidate['execute'])) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return candidate['execute'].every(
|
|
29
|
+
(step: unknown) => typeof step === 'object' && step !== null && 'sql' in step,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts a best-effort SQL DDL preview for CLI plan output.
|
|
35
|
+
* Presentation-only: never used to decide migration correctness.
|
|
36
|
+
*/
|
|
37
|
+
export function extractSqlDdl(operations: readonly MigrationPlanOperation[]): string[] {
|
|
38
|
+
const statements: string[] = [];
|
|
39
|
+
for (const operation of operations) {
|
|
40
|
+
if (!hasExecuteSteps(operation)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const step of operation.execute) {
|
|
44
|
+
if (typeof step.sql === 'string' && isDdlStatement(step.sql)) {
|
|
45
|
+
statements.push(step.sql.trim());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return statements;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wraps `extractSqlDdl` into the family-agnostic `OperationPreview` shape.
|
|
54
|
+
* Each statement carries `language: 'sql'`.
|
|
55
|
+
*/
|
|
56
|
+
export function sqlOperationsToPreview(
|
|
57
|
+
operations: readonly MigrationPlanOperation[],
|
|
58
|
+
): OperationPreview {
|
|
59
|
+
return {
|
|
60
|
+
statements: extractSqlDdl(operations).map((text) => ({ text, language: 'sql' })),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_FUNCTION_ATTRIBUTES: Readonly<Record<string, string>> = {
|
|
4
|
+
'autoincrement()': '@default(autoincrement())',
|
|
5
|
+
'now()': '@default(now())',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface DefaultMappingOptions {
|
|
9
|
+
readonly functionAttributes?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly fallbackFunctionAttribute?: ((expression: string) => string | undefined) | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type DefaultMappingResult = { readonly attribute: string } | { readonly comment: string };
|
|
14
|
+
|
|
15
|
+
export function mapDefault(
|
|
16
|
+
columnDefault: ColumnDefault,
|
|
17
|
+
options?: DefaultMappingOptions,
|
|
18
|
+
): DefaultMappingResult {
|
|
19
|
+
switch (columnDefault.kind) {
|
|
20
|
+
case 'literal':
|
|
21
|
+
return { attribute: `@default(${formatLiteralValue(columnDefault.value)})` };
|
|
22
|
+
case 'function': {
|
|
23
|
+
const attribute =
|
|
24
|
+
options?.functionAttributes?.[columnDefault.expression] ??
|
|
25
|
+
DEFAULT_FUNCTION_ATTRIBUTES[columnDefault.expression] ??
|
|
26
|
+
options?.fallbackFunctionAttribute?.(columnDefault.expression);
|
|
27
|
+
return attribute
|
|
28
|
+
? { attribute }
|
|
29
|
+
: { comment: `// Raw default: ${columnDefault.expression.replace(/[\r\n]+/g, ' ')}` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatLiteralValue(value: unknown): string {
|
|
35
|
+
if (value === null) {
|
|
36
|
+
return 'null';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
switch (typeof value) {
|
|
40
|
+
case 'boolean':
|
|
41
|
+
case 'number':
|
|
42
|
+
return String(value);
|
|
43
|
+
case 'string':
|
|
44
|
+
return quoteString(value);
|
|
45
|
+
default:
|
|
46
|
+
return quoteString(JSON.stringify(value));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function quoteString(str: string): string {
|
|
51
|
+
return `"${escapeString(str)}"`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function escapeString(str: string): string {
|
|
55
|
+
return JSON.stringify(str).slice(1, -1);
|
|
56
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const PSL_RESERVED_WORDS = new Set(['model', 'enum', 'types', 'type', 'generator', 'datasource']);
|
|
2
|
+
|
|
3
|
+
const IDENTIFIER_PART_PATTERN = /[A-Za-z0-9]+/g;
|
|
4
|
+
|
|
5
|
+
type NameResult = {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly map?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function hasSeparators(input: string): boolean {
|
|
11
|
+
return /[^A-Za-z0-9]/.test(input);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractIdentifierParts(input: string): string[] {
|
|
15
|
+
return input.match(IDENTIFIER_PART_PATTERN) ?? [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createSyntheticIdentifier(input: string): string {
|
|
19
|
+
let hash = 2166136261;
|
|
20
|
+
|
|
21
|
+
for (const char of input) {
|
|
22
|
+
hash ^= char.codePointAt(0) ?? 0;
|
|
23
|
+
hash = Math.imul(hash, 16777619);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `x${(hash >>> 0).toString(16)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sanitizeIdentifierCharacters(input: string): string {
|
|
30
|
+
const sanitized = input.replace(/[^\w]/g, '');
|
|
31
|
+
return sanitized.length > 0 ? sanitized : createSyntheticIdentifier(input);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function capitalize(word: string): string {
|
|
35
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function snakeToPascalCase(input: string): string {
|
|
39
|
+
const parts = extractIdentifierParts(input);
|
|
40
|
+
if (parts.length === 0) {
|
|
41
|
+
return capitalize(sanitizeIdentifierCharacters(input));
|
|
42
|
+
}
|
|
43
|
+
return parts.map(capitalize).join('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function snakeToCamelCase(input: string): string {
|
|
47
|
+
const parts = extractIdentifierParts(input);
|
|
48
|
+
if (parts.length === 0) {
|
|
49
|
+
return sanitizeIdentifierCharacters(input);
|
|
50
|
+
}
|
|
51
|
+
const [firstPart = input, ...rest] = parts;
|
|
52
|
+
return firstPart.charAt(0).toLowerCase() + firstPart.slice(1) + rest.map(capitalize).join('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function needsEscaping(name: string): boolean {
|
|
56
|
+
return PSL_RESERVED_WORDS.has(name.toLowerCase()) || /^\d/.test(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function escapeName(name: string): string {
|
|
60
|
+
return `_${name}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function escapeIfNeeded(name: string): string {
|
|
64
|
+
return needsEscaping(name) ? escapeName(name) : name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toModelName(tableName: string): NameResult {
|
|
68
|
+
let name: string;
|
|
69
|
+
|
|
70
|
+
if (hasSeparators(tableName)) {
|
|
71
|
+
name = snakeToPascalCase(tableName);
|
|
72
|
+
} else {
|
|
73
|
+
name = tableName.charAt(0).toUpperCase() + tableName.slice(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (needsEscaping(name)) {
|
|
77
|
+
const escaped = escapeName(name);
|
|
78
|
+
return { name: escaped, map: tableName };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (name !== tableName) {
|
|
82
|
+
return { name, map: tableName };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { name };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function toFieldName(columnName: string): NameResult {
|
|
89
|
+
let name: string;
|
|
90
|
+
|
|
91
|
+
if (hasSeparators(columnName)) {
|
|
92
|
+
name = snakeToCamelCase(columnName);
|
|
93
|
+
} else {
|
|
94
|
+
name = columnName.charAt(0).toLowerCase() + columnName.slice(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (needsEscaping(name)) {
|
|
98
|
+
const escaped = escapeName(name);
|
|
99
|
+
return { name: escaped, map: columnName };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (name !== columnName) {
|
|
103
|
+
return { name, map: columnName };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { name };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function toEnumName(pgTypeName: string): NameResult {
|
|
110
|
+
let name: string;
|
|
111
|
+
|
|
112
|
+
if (hasSeparators(pgTypeName)) {
|
|
113
|
+
name = snakeToPascalCase(pgTypeName);
|
|
114
|
+
} else {
|
|
115
|
+
name = pgTypeName.charAt(0).toUpperCase() + pgTypeName.slice(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (needsEscaping(name)) {
|
|
119
|
+
const escaped = escapeName(name);
|
|
120
|
+
return { name: escaped, map: pgTypeName };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (name !== pgTypeName) {
|
|
124
|
+
return { name, map: pgTypeName };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { name };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function pluralize(word: string): string {
|
|
131
|
+
if (
|
|
132
|
+
word.endsWith('s') ||
|
|
133
|
+
word.endsWith('x') ||
|
|
134
|
+
word.endsWith('z') ||
|
|
135
|
+
word.endsWith('ch') ||
|
|
136
|
+
word.endsWith('sh')
|
|
137
|
+
) {
|
|
138
|
+
return `${word}es`;
|
|
139
|
+
}
|
|
140
|
+
if (word.endsWith('y') && !/[aeiou]y$/i.test(word)) {
|
|
141
|
+
return `${word.slice(0, -1)}ies`;
|
|
142
|
+
}
|
|
143
|
+
return `${word}s`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function deriveRelationFieldName(
|
|
147
|
+
fkColumns: readonly string[],
|
|
148
|
+
referencedTableName: string,
|
|
149
|
+
): string {
|
|
150
|
+
if (fkColumns.length === 1) {
|
|
151
|
+
const [col = referencedTableName] = fkColumns;
|
|
152
|
+
const stripped = col.replace(/_id$/i, '').replace(/Id$/, '');
|
|
153
|
+
|
|
154
|
+
if (stripped.length > 0 && stripped !== col) {
|
|
155
|
+
return escapeIfNeeded(snakeToCamelCase(stripped));
|
|
156
|
+
}
|
|
157
|
+
return escapeIfNeeded(snakeToCamelCase(referencedTableName));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return escapeIfNeeded(snakeToCamelCase(referencedTableName));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function deriveBackRelationFieldName(childModelName: string, isOneToOne: boolean): string {
|
|
164
|
+
const base = childModelName.charAt(0).toLowerCase() + childModelName.slice(1);
|
|
165
|
+
return isOneToOne ? base : pluralize(base);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function toNamedTypeName(columnName: string): string {
|
|
169
|
+
let name: string;
|
|
170
|
+
|
|
171
|
+
if (hasSeparators(columnName)) {
|
|
172
|
+
name = snakeToPascalCase(columnName);
|
|
173
|
+
} else {
|
|
174
|
+
name = columnName.charAt(0).toUpperCase() + columnName.slice(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return escapeIfNeeded(name);
|
|
178
|
+
}
|