@prisma-next/target-postgres 0.5.0-dev.9 → 0.5.0

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 (184) 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 +131 -94
  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-CiNmA4ls.mjs} +20 -16
  40. package/dist/issue-planner-CiNmA4ls.mjs.map +1 -0
  41. package/dist/issue-planner.d.mts +2 -4
  42. package/dist/issue-planner.d.mts.map +1 -1
  43. package/dist/issue-planner.mjs +2 -3
  44. package/dist/migration.d.mts +8 -4
  45. package/dist/migration.d.mts.map +1 -1
  46. package/dist/migration.mjs +4 -5
  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-C3bWXKSP.d.mts → op-factory-call-BUhxZuUA.d.mts} +9 -5
  53. package/dist/op-factory-call-BUhxZuUA.d.mts.map +1 -0
  54. package/dist/{op-factory-call-BKlruaiC.mjs → op-factory-call-Bs3HWhvG.mjs} +25 -7
  55. package/dist/op-factory-call-Bs3HWhvG.mjs.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-Lhacw3uU.mjs} +22 -16
  62. package/dist/planner-Lhacw3uU.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-r9Zk1y-Y.mjs} +18 -13
  125. package/dist/tables-r9Zk1y-Y.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 +15 -3
  138. package/src/core/migrations/op-factory-call.ts +24 -2
  139. package/src/core/migrations/operations/data-transform.ts +86 -21
  140. package/src/core/migrations/operations/indexes.ts +27 -2
  141. package/src/core/migrations/planner-produced-postgres-migration.ts +17 -5
  142. package/src/core/migrations/planner.ts +62 -20
  143. package/src/core/migrations/postgres-migration.ts +3 -6
  144. package/src/core/migrations/render-ops.ts +26 -3
  145. package/src/core/migrations/render-typescript.ts +5 -9
  146. package/src/core/migrations/runner.ts +172 -151
  147. package/src/core/migrations/statement-builders.ts +49 -10
  148. package/src/core/registry.ts +11 -0
  149. package/src/exports/codec-types.ts +4 -13
  150. package/src/exports/codecs.ts +49 -2
  151. package/src/exports/runtime.ts +6 -11
  152. package/src/exports/statement-builders.ts +2 -1
  153. package/dist/codec-ids.d.mts.map +0 -1
  154. package/dist/codec-types.d.mts.map +0 -1
  155. package/dist/codecs-BQEm9_oo.d.mts +0 -319
  156. package/dist/codecs-BQEm9_oo.d.mts.map +0 -1
  157. package/dist/codecs-BoahtY_Q.mjs +0 -385
  158. package/dist/codecs-BoahtY_Q.mjs.map +0 -1
  159. package/dist/data-transform-CxFRBIUp.d.mts +0 -32
  160. package/dist/data-transform-CxFRBIUp.d.mts.map +0 -1
  161. package/dist/data-transform-VfEGzXWt.mjs.map +0 -1
  162. package/dist/descriptor-meta-BVoVtyp-.mjs +0 -120
  163. package/dist/descriptor-meta-BVoVtyp-.mjs.map +0 -1
  164. package/dist/issue-planner-CFjB0_oO.mjs.map +0 -1
  165. package/dist/native-type-normalizer-CInai_oY.mjs.map +0 -1
  166. package/dist/op-factory-call-BKlruaiC.mjs.map +0 -1
  167. package/dist/op-factory-call-C3bWXKSP.d.mts.map +0 -1
  168. package/dist/planner-CLUvVhUN.mjs.map +0 -1
  169. package/dist/planner-ddl-builders-Dxvw1LHw.mjs.map +0 -1
  170. package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts.map +0 -1
  171. package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs.map +0 -1
  172. package/dist/planner-sql-checks-7jkgm9TX.mjs.map +0 -1
  173. package/dist/planner-target-details-DH-azLu-.d.mts.map +0 -1
  174. package/dist/postgres-migration-BjA3Zmts.d.mts.map +0 -1
  175. package/dist/postgres-migration-qtmtbONe.mjs.map +0 -1
  176. package/dist/render-ops-D6_DHdOK.mjs +0 -8
  177. package/dist/render-ops-D6_DHdOK.mjs.map +0 -1
  178. package/dist/render-typescript-1rF_SB4g.mjs.map +0 -1
  179. package/dist/shared-Bxkt8pNO.d.mts.map +0 -1
  180. package/dist/sql-utils-r-Lw535w.mjs.map +0 -1
  181. package/dist/statement-builders-BPnmt6wx.mjs.map +0 -1
  182. package/dist/tables-BmdW_FWO.mjs.map +0 -1
  183. package/dist/types-ClK03Ojd.d.mts.map +0 -1
  184. package/src/core/json-schema-type-expression.ts +0 -131
@@ -1,6 +1,7 @@
1
1
  import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
2
  import type {
3
3
  MigrationOperationPolicy,
4
+ MultiSpaceRunnerResult,
4
5
  SqlControlFamilyInstance,
5
6
  SqlMigrationPlanContractInfo,
6
7
  SqlMigrationPlanOperation,
@@ -9,21 +10,22 @@ import type {
9
10
  SqlMigrationRunnerExecuteOptions,
10
11
  SqlMigrationRunnerFailure,
11
12
  SqlMigrationRunnerResult,
13
+ SqlMigrationRunnerSuccessValue,
12
14
  } from '@prisma-next/family-sql/control';
13
15
  import { runnerFailure, runnerSuccess } from '@prisma-next/family-sql/control';
14
16
  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
+ import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
18
+ import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
17
19
  import { SqlQueryError } from '@prisma-next/sql-errors';
18
20
  import { ifDefined } from '@prisma-next/utils/defined';
19
21
  import type { Result } from '@prisma-next/utils/result';
20
- import { ok, okVoid } from '@prisma-next/utils/result';
22
+ import { notOk, ok, okVoid } from '@prisma-next/utils/result';
21
23
  import { parsePostgresDefault } from '../default-normalizer';
22
24
  import { normalizeSchemaNativeType } from '../native-type-normalizer';
23
25
  import type { PostgresPlanTargetDetails } from './planner-target-details';
24
26
  import {
25
27
  buildLedgerInsertStatement,
26
- buildWriteMarkerStatements,
28
+ buildMergeMarkerStatements,
27
29
  ensureLedgerTableStatement,
28
30
  ensureMarkerTableStatement,
29
31
  ensurePrismaContractSchemaStatement,
@@ -45,18 +47,6 @@ const DEFAULT_CONFIG: RunnerConfig = {
45
47
 
46
48
  const LOCK_DOMAIN = 'prisma_next.contract.marker';
47
49
 
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
50
  /**
61
51
  * Deep clones and freezes a record object to prevent mutation.
62
52
  * Recursively clones nested objects and arrays to ensure complete isolation.
@@ -96,61 +86,98 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
96
86
  async execute(
97
87
  options: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>,
98
88
  ): Promise<SqlMigrationRunnerResult> {
99
- const schema = options.schemaName ?? this.config.defaultSchema;
100
89
  const driver = options.driver;
101
- const lockKey = `${LOCK_DOMAIN}:${schema}`;
102
90
 
103
- // Static checks - fail fast before transaction
91
+ // Static checks fail fast before any transaction work — no point
92
+ // burning a BEGIN/ROLLBACK round-trip on a destination-contract
93
+ // mismatch the caller can fix locally.
104
94
  const destinationCheck = this.ensurePlanMatchesDestinationContract(
105
95
  options.plan.destination,
106
96
  options.destinationContract,
107
97
  );
108
- if (!destinationCheck.ok) {
109
- return destinationCheck;
110
- }
98
+ if (!destinationCheck.ok) return destinationCheck;
111
99
 
112
100
  const policyCheck = this.enforcePolicyCompatibility(options.policy, options.plan.operations);
113
- if (!policyCheck.ok) {
114
- return policyCheck;
115
- }
101
+ if (!policyCheck.ok) return policyCheck;
116
102
 
117
- // Begin transaction for DB operations
118
103
  await this.beginTransaction(driver);
119
104
  let committed = false;
120
105
  try {
121
- await this.acquireLock(driver, lockKey);
122
- await this.ensureControlTables(driver);
123
- const existingMarker = await readMarker(driver);
124
-
125
- // Validate plan origin matches existing marker (needs marker from DB)
126
- const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
127
- if (!markerCheck.ok) {
128
- return markerCheck;
106
+ const result = await this.executeOnConnection(options);
107
+ if (!result.ok) {
108
+ return result;
129
109
  }
130
-
131
- // db update (origin: null) always applies; migration-apply (origin set) skips if marker matches.
132
- const markerAtDestination = this.markerMatchesDestination(existingMarker, options.plan);
133
- const skipOperations = markerAtDestination && options.plan.origin != null;
134
- let applyValue: ApplyPlanSuccessValue;
135
-
136
- if (skipOperations) {
137
- applyValue = { operationsExecuted: 0, executedOperations: [] };
138
- } else {
139
- const applyResult = await this.applyPlan(driver, options);
140
- if (!applyResult.ok) {
141
- return applyResult;
142
- }
143
- applyValue = applyResult.value;
110
+ await this.commitTransaction(driver);
111
+ committed = true;
112
+ return result;
113
+ } finally {
114
+ if (!committed) {
115
+ await this.rollbackTransaction(driver);
144
116
  }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Body of the migration runner without transaction management. The
122
+ * caller (single-space `execute(...)` above, or the multi-space
123
+ * outer-tx orchestrator at the SQL family level) owns the
124
+ * `BEGIN`/`COMMIT`/`ROLLBACK` lifecycle.
125
+ */
126
+ async executeOnConnection(
127
+ options: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>,
128
+ ): Promise<SqlMigrationRunnerResult> {
129
+ const schema = options.schemaName ?? this.config.defaultSchema;
130
+ const driver = options.driver;
131
+ if (options.space !== undefined && options.space !== options.plan.spaceId) {
132
+ throw new Error(
133
+ `SqlMigrationRunner: options.space (${options.space}) does not match plan.spaceId (${options.plan.spaceId})`,
134
+ );
135
+ }
136
+ const space = options.plan.spaceId;
137
+ const lockKey = `${LOCK_DOMAIN}:${schema}:${space}`;
145
138
 
146
- // Verify resulting schema matches contract
147
- // Step 1: Introspect live schema (DB I/O, family-owned)
139
+ // Static checks (idempotent safe to run again when the caller is
140
+ // `execute(...)` because the cost is a single object comparison).
141
+ const destinationCheck = this.ensurePlanMatchesDestinationContract(
142
+ options.plan.destination,
143
+ options.destinationContract,
144
+ );
145
+ if (!destinationCheck.ok) return destinationCheck;
146
+
147
+ const policyCheck = this.enforcePolicyCompatibility(options.policy, options.plan.operations);
148
+ if (!policyCheck.ok) return policyCheck;
149
+
150
+ await this.acquireLock(driver, lockKey);
151
+ const ensureResult = await this.ensureControlTables(driver);
152
+ if (!ensureResult.ok) return ensureResult;
153
+ const existingMarker = await this.family.readMarker({ driver, space });
154
+
155
+ const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
156
+ if (!markerCheck.ok) return markerCheck;
157
+
158
+ const markerAtDestination = this.markerMatchesDestination(existingMarker, options.plan);
159
+ const isSelfEdge = options.plan.origin?.storageHash === options.plan.destination.storageHash;
160
+ const skipOperations = markerAtDestination && options.plan.origin != null && !isSelfEdge;
161
+ let applyValue: ApplyPlanSuccessValue;
162
+
163
+ if (skipOperations) {
164
+ applyValue = { operationsExecuted: 0, executedOperations: [] };
165
+ } else {
166
+ const applyResult = await this.applyPlan(driver, options);
167
+ if (!applyResult.ok) return applyResult;
168
+ applyValue = applyResult.value;
169
+ }
170
+
171
+ // Schema verification on app-space only — extension spaces don't
172
+ // own user-facing tables in the live schema, and `verifySqlSchema`
173
+ // matches the destination contract against the database, which
174
+ // would flag every app-space table as "extra" when called against
175
+ // an extension contract.
176
+ if (space === APP_SPACE_ID) {
148
177
  const schemaIR = await this.family.introspect({
149
178
  driver,
150
179
  contract: options.destinationContract,
151
180
  });
152
-
153
- // Step 2: Pure verification (no DB I/O)
154
181
  const schemaVerifyResult = verifySqlSchema({
155
182
  contract: options.destinationContract,
156
183
  schema: schemaIR,
@@ -164,22 +191,60 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
164
191
  if (!schemaVerifyResult.ok) {
165
192
  return runnerFailure('SCHEMA_VERIFY_FAILED', schemaVerifyResult.summary, {
166
193
  why: 'The resulting database schema does not satisfy the destination contract.',
167
- meta: {
168
- issues: schemaVerifyResult.schema.issues,
169
- },
194
+ meta: { issues: schemaVerifyResult.schema.issues },
170
195
  });
171
196
  }
197
+ }
172
198
 
173
- // Record marker and ledger entries
174
- await this.upsertMarker(driver, options, existingMarker);
199
+ const incomingInvariants = options.plan.providedInvariants ?? [];
200
+ const existingInvariants = new Set(existingMarker?.invariants ?? []);
201
+ const incomingIsSubsetOfExisting = incomingInvariants.every((id) => existingInvariants.has(id));
202
+ const isSelfEdgeNoOp =
203
+ isSelfEdge && applyValue.operationsExecuted === 0 && incomingIsSubsetOfExisting;
204
+
205
+ if (!isSelfEdgeNoOp) {
206
+ await this.upsertMarker(driver, options, existingMarker, space);
175
207
  await this.recordLedgerEntry(driver, options, existingMarker, applyValue.executedOperations);
208
+ }
209
+
210
+ return runnerSuccess({
211
+ operationsPlanned: options.plan.operations.length,
212
+ operationsExecuted: applyValue.operationsExecuted,
213
+ });
214
+ }
215
+
216
+ async executeAcrossSpaces(options: {
217
+ readonly driver: ControlDriverInstance<'sql', string>;
218
+ readonly perSpaceOptions: ReadonlyArray<
219
+ SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>
220
+ >;
221
+ }): Promise<MultiSpaceRunnerResult> {
222
+ const driver = options.driver;
223
+ const perSpaceOptions = options.perSpaceOptions;
224
+
225
+ if (perSpaceOptions.length === 0) {
226
+ return ok({ perSpaceResults: [] });
227
+ }
228
+
229
+ await this.beginTransaction(driver);
230
+ let committed = false;
231
+ try {
232
+ const perSpaceResults: Array<{
233
+ space: string;
234
+ value: SqlMigrationRunnerSuccessValue;
235
+ }> = [];
236
+ for (const spaceOptions of perSpaceOptions) {
237
+ const space = spaceOptions.space ?? spaceOptions.plan.spaceId;
238
+ const result = await this.executeOnConnection({ ...spaceOptions, driver, space });
239
+ if (!result.ok) {
240
+ return notOk({ ...result.failure, failingSpace: space });
241
+ }
242
+ perSpaceResults.push({ space, value: result.value });
243
+ }
176
244
 
177
245
  await this.commitTransaction(driver);
178
246
  committed = true;
179
- return runnerSuccess({
180
- operationsPlanned: options.plan.operations.length,
181
- operationsExecuted: applyValue.operationsExecuted,
182
- });
247
+ return ok({ perSpaceResults });
183
248
  } finally {
184
249
  if (!committed) {
185
250
  await this.rollbackTransaction(driver);
@@ -201,19 +266,6 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
201
266
  for (const operation of options.plan.operations) {
202
267
  options.callbacks?.onOperationStart?.(operation);
203
268
  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
269
  // Idempotency probe: only run if both postchecks and idempotency checks are enabled
218
270
  if (runPostchecks && runIdempotency) {
219
271
  const postcheckAlreadySatisfied = await this.expectationsAreSatisfied(
@@ -266,86 +318,51 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
266
318
  return ok({ operationsExecuted, executedOperations });
267
319
  }
268
320
 
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(
321
+ private async ensureControlTables(
278
322
  driver: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>['driver'],
279
- op: DataTransformOperation,
280
- options: { runIdempotency: boolean },
281
323
  ): 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
- }
324
+ await this.executeStatement(driver, ensurePrismaContractSchemaStatement);
325
+ // Pre-1.0 zero-range guardrail: detect a pre-cleanup single-row
326
+ // marker table (no `space` column) and surface a structured failure
327
+ // rather than silently auto-migrating it to the per-space shape.
328
+ // See `specs/framework-mechanism.spec.md § 2`.
329
+ const legacyDetection = await this.detectLegacyMarkerShape(driver);
330
+ if (!legacyDetection.ok) {
331
+ return legacyDetection;
338
332
  }
339
-
333
+ await this.executeStatement(driver, ensureMarkerTableStatement);
334
+ await this.executeStatement(driver, ensureLedgerTableStatement);
340
335
  return okVoid();
341
336
  }
342
337
 
343
- private async ensureControlTables(
338
+ private async detectLegacyMarkerShape(
344
339
  driver: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>['driver'],
345
- ): Promise<void> {
346
- await this.executeStatement(driver, ensurePrismaContractSchemaStatement);
347
- await this.executeStatement(driver, ensureMarkerTableStatement);
348
- await this.executeStatement(driver, ensureLedgerTableStatement);
340
+ ): Promise<Result<void, SqlMigrationRunnerFailure>> {
341
+ const result = await driver.query<{ column_name: string }>(
342
+ `select column_name
343
+ from information_schema.columns
344
+ where table_schema = 'prisma_contract'
345
+ and table_name = 'marker'`,
346
+ );
347
+ if (result.rows.length === 0) {
348
+ return okVoid();
349
+ }
350
+ const columns = new Set(result.rows.map((row) => row.column_name));
351
+ if (columns.has('space')) {
352
+ return okVoid();
353
+ }
354
+ return runnerFailure(
355
+ 'LEGACY_MARKER_SHAPE',
356
+ 'Legacy marker-table shape detected on prisma_contract.marker (no `space` column). ' +
357
+ 'Prisma Next is in pre-1.0; the previous transitional auto-migration to the per-space-row schema has been removed. ' +
358
+ 'Drop `prisma_contract.marker` and re-run `dbInit` to reinitialise from a clean baseline.',
359
+ {
360
+ meta: {
361
+ table: 'prisma_contract.marker',
362
+ columns: [...columns].sort(),
363
+ },
364
+ },
365
+ );
349
366
  }
350
367
 
351
368
  private async runExpectationSteps(
@@ -355,7 +372,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
355
372
  phase: 'precheck' | 'postcheck',
356
373
  ): Promise<Result<void, SqlMigrationRunnerFailure>> {
357
374
  for (const step of steps) {
358
- const result = await driver.query(step.sql);
375
+ const result = await driver.query(step.sql, step.params ?? []);
359
376
  if (!this.stepResultIsTrue(result.rows)) {
360
377
  const code = phase === 'precheck' ? 'PRECHECK_FAILED' : 'POSTCHECK_FAILED';
361
378
  return runnerFailure(
@@ -381,7 +398,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
381
398
  ): Promise<Result<void, SqlMigrationRunnerFailure>> {
382
399
  for (const step of steps) {
383
400
  try {
384
- await driver.query(step.sql);
401
+ await driver.query(step.sql, step.params ?? []);
385
402
  } catch (error: unknown) {
386
403
  // Catch SqlQueryError and include normalized metadata
387
404
  if (SqlQueryError.is(error)) {
@@ -445,7 +462,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
445
462
  return false;
446
463
  }
447
464
  for (const step of steps) {
448
- const result = await driver.query(step.sql);
465
+ const result = await driver.query(step.sql, step.params ?? []);
449
466
  if (!this.stepResultIsTrue(result.rows)) {
450
467
  return false;
451
468
  }
@@ -616,8 +633,11 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
616
633
  driver: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>['driver'],
617
634
  options: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>,
618
635
  existingMarker: ContractMarkerRecord | null,
636
+ space: string,
619
637
  ): Promise<void> {
620
- const writeStatements = buildWriteMarkerStatements({
638
+ const incomingInvariants = options.plan.providedInvariants ?? [];
639
+ const writeStatements = buildMergeMarkerStatements({
640
+ space,
621
641
  storageHash: options.plan.destination.storageHash,
622
642
  profileHash:
623
643
  options.plan.destination.profileHash ??
@@ -626,6 +646,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
626
646
  contractJson: options.destinationContract,
627
647
  canonicalVersion: null,
628
648
  meta: {},
649
+ invariants: incomingInvariants,
629
650
  });
630
651
  const statement = existingMarker ? writeStatements.update : writeStatements.insert;
631
652
  await this.executeStatement(driver, statement);
@@ -1,3 +1,7 @@
1
+ import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
2
+
3
+ export { APP_SPACE_ID };
4
+
1
5
  export interface SqlStatement {
2
6
  readonly sql: string;
3
7
  readonly params: readonly unknown[];
@@ -8,16 +12,26 @@ export const ensurePrismaContractSchemaStatement: SqlStatement = {
8
12
  params: [],
9
13
  };
10
14
 
15
+ /**
16
+ * Schema for `prisma_contract.marker`. The `space text` primary key
17
+ * supports one row per loaded contract space (`'app'`,
18
+ * `'<extension-id>'`, …); on a brand-new database `CREATE TABLE IF NOT
19
+ * EXISTS` produces this shape directly. The migration runner detects
20
+ * pre-1.0 single-row markers (no `space` column) at boot and fails with
21
+ * a structured `LEGACY_MARKER_SHAPE` error rather than auto-migrating —
22
+ * see `specs/framework-mechanism.spec.md § 2`.
23
+ */
11
24
  export const ensureMarkerTableStatement: SqlStatement = {
12
25
  sql: `create table if not exists prisma_contract.marker (
13
- id smallint primary key default 1,
26
+ space text not null primary key default '${APP_SPACE_ID}',
14
27
  core_hash text not null,
15
28
  profile_hash text not null,
16
29
  contract_json jsonb,
17
30
  canonical_version int,
18
31
  updated_at timestamptz not null default now(),
19
32
  app_tag text,
20
- meta jsonb not null default '{}'
33
+ meta jsonb not null default '{}',
34
+ invariants text[] not null default '{}'
21
35
  )`,
22
36
  params: [],
23
37
  };
@@ -37,40 +51,59 @@ export const ensureLedgerTableStatement: SqlStatement = {
37
51
  params: [],
38
52
  };
39
53
 
40
- export interface WriteMarkerInput {
54
+ export interface MergeMarkerInput {
55
+ /**
56
+ * Logical space identifier for this marker row. Required at every
57
+ * call site so the type system surfaces every place that needs to
58
+ * thread the value (rather than letting an `?? APP_SPACE_ID`
59
+ * fall-through silently collapse multi-space markers onto the
60
+ * `'app'` row). App-plan callers pass {@link APP_SPACE_ID}
61
+ * (`'app'`); per-extension callers (planner / runner / verifier
62
+ * extensions over contract spaces) pass the extension's space id.
63
+ */
64
+ readonly space: string;
41
65
  readonly storageHash: string;
42
66
  readonly profileHash: string;
43
67
  readonly contractJson?: unknown;
44
68
  readonly canonicalVersion?: number | null;
45
69
  readonly appTag?: string | null;
46
70
  readonly meta?: Record<string, unknown>;
71
+ /**
72
+ * Invariants to merge into `marker.invariants`. INSERT writes them as
73
+ * the initial value (callers are expected to pass a sorted, deduped
74
+ * array). UPDATE merges them with the existing column server-side via
75
+ * a single atomic SQL expression.
76
+ */
77
+ readonly invariants: readonly string[];
47
78
  }
48
79
 
49
- export function buildWriteMarkerStatements(input: WriteMarkerInput): {
80
+ export function buildMergeMarkerStatements(input: MergeMarkerInput): {
50
81
  readonly insert: SqlStatement;
51
82
  readonly update: SqlStatement;
52
83
  } {
53
84
  const params: readonly unknown[] = [
54
- 1,
85
+ input.space,
55
86
  input.storageHash,
56
87
  input.profileHash,
57
88
  jsonParam(input.contractJson),
58
89
  input.canonicalVersion ?? null,
59
90
  input.appTag ?? null,
60
91
  jsonParam(input.meta ?? {}),
92
+ input.invariants,
61
93
  ];
62
94
 
63
95
  return {
64
96
  insert: {
65
97
  sql: `insert into prisma_contract.marker (
66
- id,
98
+ space,
67
99
  core_hash,
68
100
  profile_hash,
69
101
  contract_json,
70
102
  canonical_version,
71
103
  updated_at,
72
104
  app_tag,
73
- meta
105
+ meta,
106
+ invariants
74
107
  ) values (
75
108
  $1,
76
109
  $2,
@@ -79,11 +112,16 @@ export function buildWriteMarkerStatements(input: WriteMarkerInput): {
79
112
  $5,
80
113
  now(),
81
114
  $6,
82
- $7::jsonb
115
+ $7::jsonb,
116
+ $8::text[]
83
117
  )`,
84
118
  params,
85
119
  },
86
120
  update: {
121
+ // `invariants = array(select distinct unnest(invariants || $8::text[]) order by 1)`
122
+ // reads the current column value under the UPDATE's row lock, unions
123
+ // with the incoming array, dedupes, and sorts ascending — single
124
+ // statement, atomic, no read-then-write window.
87
125
  sql: `update prisma_contract.marker set
88
126
  core_hash = $2,
89
127
  profile_hash = $3,
@@ -91,8 +129,9 @@ export function buildWriteMarkerStatements(input: WriteMarkerInput): {
91
129
  canonical_version = $5,
92
130
  updated_at = now(),
93
131
  app_tag = $6,
94
- meta = $7::jsonb
95
- where id = $1`,
132
+ meta = $7::jsonb,
133
+ invariants = array(select distinct unnest(invariants || $8::text[]) order by 1)
134
+ where space = $1`,
96
135
  params,
97
136
  },
98
137
  };
@@ -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];
@@ -1,2 +1,49 @@
1
- export type { CodecTypes } from '../core/codecs';
2
- export { codecDefinitions, dataTypes } from '../core/codecs';
1
+ export type {
2
+ PgBitDescriptor,
3
+ PgBoolDescriptor,
4
+ PgCharDescriptor,
5
+ PgEnumDescriptor,
6
+ PgFloat4Descriptor,
7
+ PgFloat8Descriptor,
8
+ PgFloatDescriptor,
9
+ PgInt2Descriptor,
10
+ PgInt4Descriptor,
11
+ PgInt8Descriptor,
12
+ PgIntDescriptor,
13
+ PgIntervalDescriptor,
14
+ PgJsonbDescriptor,
15
+ PgJsonDescriptor,
16
+ PgNumericDescriptor,
17
+ PgTextDescriptor,
18
+ PgTimeDescriptor,
19
+ PgTimestampDescriptor,
20
+ PgTimestamptzDescriptor,
21
+ PgTimetzDescriptor,
22
+ PgVarbitDescriptor,
23
+ PgVarcharDescriptor,
24
+ } from '../core/codecs';
25
+ export {
26
+ pgBitColumn,
27
+ pgBoolColumn,
28
+ pgCharColumn,
29
+ pgEnumColumn,
30
+ pgFloat4Column,
31
+ pgFloat8Column,
32
+ pgFloatColumn,
33
+ pgInt2Column,
34
+ pgInt4Column,
35
+ pgInt8Column,
36
+ pgIntColumn,
37
+ pgIntervalColumn,
38
+ pgJsonbColumn,
39
+ pgJsonColumn,
40
+ pgNumericColumn,
41
+ pgTextColumn,
42
+ pgTimeColumn,
43
+ pgTimestampColumn,
44
+ pgTimestamptzColumn,
45
+ pgTimetzColumn,
46
+ pgVarbitColumn,
47
+ pgVarcharColumn,
48
+ } from '../core/codecs';
49
+ export { postgresCodecRegistry } from '../core/registry';