@prisma-next/target-postgres 0.5.0-dev.6 → 0.5.0-dev.61

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 (147) hide show
  1. package/dist/codec-ids-B-wNPs-9.d.mts +29 -0
  2. package/dist/codec-ids-B-wNPs-9.d.mts.map +1 -0
  3. package/dist/{codec-ids-CojIXVf9.mjs → codec-ids-ckQX9Kcg.mjs} +3 -2
  4. package/dist/{codec-ids-CojIXVf9.mjs.map → codec-ids-ckQX9Kcg.mjs.map} +1 -1
  5. package/dist/codec-ids.d.mts +2 -28
  6. package/dist/codec-ids.mjs +2 -2
  7. package/dist/codec-types-Br-rrBBQ.d.mts +80 -0
  8. package/dist/codec-types-Br-rrBBQ.d.mts.map +1 -0
  9. package/dist/codec-types.d.mts +4 -42
  10. package/dist/codec-types.mjs +1 -3
  11. package/dist/codecs-DZUnQrrl.d.mts +559 -0
  12. package/dist/codecs-DZUnQrrl.d.mts.map +1 -0
  13. package/dist/codecs.d.mts +15 -2
  14. package/dist/codecs.d.mts.map +1 -0
  15. package/dist/codecs.mjs +742 -2
  16. package/dist/codecs.mjs.map +1 -0
  17. package/dist/control.d.mts +1 -1
  18. package/dist/control.mjs +26 -68
  19. package/dist/control.mjs.map +1 -1
  20. package/dist/{data-transform-VfEGzXWt.mjs → data-transform-CR_9PvW9.mjs} +24 -5
  21. package/dist/data-transform-CR_9PvW9.mjs.map +1 -0
  22. package/dist/data-transform-T71mQkVW.d.mts +39 -0
  23. package/dist/data-transform-T71mQkVW.d.mts.map +1 -0
  24. package/dist/data-transform.d.mts +1 -1
  25. package/dist/data-transform.mjs +1 -1
  26. package/dist/{default-normalizer-DNOpRoOF.mjs → default-normalizer-D4RoM0i6.mjs} +1 -1
  27. package/dist/{default-normalizer-DNOpRoOF.mjs.map → default-normalizer-D4RoM0i6.mjs.map} +1 -1
  28. package/dist/default-normalizer.mjs +1 -1
  29. package/dist/{descriptor-meta-BVoVtyp-.mjs → descriptor-meta-B9JFfKCb.mjs} +7 -12
  30. package/dist/descriptor-meta-B9JFfKCb.mjs.map +1 -0
  31. package/dist/{errors-AFvEPZ1R.mjs → errors-BT_Duyj-.mjs} +1 -1
  32. package/dist/{errors-AFvEPZ1R.mjs.map → errors-BT_Duyj-.mjs.map} +1 -1
  33. package/dist/errors.mjs +1 -1
  34. package/dist/{issue-planner-CFjB0_oO.mjs → issue-planner-DooWabc2.mjs} +8 -8
  35. package/dist/{issue-planner-CFjB0_oO.mjs.map → issue-planner-DooWabc2.mjs.map} +1 -1
  36. package/dist/issue-planner.d.mts +3 -3
  37. package/dist/issue-planner.mjs +1 -1
  38. package/dist/migration.d.mts +3 -3
  39. package/dist/migration.mjs +3 -3
  40. package/dist/{native-type-normalizer-CInai_oY.mjs → native-type-normalizer-i4IFPL5F.mjs} +1 -1
  41. package/dist/{native-type-normalizer-CInai_oY.mjs.map → native-type-normalizer-i4IFPL5F.mjs.map} +1 -1
  42. package/dist/native-type-normalizer.mjs +1 -1
  43. package/dist/{op-factory-call-BKlruaiC.mjs → op-factory-call-Bvw39XKU.mjs} +2 -2
  44. package/dist/{op-factory-call-BKlruaiC.mjs.map → op-factory-call-Bvw39XKU.mjs.map} +1 -1
  45. package/dist/{op-factory-call-C3bWXKSP.d.mts → op-factory-call-SFMIf-Cz.d.mts} +3 -3
  46. package/dist/{op-factory-call-C3bWXKSP.d.mts.map → op-factory-call-SFMIf-Cz.d.mts.map} +1 -1
  47. package/dist/op-factory-call.d.mts +2 -2
  48. package/dist/op-factory-call.mjs +1 -1
  49. package/dist/pack.d.mts +30 -9
  50. package/dist/pack.d.mts.map +1 -1
  51. package/dist/pack.mjs +1 -1
  52. package/dist/{planner-CLUvVhUN.mjs → planner-BMtFbKfn.mjs} +9 -9
  53. package/dist/planner-BMtFbKfn.mjs.map +1 -0
  54. package/dist/{planner-ddl-builders-Dxvw1LHw.mjs → planner-ddl-builders-B6VK92UF.mjs} +3 -3
  55. package/dist/{planner-ddl-builders-Dxvw1LHw.mjs.map → planner-ddl-builders-B6VK92UF.mjs.map} +1 -1
  56. package/dist/planner-ddl-builders.d.mts +1 -1
  57. package/dist/planner-ddl-builders.mjs +1 -1
  58. package/dist/{planner-identity-values-Dju-o5GF.mjs → planner-identity-values-CC5fa5D9.mjs} +1 -1
  59. package/dist/{planner-identity-values-Dju-o5GF.mjs.map → planner-identity-values-CC5fa5D9.mjs.map} +1 -1
  60. package/dist/planner-identity-values.mjs +1 -1
  61. package/dist/{planner-produced-postgres-migration-DSSPq8QS.mjs → planner-produced-postgres-migration-C0vaAvA8.mjs} +4 -5
  62. package/dist/{planner-produced-postgres-migration-DSSPq8QS.mjs.map → planner-produced-postgres-migration-C0vaAvA8.mjs.map} +1 -1
  63. package/dist/{planner-produced-postgres-migration-CRRTno6Z.d.mts → planner-produced-postgres-migration-CyzRgqsq.d.mts} +4 -4
  64. package/dist/planner-produced-postgres-migration-CyzRgqsq.d.mts.map +1 -0
  65. package/dist/planner-produced-postgres-migration.d.mts +5 -4
  66. package/dist/planner-produced-postgres-migration.mjs +1 -1
  67. package/dist/{planner-schema-lookup-B7lkypwn.mjs → planner-schema-lookup-B3talum5.mjs} +1 -1
  68. package/dist/{planner-schema-lookup-B7lkypwn.mjs.map → planner-schema-lookup-B3talum5.mjs.map} +1 -1
  69. package/dist/planner-schema-lookup.mjs +1 -1
  70. package/dist/{planner-sql-checks-7jkgm9TX.mjs → planner-sql-checks-uDnwA68k.mjs} +2 -2
  71. package/dist/{planner-sql-checks-7jkgm9TX.mjs.map → planner-sql-checks-uDnwA68k.mjs.map} +1 -1
  72. package/dist/planner-sql-checks.mjs +1 -1
  73. package/dist/{planner-target-details-DH-azLu-.d.mts → planner-target-details-COAiKZjW.d.mts} +1 -1
  74. package/dist/{planner-target-details-DH-azLu-.d.mts.map → planner-target-details-COAiKZjW.d.mts.map} +1 -1
  75. package/dist/planner-target-details.d.mts +1 -1
  76. package/dist/planner.d.mts +19 -12
  77. package/dist/planner.d.mts.map +1 -1
  78. package/dist/planner.mjs +2 -2
  79. package/dist/{postgres-migration-qtmtbONe.mjs → postgres-migration-BAgHXrjO.mjs} +3 -3
  80. package/dist/postgres-migration-BAgHXrjO.mjs.map +1 -0
  81. package/dist/{postgres-migration-BjA3Zmts.d.mts → postgres-migration-Dzxr5BCy.d.mts} +5 -4
  82. package/dist/postgres-migration-Dzxr5BCy.d.mts.map +1 -0
  83. package/dist/{render-ops-D6_DHdOK.mjs → render-ops-DddkYOIB.mjs} +1 -1
  84. package/dist/{render-ops-D6_DHdOK.mjs.map → render-ops-DddkYOIB.mjs.map} +1 -1
  85. package/dist/render-ops.d.mts +3 -3
  86. package/dist/render-ops.mjs +1 -1
  87. package/dist/{render-typescript-1rF_SB4g.mjs → render-typescript-0EtwW-Ip.mjs} +1 -2
  88. package/dist/render-typescript-0EtwW-Ip.mjs.map +1 -0
  89. package/dist/render-typescript.d.mts +3 -4
  90. package/dist/render-typescript.d.mts.map +1 -1
  91. package/dist/render-typescript.mjs +1 -1
  92. package/dist/runtime.d.mts +5 -9
  93. package/dist/runtime.d.mts.map +1 -1
  94. package/dist/runtime.mjs +5 -10
  95. package/dist/runtime.mjs.map +1 -1
  96. package/dist/{shared-Bxkt8pNO.d.mts → shared-DSVRy4AX.d.mts} +2 -2
  97. package/dist/{shared-Bxkt8pNO.d.mts.map → shared-DSVRy4AX.d.mts.map} +1 -1
  98. package/dist/{sql-utils-r-Lw535w.mjs → sql-utils-C9dyHV0x.mjs} +1 -1
  99. package/dist/{sql-utils-r-Lw535w.mjs.map → sql-utils-C9dyHV0x.mjs.map} +1 -1
  100. package/dist/sql-utils.mjs +1 -1
  101. package/dist/{statement-builders-BPnmt6wx.mjs → statement-builders-Ckkq4ryf.mjs} +13 -8
  102. package/dist/statement-builders-Ckkq4ryf.mjs.map +1 -0
  103. package/dist/statement-builders.d.mts +10 -3
  104. package/dist/statement-builders.d.mts.map +1 -1
  105. package/dist/statement-builders.mjs +2 -2
  106. package/dist/{tables-BmdW_FWO.mjs → tables-CnvPb0Iz.mjs} +3 -3
  107. package/dist/{tables-BmdW_FWO.mjs.map → tables-CnvPb0Iz.mjs.map} +1 -1
  108. package/dist/{types-ClK03Ojd.d.mts → types-DWZq_XTl.d.mts} +1 -1
  109. package/dist/types-DWZq_XTl.d.mts.map +1 -0
  110. package/dist/types.d.mts +1 -1
  111. package/package.json +17 -15
  112. package/src/core/authoring.ts +5 -11
  113. package/src/core/codec-helpers.ts +135 -0
  114. package/src/core/codec-ids.ts +1 -0
  115. package/src/core/codec-type-map.ts +81 -0
  116. package/src/core/codecs.ts +941 -547
  117. package/src/core/descriptor-meta.ts +1 -1
  118. package/src/core/migrations/operations/data-transform.ts +86 -21
  119. package/src/core/migrations/planner-produced-postgres-migration.ts +0 -1
  120. package/src/core/migrations/planner.ts +17 -11
  121. package/src/core/migrations/postgres-migration.ts +3 -6
  122. package/src/core/migrations/render-typescript.ts +1 -5
  123. package/src/core/migrations/runner.ts +43 -112
  124. package/src/core/migrations/statement-builders.ts +22 -6
  125. package/src/core/registry.ts +11 -0
  126. package/src/exports/codec-types.ts +4 -13
  127. package/src/exports/codecs.ts +49 -2
  128. package/src/exports/runtime.ts +6 -11
  129. package/src/exports/statement-builders.ts +1 -1
  130. package/dist/codec-ids.d.mts.map +0 -1
  131. package/dist/codec-types.d.mts.map +0 -1
  132. package/dist/codecs-BoahtY_Q.mjs +0 -385
  133. package/dist/codecs-BoahtY_Q.mjs.map +0 -1
  134. package/dist/codecs-D-F2KJqt.d.mts +0 -299
  135. package/dist/codecs-D-F2KJqt.d.mts.map +0 -1
  136. package/dist/data-transform-CxFRBIUp.d.mts +0 -32
  137. package/dist/data-transform-CxFRBIUp.d.mts.map +0 -1
  138. package/dist/data-transform-VfEGzXWt.mjs.map +0 -1
  139. package/dist/descriptor-meta-BVoVtyp-.mjs.map +0 -1
  140. package/dist/planner-CLUvVhUN.mjs.map +0 -1
  141. package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts.map +0 -1
  142. package/dist/postgres-migration-BjA3Zmts.d.mts.map +0 -1
  143. package/dist/postgres-migration-qtmtbONe.mjs.map +0 -1
  144. package/dist/render-typescript-1rF_SB4g.mjs.map +0 -1
  145. package/dist/statement-builders-BPnmt6wx.mjs.map +0 -1
  146. package/dist/types-ClK03Ojd.d.mts.map +0 -1
  147. 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',
@@ -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
 
@@ -60,7 +60,6 @@ export class TypeScriptRenderablePostgresMigration
60
60
  return renderCallsToTypeScript(this.#calls, {
61
61
  from: this.#meta.from,
62
62
  to: this.#meta.to,
63
- ...ifDefined('kind', this.#meta.kind),
64
63
  ...ifDefined('labels', this.#meta.labels),
65
64
  });
66
65
  }
@@ -1,3 +1,4 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
1
2
  import type {
2
3
  MigrationOperationPolicy,
3
4
  SqlMigrationPlannerPlanOptions,
@@ -83,20 +84,25 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
83
84
  readonly contract: unknown;
84
85
  readonly schema: unknown;
85
86
  readonly policy: MigrationOperationPolicy;
86
- readonly fromHash?: string;
87
87
  /**
88
88
  * The "from" contract (state the planner assumes the database starts
89
- * at). Only `migration plan` supplies this; `db update` / `db init`
90
- * reconcile against the live schema with no old contract. When present
91
- * alongside the `'data'` operation class, strategies that need from/to
92
- * column shape comparisons (unsafe type change, nullability tightening)
93
- * activate.
89
+ * at), or `null` for reconciliation flows. Only `migration plan` ever
90
+ * supplies a non-null value; `db update` / `db init` reconcile against
91
+ * the live schema and pass `null`. When present alongside the
92
+ * `'data'` operation class, strategies that need from/to column-shape
93
+ * comparisons (unsafe type change, nullability tightening) activate.
94
+ *
95
+ * Typed as the framework `Contract | null` to satisfy the
96
+ * `MigrationPlanner` interface contract; `planSql` narrows to the SQL
97
+ * shape via `SqlMigrationPlannerPlanOptions`. Used to populate
98
+ * `describe().from` on the produced plan as
99
+ * `fromContract?.storage.storageHash ?? null`.
94
100
  */
95
- readonly fromContract?: unknown;
101
+ readonly fromContract: Contract | null;
96
102
  readonly schemaName?: string;
97
103
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
98
104
  }): PostgresPlanResult {
99
- return this.planSql(options as SqlMigrationPlannerPlanOptions, options.fromHash ?? '');
105
+ return this.planSql(options as SqlMigrationPlannerPlanOptions);
100
106
  }
101
107
 
102
108
  emptyMigration(context: MigrationScaffoldContext): MigrationPlanWithAuthoringSurface {
@@ -106,7 +112,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
106
112
  });
107
113
  }
108
114
 
109
- private planSql(options: SqlMigrationPlannerPlanOptions, fromHash: string): PostgresPlanResult {
115
+ private planSql(options: SqlMigrationPlannerPlanOptions): PostgresPlanResult {
110
116
  const schemaName = options.schemaName ?? this.config.defaultSchema;
111
117
  const policyResult = this.ensureAdditivePolicy(options.policy);
112
118
  if (policyResult) {
@@ -125,7 +131,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
125
131
  // from/to comparisons (unsafe type change, nullable tightening) are
126
132
  // inapplicable there — reconciliation falls through to
127
133
  // `mapIssueToCall`'s direct destructive handlers.
128
- fromContract: options.fromContract ?? null,
134
+ fromContract: options.fromContract,
129
135
  schemaName,
130
136
  codecHooks,
131
137
  storageTypes,
@@ -142,7 +148,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
142
148
  return Object.freeze({
143
149
  kind: 'success' as const,
144
150
  plan: new TypeScriptRenderablePostgresMigration(result.value.calls, {
145
- from: fromHash,
151
+ from: options.fromContract?.storage.storageHash ?? null,
146
152
  to: options.contract.storage.storageHash,
147
153
  }),
148
154
  });
@@ -1,14 +1,11 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
+ import type { SqlMigrationPlanOperation } from '@prisma-next/family-sql/control';
2
3
  import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
3
4
  import { Migration as SqlMigration } from '@prisma-next/family-sql/migration';
4
5
  import type { ControlStack } from '@prisma-next/framework-components/control';
5
6
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
6
7
  import { errorPostgresMigrationStackMissing } from '../errors';
7
- import {
8
- type DataTransformOptions,
9
- dataTransform,
10
- type PostgresDataTransformOperation,
11
- } from './operations/data-transform';
8
+ import { type DataTransformOptions, dataTransform } from './operations/data-transform';
12
9
  import type { PostgresPlanTargetDetails } from './planner-target-details';
13
10
 
14
11
  /**
@@ -64,7 +61,7 @@ export abstract class PostgresMigration extends SqlMigration<
64
61
  contract: TContract,
65
62
  name: string,
66
63
  options: DataTransformOptions,
67
- ): PostgresDataTransformOperation {
64
+ ): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
68
65
  if (!this.controlAdapter) {
69
66
  throw errorPostgresMigrationStackMissing();
70
67
  }
@@ -14,9 +14,8 @@ import { type ImportRequirement, jsonToTsSource, renderImports } from '@prisma-n
14
14
  import type { PostgresOpFactoryCall } from './op-factory-call';
15
15
 
16
16
  export interface RenderMigrationMeta {
17
- readonly from: string;
17
+ readonly from: string | null;
18
18
  readonly to: string;
19
- readonly kind?: string;
20
19
  readonly labels?: readonly string[];
21
20
  }
22
21
 
@@ -84,9 +83,6 @@ function buildDescribeMethod(meta: RenderMigrationMeta): string {
84
83
  lines.push(' return {');
85
84
  lines.push(` from: ${JSON.stringify(meta.from)},`);
86
85
  lines.push(` to: ${JSON.stringify(meta.to)},`);
87
- if (meta.kind) {
88
- lines.push(` kind: ${JSON.stringify(meta.kind)},`);
89
- }
90
86
  if (meta.labels && meta.labels.length > 0) {
91
87
  lines.push(` labels: ${jsonToTsSource(meta.labels)},`);
92
88
  }
@@ -12,8 +12,6 @@ import type {
12
12
  } from '@prisma-next/family-sql/control';
13
13
  import { runnerFailure, runnerSuccess } from '@prisma-next/family-sql/control';
14
14
  import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
15
- import { readMarker } from '@prisma-next/family-sql/verify';
16
- import type { DataTransformOperation } from '@prisma-next/framework-components/control';
17
15
  import { SqlQueryError } from '@prisma-next/sql-errors';
18
16
  import { ifDefined } from '@prisma-next/utils/defined';
19
17
  import type { Result } from '@prisma-next/utils/result';
@@ -23,7 +21,7 @@ import { normalizeSchemaNativeType } from '../native-type-normalizer';
23
21
  import type { PostgresPlanTargetDetails } from './planner-target-details';
24
22
  import {
25
23
  buildLedgerInsertStatement,
26
- buildWriteMarkerStatements,
24
+ buildMergeMarkerStatements,
27
25
  ensureLedgerTableStatement,
28
26
  ensureMarkerTableStatement,
29
27
  ensurePrismaContractSchemaStatement,
@@ -45,18 +43,6 @@ const DEFAULT_CONFIG: RunnerConfig = {
45
43
 
46
44
  const LOCK_DOMAIN = 'prisma_next.contract.marker';
47
45
 
48
- function isDataTransformOperation(op: unknown): op is DataTransformOperation {
49
- return (
50
- typeof op === 'object' &&
51
- op !== null &&
52
- 'operationClass' in op &&
53
- (op as { operationClass: string }).operationClass === 'data' &&
54
- 'name' in op &&
55
- 'check' in op &&
56
- 'run' in op
57
- );
58
- }
59
-
60
46
  /**
61
47
  * Deep clones and freezes a record object to prevent mutation.
62
48
  * Recursively clones nested objects and arrays to ensure complete isolation.
@@ -120,7 +106,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
120
106
  try {
121
107
  await this.acquireLock(driver, lockKey);
122
108
  await this.ensureControlTables(driver);
123
- const existingMarker = await readMarker(driver);
109
+ const existingMarker = await this.family.readMarker({ driver });
124
110
 
125
111
  // Validate plan origin matches existing marker (needs marker from DB)
126
112
  const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
@@ -128,9 +114,14 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
128
114
  return markerCheck;
129
115
  }
130
116
 
131
- // db update (origin: null) always applies; migration-apply (origin set) skips if marker matches.
117
+ // db update (origin: null) always applies; migration-apply (origin set,
118
+ // origin !== destination) skips if marker already matches destination.
119
+ // Self-edges (origin === destination) intentionally bypass the skip:
120
+ // the migration is data-only, and the data transform's own check
121
+ // decides whether `run` fires.
132
122
  const markerAtDestination = this.markerMatchesDestination(existingMarker, options.plan);
133
- const skipOperations = markerAtDestination && options.plan.origin != null;
123
+ const isSelfEdge = options.plan.origin?.storageHash === options.plan.destination.storageHash;
124
+ const skipOperations = markerAtDestination && options.plan.origin != null && !isSelfEdge;
134
125
  let applyValue: ApplyPlanSuccessValue;
135
126
 
136
127
  if (skipOperations) {
@@ -170,9 +161,34 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
170
161
  });
171
162
  }
172
163
 
173
- // Record marker and ledger entries
174
- await this.upsertMarker(driver, options, existingMarker);
175
- await this.recordLedgerEntry(driver, options, existingMarker, applyValue.executedOperations);
164
+ // Self-edge no-op detection: a self-edge migration whose ops all
165
+ // self-skipped (every op pre-satisfied) and that brings no new
166
+ // invariants produced no observable change. Skip the marker +
167
+ // ledger writes so an idempotent re-apply of a self-edge data
168
+ // transform doesn't churn updatedAt or pile up empty ledger
169
+ // entries. With data transforms now flowing through the unified
170
+ // op loop, a pre-satisfied DT lands in the
171
+ // `postcheckAlreadySatisfied` skip path which does not increment
172
+ // `operationsExecuted`, so the check below correctly distinguishes
173
+ // "every op self-skipped" from "the plan had ops that ran". `db
174
+ // update` no-ops still write a ledger entry as audit trail.
175
+ const incomingInvariants = options.plan.providedInvariants ?? [];
176
+ const existingInvariants = new Set(existingMarker?.invariants ?? []);
177
+ const incomingIsSubsetOfExisting = incomingInvariants.every((id) =>
178
+ existingInvariants.has(id),
179
+ );
180
+ const isSelfEdgeNoOp =
181
+ isSelfEdge && applyValue.operationsExecuted === 0 && incomingIsSubsetOfExisting;
182
+
183
+ if (!isSelfEdgeNoOp) {
184
+ await this.upsertMarker(driver, options, existingMarker);
185
+ await this.recordLedgerEntry(
186
+ driver,
187
+ options,
188
+ existingMarker,
189
+ applyValue.executedOperations,
190
+ );
191
+ }
176
192
 
177
193
  await this.commitTransaction(driver);
178
194
  committed = true;
@@ -201,19 +217,6 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
201
217
  for (const operation of options.plan.operations) {
202
218
  options.callbacks?.onOperationStart?.(operation);
203
219
  try {
204
- // Data transform operations have a different execution lifecycle
205
- if (operation.operationClass === 'data' && isDataTransformOperation(operation)) {
206
- const dtResult = await this.executeDataTransform(driver, operation, {
207
- runIdempotency,
208
- });
209
- if (!dtResult.ok) {
210
- return dtResult;
211
- }
212
- executedOperations.push(operation);
213
- operationsExecuted += 1;
214
- continue;
215
- }
216
-
217
220
  // Idempotency probe: only run if both postchecks and idempotency checks are enabled
218
221
  if (runPostchecks && runIdempotency) {
219
222
  const postcheckAlreadySatisfied = await this.expectationsAreSatisfied(
@@ -266,80 +269,6 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
266
269
  return ok({ operationsExecuted, executedOperations });
267
270
  }
268
271
 
269
- /**
270
- * Executes a data transform operation with the check → (skip or run) → check lifecycle.
271
- *
272
- * 1. If check is a query AST: render to SQL, execute. Empty result = already applied (skip).
273
- * 2. If check is `true`: always skip. If `false`: always run.
274
- * 3. Execute run ASTs (rendered to SQL) sequentially.
275
- * 4. Re-execute check as post-run validation. If violations remain, fail.
276
- */
277
- private async executeDataTransform(
278
- driver: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>['driver'],
279
- op: DataTransformOperation,
280
- options: { runIdempotency: boolean },
281
- ): Promise<Result<void, SqlMigrationRunnerFailure>> {
282
- // Step 1: Check (skip guard)
283
- if (op.check === true) {
284
- // Always skip, regardless of idempotency setting
285
- return okVoid();
286
- }
287
- if (options.runIdempotency && op.check !== null && op.check !== false) {
288
- const checkResult = await driver.query(op.check.sql, op.check.params);
289
- if (checkResult.rows.length === 0) {
290
- // No violations — already applied, skip
291
- return okVoid();
292
- }
293
- }
294
-
295
- // Step 2: Execute run steps
296
- if (op.run) {
297
- for (const plan of op.run) {
298
- try {
299
- await driver.query(plan.sql, plan.params);
300
- } catch (error: unknown) {
301
- if (SqlQueryError.is(error)) {
302
- return runnerFailure(
303
- 'EXECUTION_FAILED',
304
- `Data transform "${op.name}" failed: ${error.message}`,
305
- {
306
- why: error.message,
307
- meta: {
308
- operationId: op.id,
309
- dataTransformName: op.name,
310
- sql: plan.sql,
311
- sqlState: error.sqlState,
312
- },
313
- },
314
- );
315
- }
316
- throw error;
317
- }
318
- }
319
- }
320
-
321
- // Step 3: Post-run validation (check again)
322
- if (op.check !== null && op.check !== false) {
323
- const checkResult = await driver.query(op.check.sql, op.check.params);
324
- if (checkResult.rows.length > 0) {
325
- return runnerFailure(
326
- 'POSTCHECK_FAILED',
327
- `Data transform "${op.name}" did not resolve all violations (${checkResult.rows.length} remaining)`,
328
- {
329
- why: `After executing the data transform, the check query still returns ${checkResult.rows.length} violation(s).`,
330
- meta: {
331
- operationId: op.id,
332
- dataTransformName: op.name,
333
- remainingViolations: checkResult.rows.length,
334
- },
335
- },
336
- );
337
- }
338
- }
339
-
340
- return okVoid();
341
- }
342
-
343
272
  private async ensureControlTables(
344
273
  driver: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>['driver'],
345
274
  ): Promise<void> {
@@ -355,7 +284,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
355
284
  phase: 'precheck' | 'postcheck',
356
285
  ): Promise<Result<void, SqlMigrationRunnerFailure>> {
357
286
  for (const step of steps) {
358
- const result = await driver.query(step.sql);
287
+ const result = await driver.query(step.sql, step.params ?? []);
359
288
  if (!this.stepResultIsTrue(result.rows)) {
360
289
  const code = phase === 'precheck' ? 'PRECHECK_FAILED' : 'POSTCHECK_FAILED';
361
290
  return runnerFailure(
@@ -381,7 +310,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
381
310
  ): Promise<Result<void, SqlMigrationRunnerFailure>> {
382
311
  for (const step of steps) {
383
312
  try {
384
- await driver.query(step.sql);
313
+ await driver.query(step.sql, step.params ?? []);
385
314
  } catch (error: unknown) {
386
315
  // Catch SqlQueryError and include normalized metadata
387
316
  if (SqlQueryError.is(error)) {
@@ -445,7 +374,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
445
374
  return false;
446
375
  }
447
376
  for (const step of steps) {
448
- const result = await driver.query(step.sql);
377
+ const result = await driver.query(step.sql, step.params ?? []);
449
378
  if (!this.stepResultIsTrue(result.rows)) {
450
379
  return false;
451
380
  }
@@ -617,7 +546,8 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
617
546
  options: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>,
618
547
  existingMarker: ContractMarkerRecord | null,
619
548
  ): Promise<void> {
620
- const writeStatements = buildWriteMarkerStatements({
549
+ const incomingInvariants = options.plan.providedInvariants ?? [];
550
+ const writeStatements = buildMergeMarkerStatements({
621
551
  storageHash: options.plan.destination.storageHash,
622
552
  profileHash:
623
553
  options.plan.destination.profileHash ??
@@ -626,6 +556,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
626
556
  contractJson: options.destinationContract,
627
557
  canonicalVersion: null,
628
558
  meta: {},
559
+ invariants: incomingInvariants,
629
560
  });
630
561
  const statement = existingMarker ? writeStatements.update : writeStatements.insert;
631
562
  await this.executeStatement(driver, statement);
@@ -17,7 +17,8 @@ export const ensureMarkerTableStatement: SqlStatement = {
17
17
  canonical_version int,
18
18
  updated_at timestamptz not null default now(),
19
19
  app_tag text,
20
- meta jsonb not null default '{}'
20
+ meta jsonb not null default '{}',
21
+ invariants text[] not null default '{}'
21
22
  )`,
22
23
  params: [],
23
24
  };
@@ -37,16 +38,23 @@ export const ensureLedgerTableStatement: SqlStatement = {
37
38
  params: [],
38
39
  };
39
40
 
40
- export interface WriteMarkerInput {
41
+ export interface MergeMarkerInput {
41
42
  readonly storageHash: string;
42
43
  readonly profileHash: string;
43
44
  readonly contractJson?: unknown;
44
45
  readonly canonicalVersion?: number | null;
45
46
  readonly appTag?: string | null;
46
47
  readonly meta?: Record<string, unknown>;
48
+ /**
49
+ * Invariants to merge into `marker.invariants`. INSERT writes them as
50
+ * the initial value (callers are expected to pass a sorted, deduped
51
+ * array). UPDATE merges them with the existing column server-side via
52
+ * a single atomic SQL expression.
53
+ */
54
+ readonly invariants: readonly string[];
47
55
  }
48
56
 
49
- export function buildWriteMarkerStatements(input: WriteMarkerInput): {
57
+ export function buildMergeMarkerStatements(input: MergeMarkerInput): {
50
58
  readonly insert: SqlStatement;
51
59
  readonly update: SqlStatement;
52
60
  } {
@@ -58,6 +66,7 @@ export function buildWriteMarkerStatements(input: WriteMarkerInput): {
58
66
  input.canonicalVersion ?? null,
59
67
  input.appTag ?? null,
60
68
  jsonParam(input.meta ?? {}),
69
+ input.invariants,
61
70
  ];
62
71
 
63
72
  return {
@@ -70,7 +79,8 @@ export function buildWriteMarkerStatements(input: WriteMarkerInput): {
70
79
  canonical_version,
71
80
  updated_at,
72
81
  app_tag,
73
- meta
82
+ meta,
83
+ invariants
74
84
  ) values (
75
85
  $1,
76
86
  $2,
@@ -79,11 +89,16 @@ export function buildWriteMarkerStatements(input: WriteMarkerInput): {
79
89
  $5,
80
90
  now(),
81
91
  $6,
82
- $7::jsonb
92
+ $7::jsonb,
93
+ $8::text[]
83
94
  )`,
84
95
  params,
85
96
  },
86
97
  update: {
98
+ // `invariants = array(select distinct unnest(invariants || $8::text[]) order by 1)`
99
+ // reads the current column value under the UPDATE's row lock, unions
100
+ // with the incoming array, dedupes, and sorts ascending — single
101
+ // statement, atomic, no read-then-write window.
87
102
  sql: `update prisma_contract.marker set
88
103
  core_hash = $2,
89
104
  profile_hash = $3,
@@ -91,7 +106,8 @@ export function buildWriteMarkerStatements(input: WriteMarkerInput): {
91
106
  canonical_version = $5,
92
107
  updated_at = now(),
93
108
  app_tag = $6,
94
- meta = $7::jsonb
109
+ meta = $7::jsonb,
110
+ invariants = array(select distinct unnest(invariants || $8::text[]) order by 1)
95
111
  where id = $1`,
96
112
  params,
97
113
  },
@@ -0,0 +1,11 @@
1
+ import { buildCodecDescriptorRegistry } from '@prisma-next/sql-relational-core/codec-descriptor-registry';
2
+ import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
3
+ import { codecDescriptors } from './codecs';
4
+
5
+ /**
6
+ * Registry of every codec descriptor shipped by `@prisma-next/target-postgres`.
7
+ *
8
+ * Public consumer surface for the postgres codec set: the postgres adapter and any other consumer that needs to enumerate or look up a postgres codec by id consumes this rather than the raw descriptor array. See ADR 208.
9
+ */
10
+ export const postgresCodecRegistry: CodecDescriptorRegistry =
11
+ buildCodecDescriptorRegistry(codecDescriptors);
@@ -1,26 +1,17 @@
1
1
  /**
2
2
  * Codec type definitions for the Postgres target.
3
3
  *
4
- * This file exports type-only definitions for codec input/output types.
5
- * These types are imported by generated `contract.d.ts` files for compile-time
6
- * type inference.
4
+ * This file is the public origin of `CodecTypes`. The `Resolve<...>` materialisation happens here (rather than in `core/codec-type-map.ts`) so the tsdown DTS bundler resolves consumer-side `.d.mts` references via this public entry point rather than a hash-named internal chunk (the `TS2742` family).
7
5
  *
8
- * Lives in `target-postgres` because codec types describe the target's value
9
- * space - both the control adapter (introspection / schema verification) and
10
- * the runtime adapter (encode/decode) share the same definitions, and the
11
- * target package is the natural home that both adapters depend on.
12
- *
13
- * Runtime codec implementations are provided by the runtime adapter's
14
- * codec registry, which is built from `core/codecs.ts`.
6
+ * Lives in `target-postgres` because codec types describe the target's value space — both the control adapter (introspection / schema verification) and the runtime adapter (encode/decode) share the same definitions, and the target package is the natural home that both adapters depend on.
15
7
  */
16
8
 
17
9
  import type { JsonValue } from '@prisma-next/contract/types';
18
- import type { CodecTypes as CoreCodecTypes } from '../core/codecs';
10
+ import type { ExtractedCodecTypes, Resolve } from '../core/codec-type-map';
19
11
 
20
- export type CodecTypes = CoreCodecTypes;
12
+ export type CodecTypes = Resolve<ExtractedCodecTypes>;
21
13
 
22
14
  export type { JsonValue };
23
- export { dataTypes } from '../core/codecs';
24
15
 
25
16
  type Branded<T, Shape extends Record<string, unknown>> = T & {
26
17
  readonly [K in keyof Shape]: Shape[K];