@prisma-next/target-postgres 0.14.0-dev.55 → 0.14.0-dev.57

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 (117) hide show
  1. package/dist/{authoring-BBIGcFZM.mjs → authoring-WkqG_MkU.mjs} +38 -6
  2. package/dist/authoring-WkqG_MkU.mjs.map +1 -0
  3. package/dist/contract-free.d.mts +27 -2
  4. package/dist/contract-free.d.mts.map +1 -1
  5. package/dist/contract-free.mjs +3 -3
  6. package/dist/{control-policy-DievMCOV.mjs → control-policy-BtOZBVxL.mjs} +38 -13
  7. package/dist/control-policy-BtOZBVxL.mjs.map +1 -0
  8. package/dist/control.mjs +6 -6
  9. package/dist/{ddl-D5j4JiAB.mjs → ddl-B-krEXY1.mjs} +18 -4
  10. package/dist/{ddl-D5j4JiAB.mjs.map → ddl-B-krEXY1.mjs.map} +1 -1
  11. package/dist/ddl.d.mts +2 -2
  12. package/dist/ddl.mjs +2 -2
  13. package/dist/{descriptor-meta-C-tyS25s.mjs → descriptor-meta-BOcKPaLE.mjs} +4 -3
  14. package/dist/descriptor-meta-BOcKPaLE.mjs.map +1 -0
  15. package/dist/{diff-database-schema-ff3ioNtK.mjs → diff-database-schema-CWb-jeLy.mjs} +19 -11
  16. package/dist/diff-database-schema-CWb-jeLy.mjs.map +1 -0
  17. package/dist/diff-database-schema.d.mts +1 -1
  18. package/dist/diff-database-schema.d.mts.map +1 -1
  19. package/dist/diff-database-schema.mjs +1 -1
  20. package/dist/issue-planner.d.mts +1 -1
  21. package/dist/issue-planner.d.mts.map +1 -1
  22. package/dist/issue-planner.mjs +1 -1
  23. package/dist/migration.d.mts +2 -2
  24. package/dist/migration.mjs +2 -2
  25. package/dist/{nodes-Dh__NS58.mjs → nodes-B815pj_X.mjs} +34 -2
  26. package/dist/{nodes-Dh__NS58.mjs.map → nodes-B815pj_X.mjs.map} +1 -1
  27. package/dist/{nodes-CSkPX3rI.d.mts → nodes-BtQyZha6.d.mts} +29 -3
  28. package/dist/nodes-BtQyZha6.d.mts.map +1 -0
  29. package/dist/{op-factory-call-Dt-Jx5JS.d.mts → op-factory-call-D6_7-7Vc.d.mts} +24 -2
  30. package/dist/{op-factory-call-Dt-Jx5JS.d.mts.map → op-factory-call-D6_7-7Vc.d.mts.map} +1 -1
  31. package/dist/{op-factory-call-DZWTTjKM.mjs → op-factory-call-DxNNKUeZ.mjs} +99 -4
  32. package/dist/op-factory-call-DxNNKUeZ.mjs.map +1 -0
  33. package/dist/op-factory-call.d.mts +1 -1
  34. package/dist/op-factory-call.mjs +1 -1
  35. package/dist/pack.d.mts +29 -2
  36. package/dist/pack.d.mts.map +1 -1
  37. package/dist/pack.mjs +1 -1
  38. package/dist/{planner-DWQYPxP8.mjs → planner-BryLk0Ie.mjs} +80 -24
  39. package/dist/planner-BryLk0Ie.mjs.map +1 -0
  40. package/dist/{planner-produced-postgres-migration-Ch1L8RiV.mjs → planner-produced-postgres-migration-BLJH1zIc.mjs} +2 -2
  41. package/dist/{planner-produced-postgres-migration-Ch1L8RiV.mjs.map → planner-produced-postgres-migration-BLJH1zIc.mjs.map} +1 -1
  42. package/dist/{planner-produced-postgres-migration-C0a7I1-e.d.mts → planner-produced-postgres-migration-CGifB6VH.d.mts} +2 -2
  43. package/dist/{planner-produced-postgres-migration-C0a7I1-e.d.mts.map → planner-produced-postgres-migration-CGifB6VH.d.mts.map} +1 -1
  44. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  45. package/dist/planner-produced-postgres-migration.mjs +1 -1
  46. package/dist/{planner-sql-checks-D8Q5ck4o.mjs → planner-sql-checks-AXoxB1Jl.mjs} +2 -2
  47. package/dist/{planner-sql-checks-D8Q5ck4o.mjs.map → planner-sql-checks-AXoxB1Jl.mjs.map} +1 -1
  48. package/dist/planner-sql-checks.mjs +1 -1
  49. package/dist/planner.d.mts +23 -6
  50. package/dist/planner.d.mts.map +1 -1
  51. package/dist/planner.mjs +2 -2
  52. package/dist/{postgres-contract-view-C-By7rOB.mjs → postgres-contract-view-Dg-N7KA7.mjs} +4 -3
  53. package/dist/postgres-contract-view-Dg-N7KA7.mjs.map +1 -0
  54. package/dist/{postgres-database-schema-node-UpzA0Ug5.d.mts → postgres-database-schema-node-CfdTY-Qs.d.mts} +18 -3
  55. package/dist/postgres-database-schema-node-CfdTY-Qs.d.mts.map +1 -0
  56. package/dist/{postgres-migration-D3XACWKM.mjs → postgres-migration-BahMHTbj.mjs} +3 -3
  57. package/dist/{postgres-migration-D3XACWKM.mjs.map → postgres-migration-BahMHTbj.mjs.map} +1 -1
  58. package/dist/{postgres-migration-DFaxuRyI.d.mts → postgres-migration-cyBe2Ee0.d.mts} +2 -2
  59. package/dist/{postgres-migration-DFaxuRyI.d.mts.map → postgres-migration-cyBe2Ee0.d.mts.map} +1 -1
  60. package/dist/{postgres-table-schema-node-BZWWEo7B.mjs → postgres-role-schema-node-BiSTCTNg.mjs} +4 -69
  61. package/dist/postgres-role-schema-node-BiSTCTNg.mjs.map +1 -0
  62. package/dist/{postgres-schema-9vFZLwGo.d.mts → postgres-schema-CU4gF6QZ.d.mts} +30 -2
  63. package/dist/postgres-schema-CU4gF6QZ.d.mts.map +1 -0
  64. package/dist/{postgres-schema-DcG72fcT.mjs → postgres-schema-DTLA_aRi.mjs} +41 -2
  65. package/dist/postgres-schema-DTLA_aRi.mjs.map +1 -0
  66. package/dist/postgres-table-schema-node-9FCtCUz2.mjs +153 -0
  67. package/dist/postgres-table-schema-node-9FCtCUz2.mjs.map +1 -0
  68. package/dist/{resolve-ddl-schema-BOtCoeKV.mjs → resolve-ddl-schema-Ca88Tg6W.mjs} +2 -2
  69. package/dist/{resolve-ddl-schema-BOtCoeKV.mjs.map → resolve-ddl-schema-Ca88Tg6W.mjs.map} +1 -1
  70. package/dist/rls-canonicalize.d.mts +19 -1
  71. package/dist/rls-canonicalize.d.mts.map +1 -0
  72. package/dist/rls-canonicalize.mjs +2 -2
  73. package/dist/runtime.d.mts.map +1 -1
  74. package/dist/runtime.mjs +1 -1
  75. package/dist/schema-ir-annotations.mjs +1 -1
  76. package/dist/types.d.mts +3 -3
  77. package/dist/types.mjs +4 -3
  78. package/dist/{canonicalize-Bn3BM1Cn.mjs → wire-name-rHHtr1h0.mjs} +31 -2
  79. package/dist/wire-name-rHHtr1h0.mjs.map +1 -0
  80. package/package.json +20 -20
  81. package/src/contract-free/ddl.ts +26 -0
  82. package/src/core/authoring.ts +44 -1
  83. package/src/core/ddl/nodes.ts +48 -1
  84. package/src/core/descriptor-meta.ts +2 -0
  85. package/src/core/entity-kinds.ts +11 -0
  86. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +9 -0
  87. package/src/core/migrations/control-policy.ts +31 -1
  88. package/src/core/migrations/diff-database-schema.ts +1 -0
  89. package/src/core/migrations/issue-planner.ts +49 -4
  90. package/src/core/migrations/op-factory-call.ts +81 -1
  91. package/src/core/migrations/operations/rls.ts +82 -1
  92. package/src/core/migrations/planner.ts +110 -26
  93. package/src/core/postgres-contract-serializer.ts +7 -1
  94. package/src/core/postgres-rls-enablement.ts +34 -0
  95. package/src/core/postgres-schema.ts +19 -3
  96. package/src/core/postgres-validators.ts +6 -0
  97. package/src/core/rls/wire-name.ts +33 -0
  98. package/src/core/schema-ir/postgres-table-schema-node.ts +25 -3
  99. package/src/exports/contract-free.ts +4 -0
  100. package/src/exports/ddl.ts +2 -0
  101. package/src/exports/rls-canonicalize.ts +1 -0
  102. package/src/exports/types.ts +4 -0
  103. package/dist/authoring-BBIGcFZM.mjs.map +0 -1
  104. package/dist/canonicalize-Bn3BM1Cn.mjs.map +0 -1
  105. package/dist/control-policy-DievMCOV.mjs.map +0 -1
  106. package/dist/descriptor-meta-C-tyS25s.mjs.map +0 -1
  107. package/dist/diff-database-schema-ff3ioNtK.mjs.map +0 -1
  108. package/dist/nodes-CSkPX3rI.d.mts.map +0 -1
  109. package/dist/op-factory-call-DZWTTjKM.mjs.map +0 -1
  110. package/dist/planner-DWQYPxP8.mjs.map +0 -1
  111. package/dist/postgres-contract-view-C-By7rOB.mjs.map +0 -1
  112. package/dist/postgres-database-schema-node-UpzA0Ug5.d.mts.map +0 -1
  113. package/dist/postgres-schema-9vFZLwGo.d.mts.map +0 -1
  114. package/dist/postgres-schema-DcG72fcT.mjs.map +0 -1
  115. package/dist/postgres-table-schema-node-BZWWEo7B.mjs.map +0 -1
  116. package/dist/schema-node-kinds-ClScchhi.mjs +0 -76
  117. package/dist/schema-node-kinds-ClScchhi.mjs.map +0 -1
@@ -63,6 +63,7 @@ import {
63
63
  CreateIndexCall,
64
64
  CreateSchemaCall,
65
65
  CreateTableCall,
66
+ DisableRowLevelSecurityCall,
66
67
  DropCheckConstraintCall,
67
68
  DropColumnCall,
68
69
  DropConstraintCall,
@@ -70,6 +71,7 @@ import {
70
71
  DropIndexCall,
71
72
  DropNotNullCall,
72
73
  DropTableCall,
74
+ EnableRowLevelSecurityCall,
73
75
  type PostgresOpFactoryCall,
74
76
  SetDefaultCall,
75
77
  SetNotNullCall,
@@ -163,6 +165,7 @@ function classifyCall(call: PostgresOpFactoryCall): CallCategory {
163
165
  case 'createTable':
164
166
  return 'table';
165
167
  case 'enableRowLevelSecurity':
168
+ case 'disableRowLevelSecurity':
166
169
  return 'rlsEnable';
167
170
  case 'createRlsPolicy':
168
171
  return 'rlsPolicy';
@@ -465,6 +468,7 @@ function fkSpecFromNode(fk: SqlForeignKeyIR, tableName: string): ForeignKeySpec
465
468
  */
466
469
  function buildCreateTableCallsFromNode(
467
470
  schemaName: string,
471
+ ddlSchemaName: string,
468
472
  table: PostgresTableSchemaNode,
469
473
  codecHooks: ReadonlyMap<string, CodecControlHooks>,
470
474
  ): PostgresOpFactoryCall[] {
@@ -509,6 +513,13 @@ function buildCreateTableCallsFromNode(
509
513
  const constraintName = unique.name ?? `${table.name}_${unique.columns.join('_')}_key`;
510
514
  calls.push(new AddUniqueCall(schemaName, table.name, constraintName, [...unique.columns]));
511
515
  }
516
+ // Marker-driven: a newly-created table that is RLS-controlled enables RLS
517
+ // as part of its creation bundle. The policy set never decides this. The
518
+ // resolved schema (not the emission sentinel) binds into the op's
519
+ // relrowsecurity checks.
520
+ if (table.rlsEnabled) {
521
+ calls.push(new EnableRowLevelSecurityCall(ddlSchemaName, table.name));
522
+ }
512
523
  return calls;
513
524
  }
514
525
 
@@ -519,6 +530,11 @@ function nodeConflict(kind: SqlPlannerConflict['kind'], message: string): SqlPla
519
530
  function mapTableNodeIssue(
520
531
  issue: SchemaDiffIssue,
521
532
  schemaName: string,
533
+ // The diff tree's RESOLVED physical schema. Enablement ops bind it into
534
+ // their `pg_class`/`pg_namespace` checks (the emission sentinel would
535
+ // never match a live nspname), mirroring the schema the retired
536
+ // policy-half enable resolved via `resolveDdlSchemaForNamespaceStorage`.
537
+ ddlSchemaName: string,
522
538
  codecHooks: ReadonlyMap<string, CodecControlHooks>,
523
539
  ): Result<readonly PostgresOpFactoryCall[], SqlPlannerConflict> {
524
540
  if (issue.reason === 'not-found') {
@@ -526,7 +542,7 @@ function mapTableNodeIssue(
526
542
  PostgresTableSchemaNode,
527
543
  'a not-found table issue always carries the expected PostgresTableSchemaNode'
528
544
  >(issue.expected);
529
- return ok(buildCreateTableCallsFromNode(schemaName, table, codecHooks));
545
+ return ok(buildCreateTableCallsFromNode(schemaName, ddlSchemaName, table, codecHooks));
530
546
  }
531
547
  if (issue.reason === 'not-expected') {
532
548
  const table = blindCast<
@@ -535,8 +551,33 @@ function mapTableNodeIssue(
535
551
  >(issue.actual);
536
552
  return ok([new DropTableCall(schemaName, table.name)]);
537
553
  }
538
- // Unreachable: PostgresTableSchemaNode.isEqualTo is identity.
539
- return notOk(nodeConflict('unsupportedOperation', `Unexpected table drift: ${issue.message}`));
554
+ // A paired table `not-equal` means enablement drift TODAY, because
555
+ // `isEqualTo` compares only name + `rlsEnabled`. Key on the actual
556
+ // expected-vs-actual `rlsEnabled` delta rather than assuming it: emit ENABLE
557
+ // when it flipped on, DISABLE when it flipped off, and fail loud when
558
+ // `rlsEnabled` matches on both sides — that means a second table attribute
559
+ // drifted into `isEqualTo` and this mapper does not yet handle it. The
560
+ // expected side is authoritative (marker-driven, never the policy set).
561
+ const expected = blindCast<
562
+ PostgresTableSchemaNode,
563
+ 'a not-equal table issue always carries the expected PostgresTableSchemaNode'
564
+ >(issue.expected);
565
+ const actual = blindCast<
566
+ PostgresTableSchemaNode,
567
+ 'a not-equal table issue always carries the actual PostgresTableSchemaNode'
568
+ >(issue.actual);
569
+ if (expected.rlsEnabled && !actual.rlsEnabled) {
570
+ return ok([new EnableRowLevelSecurityCall(ddlSchemaName, expected.name)]);
571
+ }
572
+ if (!expected.rlsEnabled && actual.rlsEnabled) {
573
+ return ok([new DisableRowLevelSecurityCall(ddlSchemaName, expected.name)]);
574
+ }
575
+ return notOk(
576
+ nodeConflict(
577
+ 'unsupportedOperation',
578
+ `unhandled table-attribute drift on "${expected.name}": table not-equal with no rlsEnabled delta (a second table attribute drifted)`,
579
+ ),
580
+ );
540
581
  }
541
582
 
542
583
  function mapColumnNodeIssue(
@@ -787,7 +828,7 @@ export function mapNodeIssueToCall(
787
828
 
788
829
  switch (node.nodeKind) {
789
830
  case PostgresSchemaNodeKind.table:
790
- return mapTableNodeIssue(issue, schemaName, ctx.codecHooks);
831
+ return mapTableNodeIssue(issue, schemaName, ddlSchemaName, ctx.codecHooks);
791
832
  case RelationalSchemaNodeKind.column:
792
833
  return mapColumnNodeIssue(issue, schemaName, tableName, ctx.codecHooks);
793
834
  case RelationalSchemaNodeKind.columnDefault: {
@@ -952,6 +993,10 @@ export function planIssues(
952
993
  ...byCategory('unique'),
953
994
  ...byCategory('index'),
954
995
  ...byCategory('foreignKey'),
996
+ // Enablement changes run after all relational DDL (the table must exist)
997
+ // and before the policy calls the planner appends after `planIssues` —
998
+ // the same position the retired imperative enable-on-first-policy used.
999
+ ...byCategory('rlsEnable'),
955
1000
  ];
956
1001
 
957
1002
  return ok({ calls });
@@ -63,7 +63,13 @@ import {
63
63
  } from './operations/constraints';
64
64
  import { createExtension } from './operations/dependencies';
65
65
  import { createIndex, dropIndex } from './operations/indexes';
66
- import { createRlsPolicy, dropRlsPolicy, enableRowLevelSecurity } from './operations/rls';
66
+ import {
67
+ createRlsPolicy,
68
+ disableRowLevelSecurity,
69
+ dropRlsPolicy,
70
+ enableRowLevelSecurity,
71
+ renameRlsPolicy,
72
+ } from './operations/rls';
67
73
  import type { ForeignKeySpec } from './operations/shared';
68
74
  import { step, targetDetails } from './operations/shared';
69
75
  import { dropTable } from './operations/tables';
@@ -1461,6 +1467,78 @@ export class EnableRowLevelSecurityCall extends PostgresOpFactoryCallNode {
1461
1467
  }
1462
1468
  }
1463
1469
 
1470
+ export class DisableRowLevelSecurityCall extends PostgresOpFactoryCallNode {
1471
+ readonly factoryName = 'disableRowLevelSecurity' as const;
1472
+ readonly operationClass = 'destructive' as const;
1473
+ readonly schemaName: string;
1474
+ readonly tableName: string;
1475
+ readonly label: string;
1476
+
1477
+ constructor(schemaName: string, tableName: string) {
1478
+ super();
1479
+ this.schemaName = schemaName;
1480
+ this.tableName = tableName;
1481
+ this.label = `Disable row-level security on "${tableName}"`;
1482
+ this.freeze();
1483
+ }
1484
+
1485
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1486
+ if (lowerer === undefined) {
1487
+ throw new Error(
1488
+ `DisableRowLevelSecurityCall.toOp: a lowerer is required on the Postgres planner path (table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1489
+ );
1490
+ }
1491
+ return disableRowLevelSecurity(this.schemaName, this.tableName, lowerer);
1492
+ }
1493
+
1494
+ renderTypeScript(): string {
1495
+ return `disableRowLevelSecurity(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)})`;
1496
+ }
1497
+ }
1498
+
1499
+ export class RenamePostgresRlsPolicyCall extends PostgresOpFactoryCallNode {
1500
+ readonly factoryName = 'renameRlsPolicy' as const;
1501
+ // `widening` is chosen so the rename plans under every allowance set except
1502
+ // additive-only init — a rename is neither additive-creation nor
1503
+ // destructive, and the class vocabulary has no neutral middle class. It is
1504
+ // NOT that a rename widens anything; this is the accepted typology tradeoff.
1505
+ readonly operationClass = 'widening' as const;
1506
+ readonly schemaName: string;
1507
+ readonly tableName: string;
1508
+ readonly oldPolicyName: string;
1509
+ readonly newPolicyName: string;
1510
+ readonly label: string;
1511
+
1512
+ constructor(schemaName: string, tableName: string, oldPolicyName: string, newPolicyName: string) {
1513
+ super();
1514
+ this.schemaName = schemaName;
1515
+ this.tableName = tableName;
1516
+ this.oldPolicyName = oldPolicyName;
1517
+ this.newPolicyName = newPolicyName;
1518
+ this.label = `Rename RLS policy "${oldPolicyName}" to "${newPolicyName}" on "${tableName}"`;
1519
+ this.freeze();
1520
+ }
1521
+
1522
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1523
+ if (lowerer === undefined) {
1524
+ throw new Error(
1525
+ `RenamePostgresRlsPolicyCall.toOp: a lowerer is required on the Postgres planner path (policy "${this.oldPolicyName}" on table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1526
+ );
1527
+ }
1528
+ return renameRlsPolicy(
1529
+ this.schemaName,
1530
+ this.tableName,
1531
+ this.oldPolicyName,
1532
+ this.newPolicyName,
1533
+ lowerer,
1534
+ );
1535
+ }
1536
+
1537
+ renderTypeScript(): string {
1538
+ return `renameRlsPolicy(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.oldPolicyName)}, ${jsonToTsSource(this.newPolicyName)})`;
1539
+ }
1540
+ }
1541
+
1464
1542
  export type PostgresOpFactoryCall =
1465
1543
  | CreateTableCall
1466
1544
  | DropTableCall
@@ -1487,4 +1565,6 @@ export type PostgresOpFactoryCall =
1487
1565
  | CreatePostgresRlsPolicyCall
1488
1566
  | DropPostgresRlsPolicyCall
1489
1567
  | EnableRowLevelSecurityCall
1568
+ | DisableRowLevelSecurityCall
1569
+ | RenamePostgresRlsPolicyCall
1490
1570
  | DataTransformCall;
@@ -1,7 +1,12 @@
1
1
  import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
2
2
  import { ifDefined } from '@prisma-next/utils/defined';
3
3
  import { rlsEnabledAst, rlsPolicyExistsAst } from '../../../contract-free/checks';
4
- import { createPolicy, dropPolicy } from '../../../contract-free/ddl';
4
+ import {
5
+ alterPolicyRename,
6
+ createPolicy,
7
+ disableRowLevelSecurity as disableRowLevelSecurityDdl,
8
+ dropPolicy,
9
+ } from '../../../contract-free/ddl';
5
10
  import type { PostgresRlsPolicy } from '../../postgres-rls-policy';
6
11
  import { qualifyTableName } from '../planner-sql-checks';
7
12
  import { type Op, step, targetDetails } from './shared';
@@ -104,3 +109,79 @@ export async function enableRowLevelSecurity(
104
109
  ],
105
110
  };
106
111
  }
112
+
113
+ export async function disableRowLevelSecurity(
114
+ schemaName: string,
115
+ tableName: string,
116
+ lowerer: ExecuteRequestLowerer,
117
+ ): Promise<Op> {
118
+ const ddlNode = disableRowLevelSecurityDdl({ schema: schemaName, table: tableName });
119
+ const checks = rlsEnabledAst(schemaName, tableName);
120
+ const enabled = await lowerer.lowerToExecuteRequest(checks.rlsEnabled());
121
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
122
+ const disabled = await lowerer.lowerToExecuteRequest(checks.rlsDisabled());
123
+ return {
124
+ id: `rowLevelSecurity.${schemaName}.${tableName}.disable`,
125
+ label: `Disable row-level security on "${tableName}"`,
126
+ operationClass: 'destructive',
127
+ target: targetDetails('rowLevelSecurity', tableName, schemaName),
128
+ precheck: [
129
+ step(`check RLS is currently enabled on "${tableName}"`, enabled.sql, enabled.params),
130
+ ],
131
+ execute: [step(`disable row-level security on "${tableName}"`, execute.sql, execute.params)],
132
+ postcheck: [
133
+ step(
134
+ `verify row-level security is disabled on "${tableName}"`,
135
+ disabled.sql,
136
+ disabled.params,
137
+ ),
138
+ ],
139
+ };
140
+ }
141
+
142
+ export async function renameRlsPolicy(
143
+ schemaName: string,
144
+ tableName: string,
145
+ oldPolicyName: string,
146
+ newPolicyName: string,
147
+ lowerer: ExecuteRequestLowerer,
148
+ ): Promise<Op> {
149
+ const ddlNode = alterPolicyRename({
150
+ schema: schemaName,
151
+ table: tableName,
152
+ name: oldPolicyName,
153
+ newName: newPolicyName,
154
+ });
155
+ const oldChecks = rlsPolicyExistsAst({
156
+ schema: schemaName,
157
+ table: tableName,
158
+ policyName: oldPolicyName,
159
+ });
160
+ const newChecks = rlsPolicyExistsAst({
161
+ schema: schemaName,
162
+ table: tableName,
163
+ policyName: newPolicyName,
164
+ });
165
+ const oldPresent = await lowerer.lowerToExecuteRequest(oldChecks.policyPresent());
166
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
167
+ const newPresent = await lowerer.lowerToExecuteRequest(newChecks.policyPresent());
168
+ return {
169
+ id: `rlsPolicy.${schemaName}.${tableName}.${oldPolicyName}.rename`,
170
+ label: `Rename RLS policy "${oldPolicyName}" to "${newPolicyName}" on "${tableName}"`,
171
+ operationClass: 'widening',
172
+ target: targetDetails('rlsPolicy', newPolicyName, schemaName, tableName),
173
+ precheck: [
174
+ step(`ensure RLS policy "${oldPolicyName}" exists`, oldPresent.sql, oldPresent.params),
175
+ ],
176
+ execute: [
177
+ step(
178
+ `rename RLS policy "${oldPolicyName}" to "${newPolicyName}"`,
179
+ execute.sql,
180
+ execute.params,
181
+ ),
182
+ ],
183
+ postcheck: [
184
+ step(`verify RLS policy "${newPolicyName}" exists`, newPresent.sql, newPresent.params),
185
+ ],
186
+ };
187
+ }
@@ -27,6 +27,7 @@ import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
27
27
  import { blindCast } from '@prisma-next/utils/casts';
28
28
  import { ifDefined } from '@prisma-next/utils/defined';
29
29
  import { PostgresRlsPolicy } from '../postgres-rls-policy';
30
+ import { parseRlsPolicyWireName } from '../rls/wire-name';
30
31
  import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
31
32
  import { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
32
33
  import { PostgresSchemaNodeKind, type SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
@@ -49,7 +50,7 @@ import type { PostgresOpFactoryCall } from './op-factory-call';
49
50
  import {
50
51
  CreatePostgresRlsPolicyCall,
51
52
  DropPostgresRlsPolicyCall,
52
- EnableRowLevelSecurityCall,
53
+ RenamePostgresRlsPolicyCall,
53
54
  } from './op-factory-call';
54
55
  import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
55
56
  import { postgresPlannerStrategies } from './planner-strategies';
@@ -335,17 +336,40 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
335
336
 
336
337
  /**
337
338
  * Maps the RLS policy presence findings of the one combined tree diff
338
- * (`buildPostgresPlanDiff`, already ownership-filtered) into `ENABLE RLS`
339
- * / `CREATE POLICY` / `DROP POLICY` ops. It does not re-diff it consumes
340
- * exactly the policy-node subset of the shared diff's issues.
339
+ * (`buildPostgresPlanDiff`, already ownership-filtered) into
340
+ * `CREATE POLICY` / `DROP POLICY` / `ALTER POLICY RENAME TO` ops. It
341
+ * does not re-diff — it consumes exactly the policy-node subset of the
342
+ * shared diff's issues. Enablement is NOT decided here: `ENABLE`/`DISABLE
343
+ * ROW LEVEL SECURITY` derive from the table's marker-driven `rlsEnabled`
344
+ * attribute diff on the relational side.
345
+ *
346
+ * Rename post-pass: a `not-found` and a `not-expected` policy on the SAME
347
+ * table whose wire-name content hashes match but prefixes differ are one
348
+ * prefix-only rename, collapsed into a single non-destructive
349
+ * `RenamePostgresRlsPolicyCall`. Multi-candidate hash groups pair
350
+ * deterministically by sorted wire name; leftovers proceed as
351
+ * create/drop. Unparseable wire names never pair.
352
+ *
353
+ * The pairing runs only when the policy allows `widening` (rename's
354
+ * class). Without it (db-init's additive-only set), pairing degrades
355
+ * deliberately to the additive half: the new name is CREATEd and the old
356
+ * policy survives live until a widening/destructive-allowed plan runs —
357
+ * emitting an ungated widening rename would only fail at the runner's
358
+ * class re-enforcement.
341
359
  */
342
360
  private planPostgresSchemaDiff(
343
361
  options: PlannerOptionsWithComponents,
344
362
  filteredDiffIssues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
345
363
  ): readonly PostgresOpFactoryCall[] {
346
364
  const allowsDestructive = options.policy.allowedOperationClasses.includes('destructive');
347
- const calls: PostgresOpFactoryCall[] = [];
348
- const seenEnableTables = new Set<string>();
365
+ const allowsWidening = options.policy.allowedOperationClasses.includes('widening');
366
+
367
+ interface PolicyFinding {
368
+ readonly node: PostgresPolicySchemaNode;
369
+ readonly schemaForTable: string;
370
+ }
371
+ const missing: PolicyFinding[] = [];
372
+ const extra: PolicyFinding[] = [];
349
373
 
350
374
  for (const issue of filteredDiffIssues) {
351
375
  // 'not-equal' is unreachable for content-addressed policies: the wire name
@@ -356,30 +380,90 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
356
380
  PostgresPolicySchemaNode.assert(expected);
357
381
  // expected.namespaceId is the DDL schema name (resolved during projection);
358
382
  // this re-resolution is a no-op as long as PostgresSchema.ddlSchemaName() returns this.id.
359
- const schemaForTable = resolveDdlSchemaForNamespaceStorage(
360
- options.contract.storage,
361
- expected.namespaceId,
362
- );
363
- const tableKey = `${schemaForTable}.${expected.tableName}`;
364
- if (!seenEnableTables.has(tableKey)) {
365
- seenEnableTables.add(tableKey);
366
- calls.push(new EnableRowLevelSecurityCall(schemaForTable, expected.tableName));
367
- }
368
- calls.push(
369
- new CreatePostgresRlsPolicyCall(
370
- schemaForTable,
371
- expected.tableName,
372
- policyNodeToContractPolicy(expected),
383
+ missing.push({
384
+ node: expected,
385
+ schemaForTable: resolveDdlSchemaForNamespaceStorage(
386
+ options.contract.storage,
387
+ expected.namespaceId,
373
388
  ),
374
- );
375
- } else if (issue.reason === 'not-expected' && allowsDestructive) {
389
+ });
390
+ } else if (issue.reason === 'not-expected') {
376
391
  const actual = issue.actual;
377
392
  PostgresPolicySchemaNode.assert(actual);
378
- const schemaForTable = resolveDdlSchemaForNamespaceStorage(
379
- options.contract.storage,
380
- actual.namespaceId,
393
+ extra.push({
394
+ node: actual,
395
+ schemaForTable: resolveDdlSchemaForNamespaceStorage(
396
+ options.contract.storage,
397
+ actual.namespaceId,
398
+ ),
399
+ });
400
+ }
401
+ }
402
+
403
+ const calls: PostgresOpFactoryCall[] = [];
404
+ const renamedExtras = new Set<PolicyFinding>();
405
+ const renamedMissing = new Set<PolicyFinding>();
406
+ const pairingKey = (finding: PolicyFinding, hash: string): string =>
407
+ JSON.stringify([finding.schemaForTable, finding.node.tableName, hash]);
408
+
409
+ if (allowsWidening) {
410
+ const extrasByGroup = new Map<string, PolicyFinding[]>();
411
+ for (const finding of extra) {
412
+ const parsed = parseRlsPolicyWireName(finding.node.name);
413
+ if (parsed === undefined) continue;
414
+ const key = pairingKey(finding, parsed.hash);
415
+ const group = extrasByGroup.get(key) ?? [];
416
+ group.push(finding);
417
+ extrasByGroup.set(key, group);
418
+ }
419
+ for (const group of extrasByGroup.values()) {
420
+ group.sort((a, b) => (a.node.name < b.node.name ? -1 : a.node.name > b.node.name ? 1 : 0));
421
+ }
422
+
423
+ const sortedMissing = [...missing].sort((a, b) =>
424
+ a.node.name < b.node.name ? -1 : a.node.name > b.node.name ? 1 : 0,
425
+ );
426
+ for (const missingFinding of sortedMissing) {
427
+ const parsed = parseRlsPolicyWireName(missingFinding.node.name);
428
+ if (parsed === undefined) continue;
429
+ const candidates = extrasByGroup.get(pairingKey(missingFinding, parsed.hash));
430
+ const candidate = candidates?.shift();
431
+ if (candidate === undefined) continue;
432
+ // Same name would never surface as missing+extra (the differ pairs by
433
+ // name), so a matched candidate always differs in prefix only.
434
+ renamedExtras.add(candidate);
435
+ renamedMissing.add(missingFinding);
436
+ calls.push(
437
+ new RenamePostgresRlsPolicyCall(
438
+ missingFinding.schemaForTable,
439
+ missingFinding.node.tableName,
440
+ candidate.node.name,
441
+ missingFinding.node.name,
442
+ ),
443
+ );
444
+ }
445
+ }
446
+
447
+ for (const finding of missing) {
448
+ if (renamedMissing.has(finding)) continue;
449
+ calls.push(
450
+ new CreatePostgresRlsPolicyCall(
451
+ finding.schemaForTable,
452
+ finding.node.tableName,
453
+ policyNodeToContractPolicy(finding.node),
454
+ ),
455
+ );
456
+ }
457
+ if (allowsDestructive) {
458
+ for (const finding of extra) {
459
+ if (renamedExtras.has(finding)) continue;
460
+ calls.push(
461
+ new DropPostgresRlsPolicyCall(
462
+ finding.schemaForTable,
463
+ finding.node.tableName,
464
+ finding.node.name,
465
+ ),
381
466
  );
382
- calls.push(new DropPostgresRlsPolicyCall(schemaForTable, actual.tableName, actual.name));
383
467
  }
384
468
  }
385
469
 
@@ -19,7 +19,12 @@ import { blindCast } from '@prisma-next/utils/casts';
19
19
  import type { JsonObject } from '@prisma-next/utils/json';
20
20
  import { postgresAuthoringEntityTypes } from './authoring';
21
21
  import { PG_INT_CODEC_ID, PG_TEXT_CODEC_ID } from './codec-ids';
22
- import { nativeEnumEntityKind, policyEntityKind, roleEntityKind } from './entity-kinds';
22
+ import {
23
+ nativeEnumEntityKind,
24
+ policyEntityKind,
25
+ rlsEnablementEntityKind,
26
+ roleEntityKind,
27
+ } from './entity-kinds';
23
28
  import { PostgresSchema } from './postgres-schema';
24
29
 
25
30
  const POSTGRES_AUTHORING_CTX: AuthoringEntityContext = {
@@ -73,6 +78,7 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
73
78
  super(storageTypesHydrators, [
74
79
  policyEntityKind,
75
80
  roleEntityKind,
81
+ rlsEnablementEntityKind,
76
82
  nativeEnumEntityKind,
77
83
  ...extraPackEntityKinds,
78
84
  ]);
@@ -0,0 +1,34 @@
1
+ import { freezeNode } from '@prisma-next/framework-components/ir';
2
+ import { SqlNode } from '@prisma-next/sql-contract/types';
3
+
4
+ export interface PostgresRlsEnablementInput {
5
+ /** Name of the table this marker declares RLS-controlled. */
6
+ readonly tableName: string;
7
+ /** Namespace coordinate (schema name). Markers are schema-scoped like policies. */
8
+ readonly namespaceId: string;
9
+ }
10
+
11
+ /**
12
+ * Postgres contract-IR marker for `@@rls` on a model: the table named here is
13
+ * RLS-controlled (`ENABLE ROW LEVEL SECURITY` is driven by this marker, never
14
+ * by the policy set).
15
+ *
16
+ * This is an authored, serialized Contract-IR entity — it is registered as an
17
+ * entity kind, extends `SqlNode`, and is stored in `contract.json` under
18
+ * `entries.rls[tableName]`. Target-only concept — no SQL-family abstract.
19
+ * Frozen at construction via `freezeNode(this)`. The `kind: 'rls'`
20
+ * discriminant is enumerable so it survives JSON serialization; the literal
21
+ * matches the entries key (one-string rule).
22
+ */
23
+ export class PostgresRlsEnablement extends SqlNode {
24
+ override readonly kind = 'rls' as const;
25
+ readonly tableName: string;
26
+ readonly namespaceId: string;
27
+
28
+ constructor(input: PostgresRlsEnablementInput) {
29
+ super();
30
+ this.tableName = input.tableName;
31
+ this.namespaceId = input.namespaceId;
32
+ freezeNode(this);
33
+ }
34
+ }
@@ -19,8 +19,14 @@ import { blindCast } from '@prisma-next/utils/casts';
19
19
  import { ifDefined } from '@prisma-next/utils/defined';
20
20
  import { PostgresTableSource } from './ast/table-source';
21
21
  import { PG_TEXT_CODEC_ID } from './codec-ids';
22
- import { nativeEnumEntityKind, policyEntityKind, roleEntityKind } from './entity-kinds';
22
+ import {
23
+ nativeEnumEntityKind,
24
+ policyEntityKind,
25
+ rlsEnablementEntityKind,
26
+ roleEntityKind,
27
+ } from './entity-kinds';
23
28
  import type { PostgresNativeEnum } from './postgres-native-enum';
29
+ import type { PostgresRlsEnablement } from './postgres-rls-enablement';
24
30
  import type { PostgresRlsPolicy } from './postgres-rls-policy';
25
31
  import type { PostgresRole } from './postgres-role';
26
32
  import { escapeLiteral } from './sql-utils';
@@ -30,6 +36,7 @@ export type PostgresContract = Contract<SqlStorage> & { readonly target: 'postgr
30
36
  export type PostgresNamespaceEntries = SqlNamespaceEntries & {
31
37
  readonly policy?: Readonly<Record<string, PostgresRlsPolicy>>;
32
38
  readonly role?: Readonly<Record<string, PostgresRole>>;
39
+ readonly rls?: Readonly<Record<string, PostgresRlsEnablement>>;
33
40
  readonly native_enum?: Readonly<Record<string, PostgresNativeEnum>>;
34
41
  };
35
42
 
@@ -72,7 +79,12 @@ export class PostgresSchema extends SqlNamespaceBase {
72
79
 
73
80
  const dispatched = hydrateNamespaceEntities(
74
81
  input.entries,
75
- composeSqlEntityKinds([policyEntityKind, roleEntityKind, nativeEnumEntityKind]),
82
+ composeSqlEntityKinds([
83
+ policyEntityKind,
84
+ roleEntityKind,
85
+ rlsEnablementEntityKind,
86
+ nativeEnumEntityKind,
87
+ ]),
76
88
  'carry',
77
89
  );
78
90
 
@@ -90,7 +102,7 @@ export class PostgresSchema extends SqlNamespaceBase {
90
102
  this.entries = Object.freeze(
91
103
  blindCast<
92
104
  PostgresNamespaceEntries,
93
- 'composeSqlEntityKinds([policyEntityKind, roleEntityKind, nativeEnumEntityKind]) supplies table→StorageTable, valueSet→StorageValueSet, policy→PostgresRlsPolicy, role→PostgresRole, native_enum→PostgresNativeEnum descriptors'
105
+ 'composeSqlEntityKinds([policyEntityKind, roleEntityKind, rlsEnablementEntityKind, nativeEnumEntityKind]) supplies table→StorageTable, valueSet→StorageValueSet, policy→PostgresRlsPolicy, role→PostgresRole, rls→PostgresRlsEnablement, native_enum→PostgresNativeEnum descriptors'
94
106
  >(entriesInput),
95
107
  );
96
108
  Object.defineProperty(this, 'kind', {
@@ -118,6 +130,10 @@ export class PostgresSchema extends SqlNamespaceBase {
118
130
  return this.entries.role ?? Object.freeze({});
119
131
  }
120
132
 
133
+ get rls(): Readonly<Record<string, PostgresRlsEnablement>> {
134
+ return this.entries.rls ?? Object.freeze({});
135
+ }
136
+
121
137
  /**
122
138
  * The bare schema qualifier as it would appear in a rendered SQL
123
139
  * fragment (already quoted). The unbound-schema singleton overrides
@@ -19,6 +19,12 @@ export const PostgresRlsPolicySchema = type({
19
19
  permissive: 'boolean',
20
20
  });
21
21
 
22
+ export const PostgresRlsEnablementSchema = type({
23
+ kind: "'rls'",
24
+ tableName: 'string',
25
+ namespaceId: 'string',
26
+ });
27
+
22
28
  export const PostgresNativeEnumSchema = type({
23
29
  kind: "'postgres-enum'",
24
30
  typeName: 'string',
@@ -0,0 +1,33 @@
1
+ export interface RlsPolicyWireName {
2
+ /** The user-supplied part before the `_<8hex>` suffix. */
3
+ readonly prefix: string;
4
+ /** The 8-lowercase-hex content-hash suffix. */
5
+ readonly hash: string;
6
+ }
7
+
8
+ const WIRE_NAME_PATTERN = /^(.+)_([0-9a-f]{8})$/;
9
+
10
+ /**
11
+ * Assembles an RLS policy wire name from its user-supplied prefix and its
12
+ * 8-hex content-hash suffix. This module owns the `<prefix>_<hash>` format
13
+ * on both sides — construction here and parsing in
14
+ * {@link parseRlsPolicyWireName} — so the two never drift.
15
+ */
16
+ export function formatRlsPolicyWireName(prefix: string, hash: string): string {
17
+ return `${prefix}_${hash}`;
18
+ }
19
+
20
+ /**
21
+ * Splits an RLS policy wire name (`<prefix>_<8hex>`) into its prefix and
22
+ * content-hash suffix. Returns `undefined` when the name does not follow the
23
+ * wire-name shape (e.g. a policy created outside the toolchain) — callers
24
+ * treat such names as all-prefix. Consumed by introspection (prefix
25
+ * extraction) and by rename pairing (same hash, different prefix).
26
+ */
27
+ export function parseRlsPolicyWireName(name: string): RlsPolicyWireName | undefined {
28
+ const match = WIRE_NAME_PATTERN.exec(name);
29
+ const prefix = match?.[1];
30
+ const hash = match?.[2];
31
+ if (prefix === undefined || hash === undefined) return undefined;
32
+ return { prefix, hash };
33
+ }