@supabase/pg-delta 1.0.0-alpha.25 → 1.0.0-alpha.27

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 (62) hide show
  1. package/dist/cli/commands/catalog-export.js +22 -1
  2. package/dist/core/catalog.filter.d.ts +17 -0
  3. package/dist/core/catalog.filter.js +75 -0
  4. package/dist/core/catalog.model.js +9 -1
  5. package/dist/core/expand-replace-dependencies.js +1 -7
  6. package/dist/core/integrations/supabase.js +102 -11
  7. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +4 -0
  8. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +28 -2
  9. package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +4 -0
  10. package/dist/core/objects/foreign-data-wrapper/server/server.model.js +18 -1
  11. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +4 -0
  12. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +18 -1
  13. package/dist/core/objects/table/table.diff.js +53 -30
  14. package/dist/core/plan/hierarchy.js +4 -4
  15. package/dist/core/postgres-config.d.ts +7 -0
  16. package/dist/core/postgres-config.js +19 -5
  17. package/dist/core/sort/debug-visualization.js +1 -1
  18. package/dist/core/sort/topological-sort.js +2 -2
  19. package/package.json +34 -33
  20. package/src/cli/commands/catalog-export.ts +26 -1
  21. package/src/core/catalog.filter.ts +96 -0
  22. package/src/core/catalog.model.ts +10 -1
  23. package/src/core/catalog.snapshot.test.ts +1 -0
  24. package/src/core/expand-replace-dependencies.test.ts +12 -0
  25. package/src/core/expand-replace-dependencies.ts +1 -12
  26. package/src/core/integrations/supabase.test.ts +335 -0
  27. package/src/core/integrations/supabase.ts +102 -11
  28. package/src/core/objects/aggregate/changes/aggregate.base.ts +1 -1
  29. package/src/core/objects/collation/changes/collation.base.ts +1 -1
  30. package/src/core/objects/domain/changes/domain.base.ts +1 -1
  31. package/src/core/objects/extension/changes/extension.base.ts +1 -1
  32. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.base.ts +1 -1
  33. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.ts +1 -1
  34. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +28 -2
  35. package/src/core/objects/foreign-data-wrapper/server/changes/server.base.ts +1 -1
  36. package/src/core/objects/foreign-data-wrapper/server/server.model.ts +18 -1
  37. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.base.ts +1 -1
  38. package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +18 -1
  39. package/src/core/objects/index/changes/index.base.ts +1 -1
  40. package/src/core/objects/language/changes/language.base.ts +1 -1
  41. package/src/core/objects/materialized-view/changes/materialized-view.base.ts +1 -1
  42. package/src/core/objects/procedure/changes/procedure.base.ts +1 -1
  43. package/src/core/objects/rls-policy/changes/rls-policy.base.ts +1 -1
  44. package/src/core/objects/role/changes/role.base.ts +1 -1
  45. package/src/core/objects/schema/changes/schema.base.ts +1 -1
  46. package/src/core/objects/sequence/changes/sequence.base.ts +1 -1
  47. package/src/core/objects/table/changes/table.base.ts +1 -1
  48. package/src/core/objects/table/changes/table.comment.ts +2 -8
  49. package/src/core/objects/table/table.diff.test.ts +198 -5
  50. package/src/core/objects/table/table.diff.ts +63 -34
  51. package/src/core/objects/trigger/changes/trigger.alter.ts +1 -4
  52. package/src/core/objects/trigger/changes/trigger.base.ts +1 -1
  53. package/src/core/objects/type/composite-type/changes/composite-type.base.ts +1 -1
  54. package/src/core/objects/type/enum/changes/enum.base.ts +1 -1
  55. package/src/core/objects/type/range/changes/range.base.ts +1 -1
  56. package/src/core/objects/view/changes/view.base.ts +1 -1
  57. package/src/core/plan/hierarchy.ts +4 -4
  58. package/src/core/postgres-config.test.ts +39 -1
  59. package/src/core/postgres-config.ts +32 -16
  60. package/src/core/sort/debug-visualization.ts +1 -1
  61. package/src/core/sort/sort-changes.test.ts +1 -0
  62. package/src/core/sort/topological-sort.ts +2 -2
@@ -26,6 +26,9 @@ const serverPropsSchema = z.object({
26
26
  options: z.array(z.string()).nullable(),
27
27
  comment: z.string().nullable(),
28
28
  privileges: z.array(privilegePropsSchema),
29
+ // Parent FDW handler/validator — filter metadata only, not in dataFields.
30
+ wrapper_handler: z.string().nullable().optional(),
31
+ wrapper_validator: z.string().nullable().optional(),
29
32
  });
30
33
 
31
34
  type ServerPrivilegeProps = PrivilegeProps;
@@ -40,6 +43,8 @@ export class Server extends BasePgModel {
40
43
  public readonly options: ServerProps["options"];
41
44
  public readonly comment: ServerProps["comment"];
42
45
  public readonly privileges: ServerPrivilegeProps[];
46
+ public readonly wrapper_handler: ServerProps["wrapper_handler"];
47
+ public readonly wrapper_validator: ServerProps["wrapper_validator"];
43
48
 
44
49
  constructor(props: ServerProps) {
45
50
  super();
@@ -55,6 +60,8 @@ export class Server extends BasePgModel {
55
60
  this.options = props.options;
56
61
  this.comment = props.comment;
57
62
  this.privileges = props.privileges;
63
+ this.wrapper_handler = props.wrapper_handler ?? null;
64
+ this.wrapper_validator = props.wrapper_validator ?? null;
58
65
  }
59
66
 
60
67
  get stableId(): `server:${string}` {
@@ -112,10 +119,20 @@ export async function extractServers(pool: Pool): Promise<Server[]> {
112
119
  )
113
120
  from lateral aclexplode(srv.srvacl) as x(grantor, grantee, privilege_type, is_grantable)
114
121
  ), '[]'
115
- ) as privileges
122
+ ) as privileges,
123
+ case
124
+ when fdw.fdwhandler = 0 then null
125
+ else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
126
+ end as wrapper_handler,
127
+ case
128
+ when fdw.fdwvalidator = 0 then null
129
+ else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
130
+ end as wrapper_validator
116
131
  from
117
132
  pg_catalog.pg_foreign_server srv
118
133
  inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
134
+ left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
135
+ left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
119
136
  where
120
137
  not fdw.fdwname like any(array['pg\\_%'])
121
138
  order by
@@ -4,7 +4,7 @@ import type { UserMapping } from "../user-mapping.model.ts";
4
4
  abstract class BaseUserMappingChange extends BaseChange {
5
5
  abstract readonly userMapping: UserMapping;
6
6
  abstract readonly scope: "object";
7
- readonly objectType: "user_mapping" = "user_mapping";
7
+ readonly objectType = "user_mapping" as const;
8
8
  }
9
9
 
10
10
  export abstract class CreateUserMappingChange extends BaseUserMappingChange {
@@ -18,6 +18,9 @@ const userMappingPropsSchema = z.object({
18
18
  user: z.string(),
19
19
  server: z.string(),
20
20
  options: z.array(z.string()).nullable(),
21
+ // Parent FDW handler/validator — filter metadata only, not in dataFields.
22
+ wrapper_handler: z.string().nullable().optional(),
23
+ wrapper_validator: z.string().nullable().optional(),
21
24
  });
22
25
 
23
26
  export type UserMappingProps = z.infer<typeof userMappingPropsSchema>;
@@ -26,6 +29,8 @@ export class UserMapping extends BasePgModel {
26
29
  public readonly user: UserMappingProps["user"];
27
30
  public readonly server: UserMappingProps["server"];
28
31
  public readonly options: UserMappingProps["options"];
32
+ public readonly wrapper_handler: UserMappingProps["wrapper_handler"];
33
+ public readonly wrapper_validator: UserMappingProps["wrapper_validator"];
29
34
 
30
35
  constructor(props: UserMappingProps) {
31
36
  super();
@@ -36,6 +41,8 @@ export class UserMapping extends BasePgModel {
36
41
 
37
42
  // Data fields
38
43
  this.options = props.options;
44
+ this.wrapper_handler = props.wrapper_handler ?? null;
45
+ this.wrapper_validator = props.wrapper_validator ?? null;
39
46
  }
40
47
 
41
48
  get stableId(): `userMapping:${string}:${string}` {
@@ -74,11 +81,21 @@ export async function extractUserMappings(pool: Pool): Promise<UserMapping[]> {
74
81
  else um.umuser::regrole::text
75
82
  end as user,
76
83
  quote_ident(srv.srvname) as server,
77
- coalesce(um.umoptions, array[]::text[]) as options
84
+ coalesce(um.umoptions, array[]::text[]) as options,
85
+ case
86
+ when fdw.fdwhandler = 0 then null
87
+ else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
88
+ end as wrapper_handler,
89
+ case
90
+ when fdw.fdwvalidator = 0 then null
91
+ else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
92
+ end as wrapper_validator
78
93
  from
79
94
  pg_catalog.pg_user_mapping um
80
95
  inner join pg_catalog.pg_foreign_server srv on srv.oid = um.umserver
81
96
  inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
97
+ left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
98
+ left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
82
99
  where
83
100
  not fdw.fdwname like any(array['pg\\_%'])
84
101
  order by
@@ -4,7 +4,7 @@ import type { Index } from "../index.model.ts";
4
4
  abstract class BaseIndexChange extends BaseChange {
5
5
  abstract readonly index: Index;
6
6
  abstract readonly scope: "object" | "comment";
7
- readonly objectType: "index" = "index";
7
+ readonly objectType = "index" as const;
8
8
  }
9
9
 
10
10
  export abstract class CreateIndexChange extends BaseIndexChange {
@@ -4,7 +4,7 @@ import type { Language } from "../language.model.ts";
4
4
  abstract class BaseLanguageChange extends BaseChange {
5
5
  abstract readonly language: Language;
6
6
  abstract readonly scope: "object" | "comment" | "privilege";
7
- readonly objectType: "language" = "language";
7
+ readonly objectType = "language" as const;
8
8
  }
9
9
 
10
10
  export abstract class CreateLanguageChange extends BaseLanguageChange {
@@ -8,7 +8,7 @@ abstract class BaseMaterializedViewChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "materialized_view" = "materialized_view";
11
+ readonly objectType = "materialized_view" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateMaterializedViewChange extends BaseMaterializedViewChange {
@@ -8,7 +8,7 @@ abstract class BaseProcedureChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "procedure" = "procedure";
11
+ readonly objectType = "procedure" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateProcedureChange extends BaseProcedureChange {
@@ -4,7 +4,7 @@ import type { RlsPolicy } from "../rls-policy.model.ts";
4
4
  abstract class BaseRlsPolicyChange extends BaseChange {
5
5
  abstract readonly policy: RlsPolicy;
6
6
  abstract readonly scope: "object" | "comment";
7
- readonly objectType: "rls_policy" = "rls_policy";
7
+ readonly objectType = "rls_policy" as const;
8
8
  }
9
9
 
10
10
  export abstract class CreateRlsPolicyChange extends BaseRlsPolicyChange {
@@ -9,7 +9,7 @@ abstract class BaseRoleChange extends BaseChange {
9
9
  | "membership"
10
10
  | "default_privilege"
11
11
  | "security_label";
12
- readonly objectType: "role" = "role";
12
+ readonly objectType = "role" as const;
13
13
  }
14
14
 
15
15
  export abstract class CreateRoleChange extends BaseRoleChange {
@@ -8,7 +8,7 @@ abstract class BaseSchemaChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "schema" = "schema";
11
+ readonly objectType = "schema" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateSchemaChange extends BaseSchemaChange {
@@ -8,7 +8,7 @@ abstract class BaseSequenceChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "sequence" = "sequence";
11
+ readonly objectType = "sequence" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateSequenceChange extends BaseSequenceChange {
@@ -8,7 +8,7 @@ abstract class BaseTableChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "table" = "table";
11
+ readonly objectType = "table" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateTableChange extends BaseTableChange {
@@ -179,10 +179,7 @@ export class CreateCommentOnConstraint extends CreateTableChange {
179
179
  public readonly constraint: TableConstraintProps;
180
180
  public readonly scope = "comment" as const;
181
181
 
182
- constructor(props: {
183
- table: Table;
184
- constraint: TableConstraintProps;
185
- }) {
182
+ constructor(props: { table: Table; constraint: TableConstraintProps }) {
186
183
  super();
187
184
  this.table = props.table;
188
185
  this.constraint = props.constraint;
@@ -228,10 +225,7 @@ export class DropCommentOnConstraint extends DropTableChange {
228
225
  public readonly constraint: TableConstraintProps;
229
226
  public readonly scope = "comment" as const;
230
227
 
231
- constructor(props: {
232
- table: Table;
233
- constraint: TableConstraintProps;
234
- }) {
228
+ constructor(props: { table: Table; constraint: TableConstraintProps }) {
235
229
  super();
236
230
  this.table = props.table;
237
231
  this.constraint = props.constraint;
@@ -75,7 +75,7 @@ describe.concurrent("table.diff", () => {
75
75
  expect(dropped[0]).toBeInstanceOf(DropTable);
76
76
  });
77
77
 
78
- test("created NOT VALID CHECK emits AddConstraint + ValidateConstraint", () => {
78
+ test("created NOT VALID CHECK emits AddConstraint only (no Validate)", () => {
79
79
  const main = new Table({
80
80
  ...base,
81
81
  name: "t_nv",
@@ -102,6 +102,7 @@ describe.concurrent("table.diff", () => {
102
102
  constraints: [],
103
103
  });
104
104
  const branch = new Table({
105
+ // oxlint-disable-next-line typescript/no-misused-spread
105
106
  ...main,
106
107
  constraints: [
107
108
  {
@@ -132,20 +133,208 @@ describe.concurrent("table.diff", () => {
132
133
  match_type: null,
133
134
  check_expression: "a > 0",
134
135
  owner: "o1",
136
+ definition: "CHECK (a > 0) NOT VALID",
137
+ },
138
+ ],
139
+ });
140
+ const changes = diffTables(
141
+ testContext,
142
+ { [main.stableId]: main },
143
+ { [branch.stableId]: branch },
144
+ );
145
+ const add = changes.find((c) => c instanceof AlterTableAddConstraint);
146
+ expect(add).toBeInstanceOf(AlterTableAddConstraint);
147
+ expect(add?.serialize()).toContain("NOT VALID");
148
+ expect(changes.some((c) => c instanceof AlterTableValidateConstraint)).toBe(
149
+ false,
150
+ );
151
+ });
152
+
153
+ test("NOT VALID -> validated emits only VALIDATE CONSTRAINT (no drop+add)", () => {
154
+ const sharedConstraint = {
155
+ name: "ck_nv",
156
+ constraint_type: "c" as const,
157
+ deferrable: false,
158
+ initially_deferred: false,
159
+ is_local: true,
160
+ no_inherit: false,
161
+ is_temporal: false,
162
+ is_partition_clone: false,
163
+ parent_constraint_schema: null,
164
+ parent_constraint_name: null,
165
+ parent_table_schema: null,
166
+ parent_table_name: null,
167
+ key_columns: [],
168
+ foreign_key_columns: null,
169
+ foreign_key_table: null,
170
+ foreign_key_schema: null,
171
+ foreign_key_table_is_partition: null,
172
+ foreign_key_parent_schema: null,
173
+ foreign_key_parent_table: null,
174
+ foreign_key_effective_schema: null,
175
+ foreign_key_effective_table: null,
176
+ on_update: null,
177
+ on_delete: null,
178
+ match_type: null,
179
+ check_expression: "a > 0",
180
+ owner: "o1",
181
+ comment: null,
182
+ };
183
+
184
+ const main = new Table({
185
+ ...base,
186
+ name: "t_nv",
187
+ columns: [
188
+ {
189
+ name: "a",
190
+ position: 1,
191
+ data_type: "integer",
192
+ data_type_str: "integer",
193
+ is_custom_type: false,
194
+ custom_type_type: null,
195
+ custom_type_category: null,
196
+ custom_type_schema: null,
197
+ custom_type_name: null,
198
+ not_null: false,
199
+ is_identity: false,
200
+ is_identity_always: false,
201
+ is_generated: false,
202
+ collation: null,
203
+ default: null,
204
+ comment: null,
205
+ },
206
+ ],
207
+ constraints: [
208
+ {
209
+ ...sharedConstraint,
210
+ validated: false,
211
+ definition: "CHECK (a > 0) NOT VALID",
212
+ },
213
+ ],
214
+ });
215
+ const branch = new Table({
216
+ // oxlint-disable-next-line typescript/no-misused-spread
217
+ ...main,
218
+ constraints: [
219
+ {
220
+ ...sharedConstraint,
221
+ validated: true,
135
222
  definition: "CHECK (a > 0)",
136
223
  },
137
224
  ],
138
225
  });
226
+
227
+ const changes = diffTables(
228
+ testContext,
229
+ { [main.stableId]: main },
230
+ { [branch.stableId]: branch },
231
+ );
232
+
233
+ const validate = changes.find(
234
+ (c) => c instanceof AlterTableValidateConstraint,
235
+ );
236
+ expect(validate).toBeInstanceOf(AlterTableValidateConstraint);
237
+ expect(validate?.serialize()).toMatchInlineSnapshot(
238
+ `"ALTER TABLE public.t_nv VALIDATE CONSTRAINT ck_nv"`,
239
+ );
240
+
241
+ expect(changes.some((c) => c instanceof AlterTableDropConstraint)).toBe(
242
+ false,
243
+ );
244
+ expect(changes.some((c) => c instanceof AlterTableAddConstraint)).toBe(
245
+ false,
246
+ );
247
+ });
248
+
249
+ test("NOT VALID -> validated + other field change still drops+adds (no shortcut)", () => {
250
+ const sharedConstraint = {
251
+ name: "ck_nv",
252
+ constraint_type: "c" as const,
253
+ deferrable: false,
254
+ initially_deferred: false,
255
+ is_local: true,
256
+ no_inherit: false,
257
+ is_temporal: false,
258
+ is_partition_clone: false,
259
+ parent_constraint_schema: null,
260
+ parent_constraint_name: null,
261
+ parent_table_schema: null,
262
+ parent_table_name: null,
263
+ key_columns: [],
264
+ foreign_key_columns: null,
265
+ foreign_key_table: null,
266
+ foreign_key_schema: null,
267
+ foreign_key_table_is_partition: null,
268
+ foreign_key_parent_schema: null,
269
+ foreign_key_parent_table: null,
270
+ foreign_key_effective_schema: null,
271
+ foreign_key_effective_table: null,
272
+ on_update: null,
273
+ on_delete: null,
274
+ match_type: null,
275
+ owner: "o1",
276
+ comment: null,
277
+ };
278
+
279
+ const main = new Table({
280
+ ...base,
281
+ name: "t_nv",
282
+ columns: [
283
+ {
284
+ name: "a",
285
+ position: 1,
286
+ data_type: "integer",
287
+ data_type_str: "integer",
288
+ is_custom_type: false,
289
+ custom_type_type: null,
290
+ custom_type_category: null,
291
+ custom_type_schema: null,
292
+ custom_type_name: null,
293
+ not_null: false,
294
+ is_identity: false,
295
+ is_identity_always: false,
296
+ is_generated: false,
297
+ collation: null,
298
+ default: null,
299
+ comment: null,
300
+ },
301
+ ],
302
+ constraints: [
303
+ {
304
+ ...sharedConstraint,
305
+ validated: false,
306
+ check_expression: "a > 0",
307
+ definition: "CHECK (a > 0) NOT VALID",
308
+ },
309
+ ],
310
+ });
311
+ const branch = new Table({
312
+ // oxlint-disable-next-line typescript/no-misused-spread
313
+ ...main,
314
+ constraints: [
315
+ {
316
+ ...sharedConstraint,
317
+ validated: true,
318
+ check_expression: "a > 1",
319
+ definition: "CHECK (a > 1)",
320
+ },
321
+ ],
322
+ });
323
+
139
324
  const changes = diffTables(
140
325
  testContext,
141
326
  { [main.stableId]: main },
142
327
  { [branch.stableId]: branch },
143
328
  );
329
+
330
+ expect(changes.some((c) => c instanceof AlterTableDropConstraint)).toBe(
331
+ true,
332
+ );
144
333
  expect(changes.some((c) => c instanceof AlterTableAddConstraint)).toBe(
145
334
  true,
146
335
  );
147
336
  expect(changes.some((c) => c instanceof AlterTableValidateConstraint)).toBe(
148
- true,
337
+ false,
149
338
  );
150
339
  });
151
340
 
@@ -362,7 +551,7 @@ describe.concurrent("table.diff", () => {
362
551
  true,
363
552
  );
364
553
  expect(created.some((c) => c instanceof AlterTableValidateConstraint)).toBe(
365
- true,
554
+ false,
366
555
  );
367
556
 
368
557
  const dropped = diffTables(
@@ -480,6 +669,7 @@ describe.concurrent("table.diff", () => {
480
669
  ],
481
670
  });
482
671
  const tBranch = new Table({
672
+ // oxlint-disable-next-line typescript/no-misused-spread
483
673
  ...tMain,
484
674
  constraints: [
485
675
  {
@@ -501,7 +691,7 @@ describe.concurrent("table.diff", () => {
501
691
  );
502
692
  });
503
693
 
504
- test("altered foreign key properties triggers drop+add and validate when not validated", () => {
694
+ test("altered foreign key to NOT VALID triggers drop+add without validate", () => {
505
695
  const tMain = new Table({
506
696
  ...base,
507
697
  name: "t_fk",
@@ -559,12 +749,14 @@ describe.concurrent("table.diff", () => {
559
749
  ],
560
750
  });
561
751
  const tBranch = new Table({
752
+ // oxlint-disable-next-line typescript/no-misused-spread
562
753
  ...tMain,
563
754
  constraints: [
564
755
  {
565
756
  ...(tMain.constraints[0] as (typeof tMain.constraints)[number]),
566
757
  on_delete: "c",
567
758
  validated: false,
759
+ definition: "FOREIGN KEY (a) REFERENCES other(a) NOT VALID",
568
760
  },
569
761
  ],
570
762
  });
@@ -606,7 +798,7 @@ describe.concurrent("table.diff", () => {
606
798
  true,
607
799
  );
608
800
  expect(changes.some((c) => c instanceof AlterTableValidateConstraint)).toBe(
609
- true,
801
+ false,
610
802
  );
611
803
  });
612
804
 
@@ -686,6 +878,7 @@ describe.concurrent("table.diff", () => {
686
878
  ],
687
879
  });
688
880
  const tBranch = new Table({
881
+ // oxlint-disable-next-line typescript/no-misused-spread
689
882
  ...tMain,
690
883
  constraints: [
691
884
  {
@@ -86,14 +86,6 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
86
86
  constraint: c,
87
87
  }),
88
88
  );
89
- if (!c.validated) {
90
- changes.push(
91
- new AlterTableValidateConstraint({
92
- table: branchTable,
93
- constraint: c,
94
- }),
95
- );
96
- }
97
89
  // Add comment for newly created constraint
98
90
  if (c.comment !== null) {
99
91
  changes.push(
@@ -120,7 +112,7 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
120
112
  }
121
113
  }
122
114
 
123
- // Altered constraints -> drop + add
115
+ // Altered constraints -> drop + add (or VALIDATE-only shortcut)
124
116
  for (const [name, mainC] of mainByName) {
125
117
  const branchC = branchByName.get(name);
126
118
  if (!branchC) continue;
@@ -130,24 +122,68 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
130
122
  continue;
131
123
  }
132
124
 
125
+ // Cheap scalar `===` checks first; only fall through to JSON.stringify
126
+ // on the array fields when every scalar has already matched.
127
+ const fieldsEqualExceptValidated =
128
+ mainC.constraint_type === branchC.constraint_type &&
129
+ mainC.deferrable === branchC.deferrable &&
130
+ mainC.initially_deferred === branchC.initially_deferred &&
131
+ mainC.is_local === branchC.is_local &&
132
+ mainC.no_inherit === branchC.no_inherit &&
133
+ mainC.is_temporal === branchC.is_temporal &&
134
+ mainC.foreign_key_table === branchC.foreign_key_table &&
135
+ mainC.foreign_key_schema === branchC.foreign_key_schema &&
136
+ mainC.on_update === branchC.on_update &&
137
+ mainC.on_delete === branchC.on_delete &&
138
+ mainC.match_type === branchC.match_type &&
139
+ mainC.check_expression === branchC.check_expression &&
140
+ JSON.stringify(mainC.key_columns) ===
141
+ JSON.stringify(branchC.key_columns) &&
142
+ JSON.stringify(mainC.foreign_key_columns) ===
143
+ JSON.stringify(branchC.foreign_key_columns);
144
+
145
+ // Safe-migration shortcut: when the only difference is `validated`
146
+ // flipping from false to true, emit a single `ALTER TABLE ... VALIDATE
147
+ // CONSTRAINT` instead of drop+add. VALIDATE CONSTRAINT only takes
148
+ // SHARE UPDATE EXCLUSIVE (concurrent reads/writes proceed), whereas
149
+ // dropping and re-adding takes ACCESS EXCLUSIVE for the entire scan.
150
+ // Postgres has no reverse command, so `true -> false` must still go
151
+ // through drop+add below.
152
+ if (
153
+ fieldsEqualExceptValidated &&
154
+ mainC.validated === false &&
155
+ branchC.validated === true
156
+ ) {
157
+ changes.push(
158
+ new AlterTableValidateConstraint({
159
+ table: branchTable,
160
+ constraint: branchC,
161
+ }),
162
+ );
163
+ // VALIDATE preserves the constraint OID, so its comment is preserved
164
+ // too. Only emit a comment change if it actually differs.
165
+ if (mainC.comment !== branchC.comment) {
166
+ if (branchC.comment === null) {
167
+ changes.push(
168
+ new DropCommentOnConstraint({
169
+ table: mainTable,
170
+ constraint: mainC,
171
+ }),
172
+ );
173
+ } else {
174
+ changes.push(
175
+ new CreateCommentOnConstraint({
176
+ table: branchTable,
177
+ constraint: branchC,
178
+ }),
179
+ );
180
+ }
181
+ }
182
+ continue;
183
+ }
184
+
133
185
  const changed =
134
- mainC.constraint_type !== branchC.constraint_type ||
135
- mainC.deferrable !== branchC.deferrable ||
136
- mainC.initially_deferred !== branchC.initially_deferred ||
137
- mainC.validated !== branchC.validated ||
138
- mainC.is_local !== branchC.is_local ||
139
- mainC.no_inherit !== branchC.no_inherit ||
140
- mainC.is_temporal !== branchC.is_temporal ||
141
- JSON.stringify(mainC.key_columns) !==
142
- JSON.stringify(branchC.key_columns) ||
143
- JSON.stringify(mainC.foreign_key_columns) !==
144
- JSON.stringify(branchC.foreign_key_columns) ||
145
- mainC.foreign_key_table !== branchC.foreign_key_table ||
146
- mainC.foreign_key_schema !== branchC.foreign_key_schema ||
147
- mainC.on_update !== branchC.on_update ||
148
- mainC.on_delete !== branchC.on_delete ||
149
- mainC.match_type !== branchC.match_type ||
150
- mainC.check_expression !== branchC.check_expression;
186
+ mainC.validated !== branchC.validated || !fieldsEqualExceptValidated;
151
187
  if (changed) {
152
188
  changes.push(
153
189
  new AlterTableDropConstraint({
@@ -161,14 +197,6 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
161
197
  constraint: branchC,
162
198
  }),
163
199
  );
164
- if (!branchC.validated) {
165
- changes.push(
166
- new AlterTableValidateConstraint({
167
- table: branchTable,
168
- constraint: branchC,
169
- }),
170
- );
171
- }
172
200
  // Ensure constraint comment is applied after re-creation
173
201
  if (branchC.comment !== null) {
174
202
  changes.push(
@@ -265,6 +293,7 @@ export function diffTables(
265
293
  ...createAlterConstraintChange(
266
294
  // Create a dummy table with no constraints do diff constraints against
267
295
  new Table({
296
+ // oxlint-disable-next-line typescript/no-misused-spread
268
297
  ...branchTable,
269
298
  constraints: [],
270
299
  }),
@@ -28,10 +28,7 @@ export class ReplaceTrigger extends AlterTriggerChange {
28
28
  public readonly indexableObject?: TableLikeObject;
29
29
  public readonly scope = "object" as const;
30
30
 
31
- constructor(props: {
32
- trigger: Trigger;
33
- indexableObject?: TableLikeObject;
34
- }) {
31
+ constructor(props: { trigger: Trigger; indexableObject?: TableLikeObject }) {
35
32
  super();
36
33
  this.trigger = props.trigger;
37
34
  this.indexableObject = props.indexableObject;
@@ -4,7 +4,7 @@ import type { Trigger } from "../trigger.model.ts";
4
4
  abstract class BaseTriggerChange extends BaseChange {
5
5
  abstract readonly trigger: Trigger;
6
6
  abstract readonly scope: "object" | "comment";
7
- readonly objectType: "trigger" = "trigger";
7
+ readonly objectType = "trigger" as const;
8
8
  }
9
9
 
10
10
  export abstract class CreateTriggerChange extends BaseTriggerChange {
@@ -8,7 +8,7 @@ abstract class BaseCompositeTypeChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "composite_type" = "composite_type";
11
+ readonly objectType = "composite_type" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateCompositeTypeChange extends BaseCompositeTypeChange {
@@ -8,7 +8,7 @@ abstract class BaseEnumChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "enum" = "enum";
11
+ readonly objectType = "enum" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateEnumChange extends BaseEnumChange {