@prisma-next/target-postgres 0.3.0-pr.99.6 → 0.4.0-dev.2

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 (66) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +17 -8
  3. package/dist/control.d.mts +19 -0
  4. package/dist/control.d.mts.map +1 -0
  5. package/dist/control.mjs +5382 -0
  6. package/dist/control.mjs.map +1 -0
  7. package/dist/descriptor-meta-CAf16lsJ.mjs +32 -0
  8. package/dist/descriptor-meta-CAf16lsJ.mjs.map +1 -0
  9. package/dist/migration-builders.d.mts +88 -0
  10. package/dist/migration-builders.d.mts.map +1 -0
  11. package/dist/migration-builders.mjs +3 -0
  12. package/dist/operation-descriptors-CxymFSgK.mjs +52 -0
  13. package/dist/operation-descriptors-CxymFSgK.mjs.map +1 -0
  14. package/dist/pack.d.mts +45 -0
  15. package/dist/pack.d.mts.map +1 -0
  16. package/dist/pack.mjs +9 -0
  17. package/dist/pack.mjs.map +1 -0
  18. package/dist/runtime.d.mts +9 -0
  19. package/dist/runtime.d.mts.map +1 -0
  20. package/dist/runtime.mjs +20 -0
  21. package/dist/runtime.mjs.map +1 -0
  22. package/package.json +31 -29
  23. package/src/core/authoring.ts +15 -0
  24. package/src/core/descriptor-meta.ts +5 -0
  25. package/src/core/migrations/descriptor-planner.ts +466 -0
  26. package/src/core/migrations/operation-descriptors.ts +166 -0
  27. package/src/core/migrations/operation-resolver.ts +929 -0
  28. package/src/core/migrations/planner-ddl-builders.ts +256 -0
  29. package/src/core/migrations/planner-identity-values.ts +135 -0
  30. package/src/core/migrations/planner-recipes.ts +91 -0
  31. package/src/core/migrations/planner-reconciliation.ts +798 -0
  32. package/src/core/migrations/planner-schema-lookup.ts +54 -0
  33. package/src/core/migrations/planner-sql-checks.ts +322 -0
  34. package/src/core/migrations/planner-strategies.ts +262 -0
  35. package/src/core/migrations/planner-target-details.ts +38 -0
  36. package/src/core/migrations/planner-type-resolution.ts +26 -0
  37. package/src/core/migrations/planner.ts +410 -460
  38. package/src/core/migrations/runner.ts +134 -38
  39. package/src/core/migrations/statement-builders.ts +6 -6
  40. package/src/core/types.ts +5 -0
  41. package/src/exports/control.ts +182 -12
  42. package/src/exports/migration-builders.ts +56 -0
  43. package/src/exports/pack.ts +7 -3
  44. package/src/exports/runtime.ts +6 -12
  45. package/dist/chunk-RKEXRSSI.js +0 -14
  46. package/dist/chunk-RKEXRSSI.js.map +0 -1
  47. package/dist/core/descriptor-meta.d.ts +0 -9
  48. package/dist/core/descriptor-meta.d.ts.map +0 -1
  49. package/dist/core/migrations/planner.d.ts +0 -14
  50. package/dist/core/migrations/planner.d.ts.map +0 -1
  51. package/dist/core/migrations/runner.d.ts +0 -8
  52. package/dist/core/migrations/runner.d.ts.map +0 -1
  53. package/dist/core/migrations/statement-builders.d.ts +0 -30
  54. package/dist/core/migrations/statement-builders.d.ts.map +0 -1
  55. package/dist/exports/control.d.ts +0 -8
  56. package/dist/exports/control.d.ts.map +0 -1
  57. package/dist/exports/control.js +0 -1260
  58. package/dist/exports/control.js.map +0 -1
  59. package/dist/exports/pack.d.ts +0 -4
  60. package/dist/exports/pack.d.ts.map +0 -1
  61. package/dist/exports/pack.js +0 -11
  62. package/dist/exports/pack.js.map +0 -1
  63. package/dist/exports/runtime.d.ts +0 -12
  64. package/dist/exports/runtime.d.ts.map +0 -1
  65. package/dist/exports/runtime.js +0 -19
  66. package/dist/exports/runtime.js.map +0 -1
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Descriptor-based migration planner.
3
+ *
4
+ * Takes schema issues (from verifySqlSchema) and emits PostgresMigrationOpDescriptor[].
5
+ * Migration strategies consume issues they recognize and produce specialized op
6
+ * sequences (e.g., NOT NULL backfill → addColumn(nullable) + dataTransform + setNotNull).
7
+ * Remaining issues get default descriptor mapping.
8
+ *
9
+ * This planner does NOT produce SqlMigrationPlanOperation — that's the resolver's job.
10
+ * The separation means the same descriptors work for both planner-generated and
11
+ * user-authored migrations.
12
+ */
13
+
14
+ import type { Contract } from '@prisma-next/contract/types';
15
+ import type { SqlPlannerConflict } from '@prisma-next/family-sql/control';
16
+ import type { SchemaIssue } from '@prisma-next/framework-components/control';
17
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
18
+ import type { Result } from '@prisma-next/utils/result';
19
+ import { notOk, ok } from '@prisma-next/utils/result';
20
+ import {
21
+ addColumn,
22
+ addForeignKey,
23
+ addPrimaryKey,
24
+ addUnique,
25
+ alterColumnType,
26
+ createDependency,
27
+ createEnumType,
28
+ createIndex,
29
+ createTable,
30
+ dropColumn,
31
+ dropConstraint,
32
+ dropDefault,
33
+ dropIndex,
34
+ dropNotNull,
35
+ dropTable,
36
+ type PostgresMigrationOpDescriptor,
37
+ setDefault,
38
+ setNotNull,
39
+ } from './operation-descriptors';
40
+ import {
41
+ type MigrationStrategy,
42
+ migrationPlanStrategies,
43
+ type StrategyContext,
44
+ } from './planner-strategies';
45
+
46
+ export type { MigrationStrategy, StrategyContext };
47
+
48
+ // ============================================================================
49
+ // Issue kind ordering (dependency order)
50
+ // ============================================================================
51
+
52
+ const ISSUE_KIND_ORDER: Record<string, number> = {
53
+ // Dependencies and types first
54
+ dependency_missing: 1,
55
+ type_missing: 2,
56
+ type_values_mismatch: 3,
57
+ enum_values_changed: 3,
58
+
59
+ // Drops (reconciliation — clear the way for creates)
60
+ // FKs dropped first (they depend on other constraints)
61
+ extra_foreign_key: 10,
62
+ extra_unique_constraint: 11,
63
+ extra_primary_key: 12,
64
+ extra_index: 13,
65
+ extra_default: 14,
66
+ extra_column: 15,
67
+ extra_table: 16,
68
+
69
+ // Tables before columns
70
+ missing_table: 20,
71
+
72
+ // Columns before constraints
73
+ missing_column: 30,
74
+
75
+ // Reconciliation alters (on existing objects)
76
+ type_mismatch: 40,
77
+ nullability_mismatch: 41,
78
+ default_missing: 42,
79
+ default_mismatch: 43,
80
+
81
+ // Constraints after columns exist
82
+ primary_key_mismatch: 50,
83
+ unique_constraint_mismatch: 51,
84
+ index_mismatch: 52,
85
+ foreign_key_mismatch: 60,
86
+ };
87
+
88
+ function issueOrder(issue: SchemaIssue): number {
89
+ return ISSUE_KIND_ORDER[issue.kind] ?? 99;
90
+ }
91
+
92
+ // ============================================================================
93
+ // Conflict helpers
94
+ // ============================================================================
95
+
96
+ function issueConflict(
97
+ kind: SqlPlannerConflict['kind'],
98
+ summary: string,
99
+ location?: SqlPlannerConflict['location'],
100
+ ): SqlPlannerConflict {
101
+ return {
102
+ kind,
103
+ summary,
104
+ why: 'Use `migration new` to author a custom migration for this change.',
105
+ ...(location ? { location } : {}),
106
+ };
107
+ }
108
+
109
+ // ============================================================================
110
+ // Default issue-to-descriptor mapping
111
+ // ============================================================================
112
+
113
+ function isMissing(issue: SchemaIssue): boolean {
114
+ if (issue.kind === 'enum_values_changed') return false;
115
+ return issue.actual === undefined;
116
+ }
117
+
118
+ function mapIssue(
119
+ issue: SchemaIssue,
120
+ ctx: StrategyContext,
121
+ ): Result<readonly PostgresMigrationOpDescriptor[], SqlPlannerConflict> {
122
+ switch (issue.kind) {
123
+ // Additive — missing structures
124
+ case 'missing_table': {
125
+ if (!issue.table)
126
+ return notOk(
127
+ issueConflict('unsupportedOperation', 'Missing table issue has no table name'),
128
+ );
129
+ const contractTable = ctx.toContract.storage.tables[issue.table];
130
+ if (!contractTable) {
131
+ return notOk(
132
+ issueConflict(
133
+ 'unsupportedOperation',
134
+ `Table "${issue.table}" reported missing but not found in destination contract`,
135
+ ),
136
+ );
137
+ }
138
+ const ops: PostgresMigrationOpDescriptor[] = [createTable(issue.table)];
139
+ for (const index of contractTable.indexes) {
140
+ ops.push(createIndex(issue.table, [...index.columns]));
141
+ }
142
+ const explicitIndexColumnSets = new Set(
143
+ contractTable.indexes.map((idx) => idx.columns.join(',')),
144
+ );
145
+ for (const fk of contractTable.foreignKeys) {
146
+ if (fk.constraint) {
147
+ ops.push(addForeignKey(issue.table, [...fk.columns]));
148
+ }
149
+ if (fk.index && !explicitIndexColumnSets.has(fk.columns.join(','))) {
150
+ ops.push(createIndex(issue.table, [...fk.columns]));
151
+ }
152
+ }
153
+ for (const unique of contractTable.uniques) {
154
+ ops.push(addUnique(issue.table, [...unique.columns]));
155
+ }
156
+ return ok(ops);
157
+ }
158
+
159
+ case 'missing_column':
160
+ if (!issue.table || !issue.column)
161
+ return notOk(
162
+ issueConflict('unsupportedOperation', 'Missing column issue has no table/column name'),
163
+ );
164
+ return ok([addColumn(issue.table, issue.column)]);
165
+
166
+ case 'default_missing':
167
+ if (!issue.table || !issue.column)
168
+ return notOk(
169
+ issueConflict('unsupportedOperation', 'Default missing issue has no table/column name'),
170
+ );
171
+ return ok([setDefault(issue.table, issue.column)]);
172
+
173
+ // Destructive — extra structures
174
+ case 'extra_table':
175
+ if (!issue.table)
176
+ return notOk(issueConflict('unsupportedOperation', 'Extra table issue has no table name'));
177
+ return ok([dropTable(issue.table)]);
178
+
179
+ case 'extra_column':
180
+ if (!issue.table || !issue.column)
181
+ return notOk(
182
+ issueConflict('unsupportedOperation', 'Extra column issue has no table/column name'),
183
+ );
184
+ return ok([dropColumn(issue.table, issue.column)]);
185
+
186
+ case 'extra_index':
187
+ if (!issue.table || !issue.indexOrConstraint)
188
+ return notOk(
189
+ issueConflict('unsupportedOperation', 'Extra index issue has no table/index name'),
190
+ );
191
+ return ok([dropIndex(issue.table, issue.indexOrConstraint)]);
192
+
193
+ case 'extra_unique_constraint':
194
+ case 'extra_foreign_key':
195
+ case 'extra_primary_key':
196
+ if (!issue.table || !issue.indexOrConstraint)
197
+ return notOk(
198
+ issueConflict(
199
+ 'unsupportedOperation',
200
+ 'Extra constraint issue has no table/constraint name',
201
+ ),
202
+ );
203
+ return ok([dropConstraint(issue.table, issue.indexOrConstraint)]);
204
+
205
+ case 'extra_default':
206
+ if (!issue.table || !issue.column)
207
+ return notOk(
208
+ issueConflict('unsupportedOperation', 'Extra default issue has no table/column name'),
209
+ );
210
+ return ok([dropDefault(issue.table, issue.column)]);
211
+
212
+ // Nullability changes
213
+ case 'nullability_mismatch': {
214
+ if (!issue.table || !issue.column)
215
+ return notOk(
216
+ issueConflict('nullabilityConflict', 'Nullability mismatch has no table/column name'),
217
+ );
218
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
219
+ if (!column)
220
+ return notOk(
221
+ issueConflict(
222
+ 'nullabilityConflict',
223
+ `Column "${issue.table}"."${issue.column}" not found in destination contract`,
224
+ ),
225
+ );
226
+ return ok(
227
+ column.nullable
228
+ ? [dropNotNull(issue.table, issue.column)]
229
+ : [setNotNull(issue.table, issue.column)],
230
+ );
231
+ }
232
+
233
+ // Type changes
234
+ case 'type_mismatch':
235
+ if (!issue.table || !issue.column)
236
+ return notOk(issueConflict('typeMismatch', 'Type mismatch has no table/column name'));
237
+ return ok([alterColumnType(issue.table, issue.column)]);
238
+
239
+ // Default changes
240
+ case 'default_mismatch':
241
+ if (!issue.table || !issue.column)
242
+ return notOk(
243
+ issueConflict('unsupportedOperation', 'Default mismatch has no table/column name'),
244
+ );
245
+ return ok([setDefault(issue.table, issue.column)]);
246
+
247
+ // Constraints — missing (actual undefined) vs mismatched (actual defined)
248
+ case 'primary_key_mismatch':
249
+ if (!issue.table)
250
+ return notOk(issueConflict('indexIncompatible', 'Primary key issue has no table name'));
251
+ if (isMissing(issue)) return ok([addPrimaryKey(issue.table)]);
252
+ return notOk(
253
+ issueConflict(
254
+ 'indexIncompatible',
255
+ `Primary key on "${issue.table}" has different columns (expected: ${issue.expected}, actual: ${issue.actual})`,
256
+ { table: issue.table },
257
+ ),
258
+ );
259
+
260
+ case 'unique_constraint_mismatch':
261
+ if (!issue.table)
262
+ return notOk(
263
+ issueConflict('indexIncompatible', 'Unique constraint issue has no table name'),
264
+ );
265
+ if (isMissing(issue) && issue.expected) {
266
+ const columns = issue.expected.split(', ');
267
+ return ok([addUnique(issue.table, columns)]);
268
+ }
269
+ return notOk(
270
+ issueConflict(
271
+ 'indexIncompatible',
272
+ `Unique constraint on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`,
273
+ { table: issue.table },
274
+ ),
275
+ );
276
+
277
+ case 'index_mismatch':
278
+ if (!issue.table)
279
+ return notOk(issueConflict('indexIncompatible', 'Index issue has no table name'));
280
+ if (isMissing(issue) && issue.expected) {
281
+ const columns = issue.expected.split(', ');
282
+ return ok([createIndex(issue.table, columns)]);
283
+ }
284
+ return notOk(
285
+ issueConflict(
286
+ 'indexIncompatible',
287
+ `Index on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`,
288
+ { table: issue.table },
289
+ ),
290
+ );
291
+
292
+ case 'foreign_key_mismatch':
293
+ if (!issue.table)
294
+ return notOk(issueConflict('foreignKeyConflict', 'Foreign key issue has no table name'));
295
+ if (isMissing(issue) && issue.expected) {
296
+ const arrowIdx = issue.expected.indexOf(' -> ');
297
+ if (arrowIdx >= 0) {
298
+ const columns = issue.expected.slice(0, arrowIdx).split(', ');
299
+ return ok([addForeignKey(issue.table, columns)]);
300
+ }
301
+ }
302
+ return notOk(
303
+ issueConflict(
304
+ 'foreignKeyConflict',
305
+ `Foreign key on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`,
306
+ { table: issue.table },
307
+ ),
308
+ );
309
+
310
+ // Types
311
+ case 'type_missing': {
312
+ if (!issue.typeName)
313
+ return notOk(issueConflict('unsupportedOperation', 'Type missing issue has no typeName'));
314
+ const typeInstance = ctx.toContract.storage.types?.[issue.typeName];
315
+ if (!typeInstance) {
316
+ return notOk(
317
+ issueConflict(
318
+ 'unsupportedOperation',
319
+ `Type "${issue.typeName}" reported missing but not found in destination contract`,
320
+ ),
321
+ );
322
+ }
323
+ // TODO: codec-specific descriptor dispatch should be driven by a registry, not hardcoded prefix checks
324
+ if (typeInstance.codecId.startsWith('pg/enum')) {
325
+ return ok([createEnumType(issue.typeName)]);
326
+ }
327
+ return notOk(
328
+ issueConflict(
329
+ 'unsupportedOperation',
330
+ `Type "${issue.typeName}" uses codec "${typeInstance.codecId}" — only enum types are supported by the descriptor planner`,
331
+ ),
332
+ );
333
+ }
334
+
335
+ case 'type_values_mismatch':
336
+ return notOk(
337
+ issueConflict(
338
+ 'unsupportedOperation',
339
+ `Type "${issue.typeName ?? 'unknown'}" values differ — type alteration not yet supported by descriptor planner`,
340
+ ),
341
+ );
342
+
343
+ // Dependencies
344
+ case 'dependency_missing':
345
+ if (!issue.dependencyId)
346
+ return notOk(
347
+ issueConflict('unsupportedOperation', 'Dependency missing issue has no dependencyId'),
348
+ );
349
+ return ok([createDependency(issue.dependencyId)]);
350
+ default:
351
+ return notOk(
352
+ issueConflict(
353
+ 'unsupportedOperation',
354
+ `Unhandled issue kind: ${(issue as SchemaIssue).kind}`,
355
+ ),
356
+ );
357
+ }
358
+ }
359
+
360
+ // ============================================================================
361
+ // Planner entry point
362
+ // ============================================================================
363
+
364
+ export interface DescriptorPlannerOptions {
365
+ readonly issues: readonly SchemaIssue[];
366
+ readonly toContract: Contract<SqlStorage>;
367
+ readonly fromContract: Contract<SqlStorage> | null;
368
+ readonly strategies?: readonly MigrationStrategy[];
369
+ }
370
+
371
+ export interface DescriptorPlannerValue {
372
+ readonly descriptors: readonly PostgresMigrationOpDescriptor[];
373
+ readonly needsDataMigration: boolean;
374
+ }
375
+
376
+ export function planDescriptors(
377
+ options: DescriptorPlannerOptions,
378
+ ): Result<DescriptorPlannerValue, readonly SqlPlannerConflict[]> {
379
+ const context: StrategyContext = {
380
+ toContract: options.toContract,
381
+ fromContract: options.fromContract,
382
+ };
383
+
384
+ const strategies = options.strategies ?? migrationPlanStrategies;
385
+
386
+ // Phase 1: Pattern matching — consume recognized issues
387
+ let remaining = options.issues;
388
+ const patternOps: PostgresMigrationOpDescriptor[] = [];
389
+
390
+ for (const strategy of strategies) {
391
+ const result = strategy(remaining, context);
392
+ if (result.kind === 'match') {
393
+ remaining = result.issues;
394
+ patternOps.push(...result.ops);
395
+ }
396
+ }
397
+
398
+ // Phase 2: Sort remaining issues by dependency order
399
+ const sorted = [...remaining].sort((a, b) => issueOrder(a) - issueOrder(b));
400
+
401
+ // Phase 3: Map remaining issues to descriptors, collecting conflicts
402
+ const defaultOps: PostgresMigrationOpDescriptor[] = [];
403
+ const conflicts: SqlPlannerConflict[] = [];
404
+
405
+ for (const issue of sorted) {
406
+ const result = mapIssue(issue, context);
407
+ if (result.ok) {
408
+ defaultOps.push(...result.value);
409
+ } else {
410
+ conflicts.push(result.failure);
411
+ }
412
+ }
413
+
414
+ if (conflicts.length > 0) {
415
+ return notOk(conflicts);
416
+ }
417
+
418
+ // Phase 4: Order descriptors by operation kind
419
+ const depOps = defaultOps.filter(
420
+ (op) =>
421
+ op.kind === 'createDependency' ||
422
+ op.kind === 'createEnumType' ||
423
+ op.kind === 'addEnumValues' ||
424
+ op.kind === 'dropEnumType' ||
425
+ op.kind === 'renameType',
426
+ );
427
+ const dropOps = defaultOps.filter(
428
+ (op) =>
429
+ op.kind === 'dropTable' ||
430
+ op.kind === 'dropColumn' ||
431
+ op.kind === 'dropConstraint' ||
432
+ op.kind === 'dropIndex' ||
433
+ op.kind === 'dropDefault',
434
+ );
435
+ const tableOps = defaultOps.filter((op) => op.kind === 'createTable');
436
+ const columnOps = defaultOps.filter((op) => op.kind === 'addColumn');
437
+ const alterOps = defaultOps.filter(
438
+ (op) =>
439
+ op.kind === 'alterColumnType' ||
440
+ op.kind === 'setNotNull' ||
441
+ op.kind === 'dropNotNull' ||
442
+ op.kind === 'setDefault',
443
+ );
444
+ const constraintOps = defaultOps.filter(
445
+ (op) =>
446
+ op.kind === 'addPrimaryKey' ||
447
+ op.kind === 'addUnique' ||
448
+ op.kind === 'createIndex' ||
449
+ op.kind === 'addForeignKey',
450
+ );
451
+
452
+ const descriptors: PostgresMigrationOpDescriptor[] = [
453
+ ...depOps,
454
+ ...dropOps,
455
+ ...tableOps,
456
+ ...columnOps,
457
+ ...patternOps,
458
+ ...alterOps,
459
+ ...constraintOps,
460
+ ];
461
+
462
+ return ok({
463
+ descriptors,
464
+ needsDataMigration: descriptors.some((op) => op.kind === 'dataTransform'),
465
+ });
466
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Postgres migration operation descriptors.
3
+ *
4
+ * Re-exports all structural SQL descriptors from @prisma-next/family-sql
5
+ * and adds data transform support with typed query builder callbacks.
6
+ */
7
+
8
+ import type { Db, TableProxyContract } from '@prisma-next/sql-builder/types';
9
+ import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
10
+
11
+ // Re-export structural descriptors from sql-family
12
+ export {
13
+ type AddColumnDescriptor,
14
+ type AddEnumValuesDescriptor,
15
+ type AddForeignKeyDescriptor,
16
+ type AddPrimaryKeyDescriptor,
17
+ type AddUniqueDescriptor,
18
+ type AlterColumnTypeDescriptor,
19
+ addColumn,
20
+ addEnumValues,
21
+ addForeignKey,
22
+ addPrimaryKey,
23
+ addUnique,
24
+ alterColumnType,
25
+ type Buildable,
26
+ type CreateDependencyDescriptor,
27
+ type CreateEnumTypeDescriptor,
28
+ type CreateIndexDescriptor,
29
+ type CreateTableDescriptor,
30
+ createDependency,
31
+ createEnumType,
32
+ createIndex,
33
+ createTable,
34
+ type DropColumnDescriptor,
35
+ type DropConstraintDescriptor,
36
+ type DropDefaultDescriptor,
37
+ type DropEnumTypeDescriptor,
38
+ type DropIndexDescriptor,
39
+ type DropNotNullDescriptor,
40
+ type DropTableDescriptor,
41
+ dropColumn,
42
+ dropConstraint,
43
+ dropDefault,
44
+ dropEnumType,
45
+ dropIndex,
46
+ dropNotNull,
47
+ dropTable,
48
+ type RenameTypeDescriptor,
49
+ renameType,
50
+ type SetDefaultDescriptor,
51
+ type SetNotNullDescriptor,
52
+ type SqlMigrationOpDescriptor,
53
+ setDefault,
54
+ setNotNull,
55
+ TODO,
56
+ type TodoMarker,
57
+ } from '@prisma-next/family-sql/operation-descriptors';
58
+
59
+ import {
60
+ type Buildable,
61
+ type DataTransformDescriptor,
62
+ type SqlMigrationOpDescriptor,
63
+ builders as structuralBuilders,
64
+ TODO,
65
+ type TodoMarker,
66
+ } from '@prisma-next/family-sql/operation-descriptors';
67
+
68
+ export type { DataTransformDescriptor };
69
+
70
+ // ============================================================================
71
+ // Typed data transform inputs (for createBuilders<Contract>())
72
+ // ============================================================================
73
+
74
+ /**
75
+ * A single query plan input — callback, pre-built plan, or TODO placeholder.
76
+ * @template TContract - The contract type for the Db client. Defaults to any
77
+ * (untyped). Use createBuilders<Contract>() to get typed callbacks.
78
+ */
79
+ // biome-ignore lint/suspicious/noExplicitAny: default is untyped; createBuilders narrows this
80
+ export type QueryPlanInput<TContract extends TableProxyContract = any> =
81
+ | ((db: Db<TContract>) => Buildable)
82
+ | SqlQueryPlan
83
+ | TodoMarker;
84
+
85
+ /** Run input — a callback returning one or many buildables, or a pre-built plan/TODO. */
86
+ // biome-ignore lint/suspicious/noExplicitAny: default is untyped; createBuilders narrows this
87
+ export type RunInput<TContract extends TableProxyContract = any> =
88
+ | ((db: Db<TContract>) => Buildable | readonly Buildable[])
89
+ | SqlQueryPlan
90
+ | TodoMarker;
91
+
92
+ // ============================================================================
93
+ // Postgres descriptor union = SQL structural + data transforms
94
+ // ============================================================================
95
+
96
+ export type PostgresMigrationOpDescriptor = SqlMigrationOpDescriptor | DataTransformDescriptor;
97
+
98
+ // ============================================================================
99
+ // Data transform builder
100
+ // ============================================================================
101
+
102
+ function resolveInput(input: QueryPlanInput): QueryPlanInput {
103
+ if (typeof input === 'symbol' || typeof input === 'function') return input;
104
+ if ('build' in input && typeof (input as Buildable).build === 'function') {
105
+ return (input as Buildable).build();
106
+ }
107
+ return input;
108
+ }
109
+
110
+ // biome-ignore lint/suspicious/noExplicitAny: default is untyped; createBuilders narrows this
111
+ export function dataTransform<TContract extends TableProxyContract = any>(
112
+ name: string,
113
+ options: {
114
+ check: QueryPlanInput<TContract> | Buildable | boolean;
115
+ run: RunInput<TContract> | Buildable;
116
+ },
117
+ ): DataTransformDescriptor {
118
+ const check =
119
+ typeof options.check === 'boolean'
120
+ ? options.check
121
+ : resolveInput(options.check as QueryPlanInput);
122
+
123
+ const run: (symbol | object | ((...args: never[]) => unknown))[] = [];
124
+ if (typeof options.run === 'function') {
125
+ run.push(options.run);
126
+ } else if (typeof options.run === 'symbol') {
127
+ run.push(options.run);
128
+ } else {
129
+ run.push(resolveInput(options.run as QueryPlanInput));
130
+ }
131
+ return {
132
+ kind: 'dataTransform' as const,
133
+ name,
134
+ source: 'migration.ts',
135
+ check,
136
+ run,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Creates typed migration builder functions parameterized by the contract type.
142
+ * The dataTransform callback receives a fully typed Db<TContract> client.
143
+ *
144
+ * Usage:
145
+ * ```typescript
146
+ * import type { Contract } from "../../src/prisma/contract.d"
147
+ * import { createBuilders } from "@prisma-next/target-postgres/migration-builders"
148
+ *
149
+ * const { addColumn, dataTransform, setNotNull } = createBuilders<Contract>()
150
+ * ```
151
+ */
152
+ export function createBuilders<TContract extends TableProxyContract>() {
153
+ return {
154
+ ...structuralBuilders,
155
+ dataTransform: dataTransform<TContract>,
156
+ TODO,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * All builder functions keyed by descriptor kind.
162
+ */
163
+ export const builders = {
164
+ ...structuralBuilders,
165
+ dataTransform,
166
+ } as const;