@prisma-next/target-postgres 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.
Files changed (188) hide show
  1. package/dist/{codec-ids-CojIXVf9.mjs → codec-ids-C5qzBqus.mjs} +4 -4
  2. package/dist/{codec-ids-CojIXVf9.mjs.map → codec-ids-C5qzBqus.mjs.map} +1 -1
  3. package/dist/codec-ids-CplrEfmx.d.mts +29 -0
  4. package/dist/codec-ids-CplrEfmx.d.mts.map +1 -0
  5. package/dist/codec-ids.d.mts +2 -28
  6. package/dist/codec-ids.mjs +2 -3
  7. package/dist/codec-types-lrsb3N07.d.mts +79 -0
  8. package/dist/codec-types-lrsb3N07.d.mts.map +1 -0
  9. package/dist/codec-types.d.mts +2 -42
  10. package/dist/codec-types.mjs +1 -3
  11. package/dist/codecs-Cue97Xqf.d.mts +558 -0
  12. package/dist/codecs-Cue97Xqf.d.mts.map +1 -0
  13. package/dist/codecs.d.mts +13 -2
  14. package/dist/codecs.d.mts.map +1 -0
  15. package/dist/codecs.mjs +738 -2
  16. package/dist/codecs.mjs.map +1 -0
  17. package/dist/control.d.mts +1 -1
  18. package/dist/control.d.mts.map +1 -1
  19. package/dist/control.mjs +132 -96
  20. package/dist/control.mjs.map +1 -1
  21. package/dist/{data-transform-VfEGzXWt.mjs → data-transform-DKWXdHuZ.mjs} +25 -7
  22. package/dist/data-transform-DKWXdHuZ.mjs.map +1 -0
  23. package/dist/data-transform-bIeAcZIJ.d.mts +38 -0
  24. package/dist/data-transform-bIeAcZIJ.d.mts.map +1 -0
  25. package/dist/data-transform.d.mts +1 -1
  26. package/dist/data-transform.mjs +2 -3
  27. package/dist/{default-normalizer-DNOpRoOF.mjs → default-normalizer-C8XyZj85.mjs} +2 -2
  28. package/dist/{default-normalizer-DNOpRoOF.mjs.map → default-normalizer-C8XyZj85.mjs.map} +1 -1
  29. package/dist/default-normalizer.d.mts +0 -1
  30. package/dist/default-normalizer.d.mts.map +1 -1
  31. package/dist/default-normalizer.mjs +2 -3
  32. package/dist/descriptor-meta-Dde_BS3K.mjs +99 -0
  33. package/dist/descriptor-meta-Dde_BS3K.mjs.map +1 -0
  34. package/dist/{errors-AFvEPZ1R.mjs → errors-Chm2bKcS.mjs} +2 -3
  35. package/dist/{errors-AFvEPZ1R.mjs.map → errors-Chm2bKcS.mjs.map} +1 -1
  36. package/dist/errors.d.mts +0 -1
  37. package/dist/errors.d.mts.map +1 -1
  38. package/dist/errors.mjs +2 -3
  39. package/dist/{issue-planner-CFjB0_oO.mjs → issue-planner-DQ6WJkad.mjs} +24 -91
  40. package/dist/issue-planner-DQ6WJkad.mjs.map +1 -0
  41. package/dist/issue-planner.d.mts +4 -7
  42. package/dist/issue-planner.d.mts.map +1 -1
  43. package/dist/issue-planner.mjs +2 -3
  44. package/dist/migration.d.mts +29 -5
  45. package/dist/migration.d.mts.map +1 -1
  46. package/dist/migration.mjs +5 -6
  47. package/dist/migration.mjs.map +1 -1
  48. package/dist/{native-type-normalizer-CInai_oY.mjs → native-type-normalizer-Cry4QoLf.mjs} +2 -2
  49. package/dist/native-type-normalizer-Cry4QoLf.mjs.map +1 -0
  50. package/dist/native-type-normalizer.d.mts.map +1 -1
  51. package/dist/native-type-normalizer.mjs +2 -3
  52. package/dist/{op-factory-call-BKlruaiC.mjs → op-factory-call-DeaFxa8_.mjs} +27 -10
  53. package/dist/op-factory-call-DeaFxa8_.mjs.map +1 -0
  54. package/dist/{op-factory-call-C3bWXKSP.d.mts → op-factory-call-UFpUPJL6.d.mts} +11 -8
  55. package/dist/op-factory-call-UFpUPJL6.d.mts.map +1 -0
  56. package/dist/op-factory-call.d.mts +1 -2
  57. package/dist/op-factory-call.mjs +2 -3
  58. package/dist/pack.d.mts +28 -9
  59. package/dist/pack.d.mts.map +1 -1
  60. package/dist/pack.mjs +2 -3
  61. package/dist/{planner-CLUvVhUN.mjs → planner-CYtKhLYa.mjs} +22 -16
  62. package/dist/planner-CYtKhLYa.mjs.map +1 -0
  63. package/dist/{planner-ddl-builders-Dxvw1LHw.mjs → planner-ddl-builders-CLB7Umhh.mjs} +4 -5
  64. package/dist/planner-ddl-builders-CLB7Umhh.mjs.map +1 -0
  65. package/dist/planner-ddl-builders.d.mts +1 -1
  66. package/dist/planner-ddl-builders.d.mts.map +1 -1
  67. package/dist/planner-ddl-builders.mjs +2 -3
  68. package/dist/{planner-identity-values-Dju-o5GF.mjs → planner-identity-values-DTx0gePL.mjs} +2 -3
  69. package/dist/{planner-identity-values-Dju-o5GF.mjs.map → planner-identity-values-DTx0gePL.mjs.map} +1 -1
  70. package/dist/planner-identity-values.d.mts +0 -1
  71. package/dist/planner-identity-values.d.mts.map +1 -1
  72. package/dist/planner-identity-values.mjs +2 -3
  73. package/dist/{planner-produced-postgres-migration-CRRTno6Z.d.mts → planner-produced-postgres-migration-CjxWIVgh.d.mts} +11 -7
  74. package/dist/planner-produced-postgres-migration-CjxWIVgh.d.mts.map +1 -0
  75. package/dist/{planner-produced-postgres-migration-DSSPq8QS.mjs → planner-produced-postgres-migration-DphktB2N.mjs} +16 -8
  76. package/dist/planner-produced-postgres-migration-DphktB2N.mjs.map +1 -0
  77. package/dist/planner-produced-postgres-migration.d.mts +1 -4
  78. package/dist/planner-produced-postgres-migration.mjs +2 -3
  79. package/dist/{planner-schema-lookup-B7lkypwn.mjs → planner-schema-lookup-B1ags8ys.mjs} +2 -2
  80. package/dist/{planner-schema-lookup-B7lkypwn.mjs.map → planner-schema-lookup-B1ags8ys.mjs.map} +1 -1
  81. package/dist/planner-schema-lookup.d.mts +0 -1
  82. package/dist/planner-schema-lookup.d.mts.map +1 -1
  83. package/dist/planner-schema-lookup.mjs +2 -3
  84. package/dist/{planner-sql-checks-7jkgm9TX.mjs → planner-sql-checks-DwZvGlV4.mjs} +3 -5
  85. package/dist/planner-sql-checks-DwZvGlV4.mjs.map +1 -0
  86. package/dist/planner-sql-checks.d.mts.map +1 -1
  87. package/dist/planner-sql-checks.mjs +2 -3
  88. package/dist/{planner-target-details-DH-azLu-.d.mts → planner-target-details-bVVcanWh.d.mts} +1 -1
  89. package/dist/planner-target-details-bVVcanWh.d.mts.map +1 -0
  90. package/dist/planner-target-details.d.mts +1 -1
  91. package/dist/planner-target-details.mjs +1 -1
  92. package/dist/planner.d.mts +21 -12
  93. package/dist/planner.d.mts.map +1 -1
  94. package/dist/planner.mjs +2 -4
  95. package/dist/{postgres-migration-qtmtbONe.mjs → postgres-migration-Bkv140RW.mjs} +4 -5
  96. package/dist/postgres-migration-Bkv140RW.mjs.map +1 -0
  97. package/dist/{postgres-migration-BjA3Zmts.d.mts → postgres-migration-UkcHfZAA.d.mts} +6 -6
  98. package/dist/postgres-migration-UkcHfZAA.d.mts.map +1 -0
  99. package/dist/render-ops--1nnfNus.mjs +23 -0
  100. package/dist/render-ops--1nnfNus.mjs.map +1 -0
  101. package/dist/render-ops.d.mts +3 -4
  102. package/dist/render-ops.d.mts.map +1 -1
  103. package/dist/render-ops.mjs +2 -3
  104. package/dist/{render-typescript-1rF_SB4g.mjs → render-typescript-D3doH-vX.mjs} +2 -14
  105. package/dist/render-typescript-D3doH-vX.mjs.map +1 -0
  106. package/dist/render-typescript.d.mts +3 -6
  107. package/dist/render-typescript.d.mts.map +1 -1
  108. package/dist/render-typescript.mjs +2 -3
  109. package/dist/runtime.d.mts +5 -9
  110. package/dist/runtime.d.mts.map +1 -1
  111. package/dist/runtime.mjs +7 -14
  112. package/dist/runtime.mjs.map +1 -1
  113. package/dist/{shared-Bxkt8pNO.d.mts → shared-MpwjwAjM.d.mts} +2 -2
  114. package/dist/shared-MpwjwAjM.d.mts.map +1 -0
  115. package/dist/{sql-utils-r-Lw535w.mjs → sql-utils-CggjWNij.mjs} +4 -2
  116. package/dist/sql-utils-CggjWNij.mjs.map +1 -0
  117. package/dist/sql-utils.d.mts.map +1 -1
  118. package/dist/sql-utils.mjs +2 -3
  119. package/dist/{statement-builders-BPnmt6wx.mjs → statement-builders-BT889jV0.mjs} +28 -13
  120. package/dist/statement-builders-BT889jV0.mjs.map +1 -0
  121. package/dist/statement-builders.d.mts +31 -3
  122. package/dist/statement-builders.d.mts.map +1 -1
  123. package/dist/statement-builders.mjs +2 -3
  124. package/dist/{tables-BmdW_FWO.mjs → tables-DgYIXjUt.mjs} +53 -14
  125. package/dist/tables-DgYIXjUt.mjs.map +1 -0
  126. package/dist/{types-ClK03Ojd.d.mts → types-CTqpysRY.d.mts} +1 -1
  127. package/dist/types-CTqpysRY.d.mts.map +1 -0
  128. package/dist/types.d.mts +1 -1
  129. package/dist/types.mjs +1 -1
  130. package/package.json +21 -19
  131. package/src/core/authoring.ts +5 -11
  132. package/src/core/codec-helpers.ts +135 -0
  133. package/src/core/codec-ids.ts +1 -0
  134. package/src/core/codec-type-map.ts +81 -0
  135. package/src/core/codecs.ts +941 -547
  136. package/src/core/descriptor-meta.ts +1 -1
  137. package/src/core/migrations/issue-planner.ts +23 -35
  138. package/src/core/migrations/op-factory-call.ts +26 -5
  139. package/src/core/migrations/operations/data-transform.ts +86 -21
  140. package/src/core/migrations/operations/dependencies.ts +52 -0
  141. package/src/core/migrations/operations/indexes.ts +27 -2
  142. package/src/core/migrations/planner-produced-postgres-migration.ts +17 -5
  143. package/src/core/migrations/planner-strategies.ts +4 -86
  144. package/src/core/migrations/planner.ts +62 -20
  145. package/src/core/migrations/postgres-migration.ts +3 -6
  146. package/src/core/migrations/render-ops.ts +26 -3
  147. package/src/core/migrations/render-typescript.ts +5 -9
  148. package/src/core/migrations/runner.ts +172 -151
  149. package/src/core/migrations/statement-builders.ts +49 -10
  150. package/src/core/registry.ts +11 -0
  151. package/src/exports/codec-types.ts +4 -13
  152. package/src/exports/codecs.ts +49 -2
  153. package/src/exports/control.ts +0 -1
  154. package/src/exports/migration.ts +5 -1
  155. package/src/exports/runtime.ts +6 -11
  156. package/src/exports/statement-builders.ts +2 -1
  157. package/dist/codec-ids.d.mts.map +0 -1
  158. package/dist/codec-types.d.mts.map +0 -1
  159. package/dist/codecs-BQEm9_oo.d.mts +0 -319
  160. package/dist/codecs-BQEm9_oo.d.mts.map +0 -1
  161. package/dist/codecs-BoahtY_Q.mjs +0 -385
  162. package/dist/codecs-BoahtY_Q.mjs.map +0 -1
  163. package/dist/data-transform-CxFRBIUp.d.mts +0 -32
  164. package/dist/data-transform-CxFRBIUp.d.mts.map +0 -1
  165. package/dist/data-transform-VfEGzXWt.mjs.map +0 -1
  166. package/dist/descriptor-meta-BVoVtyp-.mjs +0 -120
  167. package/dist/descriptor-meta-BVoVtyp-.mjs.map +0 -1
  168. package/dist/issue-planner-CFjB0_oO.mjs.map +0 -1
  169. package/dist/native-type-normalizer-CInai_oY.mjs.map +0 -1
  170. package/dist/op-factory-call-BKlruaiC.mjs.map +0 -1
  171. package/dist/op-factory-call-C3bWXKSP.d.mts.map +0 -1
  172. package/dist/planner-CLUvVhUN.mjs.map +0 -1
  173. package/dist/planner-ddl-builders-Dxvw1LHw.mjs.map +0 -1
  174. package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts.map +0 -1
  175. package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs.map +0 -1
  176. package/dist/planner-sql-checks-7jkgm9TX.mjs.map +0 -1
  177. package/dist/planner-target-details-DH-azLu-.d.mts.map +0 -1
  178. package/dist/postgres-migration-BjA3Zmts.d.mts.map +0 -1
  179. package/dist/postgres-migration-qtmtbONe.mjs.map +0 -1
  180. package/dist/render-ops-D6_DHdOK.mjs +0 -8
  181. package/dist/render-ops-D6_DHdOK.mjs.map +0 -1
  182. package/dist/render-typescript-1rF_SB4g.mjs.map +0 -1
  183. package/dist/shared-Bxkt8pNO.d.mts.map +0 -1
  184. package/dist/sql-utils-r-Lw535w.mjs.map +0 -1
  185. package/dist/statement-builders-BPnmt6wx.mjs.map +0 -1
  186. package/dist/tables-BmdW_FWO.mjs.map +0 -1
  187. package/dist/types-ClK03Ojd.d.mts.map +0 -1
  188. package/src/core/json-schema-type-expression.ts +0 -131
@@ -1,5 +1,5 @@
1
+ import type { CodecTypes } from '../exports/codec-types';
1
2
  import { postgresAuthoringFieldPresets, postgresAuthoringTypes } from './authoring';
2
- import type { CodecTypes } from './codecs';
3
3
 
4
4
  const postgresTargetDescriptorMetaBase = {
5
5
  kind: 'target',
@@ -15,6 +15,7 @@ import type {
15
15
  SqlPlannerConflict,
16
16
  SqlPlannerConflictLocation,
17
17
  } from '@prisma-next/family-sql/control';
18
+ import { arraysEqual } from '@prisma-next/family-sql/schema-verify';
18
19
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
19
20
  import type { SchemaIssue } from '@prisma-next/framework-components/control';
20
21
  import type {
@@ -32,9 +33,7 @@ import {
32
33
  AddUniqueCall,
33
34
  AlterColumnTypeCall,
34
35
  CreateEnumTypeCall,
35
- CreateExtensionCall,
36
36
  CreateIndexCall,
37
- CreateSchemaCall,
38
37
  CreateTableCall,
39
38
  DropColumnCall,
40
39
  DropConstraintCall,
@@ -62,8 +61,7 @@ export type { CallMigrationStrategy, StrategyContext };
62
61
  // ============================================================================
63
62
 
64
63
  const ISSUE_KIND_ORDER: Record<string, number> = {
65
- // Dependencies and types first
66
- dependency_missing: 1,
64
+ // Types first
67
65
  type_missing: 2,
68
66
  type_values_mismatch: 3,
69
67
  enum_values_changed: 3,
@@ -149,9 +147,8 @@ export interface IssuePlannerOptions {
149
147
  */
150
148
  readonly policy?: MigrationOperationPolicy;
151
149
  /**
152
- * Framework components participating in this composition. Used by the
153
- * dependency-install strategy to dispatch `databaseDependencies.init` at
154
- * plan time.
150
+ * Framework components participating in this composition. Available to
151
+ * future strategies that may consult component metadata at plan time.
155
152
  */
156
153
  readonly frameworkComponents?: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
157
154
  readonly strategies?: readonly CallMigrationStrategy[];
@@ -211,7 +208,12 @@ function mapIssueToCall(
211
208
  ];
212
209
  for (const index of contractTable.indexes) {
213
210
  const indexName = index.name ?? `${issue.table}_${index.columns.join('_')}_idx`;
214
- calls.push(new CreateIndexCall(schemaName, issue.table, indexName, [...index.columns]));
211
+ const extras: { type?: string; options?: Record<string, unknown> } = {};
212
+ if (index.type !== undefined) extras.type = index.type;
213
+ if (index.options !== undefined) extras.options = index.options;
214
+ calls.push(
215
+ new CreateIndexCall(schemaName, issue.table, indexName, [...index.columns], extras),
216
+ );
215
217
  }
216
218
  const explicitIndexColumnSets = new Set(
217
219
  contractTable.indexes.map((idx) => idx.columns.join(',')),
@@ -446,8 +448,14 @@ function mapIssueToCall(
446
448
  return notOk(issueConflict('indexIncompatible', 'Index issue has no table name'));
447
449
  if (isMissing(issue) && issue.expected) {
448
450
  const columns = issue.expected.split(', ');
449
- const indexName = `${issue.table}_${columns.join('_')}_idx`;
450
- return ok([new CreateIndexCall(schemaName, issue.table, indexName, columns)]);
451
+ const contractIndex = ctx.toContract.storage.tables[issue.table]?.indexes.find((idx) =>
452
+ arraysEqual(idx.columns, columns),
453
+ );
454
+ const indexName = contractIndex?.name ?? `${issue.table}_${columns.join('_')}_idx`;
455
+ const extras: { type?: string; options?: Record<string, unknown> } = {};
456
+ if (contractIndex?.type !== undefined) extras.type = contractIndex.type;
457
+ if (contractIndex?.options !== undefined) extras.options = contractIndex.options;
458
+ return ok([new CreateIndexCall(schemaName, issue.table, indexName, columns, extras)]);
451
459
  }
452
460
  return notOk(
453
461
  issueConflict(
@@ -527,21 +535,6 @@ function mapIssueToCall(
527
535
  ),
528
536
  );
529
537
 
530
- case 'dependency_missing':
531
- if (!issue.dependencyId)
532
- return notOk(
533
- issueConflict('unsupportedOperation', 'Dependency missing issue has no dependencyId'),
534
- );
535
- if (issue.dependencyId.startsWith('ext:')) {
536
- return ok([new CreateExtensionCall(issue.dependencyId.slice(4))]);
537
- }
538
- if (issue.dependencyId.startsWith('schema:')) {
539
- return ok([new CreateSchemaCall(issue.dependencyId.slice(7))]);
540
- }
541
- return notOk(
542
- issueConflict('unsupportedOperation', `Unknown dependency type: ${issue.dependencyId}`),
543
- );
544
-
545
538
  default:
546
539
  return notOk(
547
540
  issueConflict(
@@ -605,24 +598,19 @@ function classifyCall(call: PostgresOpFactoryCall): CallCategory {
605
598
  case 'addForeignKey':
606
599
  return 'foreignKey';
607
600
  case 'rawSql': {
608
- // Install ops (`dependencyInstallCallStrategy`) and type ops
609
- // (`storageTypePlanCallStrategy`) both lift raw `SqlMigrationPlanOperation`s
610
- // through `RawSqlCall` to preserve the component-declared label and
611
- // precheck/postcheck. Classification falls back to inspecting the
612
- // underlying op's target details (`objectType: 'type'`) and id prefix
613
- // (`extension.*` / `schema.*`).
601
+ // Type ops lifted through `RawSqlCall` by `storageTypePlanCallStrategy`
602
+ // to preserve the codec-emitted label and precheck/postcheck.
603
+ // Classification falls back to inspecting the underlying op's target
604
+ // details (`objectType: 'type'`).
614
605
  const op = (
615
606
  call as {
616
607
  op?: {
617
- id?: string;
618
608
  target?: { details?: { objectType?: string } };
619
609
  };
620
610
  }
621
611
  ).op;
622
612
  const objectType = op?.target?.details?.objectType;
623
613
  if (objectType === 'type') return 'dep';
624
- const id = typeof op?.id === 'string' ? op.id : '';
625
- if (id.startsWith('extension.') || id.startsWith('schema.')) return 'dep';
626
614
  return 'alter';
627
615
  }
628
616
  default:
@@ -651,7 +639,7 @@ const DEFAULT_POLICY: MigrationOperationPolicy = {
651
639
  };
652
640
 
653
641
  function emptySchemaIR(): SqlSchemaIR {
654
- return { tables: {}, dependencies: [] };
642
+ return { tables: {} };
655
643
  }
656
644
 
657
645
  function conflictKindForCall(call: PostgresOpFactoryCall): SqlPlannerConflict['kind'] {
@@ -506,6 +506,10 @@ export class CreateIndexCall extends PostgresOpFactoryCallNode {
506
506
  readonly tableName: string;
507
507
  readonly indexName: string;
508
508
  readonly columns: readonly string[];
509
+ // Named indexType (not typeName) to avoid collision with CreateEnumTypeCall.typeName,
510
+ // which identifies a CREATE TYPE target and is read by `locationForCall` in issue-planner.ts.
511
+ readonly indexType: string | undefined;
512
+ readonly options: Record<string, unknown> | undefined;
509
513
  readonly label: string;
510
514
 
511
515
  constructor(
@@ -513,22 +517,40 @@ export class CreateIndexCall extends PostgresOpFactoryCallNode {
513
517
  tableName: string,
514
518
  indexName: string,
515
519
  columns: readonly string[],
520
+ extras?: { readonly type?: string; readonly options?: Record<string, unknown> },
516
521
  ) {
517
522
  super();
518
523
  this.schemaName = schemaName;
519
524
  this.tableName = tableName;
520
525
  this.indexName = indexName;
521
526
  this.columns = columns;
527
+ this.indexType = extras?.type;
528
+ this.options = extras?.options;
522
529
  this.label = `Create index "${indexName}" on "${tableName}"`;
523
530
  this.freeze();
524
531
  }
525
532
 
526
533
  toOp(): Op {
527
- return createIndex(this.schemaName, this.tableName, this.indexName, this.columns);
534
+ const extras: { type?: string; options?: Record<string, unknown> } = {};
535
+ if (this.indexType !== undefined) extras.type = this.indexType;
536
+ if (this.options !== undefined) extras.options = this.options;
537
+ return createIndex(this.schemaName, this.tableName, this.indexName, this.columns, extras);
528
538
  }
529
539
 
530
540
  renderTypeScript(): string {
531
- return `createIndex(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.indexName)}, ${jsonToTsSource(this.columns)})`;
541
+ const args = [
542
+ jsonToTsSource(this.schemaName),
543
+ jsonToTsSource(this.tableName),
544
+ jsonToTsSource(this.indexName),
545
+ jsonToTsSource(this.columns),
546
+ ];
547
+ if (this.indexType !== undefined || this.options !== undefined) {
548
+ const extrasParts: string[] = [];
549
+ if (this.indexType !== undefined) extrasParts.push(`type: ${jsonToTsSource(this.indexType)}`);
550
+ if (this.options !== undefined) extrasParts.push(`options: ${jsonToTsSource(this.options)}`);
551
+ args.push(`{ ${extrasParts.join(', ')} }`);
552
+ }
553
+ return `createIndex(${args.join(', ')})`;
532
554
  }
533
555
  }
534
556
 
@@ -674,9 +696,8 @@ export class RenameTypeCall extends PostgresOpFactoryCallNode {
674
696
  * Laundered pre-built operation.
675
697
  *
676
698
  * Wraps an already-materialized `SqlMigrationPlanOperation` — typically one
677
- * produced by a SQL-family method, a codec control hook, or a component
678
- * `databaseDependencies.init` declaration so the planner can carry it
679
- * alongside IR nodes without reverse-engineering it into a
699
+ * produced by a SQL-family method or a codec control hook so the planner
700
+ * can carry it alongside IR nodes without reverse-engineering it into a
680
701
  * structured call class. Doubles as the user-facing escape hatch for raw
681
702
  * migrations: authors can pass a full op shape to `rawSql({...})`.
682
703
  *
@@ -11,7 +11,7 @@
11
11
  * override get operations() {
12
12
  * return [
13
13
  * this.dataTransform(endContract, 'backfill emails', {
14
- * check: () => db.users.count().where(({ email }) => email.isNull()),
14
+ * check: () => db.users.select('id').where(({ email }) => email.isNull()).limit(1),
15
15
  * run: () => db.users.update({ email: '' }).where(({ email }) => email.isNull()),
16
16
  * }),
17
17
  * ];
@@ -23,20 +23,49 @@
23
23
  * invokes each one, asserts that its `meta.storageHash` matches the
24
24
  * `contract` it was handed (→ `PN-MIG-2005` on mismatch), and lowers the
25
25
  * plan via the supplied control adapter to a serialized `{sql, params}`
26
- * payload for `ops.json`. The free factory remains usable standalone
27
- * (tests, ad-hoc tooling, non-class contexts) by passing the adapter
28
- * explicitly as the fourth argument.
26
+ * payload.
27
+ *
28
+ * The factory then lowers the data transform to the unified migration-op
29
+ * shape `{ precheck, execute, postcheck }`. The user's `check` plan is
30
+ * wrapped twice with opposite truth values:
31
+ *
32
+ * - precheck `SELECT EXISTS (<check>) AS ok` asserts there is work to do
33
+ * (precheck is short-circuited by the runner's pre-satisfied-skip path
34
+ * when nothing remains to backfill).
35
+ * - postcheck `SELECT NOT EXISTS (<check>) AS ok` asserts the work is
36
+ * complete after the run steps execute.
37
+ *
38
+ * The `check` plan is therefore expected to be a **rowset query whose
39
+ * presence of any row signals "work remains"** — typically `select('id')
40
+ * .where(<violation predicate>).limit(1)`. Scalar/aggregate shapes
41
+ * (`count(*)`, `bool_and(...)`) do not work under this contract: they
42
+ * always return exactly one row, so `EXISTS` is always true and
43
+ * `NOT EXISTS` is always false. (This is the same row-presence contract
44
+ * the pre-unification runner relied on; the wrapping is just lifting it
45
+ * into SQL.)
46
+ *
47
+ * Each `run` plan becomes an execute step. Because the `Step.params`
48
+ * field threads through `driver.query(sql, params)`, the user's bound
49
+ * values flow through the driver's parameter binder rather than being
50
+ * inlined into the SQL text.
51
+ *
52
+ * The free factory remains usable standalone (tests, ad-hoc tooling,
53
+ * non-class contexts) by passing the adapter explicitly as the fourth
54
+ * argument.
29
55
  */
30
56
 
31
57
  import type { Contract } from '@prisma-next/contract/types';
32
58
  import { errorDataTransformContractMismatch } from '@prisma-next/errors/migration';
33
- import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
34
59
  import type {
35
- DataTransformOperation,
36
- SerializedQueryPlan,
37
- } from '@prisma-next/framework-components/control';
60
+ SqlMigrationPlanOperation,
61
+ SqlMigrationPlanOperationStep,
62
+ } from '@prisma-next/family-sql/control';
63
+ import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
64
+ import type { SerializedQueryPlan } from '@prisma-next/framework-components/control';
38
65
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
39
66
  import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
67
+ import { ifDefined } from '@prisma-next/utils/defined';
68
+ import type { PostgresPlanTargetDetails } from '../planner-target-details';
40
69
 
41
70
  interface Buildable<R = unknown> {
42
71
  build(): SqlQueryPlan<R>;
@@ -49,36 +78,72 @@ interface Buildable<R = unknown> {
49
78
  export type DataTransformClosure = () => SqlQueryPlan | Buildable;
50
79
 
51
80
  export interface DataTransformOptions {
52
- /** Optional pre-flight query. `undefined` means "no check". */
81
+ /**
82
+ * Optional opt-in routing identity. Presence opts the transform into
83
+ * invariant-aware routing; absence means it is path-dependent and
84
+ * not referenceable from refs.
85
+ */
86
+ readonly invariantId?: string;
87
+ /**
88
+ * Optional pre-flight query. `undefined` means "no check". When
89
+ * supplied, the closure must return a **rowset query** whose
90
+ * presence of any row signals "violations remain". Conventional
91
+ * shape: `db.<table>.select('id').where(<violation>).limit(1)`.
92
+ * Scalar/aggregate shapes do not satisfy this contract.
93
+ */
53
94
  readonly check?: DataTransformClosure;
54
95
  /** One or more mutation queries to execute. */
55
96
  readonly run: DataTransformClosure | readonly DataTransformClosure[];
56
97
  }
57
98
 
58
- /**
59
- * Concrete Postgres flavor of `DataTransformOperation`, re-exported so the
60
- * `PostgresMigration.dataTransform` instance method can name it without
61
- * leaking the framework-components symbol into call sites.
62
- */
63
- export type PostgresDataTransformOperation = DataTransformOperation;
64
-
65
99
  export function dataTransform<TContract extends Contract<SqlStorage>>(
66
100
  contract: TContract,
67
101
  name: string,
68
102
  options: DataTransformOptions,
69
103
  adapter: SqlControlAdapter<'postgres'>,
70
- ): DataTransformOperation {
104
+ ): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
71
105
  const runClosures: readonly DataTransformClosure[] = Array.isArray(options.run)
72
106
  ? options.run
73
107
  : [options.run as DataTransformClosure];
108
+
109
+ const checkPlan = options.check ? invokeAndLower(options.check, contract, adapter, name) : null;
110
+ const runPlans = runClosures.map((closure) => invokeAndLower(closure, contract, adapter, name));
111
+
112
+ const precheck: readonly SqlMigrationPlanOperationStep[] = checkPlan
113
+ ? [
114
+ {
115
+ description: `Check ${name} has work to do`,
116
+ sql: `SELECT EXISTS (${checkPlan.sql}) AS ok`,
117
+ params: checkPlan.params,
118
+ },
119
+ ]
120
+ : [];
121
+
122
+ const execute: readonly SqlMigrationPlanOperationStep[] = runPlans.map((plan) => ({
123
+ description: `Run ${name}`,
124
+ sql: plan.sql,
125
+ params: plan.params,
126
+ }));
127
+
128
+ const postcheck: readonly SqlMigrationPlanOperationStep[] = checkPlan
129
+ ? [
130
+ {
131
+ description: `Verify ${name} resolved all violations`,
132
+ sql: `SELECT NOT EXISTS (${checkPlan.sql}) AS ok`,
133
+ params: checkPlan.params,
134
+ },
135
+ ]
136
+ : [];
137
+
74
138
  return {
75
139
  id: `data_migration.${name}`,
76
140
  label: `Data transform: ${name}`,
77
141
  operationClass: 'data',
78
- name,
79
- source: 'migration.ts',
80
- check: options.check ? invokeAndLower(options.check, contract, adapter, name) : null,
81
- run: runClosures.map((closure) => invokeAndLower(closure, contract, adapter, name)),
142
+ ...ifDefined('invariantId', options.invariantId),
143
+ target: { id: 'postgres' },
144
+ precheck,
145
+ execute,
146
+ postcheck,
82
147
  };
83
148
  }
84
149
 
@@ -18,6 +18,58 @@ export function createExtension(extensionName: string): Op {
18
18
  };
19
19
  }
20
20
 
21
+ /**
22
+ * Install a Postgres extension as the baseline op for an extension-pack
23
+ * contract space. Layered on top of {@link createExtension}: stamps an
24
+ * `invariantId` (required so the per-space marker records the install),
25
+ * scopes the op `id` under a caller-chosen namespace (e.g. `pgvector.`),
26
+ * and emits pre- and postcheck SQL probing `pg_extension`. The richer
27
+ * shape lets the runner's idempotency probe skip the install on re-run
28
+ * (postcheck-pre-satisfied) without firing the precheck.
29
+ *
30
+ * Use this for hand-rolled baseline migrations in contract-space
31
+ * extension packages (e.g. `extension-pgvector`, `extension-paradedb`);
32
+ * use the bare {@link createExtension} for planner-emitted ops where the
33
+ * caller already controls idempotency through the surrounding plan.
34
+ */
35
+ export function installExtension(options: {
36
+ readonly extensionName: string;
37
+ readonly invariantId: string;
38
+ readonly id: string;
39
+ readonly label?: string;
40
+ }): Op {
41
+ const { extensionName, invariantId, id } = options;
42
+ const label = options.label ?? `Enable extension "${extensionName}"`;
43
+ return {
44
+ id,
45
+ label,
46
+ operationClass: 'additive',
47
+ invariantId,
48
+ target: {
49
+ id: 'postgres',
50
+ details: { schema: 'public', objectType: 'dependency', name: extensionName },
51
+ },
52
+ precheck: [
53
+ step(
54
+ `verify extension "${extensionName}" is not already enabled`,
55
+ `SELECT NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = '${extensionName}')`,
56
+ ),
57
+ ],
58
+ execute: [
59
+ step(
60
+ `create extension "${extensionName}"`,
61
+ `CREATE EXTENSION IF NOT EXISTS ${extensionName}`,
62
+ ),
63
+ ],
64
+ postcheck: [
65
+ step(
66
+ `confirm extension "${extensionName}" is enabled`,
67
+ `SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = '${extensionName}')`,
68
+ ),
69
+ ],
70
+ };
71
+ }
72
+
21
73
  export function createSchema(schemaName: string): Op {
22
74
  return {
23
75
  id: `schema.${schemaName}`,
@@ -1,15 +1,40 @@
1
- import { quoteIdentifier } from '../../sql-utils';
1
+ import { escapeLiteral, quoteIdentifier } from '../../sql-utils';
2
2
  import { qualifyTableName, toRegclassLiteral } from '../planner-sql-checks';
3
3
  import { type Op, step, targetDetails } from './shared';
4
4
 
5
+ export interface CreateIndexExtras {
6
+ readonly type?: string;
7
+ readonly options?: Record<string, unknown>;
8
+ }
9
+
10
+ function renderIndexOptionValue(key: string, value: unknown): string {
11
+ if (typeof value === 'string') return `'${escapeLiteral(value)}'`;
12
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
13
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
14
+ throw new Error(
15
+ `Index option "${key}" must be a string, finite number, or boolean; got ${typeof value}`,
16
+ );
17
+ }
18
+
19
+ function renderIndexOptions(options: Record<string, unknown>): string {
20
+ return Object.entries(options)
21
+ .map(([key, value]) => `${quoteIdentifier(key)} = ${renderIndexOptionValue(key, value)}`)
22
+ .join(', ');
23
+ }
24
+
5
25
  export function createIndex(
6
26
  schemaName: string,
7
27
  tableName: string,
8
28
  indexName: string,
9
29
  columns: readonly string[],
30
+ extras?: CreateIndexExtras,
10
31
  ): Op {
11
32
  const qualified = qualifyTableName(schemaName, tableName);
12
33
  const columnList = columns.map(quoteIdentifier).join(', ');
34
+ const using = extras?.type ? ` USING ${quoteIdentifier(extras.type)}` : '';
35
+ const options = extras?.options;
36
+ const withClause =
37
+ options && Object.keys(options).length > 0 ? ` WITH (${renderIndexOptions(options)})` : '';
13
38
  return {
14
39
  id: `index.${tableName}.${indexName}`,
15
40
  label: `Create index "${indexName}" on "${tableName}"`,
@@ -24,7 +49,7 @@ export function createIndex(
24
49
  execute: [
25
50
  step(
26
51
  `create index "${indexName}"`,
27
- `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualified} (${columnList})`,
52
+ `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualified}${using} (${columnList})${withClause}`,
28
53
  ),
29
54
  ],
30
55
  postcheck: [
@@ -24,10 +24,12 @@
24
24
  */
25
25
 
26
26
  import type { SqlMigrationPlanOperation } from '@prisma-next/family-sql/control';
27
- import type { MigrationPlanWithAuthoringSurface } from '@prisma-next/framework-components/control';
27
+ import type {
28
+ MigrationPlanWithAuthoringSurface,
29
+ OpFactoryCall,
30
+ } from '@prisma-next/framework-components/control';
28
31
  import type { MigrationMeta } from '@prisma-next/migration-tools/migration';
29
32
  import { ifDefined } from '@prisma-next/utils/defined';
30
- import type { PostgresOpFactoryCall } from './op-factory-call';
31
33
  import type { PostgresPlanTargetDetails } from './planner-target-details';
32
34
  import { PostgresMigration } from './postgres-migration';
33
35
  import { renderOps } from './render-ops';
@@ -39,13 +41,15 @@ export class TypeScriptRenderablePostgresMigration
39
41
  extends PostgresMigration
40
42
  implements MigrationPlanWithAuthoringSurface
41
43
  {
42
- readonly #calls: readonly PostgresOpFactoryCall[];
44
+ readonly #calls: readonly OpFactoryCall[];
43
45
  readonly #meta: MigrationMeta;
46
+ readonly #spaceId: string;
44
47
 
45
- constructor(calls: readonly PostgresOpFactoryCall[], meta: MigrationMeta) {
48
+ constructor(calls: readonly OpFactoryCall[], meta: MigrationMeta, spaceId: string) {
46
49
  super();
47
50
  this.#calls = calls;
48
51
  this.#meta = meta;
52
+ this.#spaceId = spaceId;
49
53
  }
50
54
 
51
55
  override get operations(): readonly Op[] {
@@ -56,11 +60,19 @@ export class TypeScriptRenderablePostgresMigration
56
60
  return this.#meta;
57
61
  }
58
62
 
63
+ /**
64
+ * Contract space this planner-produced plan applies to. Threaded
65
+ * from the planner options so the runner keys the marker row by
66
+ * the right space when executing the plan.
67
+ */
68
+ get spaceId(): string {
69
+ return this.#spaceId;
70
+ }
71
+
59
72
  renderTypeScript(): string {
60
73
  return renderCallsToTypeScript(this.#calls, {
61
74
  from: this.#meta.from,
62
75
  to: this.#meta.to,
63
- ...ifDefined('kind', this.#meta.kind),
64
76
  ...ifDefined('labels', this.#meta.labels),
65
77
  });
66
78
  }
@@ -14,19 +14,16 @@
14
14
  * nullability tightening, unsafe type changes, enum shrink/rebuild) emit
15
15
  * `DataTransformCall` placeholders that the user fills in.
16
16
  * - When `'data'` is excluded, those strategies short-circuit so the
17
- * downstream walk-schema strategies (codec-hook type ops, dependency
18
- * installs, temp-default backfill) and `mapIssueToCall` defaults emit
19
- * direct DDL instead.
17
+ * downstream walk-schema strategies (codec-hook type ops and temp-default
18
+ * backfill) and `mapIssueToCall` defaults emit direct DDL instead.
20
19
  */
21
20
 
22
21
  import type { Contract } from '@prisma-next/contract/types';
23
22
  import type {
24
23
  CodecControlHooks,
25
- ComponentDatabaseDependency,
26
24
  MigrationOperationPolicy,
27
25
  SqlMigrationPlanOperation,
28
26
  } from '@prisma-next/family-sql/control';
29
- import { collectInitDependencies } from '@prisma-next/family-sql/control';
30
27
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
31
28
  import type { SchemaIssue } from '@prisma-next/framework-components/control';
32
29
  import type { SqlStorage, StorageTypeInstance } from '@prisma-next/sql-contract/types';
@@ -428,47 +425,6 @@ export const storageTypePlanCallStrategy: CallMigrationStrategy = (issues, ctx)
428
425
  return { kind: 'match', issues: remaining, calls };
429
426
  };
430
427
 
431
- /**
432
- * Dispatches component-declared database dependencies. Replaces the
433
- * walk-schema `buildDatabaseDependencyOperations` path. Rather than consuming
434
- * `dependency_missing` issues (which only carry the id), this strategy
435
- * re-invokes `collectInitDependencies(frameworkComponents)` at plan time so
436
- * the handler has access to the structured `install` ops each component
437
- * declared — including arbitrary SQL launders — and dedupes by dependency id
438
- * plus per-op id.
439
- */
440
- export const dependencyInstallCallStrategy: CallMigrationStrategy = (issues, ctx) => {
441
- const installedIds = new Set(ctx.schema.dependencies.map((d) => d.id));
442
- const dependencies = sortDependencies(
443
- collectInitDependencies(ctx.frameworkComponents).filter(isPostgresPlannerDependency),
444
- );
445
-
446
- const calls: PostgresOpFactoryCall[] = [];
447
- const handledDependencyIds = new Set<string>();
448
- const seenOperationIds = new Set<string>();
449
-
450
- for (const dep of dependencies) {
451
- handledDependencyIds.add(dep.id);
452
- if (installedIds.has(dep.id)) continue;
453
- for (const installOp of dep.install) {
454
- if (seenOperationIds.has(installOp.id)) continue;
455
- seenOperationIds.add(installOp.id);
456
- calls.push(liftInstallOpToCall(installOp));
457
- }
458
- }
459
-
460
- // Consume ALL `dependency_missing` issues — even non-postgres ones. The
461
- // walk-schema predecessor silently skipped non-postgres deps; leaving those
462
- // issues in the stream would let `mapIssueToCall` reject them as
463
- // "Unknown dependency type".
464
- const remaining = issues.filter((issue) => issue.kind !== 'dependency_missing');
465
-
466
- if (calls.length === 0 && remaining.length === issues.length) {
467
- return { kind: 'no_match' };
468
- }
469
- return { kind: 'match', issues: remaining, calls };
470
- };
471
-
472
428
  /**
473
429
  * Handles `missing_column` issues for NOT NULL columns without a contract
474
430
  * default. Replaces the walk-schema `buildAddColumnItem` non-default branches.
@@ -633,43 +589,6 @@ function canUseSharedTemporaryDefaultStrategy(options: {
633
589
  return true;
634
590
  }
635
591
 
636
- type PlannerDatabaseDependency = ComponentDatabaseDependency<unknown> & {
637
- readonly install: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
638
- };
639
-
640
- function isPostgresPlannerDependency(
641
- dependency: ComponentDatabaseDependency<unknown>,
642
- ): dependency is PlannerDatabaseDependency {
643
- return dependency.install.every((operation) => operation.target.id === 'postgres');
644
- }
645
-
646
- function sortDependencies(
647
- dependencies: ReadonlyArray<PlannerDatabaseDependency>,
648
- ): ReadonlyArray<PlannerDatabaseDependency> {
649
- return [...dependencies].sort((a, b) => a.id.localeCompare(b.id));
650
- }
651
-
652
- /**
653
- * Lift a component install op into migration IR. Structured shapes — extension
654
- * and schema installs with predictable SQL — collapse to typed `*Call`
655
- * subclasses so the scaffolded migration authoring surface stays readable.
656
- * Everything else (arbitrary SQL) falls through to `RawSqlCall` as an escape
657
- * hatch.
658
- */
659
- /**
660
- * Component-declared install ops are wrapped as `RawSqlCall` so the
661
- * component's original `label`, `precheck`, `execute`, `postcheck`, and op
662
- * id are preserved verbatim. Structured conversion (to e.g.
663
- * `CreateExtensionCall`) would drop the precheck/postcheck pair and
664
- * change the DDL label, breaking walk-schema output parity. Classification
665
- * as `'dep'` happens in `classifyCall` via the underlying op's id prefix.
666
- */
667
- function liftInstallOpToCall(
668
- op: SqlMigrationPlanOperation<PostgresPlanTargetDetails>,
669
- ): PostgresOpFactoryCall {
670
- return new RawSqlCall(op);
671
- }
672
-
673
592
  /**
674
593
  * Ordered list of Postgres planner strategies, shared by `migration plan`
675
594
  * and `db update` / `db init`. The issue planner runs each strategy in
@@ -685,8 +604,8 @@ function liftInstallOpToCall(
685
604
  * - When `'data'` is not allowed (`db update` / `db init`), each data-safe
686
605
  * strategy short-circuits to `no_match`, leaving the issue for the
687
606
  * downstream walk-schema strategies (`storageTypePlanCallStrategy`,
688
- * `dependencyInstallCallStrategy`, `notNullAddColumnCallStrategy`) or the
689
- * `mapIssueToCall` default to handle with direct DDL.
607
+ * `notNullAddColumnCallStrategy`) or the `mapIssueToCall` default to handle
608
+ * with direct DDL.
690
609
  *
691
610
  * Order matters: data-safe strategies must run before the walk-schema
692
611
  * strategies on overlapping issue kinds (e.g. `enum_values_changed`,
@@ -698,6 +617,5 @@ export const postgresPlannerStrategies: readonly CallMigrationStrategy[] = [
698
617
  typeChangeCallStrategy,
699
618
  nullableTighteningCallStrategy,
700
619
  storageTypePlanCallStrategy,
701
- dependencyInstallCallStrategy,
702
620
  notNullAddColumnCallStrategy,
703
621
  ];