@prisma-next/target-postgres 0.4.2 → 0.4.3

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 (63) hide show
  1. package/dist/codec-types.d.mts +1 -1
  2. package/dist/codec-types.d.mts.map +1 -1
  3. package/dist/codec-types.mjs +1 -1
  4. package/dist/{codecs-D-F2KJqt.d.mts → codecs-CE5EUsNM.d.mts} +54 -30
  5. package/dist/codecs-CE5EUsNM.d.mts.map +1 -0
  6. package/dist/{codecs-BoahtY_Q.mjs → codecs-dzZ_dMpK.mjs} +7 -102
  7. package/dist/codecs-dzZ_dMpK.mjs.map +1 -0
  8. package/dist/codecs.d.mts +1 -1
  9. package/dist/codecs.mjs +1 -1
  10. package/dist/control.mjs +17 -9
  11. package/dist/control.mjs.map +1 -1
  12. package/dist/{data-transform-VfEGzXWt.mjs → data-transform-C83dy0vk.mjs} +3 -1
  13. package/dist/data-transform-C83dy0vk.mjs.map +1 -0
  14. package/dist/{data-transform-CxFRBIUp.d.mts → data-transform-D8x5m1YV.d.mts} +7 -1
  15. package/dist/data-transform-D8x5m1YV.d.mts.map +1 -0
  16. package/dist/data-transform.d.mts +1 -1
  17. package/dist/data-transform.mjs +1 -1
  18. package/dist/migration.d.mts +2 -2
  19. package/dist/migration.mjs +2 -2
  20. package/dist/pack.d.mts +1 -1
  21. package/dist/{planner-CLUvVhUN.mjs → planner-B4ZSLHRI.mjs} +6 -6
  22. package/dist/planner-B4ZSLHRI.mjs.map +1 -0
  23. package/dist/{planner-produced-postgres-migration-DSSPq8QS.mjs → planner-produced-postgres-migration-C0GNhHGw.mjs} +3 -4
  24. package/dist/{planner-produced-postgres-migration-DSSPq8QS.mjs.map → planner-produced-postgres-migration-C0GNhHGw.mjs.map} +1 -1
  25. package/dist/{planner-produced-postgres-migration-CRRTno6Z.d.mts → planner-produced-postgres-migration-Dw_mPMKt.d.mts} +2 -2
  26. package/dist/planner-produced-postgres-migration-Dw_mPMKt.d.mts.map +1 -0
  27. package/dist/planner-produced-postgres-migration.d.mts +2 -2
  28. package/dist/planner-produced-postgres-migration.mjs +1 -1
  29. package/dist/planner.d.mts +15 -9
  30. package/dist/planner.d.mts.map +1 -1
  31. package/dist/planner.mjs +1 -1
  32. package/dist/{postgres-migration-BjA3Zmts.d.mts → postgres-migration-DcfWGqhe.d.mts} +2 -2
  33. package/dist/{postgres-migration-BjA3Zmts.d.mts.map → postgres-migration-DcfWGqhe.d.mts.map} +1 -1
  34. package/dist/{postgres-migration-qtmtbONe.mjs → postgres-migration-EGSlO4jO.mjs} +2 -2
  35. package/dist/{postgres-migration-qtmtbONe.mjs.map → postgres-migration-EGSlO4jO.mjs.map} +1 -1
  36. package/dist/{render-typescript-1rF_SB4g.mjs → render-typescript-Co3Emwgz.mjs} +1 -2
  37. package/dist/render-typescript-Co3Emwgz.mjs.map +1 -0
  38. package/dist/render-typescript.d.mts +1 -2
  39. package/dist/render-typescript.d.mts.map +1 -1
  40. package/dist/render-typescript.mjs +1 -1
  41. package/dist/{statement-builders-BPnmt6wx.mjs → statement-builders-CHqCtSfe.mjs} +13 -8
  42. package/dist/statement-builders-CHqCtSfe.mjs.map +1 -0
  43. package/dist/statement-builders.d.mts +10 -3
  44. package/dist/statement-builders.d.mts.map +1 -1
  45. package/dist/statement-builders.mjs +2 -2
  46. package/package.json +14 -14
  47. package/src/core/codecs.ts +17 -40
  48. package/src/core/migrations/operations/data-transform.ts +8 -0
  49. package/src/core/migrations/planner-produced-postgres-migration.ts +0 -1
  50. package/src/core/migrations/planner.ts +17 -11
  51. package/src/core/migrations/render-typescript.ts +1 -5
  52. package/src/core/migrations/runner.ts +45 -9
  53. package/src/core/migrations/statement-builders.ts +22 -6
  54. package/src/exports/statement-builders.ts +1 -1
  55. package/dist/codecs-BoahtY_Q.mjs.map +0 -1
  56. package/dist/codecs-D-F2KJqt.d.mts.map +0 -1
  57. package/dist/data-transform-CxFRBIUp.d.mts.map +0 -1
  58. package/dist/data-transform-VfEGzXWt.mjs.map +0 -1
  59. package/dist/planner-CLUvVhUN.mjs.map +0 -1
  60. package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts.map +0 -1
  61. package/dist/render-typescript-1rF_SB4g.mjs.map +0 -1
  62. package/dist/statement-builders-BPnmt6wx.mjs.map +0 -1
  63. package/src/core/json-schema-type-expression.ts +0 -131
@@ -4,9 +4,8 @@ import { b as PostgresOpFactoryCall } from "./op-factory-call-C3bWXKSP.mjs";
4
4
  //#region src/core/migrations/render-typescript.d.ts
5
5
 
6
6
  interface RenderMigrationMeta {
7
- readonly from: string;
7
+ readonly from: string | null;
8
8
  readonly to: string;
9
- readonly kind?: string;
10
9
  readonly labels?: readonly string[];
11
10
  }
12
11
  declare function renderCallsToTypeScript(calls: ReadonlyArray<PostgresOpFactoryCall>, meta: RenderMigrationMeta): string;
@@ -1 +1 @@
1
- {"version":3,"file":"render-typescript.d.mts","names":[],"sources":["../src/core/migrations/render-typescript.ts"],"sourcesContent":[],"mappings":";;;;;UAeiB,mBAAA;;;;;;iBA8BD,uBAAA,QACP,cAAc,8BACf"}
1
+ {"version":3,"file":"render-typescript.d.mts","names":[],"sources":["../src/core/migrations/render-typescript.ts"],"sourcesContent":[],"mappings":";;;;;UAeiB,mBAAA;;;;;iBA6BD,uBAAA,QACP,cAAc,8BACf"}
@@ -1,3 +1,3 @@
1
- import { t as renderCallsToTypeScript } from "./render-typescript-1rF_SB4g.mjs";
1
+ import { t as renderCallsToTypeScript } from "./render-typescript-Co3Emwgz.mjs";
2
2
 
3
3
  export { renderCallsToTypeScript };
@@ -12,7 +12,8 @@ const ensureMarkerTableStatement = {
12
12
  canonical_version int,
13
13
  updated_at timestamptz not null default now(),
14
14
  app_tag text,
15
- meta jsonb not null default '{}'
15
+ meta jsonb not null default '{}',
16
+ invariants text[] not null default '{}'
16
17
  )`,
17
18
  params: []
18
19
  };
@@ -30,7 +31,7 @@ const ensureLedgerTableStatement = {
30
31
  )`,
31
32
  params: []
32
33
  };
33
- function buildWriteMarkerStatements(input) {
34
+ function buildMergeMarkerStatements(input) {
34
35
  const params = [
35
36
  1,
36
37
  input.storageHash,
@@ -38,7 +39,8 @@ function buildWriteMarkerStatements(input) {
38
39
  jsonParam(input.contractJson),
39
40
  input.canonicalVersion ?? null,
40
41
  input.appTag ?? null,
41
- jsonParam(input.meta ?? {})
42
+ jsonParam(input.meta ?? {}),
43
+ input.invariants
42
44
  ];
43
45
  return {
44
46
  insert: {
@@ -50,7 +52,8 @@ function buildWriteMarkerStatements(input) {
50
52
  canonical_version,
51
53
  updated_at,
52
54
  app_tag,
53
- meta
55
+ meta,
56
+ invariants
54
57
  ) values (
55
58
  $1,
56
59
  $2,
@@ -59,7 +62,8 @@ function buildWriteMarkerStatements(input) {
59
62
  $5,
60
63
  now(),
61
64
  $6,
62
- $7::jsonb
65
+ $7::jsonb,
66
+ $8::text[]
63
67
  )`,
64
68
  params
65
69
  },
@@ -71,7 +75,8 @@ function buildWriteMarkerStatements(input) {
71
75
  canonical_version = $5,
72
76
  updated_at = now(),
73
77
  app_tag = $6,
74
- meta = $7::jsonb
78
+ meta = $7::jsonb,
79
+ invariants = array(select distinct unnest(invariants || $8::text[]) order by 1)
75
80
  where id = $1`,
76
81
  params
77
82
  }
@@ -112,5 +117,5 @@ function jsonParam(value) {
112
117
  }
113
118
 
114
119
  //#endregion
115
- export { ensurePrismaContractSchemaStatement as a, ensureMarkerTableStatement as i, buildWriteMarkerStatements as n, ensureLedgerTableStatement as r, buildLedgerInsertStatement as t };
116
- //# sourceMappingURL=statement-builders-BPnmt6wx.mjs.map
120
+ export { ensurePrismaContractSchemaStatement as a, ensureMarkerTableStatement as i, buildMergeMarkerStatements as n, ensureLedgerTableStatement as r, buildLedgerInsertStatement as t };
121
+ //# sourceMappingURL=statement-builders-CHqCtSfe.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statement-builders-CHqCtSfe.mjs","names":["ensurePrismaContractSchemaStatement: SqlStatement","ensureMarkerTableStatement: SqlStatement","ensureLedgerTableStatement: SqlStatement","params: readonly unknown[]"],"sources":["../src/core/migrations/statement-builders.ts"],"sourcesContent":["export interface SqlStatement {\n readonly sql: string;\n readonly params: readonly unknown[];\n}\n\nexport const ensurePrismaContractSchemaStatement: SqlStatement = {\n sql: 'create schema if not exists prisma_contract',\n params: [],\n};\n\nexport const ensureMarkerTableStatement: SqlStatement = {\n sql: `create table if not exists prisma_contract.marker (\n id smallint primary key default 1,\n core_hash text not null,\n profile_hash text not null,\n contract_json jsonb,\n canonical_version int,\n updated_at timestamptz not null default now(),\n app_tag text,\n meta jsonb not null default '{}',\n invariants text[] not null default '{}'\n )`,\n params: [],\n};\n\nexport const ensureLedgerTableStatement: SqlStatement = {\n sql: `create table if not exists prisma_contract.ledger (\n id bigserial primary key,\n created_at timestamptz not null default now(),\n origin_core_hash text,\n origin_profile_hash text,\n destination_core_hash text not null,\n destination_profile_hash text,\n contract_json_before jsonb,\n contract_json_after jsonb,\n operations jsonb not null\n )`,\n params: [],\n};\n\nexport interface MergeMarkerInput {\n readonly storageHash: string;\n readonly profileHash: string;\n readonly contractJson?: unknown;\n readonly canonicalVersion?: number | null;\n readonly appTag?: string | null;\n readonly meta?: Record<string, unknown>;\n /**\n * Invariants to merge into `marker.invariants`. INSERT writes them as\n * the initial value (callers are expected to pass a sorted, deduped\n * array). UPDATE merges them with the existing column server-side via\n * a single atomic SQL expression.\n */\n readonly invariants: readonly string[];\n}\n\nexport function buildMergeMarkerStatements(input: MergeMarkerInput): {\n readonly insert: SqlStatement;\n readonly update: SqlStatement;\n} {\n const params: readonly unknown[] = [\n 1,\n input.storageHash,\n input.profileHash,\n jsonParam(input.contractJson),\n input.canonicalVersion ?? null,\n input.appTag ?? null,\n jsonParam(input.meta ?? {}),\n input.invariants,\n ];\n\n return {\n insert: {\n sql: `insert into prisma_contract.marker (\n id,\n core_hash,\n profile_hash,\n contract_json,\n canonical_version,\n updated_at,\n app_tag,\n meta,\n invariants\n ) values (\n $1,\n $2,\n $3,\n $4::jsonb,\n $5,\n now(),\n $6,\n $7::jsonb,\n $8::text[]\n )`,\n params,\n },\n update: {\n // `invariants = array(select distinct unnest(invariants || $8::text[]) order by 1)`\n // reads the current column value under the UPDATE's row lock, unions\n // with the incoming array, dedupes, and sorts ascending — single\n // statement, atomic, no read-then-write window.\n sql: `update prisma_contract.marker set\n core_hash = $2,\n profile_hash = $3,\n contract_json = $4::jsonb,\n canonical_version = $5,\n updated_at = now(),\n app_tag = $6,\n meta = $7::jsonb,\n invariants = array(select distinct unnest(invariants || $8::text[]) order by 1)\n where id = $1`,\n params,\n },\n };\n}\n\nexport interface LedgerInsertInput {\n readonly originStorageHash?: string | null;\n readonly originProfileHash?: string | null;\n readonly destinationStorageHash: string;\n readonly destinationProfileHash?: string | null;\n readonly contractJsonBefore?: unknown;\n readonly contractJsonAfter?: unknown;\n readonly operations: unknown;\n}\n\nexport function buildLedgerInsertStatement(input: LedgerInsertInput): SqlStatement {\n return {\n sql: `insert into prisma_contract.ledger (\n origin_core_hash,\n origin_profile_hash,\n destination_core_hash,\n destination_profile_hash,\n contract_json_before,\n contract_json_after,\n operations\n ) values (\n $1,\n $2,\n $3,\n $4,\n $5::jsonb,\n $6::jsonb,\n $7::jsonb\n )`,\n params: [\n input.originStorageHash ?? null,\n input.originProfileHash ?? null,\n input.destinationStorageHash,\n input.destinationProfileHash ?? null,\n jsonParam(input.contractJsonBefore),\n jsonParam(input.contractJsonAfter),\n jsonParam(input.operations),\n ],\n };\n}\n\nfunction jsonParam(value: unknown): string {\n return JSON.stringify(value ?? null);\n}\n"],"mappings":";AAKA,MAAaA,sCAAoD;CAC/D,KAAK;CACL,QAAQ,EAAE;CACX;AAED,MAAaC,6BAA2C;CACtD,KAAK;;;;;;;;;;;CAWL,QAAQ,EAAE;CACX;AAED,MAAaC,6BAA2C;CACtD,KAAK;;;;;;;;;;;CAWL,QAAQ,EAAE;CACX;AAkBD,SAAgB,2BAA2B,OAGzC;CACA,MAAMC,SAA6B;EACjC;EACA,MAAM;EACN,MAAM;EACN,UAAU,MAAM,aAAa;EAC7B,MAAM,oBAAoB;EAC1B,MAAM,UAAU;EAChB,UAAU,MAAM,QAAQ,EAAE,CAAC;EAC3B,MAAM;EACP;AAED,QAAO;EACL,QAAQ;GACN,KAAK;;;;;;;;;;;;;;;;;;;;;GAqBL;GACD;EACD,QAAQ;GAKN,KAAK;;;;;;;;;;GAUL;GACD;EACF;;AAaH,SAAgB,2BAA2B,OAAwC;AACjF,QAAO;EACL,KAAK;;;;;;;;;;;;;;;;;EAiBL,QAAQ;GACN,MAAM,qBAAqB;GAC3B,MAAM,qBAAqB;GAC3B,MAAM;GACN,MAAM,0BAA0B;GAChC,UAAU,MAAM,mBAAmB;GACnC,UAAU,MAAM,kBAAkB;GAClC,UAAU,MAAM,WAAW;GAC5B;EACF;;AAGH,SAAS,UAAU,OAAwB;AACzC,QAAO,KAAK,UAAU,SAAS,KAAK"}
@@ -6,18 +6,25 @@ interface SqlStatement {
6
6
  declare const ensurePrismaContractSchemaStatement: SqlStatement;
7
7
  declare const ensureMarkerTableStatement: SqlStatement;
8
8
  declare const ensureLedgerTableStatement: SqlStatement;
9
- interface WriteMarkerInput {
9
+ interface MergeMarkerInput {
10
10
  readonly storageHash: string;
11
11
  readonly profileHash: string;
12
12
  readonly contractJson?: unknown;
13
13
  readonly canonicalVersion?: number | null;
14
14
  readonly appTag?: string | null;
15
15
  readonly meta?: Record<string, unknown>;
16
+ /**
17
+ * Invariants to merge into `marker.invariants`. INSERT writes them as
18
+ * the initial value (callers are expected to pass a sorted, deduped
19
+ * array). UPDATE merges them with the existing column server-side via
20
+ * a single atomic SQL expression.
21
+ */
22
+ readonly invariants: readonly string[];
16
23
  }
17
- declare function buildWriteMarkerStatements(input: WriteMarkerInput): {
24
+ declare function buildMergeMarkerStatements(input: MergeMarkerInput): {
18
25
  readonly insert: SqlStatement;
19
26
  readonly update: SqlStatement;
20
27
  };
21
28
  //#endregion
22
- export { type SqlStatement, buildWriteMarkerStatements, ensureLedgerTableStatement, ensureMarkerTableStatement, ensurePrismaContractSchemaStatement };
29
+ export { type SqlStatement, buildMergeMarkerStatements, ensureLedgerTableStatement, ensureMarkerTableStatement, ensurePrismaContractSchemaStatement };
23
30
  //# sourceMappingURL=statement-builders.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"statement-builders.d.mts","names":[],"sources":["../src/core/migrations/statement-builders.ts"],"sourcesContent":[],"mappings":";UAAiB,YAAA;EAAA,SAAA,GAAA,EAAA,MAAY;EAKhB,SAAA,MAAA,EAAA,SAAA,OAAA,EAAA;AAKb;AAca,cAnBA,mCAmB4B,EAnBS,YAgCjD;AAEgB,cA7BJ,0BAmCW,EAnCiB,YAmCjB;AAGR,cAxBH,0BAwB6B,EAxBD,YAwBC;AAAQ,UATjC,gBAAA,CASiC;EAC/B,SAAA,WAAA,EAAA,MAAA;EACA,SAAA,WAAA,EAAA,MAAA;EAAY,SAAA,YAAA,CAAA,EAAA,OAAA;;;kBALb;;iBAGF,0BAAA,QAAkC;mBAC/B;mBACA"}
1
+ {"version":3,"file":"statement-builders.d.mts","names":[],"sources":["../src/core/migrations/statement-builders.ts"],"sourcesContent":[],"mappings":";UAAiB,YAAA;EAAA,SAAA,GAAA,EAAA,MAAY;EAKhB,SAAA,MAAA,EAAA,SAAA,OAAA,EAAA;AAKb;AAea,cApBA,mCAoB4B,EApBS,YAiCjD;AAEgB,cA9BJ,0BAoCW,EApCiB,YAoCjB;AAUR,cA/BH,0BA+B6B,EA/BD,YA+BC;AAAQ,UAhBjC,gBAAA,CAgBiC;EAC/B,SAAA,WAAA,EAAA,MAAA;EACA,SAAA,WAAA,EAAA,MAAA;EAAY,SAAA,YAAA,CAAA,EAAA,OAAA;;;kBAZb;;;;;;;;;iBAUF,0BAAA,QAAkC;mBAC/B;mBACA"}
@@ -1,3 +1,3 @@
1
- import { a as ensurePrismaContractSchemaStatement, i as ensureMarkerTableStatement, n as buildWriteMarkerStatements, r as ensureLedgerTableStatement } from "./statement-builders-BPnmt6wx.mjs";
1
+ import { a as ensurePrismaContractSchemaStatement, i as ensureMarkerTableStatement, n as buildMergeMarkerStatements, r as ensureLedgerTableStatement } from "./statement-builders-CHqCtSfe.mjs";
2
2
 
3
- export { buildWriteMarkerStatements, ensureLedgerTableStatement, ensureMarkerTableStatement, ensurePrismaContractSchemaStatement };
3
+ export { buildMergeMarkerStatements, ensureLedgerTableStatement, ensureMarkerTableStatement, ensurePrismaContractSchemaStatement };
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@prisma-next/target-postgres",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Postgres target pack for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.0.0",
9
9
  "pathe": "^2.0.3",
10
- "@prisma-next/cli": "0.4.2",
11
- "@prisma-next/contract": "0.4.2",
12
- "@prisma-next/errors": "0.4.2",
13
- "@prisma-next/family-sql": "0.4.2",
14
- "@prisma-next/framework-components": "0.4.2",
15
- "@prisma-next/migration-tools": "0.4.2",
16
- "@prisma-next/sql-contract": "0.4.2",
17
- "@prisma-next/sql-errors": "0.4.2",
18
- "@prisma-next/sql-operations": "0.4.2",
19
- "@prisma-next/ts-render": "0.4.2",
20
- "@prisma-next/sql-relational-core": "0.4.2",
21
- "@prisma-next/utils": "0.4.2",
22
- "@prisma-next/sql-schema-ir": "0.4.2"
10
+ "@prisma-next/errors": "0.4.3",
11
+ "@prisma-next/cli": "0.4.3",
12
+ "@prisma-next/contract": "0.4.3",
13
+ "@prisma-next/family-sql": "0.4.3",
14
+ "@prisma-next/framework-components": "0.4.3",
15
+ "@prisma-next/migration-tools": "0.4.3",
16
+ "@prisma-next/sql-contract": "0.4.3",
17
+ "@prisma-next/sql-errors": "0.4.3",
18
+ "@prisma-next/ts-render": "0.4.3",
19
+ "@prisma-next/sql-operations": "0.4.3",
20
+ "@prisma-next/sql-relational-core": "0.4.3",
21
+ "@prisma-next/sql-schema-ir": "0.4.3",
22
+ "@prisma-next/utils": "0.4.3"
23
23
  },
24
24
  "devDependencies": {
25
25
  "tsdown": "0.18.4",
@@ -40,7 +40,6 @@ import {
40
40
  PG_VARBIT_CODEC_ID,
41
41
  PG_VARCHAR_CODEC_ID,
42
42
  } from './codec-ids';
43
- import { renderTypeScriptTypeFromJsonSchema } from './json-schema-type-expression';
44
43
 
45
44
  const lengthParamsSchema = arktype({
46
45
  length: 'number.integer > 0',
@@ -85,19 +84,13 @@ function renderPrecision(typeName: string, typeParams: Record<string, unknown>):
85
84
  return `${typeName}<${precision}>`;
86
85
  }
87
86
 
88
- function renderJsonOutputType(typeParams: Record<string, unknown>): string {
89
- const typeName = typeParams['type'];
90
- if (typeof typeName === 'string' && typeName.trim().length > 0) {
91
- return typeName.trim();
92
- }
93
- const schema = typeParams['schemaJson'];
94
- if (schema && typeof schema === 'object') {
95
- return renderTypeScriptTypeFromJsonSchema(schema);
96
- }
97
- throw new Error(
98
- `renderOutputType: JSON codec typeParams must contain "type" (string) or "schemaJson" (object), got keys: ${Object.keys(typeParams).join(', ')}`,
99
- );
100
- }
87
+ // Phase C: postgres' raw json/jsonb codecs no longer carry a
88
+ // `renderOutputType` slot — the schema-typed JSON surface that drove
89
+ // `typeParams: { schemaJson, type? }` retired in favor of the per-library
90
+ // extension package (`@prisma-next/extension-arktype-json`). Untyped
91
+ // json/jsonb columns have no typeParams; the framework emit path falls
92
+ // through to the generic `CodecTypes['pg/jsonb@1']['output']` accessor
93
+ // (which resolves to `JsonValue` via the codec-types map).
101
94
 
102
95
  function aliasCodec<
103
96
  Id extends string,
@@ -339,22 +332,15 @@ const pgFloat8Codec = codec({
339
332
  const pgTimestampCodec = codec<
340
333
  typeof PG_TIMESTAMP_CODEC_ID,
341
334
  readonly ['equality', 'order'],
342
- string | Date,
343
- string | Date
335
+ Date,
336
+ Date
344
337
  >({
345
338
  typeId: PG_TIMESTAMP_CODEC_ID,
346
339
  targetTypes: ['timestamp'],
347
340
  traits: ['equality', 'order'],
348
- encode: (value: string | Date): string => {
349
- if (value instanceof Date) return value.toISOString();
350
- if (typeof value === 'string') return value;
351
- return String(value);
352
- },
353
- decode: (wire: string | Date): string => {
354
- if (wire instanceof Date) return wire.toISOString();
355
- return wire;
356
- },
357
- encodeJson: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),
341
+ encode: (value: Date): Date => value,
342
+ decode: (wire: Date): Date => wire,
343
+ encodeJson: (value: Date) => value.toISOString(),
358
344
  decodeJson: (json) => {
359
345
  if (typeof json !== 'string') {
360
346
  throw new Error(`Expected ISO date string for pg/timestamp@1, got ${typeof json}`);
@@ -381,22 +367,15 @@ const pgTimestampCodec = codec<
381
367
  const pgTimestamptzCodec = codec<
382
368
  typeof PG_TIMESTAMPTZ_CODEC_ID,
383
369
  readonly ['equality', 'order'],
384
- string | Date,
385
- string | Date
370
+ Date,
371
+ Date
386
372
  >({
387
373
  typeId: PG_TIMESTAMPTZ_CODEC_ID,
388
374
  targetTypes: ['timestamptz'],
389
375
  traits: ['equality', 'order'],
390
- encode: (value: string | Date): string => {
391
- if (value instanceof Date) return value.toISOString();
392
- if (typeof value === 'string') return value;
393
- return String(value);
394
- },
395
- decode: (wire: string | Date): string => {
396
- if (wire instanceof Date) return wire.toISOString();
397
- return wire;
398
- },
399
- encodeJson: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),
376
+ encode: (value: Date): Date => value,
377
+ decode: (wire: Date): Date => wire,
378
+ encodeJson: (value: Date) => value.toISOString(),
400
379
  decodeJson: (json) => {
401
380
  if (typeof json !== 'string') {
402
381
  throw new Error(`Expected ISO date string for pg/timestamptz@1, got ${typeof json}`);
@@ -576,7 +555,6 @@ const pgJsonCodec = codec({
576
555
  encode: (value: string | JsonValue): string => JSON.stringify(value),
577
556
  decode: (wire: string | JsonValue): JsonValue =>
578
557
  typeof wire === 'string' ? JSON.parse(wire) : wire,
579
- renderOutputType: renderJsonOutputType,
580
558
  meta: {
581
559
  db: {
582
560
  sql: {
@@ -595,7 +573,6 @@ const pgJsonbCodec = codec({
595
573
  encode: (value: string | JsonValue): string => JSON.stringify(value),
596
574
  decode: (wire: string | JsonValue): JsonValue =>
597
575
  typeof wire === 'string' ? JSON.parse(wire) : wire,
598
- renderOutputType: renderJsonOutputType,
599
576
  meta: {
600
577
  db: {
601
578
  sql: {
@@ -37,6 +37,7 @@ import type {
37
37
  } from '@prisma-next/framework-components/control';
38
38
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
39
39
  import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
40
+ import { ifDefined } from '@prisma-next/utils/defined';
40
41
 
41
42
  interface Buildable<R = unknown> {
42
43
  build(): SqlQueryPlan<R>;
@@ -49,6 +50,12 @@ interface Buildable<R = unknown> {
49
50
  export type DataTransformClosure = () => SqlQueryPlan | Buildable;
50
51
 
51
52
  export interface DataTransformOptions {
53
+ /**
54
+ * Optional opt-in routing identity. Presence opts the transform into
55
+ * invariant-aware routing; absence means it is path-dependent and
56
+ * not referenceable from refs.
57
+ */
58
+ readonly invariantId?: string;
52
59
  /** Optional pre-flight query. `undefined` means "no check". */
53
60
  readonly check?: DataTransformClosure;
54
61
  /** One or more mutation queries to execute. */
@@ -76,6 +83,7 @@ export function dataTransform<TContract extends Contract<SqlStorage>>(
76
83
  label: `Data transform: ${name}`,
77
84
  operationClass: 'data',
78
85
  name,
86
+ ...ifDefined('invariantId', options.invariantId),
79
87
  source: 'migration.ts',
80
88
  check: options.check ? invokeAndLower(options.check, contract, adapter, name) : null,
81
89
  run: runClosures.map((closure) => invokeAndLower(closure, contract, adapter, name)),
@@ -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
  });
@@ -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,7 +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
15
  import type { DataTransformOperation } from '@prisma-next/framework-components/control';
17
16
  import { SqlQueryError } from '@prisma-next/sql-errors';
18
17
  import { ifDefined } from '@prisma-next/utils/defined';
@@ -23,7 +22,7 @@ import { normalizeSchemaNativeType } from '../native-type-normalizer';
23
22
  import type { PostgresPlanTargetDetails } from './planner-target-details';
24
23
  import {
25
24
  buildLedgerInsertStatement,
26
- buildWriteMarkerStatements,
25
+ buildMergeMarkerStatements,
27
26
  ensureLedgerTableStatement,
28
27
  ensureMarkerTableStatement,
29
28
  ensurePrismaContractSchemaStatement,
@@ -120,7 +119,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
120
119
  try {
121
120
  await this.acquireLock(driver, lockKey);
122
121
  await this.ensureControlTables(driver);
123
- const existingMarker = await readMarker(driver);
122
+ const existingMarker = await this.family.readMarker({ driver });
124
123
 
125
124
  // Validate plan origin matches existing marker (needs marker from DB)
126
125
  const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
@@ -128,9 +127,14 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
128
127
  return markerCheck;
129
128
  }
130
129
 
131
- // db update (origin: null) always applies; migration-apply (origin set) skips if marker matches.
130
+ // db update (origin: null) always applies; migration-apply (origin set,
131
+ // origin !== destination) skips if marker already matches destination.
132
+ // Self-edges (origin === destination) intentionally bypass the skip:
133
+ // the migration is data-only, and the data transform's own check
134
+ // decides whether `run` fires.
132
135
  const markerAtDestination = this.markerMatchesDestination(existingMarker, options.plan);
133
- const skipOperations = markerAtDestination && options.plan.origin != null;
136
+ const isSelfEdge = options.plan.origin?.storageHash === options.plan.destination.storageHash;
137
+ const skipOperations = markerAtDestination && options.plan.origin != null && !isSelfEdge;
134
138
  let applyValue: ApplyPlanSuccessValue;
135
139
 
136
140
  if (skipOperations) {
@@ -170,9 +174,39 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
170
174
  });
171
175
  }
172
176
 
173
- // Record marker and ledger entries
174
- await this.upsertMarker(driver, options, existingMarker);
175
- await this.recordLedgerEntry(driver, options, existingMarker, applyValue.executedOperations);
177
+ // Self-edge no-op detection: a self-edge migration with zero ops in
178
+ // the plan that brings no new invariants produced no observable
179
+ // change. Skip the marker + ledger writes so an idempotent re-apply
180
+ // of a self-edge data transform doesn't churn updatedAt or pile up
181
+ // empty ledger entries. db update no-ops still write a ledger entry
182
+ // as audit trail.
183
+ //
184
+ // TODO(invariant-routing follow-up): `executeDataTransform` always
185
+ // counts every op it visits (including self-skips via `check === true`
186
+ // or empty idempotency probe), so `operationsExecuted === 0` here
187
+ // means "the plan had zero ops" rather than "every op self-skipped".
188
+ // The CLI is unaffected today because `migration-apply.ts` marker-
189
+ // subtraction empties `effectiveRequired` first and short-circuits
190
+ // before we run; the non-CLI re-apply path needs a per-op `executed`
191
+ // flag threaded through `executeDataTransform` to recover the
192
+ // intended check. See review thread A13 / future M5 ADR draft.
193
+ const incomingInvariants = options.plan.providedInvariants;
194
+ const existingInvariants = new Set(existingMarker?.invariants ?? []);
195
+ const incomingIsSubsetOfExisting = incomingInvariants.every((id) =>
196
+ existingInvariants.has(id),
197
+ );
198
+ const isSelfEdgeNoOp =
199
+ isSelfEdge && applyValue.operationsExecuted === 0 && incomingIsSubsetOfExisting;
200
+
201
+ if (!isSelfEdgeNoOp) {
202
+ await this.upsertMarker(driver, options, existingMarker);
203
+ await this.recordLedgerEntry(
204
+ driver,
205
+ options,
206
+ existingMarker,
207
+ applyValue.executedOperations,
208
+ );
209
+ }
176
210
 
177
211
  await this.commitTransaction(driver);
178
212
  committed = true;
@@ -617,7 +651,8 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
617
651
  options: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>,
618
652
  existingMarker: ContractMarkerRecord | null,
619
653
  ): Promise<void> {
620
- const writeStatements = buildWriteMarkerStatements({
654
+ const incomingInvariants = options.plan.providedInvariants;
655
+ const writeStatements = buildMergeMarkerStatements({
621
656
  storageHash: options.plan.destination.storageHash,
622
657
  profileHash:
623
658
  options.plan.destination.profileHash ??
@@ -626,6 +661,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
626
661
  contractJson: options.destinationContract,
627
662
  canonicalVersion: null,
628
663
  meta: {},
664
+ invariants: incomingInvariants,
629
665
  });
630
666
  const statement = existingMarker ? writeStatements.update : writeStatements.insert;
631
667
  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
  },
@@ -1,6 +1,6 @@
1
1
  export type { SqlStatement } from '../core/migrations/statement-builders';
2
2
  export {
3
- buildWriteMarkerStatements,
3
+ buildMergeMarkerStatements,
4
4
  ensureLedgerTableStatement,
5
5
  ensureMarkerTableStatement,
6
6
  ensurePrismaContractSchemaStatement,