pg-migration-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +304 -0
  3. package/package.json +52 -0
  4. package/src/behavioral/behavioral-applier.js +374 -0
  5. package/src/behavioral/behavioral-extractor.js +266 -0
  6. package/src/behavioral/behavioral-puller.js +35 -0
  7. package/src/behavioral/index.js +4 -0
  8. package/src/behavioral/phase-sorter.js +24 -0
  9. package/src/constants.js +97 -0
  10. package/src/ddl-generator/alter-generator.js +1512 -0
  11. package/src/ddl-generator/comment-generator.js +95 -0
  12. package/src/ddl-generator/create-generator.js +1426 -0
  13. package/src/ddl-generator/drop-generator.js +194 -0
  14. package/src/ddl-generator/generator.js +78 -0
  15. package/src/ddl-generator/grant-generator.js +31 -0
  16. package/src/ddl-generator/index.js +3 -0
  17. package/src/ddl-generator/pg-version.js +15 -0
  18. package/src/ddl-generator/rename-generator.js +88 -0
  19. package/src/ddl-generator/safe-patterns.js +241 -0
  20. package/src/differ/change-classifier.js +192 -0
  21. package/src/differ/dependency-resolver.js +523 -0
  22. package/src/differ/index.js +12 -0
  23. package/src/differ/object-matcher.js +510 -0
  24. package/src/differ/property-differ.js +1134 -0
  25. package/src/differ/risk-tagger.js +561 -0
  26. package/src/differ/schema-differ.js +309 -0
  27. package/src/differ/utils/levenshtein.js +210 -0
  28. package/src/differ/utils/path-builder.js +198 -0
  29. package/src/differ/utils/type-compatibility.js +472 -0
  30. package/src/errors.js +147 -0
  31. package/src/executor/drift-detector.js +221 -0
  32. package/src/executor/index.js +7 -0
  33. package/src/executor/lock-manager.js +308 -0
  34. package/src/executor/migration-executor.js +1249 -0
  35. package/src/executor/progress-tracker.js +81 -0
  36. package/src/executor/recovery-manager.js +47 -0
  37. package/src/executor/snapshot-manager.js +22 -0
  38. package/src/executor/sql-splitter.js +120 -0
  39. package/src/executor/transaction-manager.js +288 -0
  40. package/src/index.js +483 -0
  41. package/src/introspection/index.js +4 -0
  42. package/src/introspection/introspector.js +250 -0
  43. package/src/introspection/queries/access-methods.js +29 -0
  44. package/src/introspection/queries/casts.js +38 -0
  45. package/src/introspection/queries/collations.js +51 -0
  46. package/src/introspection/queries/comments.js +66 -0
  47. package/src/introspection/queries/constraints.js +65 -0
  48. package/src/introspection/queries/conversions.js +39 -0
  49. package/src/introspection/queries/databases.js +43 -0
  50. package/src/introspection/queries/default-privileges.js +37 -0
  51. package/src/introspection/queries/event-triggers.js +43 -0
  52. package/src/introspection/queries/extensions.js +21 -0
  53. package/src/introspection/queries/foreign-data.js +136 -0
  54. package/src/introspection/queries/functions.js +75 -0
  55. package/src/introspection/queries/grants.js +38 -0
  56. package/src/introspection/queries/index.js +33 -0
  57. package/src/introspection/queries/indexes.js +99 -0
  58. package/src/introspection/queries/inheritance.js +24 -0
  59. package/src/introspection/queries/multiranges.js +37 -0
  60. package/src/introspection/queries/operators.js +180 -0
  61. package/src/introspection/queries/partitions.js +42 -0
  62. package/src/introspection/queries/pg18-19.js +43 -0
  63. package/src/introspection/queries/policies.js +35 -0
  64. package/src/introspection/queries/procedural-languages.js +36 -0
  65. package/src/introspection/queries/publications.js +114 -0
  66. package/src/introspection/queries/roles.js +66 -0
  67. package/src/introspection/queries/rules.js +49 -0
  68. package/src/introspection/queries/sequences.js +37 -0
  69. package/src/introspection/queries/statistics.js +74 -0
  70. package/src/introspection/queries/subscriptions.js +99 -0
  71. package/src/introspection/queries/tables.js +151 -0
  72. package/src/introspection/queries/tablespaces.js +36 -0
  73. package/src/introspection/queries/text-search.js +146 -0
  74. package/src/introspection/queries/triggers.js +61 -0
  75. package/src/introspection/queries/types.js +106 -0
  76. package/src/introspection/queries/views.js +146 -0
  77. package/src/introspection/translator.js +1283 -0
  78. package/src/introspection/version-detector.js +25 -0
  79. package/src/planner/backfill-planner.js +36 -0
  80. package/src/planner/dry-run.js +21 -0
  81. package/src/planner/index.js +6 -0
  82. package/src/planner/migration-planner.js +356 -0
  83. package/src/planner/smart-migrator.js +82 -0
  84. package/src/planner/step-sequencer.js +61 -0
  85. package/src/planner/type-registry.js +47 -0
  86. package/src/risk/compatibility-checker.js +29 -0
  87. package/src/risk/data-loss-checker.js +32 -0
  88. package/src/risk/destructive-checker.js +38 -0
  89. package/src/risk/index.js +6 -0
  90. package/src/risk/lock-analyzer.js +39 -0
  91. package/src/risk/recommendations.js +44 -0
  92. package/src/risk/risk-engine.js +25 -0
  93. package/src/storage/index.js +5 -0
  94. package/src/storage/memory-storage.js +87 -0
  95. package/src/storage/migration-table.js +423 -0
  96. package/src/storage/rollback-generator.js +344 -0
  97. package/src/types/changes.js +136 -0
  98. package/src/types/connection.js +17 -0
  99. package/src/types/execution.js +107 -0
  100. package/src/types/index.js +1 -0
  101. package/src/types/migration.js +67 -0
  102. package/src/types/risk.js +32 -0
  103. package/src/types/schema.d.ts +1004 -0
  104. package/src/types/schema.js +561 -0
@@ -0,0 +1,1134 @@
1
+ import { getCastInfo, isSafeCast, isWideningCast } from './utils/type-compatibility.js';
2
+
3
+ /**
4
+ * Property Differ - Deep property-level diffing for all object types.
5
+ * Handles column properties (nullable, default, type, etc.) and object properties.
6
+ */
7
+
8
+ export class PropertyDiffer {
9
+ constructor() {
10
+ this.warnings = [];
11
+ }
12
+
13
+ /**
14
+ * Diff all matched objects at the property level.
15
+ */
16
+ diff(matches, desired, current) {
17
+ const changes = [];
18
+ this.warnings = [];
19
+
20
+ for (const match of matches) {
21
+ const objType = match.objectType;
22
+ const desiredObj = match.desired;
23
+ const currentObj = match.current;
24
+
25
+ const objectChanges = this.diffObject(objType, desiredObj, currentObj, match.key);
26
+ changes.push(...objectChanges);
27
+ }
28
+
29
+ return { changes, warnings: this.warnings };
30
+ }
31
+
32
+ /**
33
+ * Diff a single object of any type.
34
+ */
35
+ diffObject(objectType, desired, current, key) {
36
+ const changes = [];
37
+
38
+ switch (objectType) {
39
+ case 'table':
40
+ changes.push(...this.diffTable(desired, current, key));
41
+ break;
42
+ case 'column':
43
+ changes.push(...this.diffColumn(desired, current, key));
44
+ break;
45
+ case 'index':
46
+ changes.push(...this.diffIndex(desired, current, key));
47
+ break;
48
+ case 'constraint':
49
+ changes.push(...this.diffConstraint(desired, current, key));
50
+ break;
51
+ case 'view':
52
+ changes.push(...this.diffView(desired, current, key));
53
+ break;
54
+ case 'materializedView':
55
+ changes.push(...this.diffMaterializedView(desired, current, key));
56
+ break;
57
+ case 'function':
58
+ case 'procedure':
59
+ changes.push(...this.diffFunction(desired, current, key));
60
+ break;
61
+ case 'aggregate':
62
+ changes.push(...this.diffAggregate(desired, current, key));
63
+ break;
64
+ case 'trigger':
65
+ changes.push(...this.diffTrigger(desired, current, key));
66
+ break;
67
+ case 'policy':
68
+ changes.push(...this.diffPolicy(desired, current, key));
69
+ break;
70
+ case 'sequence':
71
+ changes.push(...this.diffSequence(desired, current, key));
72
+ break;
73
+ case 'type':
74
+ case 'domain':
75
+ changes.push(...this.diffType(desired, current, key));
76
+ break;
77
+ case 'rule':
78
+ changes.push(...this.diffRule(desired, current, key));
79
+ break;
80
+ case 'eventTrigger':
81
+ changes.push(...this.diffEventTrigger(desired, current, key));
82
+ break;
83
+ case 'operator':
84
+ changes.push(...this.diffOperator(desired, current, key));
85
+ break;
86
+ case 'operatorClass':
87
+ case 'operatorFamily':
88
+ changes.push(...this.diffOperatorClass(desired, current, key, objectType));
89
+ break;
90
+ case 'textSearchConfig':
91
+ case 'textSearchDict':
92
+ case 'textSearchParser':
93
+ case 'textSearchTemplate':
94
+ changes.push(...this.diffTextSearch(desired, current, key, objectType));
95
+ break;
96
+ case 'publication':
97
+ case 'subscription':
98
+ changes.push(...this.diffReplication(desired, current, key, objectType));
99
+ break;
100
+ case 'statistics':
101
+ changes.push(...this.diffStatistics(desired, current, key));
102
+ break;
103
+ case 'collation':
104
+ changes.push(...this.diffCollation(desired, current, key));
105
+ break;
106
+ case 'cast':
107
+ changes.push(...this.diffCast(desired, current, key));
108
+ break;
109
+ case 'foreignServer':
110
+ case 'foreignDataWrapper':
111
+ case 'userMapping':
112
+ changes.push(...this.diffForeignObject(desired, current, key, objectType));
113
+ break;
114
+ case 'conversion':
115
+ changes.push(...this.diffConversion(desired, current, key));
116
+ break;
117
+ case 'defaultPrivileges':
118
+ changes.push(...this.diffDefaultPrivileges(desired, current, key));
119
+ break;
120
+ case 'accessMethod':
121
+ changes.push(...this.diffAccessMethod(desired, current, key));
122
+ break;
123
+ case 'foreignTable':
124
+ changes.push(...this.diffForeignTable(desired, current, key));
125
+ break;
126
+ case 'extension':
127
+ changes.push(...this.diffExtension(desired, current, key));
128
+ break;
129
+ case 'schema':
130
+ changes.push(...this.diffSchema(desired, current, key));
131
+ break;
132
+ default:
133
+ changes.push(...this.diffGeneric(desired, current, key, objectType));
134
+ }
135
+
136
+ return changes;
137
+ }
138
+
139
+ diffTable(desired, current, key) {
140
+ const changes = [];
141
+
142
+ const simpleProps = [
143
+ 'tablespace',
144
+ 'owner',
145
+ 'isLogged',
146
+ 'isTemporary',
147
+ 'isUnlogged',
148
+ 'rowLevelSecurity',
149
+ 'forceRowLevelSecurity',
150
+ 'rlsEnabled',
151
+ 'rlsForced',
152
+ 'replicaIdentity',
153
+ 'hasOids',
154
+ 'comment',
155
+ 'userCatalog',
156
+ 'isPartition',
157
+ 'isDefaultPartition',
158
+ 'isForeignTable',
159
+ 'foreignServer',
160
+ 'accessMethod',
161
+ ];
162
+
163
+ for (const prop of simpleProps) {
164
+ if (desired[prop] !== current[prop] && desired[prop] !== undefined) {
165
+ changes.push(this.createPropertyChange('table', key, prop, current[prop], desired[prop]));
166
+ }
167
+ }
168
+
169
+ // Partition properties that require recreation (CRITICAL)
170
+ const criticalPartitionProps = [
171
+ 'isPartitioned',
172
+ 'partitionStrategy',
173
+ 'partitionColumns',
174
+ 'partitionKeyDef',
175
+ 'partitionExpression',
176
+ ];
177
+
178
+ for (const prop of criticalPartitionProps) {
179
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
180
+ const change = this.createPropertyChange('table', key, prop, current[prop], desired[prop]);
181
+ change.requiresRecreation = true;
182
+ change.changeType = 'PARTITION_STRUCTURE_CHANGE';
183
+ this.warnings.push({
184
+ code: 'PARTITION_STRUCTURE_CHANGE',
185
+ message: `Partition structure change detected for ${key}: ${prop} changed`,
186
+ changeKey: key,
187
+ severity: 'critical',
188
+ reason: 'partition_structure_requires_recreation',
189
+ suggestions: [
190
+ 'Changing partition structure requires DROP and CREATE',
191
+ 'Ensure all partition children are handled correctly',
192
+ ],
193
+ });
194
+ changes.push(change);
195
+ }
196
+ }
197
+
198
+ // 1. Storage Parameters (JSON stringify comparison)
199
+ const curStorage = current.storageParameters || {};
200
+ const desStorage = desired.storageParameters || {};
201
+ if (JSON.stringify(desStorage) !== JSON.stringify(curStorage) && desired.storageParameters !== undefined) {
202
+ changes.push(this.createPropertyChange('table', key, 'storageParameters', curStorage, desStorage));
203
+ }
204
+
205
+ // 1b. TOAST Storage Parameters (JSON stringify comparison)
206
+ const curToastStorage = current.toastStorageOptions || {};
207
+ const desToastStorage = desired.toastStorageOptions || {};
208
+ if (JSON.stringify(desToastStorage) !== JSON.stringify(curToastStorage) && desired.toastStorageOptions !== undefined) {
209
+ changes.push(this.createPropertyChange('table', key, 'toastStorageOptions', curToastStorage, desToastStorage));
210
+ }
211
+
212
+ // 2. Inherits From (JSON stringify comparison)
213
+ const curInherits = current.inheritsFrom || [];
214
+ const desInherits = desired.inheritsFrom || [];
215
+ if (JSON.stringify(desInherits.sort()) !== JSON.stringify(curInherits.sort()) && desired.inheritsFrom !== undefined) {
216
+ changes.push(this.createPropertyChange('table', key, 'inheritsFrom', curInherits, desInherits));
217
+ }
218
+
219
+ // 3. Partition Parent & Bound (Attach/Detach)
220
+ if ((desired.partitionParent !== current.partitionParent || desired.partitionBound !== current.partitionBound) &&
221
+ (desired.partitionParent !== undefined || desired.partitionBound !== undefined)) {
222
+ changes.push(this.createPropertyChange('table', key, 'partitionParent',
223
+ { parent: current.partitionParent, bound: current.partitionBound },
224
+ { parent: desired.partitionParent, bound: desired.partitionBound }
225
+ ));
226
+ }
227
+
228
+ // Foreign table options (JSON comparison)
229
+ const curForeignOpts = current.foreignOptions || {};
230
+ const desForeignOpts = desired.foreignOptions || {};
231
+ if (JSON.stringify(desForeignOpts) !== JSON.stringify(curForeignOpts) && desired.foreignOptions !== undefined) {
232
+ changes.push(this.createPropertyChange('table', key, 'foreignOptions', curForeignOpts, desForeignOpts));
233
+ }
234
+
235
+ // 4. Privileges (Grants & Revokes)
236
+ const curPrivs = current.privileges || [];
237
+ const desPrivs = desired.privileges || [];
238
+ const findPriv = (list, p) => list.find(x => x.privilege === p.privilege && x.grantee === p.grantee && x.isGrantable === p.isGrantable);
239
+
240
+ const grantsToMake = [];
241
+ const revokesToMake = [];
242
+
243
+ for (const dp of desPrivs) {
244
+ if (!findPriv(curPrivs, dp)) {
245
+ grantsToMake.push(dp);
246
+ }
247
+ }
248
+ for (const cp of curPrivs) {
249
+ if (!findPriv(desPrivs, cp)) {
250
+ revokesToMake.push(cp);
251
+ }
252
+ }
253
+
254
+ if (grantsToMake.length > 0 || revokesToMake.length > 0) {
255
+ changes.push(this.createPropertyChange('table', key, 'privileges', curPrivs, desPrivs, {
256
+ grantsToMake,
257
+ revokesToMake
258
+ }));
259
+ }
260
+
261
+ return changes;
262
+ }
263
+
264
+ diffColumn(desired, current, key) {
265
+ const changes = [];
266
+
267
+ // Column properties
268
+ const props = [
269
+ { name: 'dataType', critical: true },
270
+ { name: 'isNullable', critical: false },
271
+ { name: 'defaultValue', critical: false },
272
+ { name: 'isPrimaryKey', critical: false },
273
+ { name: 'isUnique', critical: false },
274
+ { name: 'isForeignKey', critical: false },
275
+ { name: 'fkRelation', critical: false },
276
+ { name: 'check', critical: false },
277
+ { name: 'collation', critical: false },
278
+ { name: 'isIdentity', critical: false },
279
+ { name: 'identityMode', critical: false },
280
+ { name: 'identityStart', critical: false },
281
+ { name: 'identityIncrement', critical: false },
282
+ { name: 'identityMin', critical: false },
283
+ { name: 'identityMax', critical: false },
284
+ { name: 'identityCycle', critical: false },
285
+ { name: 'identityCache', critical: false },
286
+ { name: 'isGenerated', critical: false },
287
+ { name: 'generatedExpression', critical: false },
288
+ { name: 'generatedStorage', critical: false },
289
+ { name: 'comment', critical: false },
290
+ { name: 'isDecimal', critical: false },
291
+ { name: 'storage', critical: false },
292
+ { name: 'statisticsTarget', critical: false },
293
+ { name: 'compression', critical: false },
294
+ { name: 'privileges', critical: false },
295
+ { name: 'foreignOptions', critical: false },
296
+ { name: 'inheritedCount', critical: false },
297
+ { name: 'isLocal', critical: false },
298
+ { name: 'length', critical: false },
299
+ { name: 'arrayDimensions', critical: false },
300
+ ];
301
+
302
+ for (const prop of props) {
303
+ if (JSON.stringify(desired[prop.name]) !== JSON.stringify(current[prop.name]) && desired[prop.name] !== undefined) {
304
+ changes.push(this.createPropertyChange('column', key, prop.name, current[prop.name], desired[prop.name]));
305
+ }
306
+ }
307
+
308
+ // Type change handling
309
+ const typeChange = changes.find(c => c.property === 'dataType');
310
+ if (typeChange && typeChange.currentValue !== undefined) {
311
+ const castInfo = getCastInfo(typeChange.currentValue, typeChange.desiredValue);
312
+
313
+ // Determine type change type
314
+ if (castInfo.canCast === false) {
315
+ typeChange.changeType = 'IMPOSSIBLE_CAST';
316
+ typeChange.dataLossRisk = true;
317
+ typeChange.requiresRecreation = true;
318
+ this.warnings.push({
319
+ code: 'IMPOSSIBLE_TYPE_CAST',
320
+ message: `No cast path from ${typeChange.currentValue} to ${typeChange.desiredValue} for column ${key}`,
321
+ changeKey: key,
322
+ severity: 'critical',
323
+ reason: 'no_cast_context_or_binary',
324
+ suggestions: [
325
+ 'Add a USING clause with explicit conversion logic',
326
+ 'Create intermediate column, copy data, then swap',
327
+ ],
328
+ });
329
+ } else if (!isSafeCast(typeChange.currentValue, typeChange.desiredValue)) {
330
+ typeChange.changeType = 'UNSAFE_CAST';
331
+
332
+ // Narrowing cast
333
+ if (!isWideningCast(typeChange.currentValue, typeChange.desiredValue)) {
334
+ typeChange.changeType = 'NARROWING_CAST';
335
+ typeChange.dataLossRisk = true;
336
+ this.warnings.push({
337
+ code: 'NARROWING_TYPE_CAST',
338
+ message: `Narrowing cast ${typeChange.currentValue} → ${typeChange.desiredValue} may truncate data`,
339
+ changeKey: key,
340
+ severity: 'high',
341
+ reason: 'precision_loss',
342
+ suggestions: [
343
+ 'Check for data that would be truncated',
344
+ 'Add USING clause to handle edge cases',
345
+ ],
346
+ });
347
+ }
348
+ } else {
349
+ typeChange.changeType = 'SAFE_CAST';
350
+ }
351
+ }
352
+
353
+ return changes;
354
+ }
355
+
356
+ diffIndex(desired, current, key) {
357
+ const changes = [];
358
+
359
+ const props = [
360
+ 'columns',
361
+ 'definition',
362
+ 'whereClause',
363
+ 'isUnique',
364
+ 'isPrimary',
365
+ 'isConcurrent',
366
+ 'tablespace',
367
+ 'storageParameters',
368
+ 'method',
369
+ 'includeColumns',
370
+ 'comment',
371
+ 'isValid',
372
+ 'isReady',
373
+ 'isLive',
374
+ 'isReplicaIdentity',
375
+ 'isClustered',
376
+ 'nullsNotDistinct',
377
+ 'brinPagesPerRange',
378
+ 'owner',
379
+ ];
380
+
381
+ for (const prop of props) {
382
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
383
+ changes.push(this.createPropertyChange('index', key, prop, current[prop], desired[prop]));
384
+ }
385
+ }
386
+
387
+ // Definition changes require recreation
388
+ const criticalProps = ['columns', 'definition', 'whereClause', 'isUnique', 'isPrimary', 'method', 'includeColumns', 'brinPagesPerRange'];
389
+ if (changes.some(c => criticalProps.includes(c.property))) {
390
+ for (const change of changes) {
391
+ change.requiresRecreation = true;
392
+ }
393
+ }
394
+
395
+ return changes;
396
+ }
397
+
398
+ diffConstraint(desired, current, key) {
399
+ const changes = [];
400
+
401
+ const props = [
402
+ 'definition',
403
+ 'isValidated',
404
+ 'tablespace',
405
+ 'deferrable',
406
+ 'initiallyDeferred',
407
+ 'deferred',
408
+ 'onUpdate',
409
+ 'onDelete',
410
+ 'matchType',
411
+ 'noInherit',
412
+ 'isInherited',
413
+ 'isLocal',
414
+ 'enforced',
415
+ 'comment',
416
+ 'indexTablespace',
417
+ 'exclusionExpression',
418
+ 'referencedTable',
419
+ 'index',
420
+ ];
421
+
422
+ for (const prop of props) {
423
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
424
+ changes.push(this.createPropertyChange('constraint', key, prop, current[prop], desired[prop]));
425
+ }
426
+ }
427
+
428
+ // Referenced columns for FK constraints
429
+ const curRefCols = current.referencedColumns || [];
430
+ const desRefCols = desired.referencedColumns || [];
431
+ if (JSON.stringify(desRefCols) !== JSON.stringify(curRefCols) && desired.referencedColumns !== undefined) {
432
+ changes.push(this.createPropertyChange('constraint', key, 'referencedColumns', curRefCols, desRefCols));
433
+ }
434
+
435
+ return changes;
436
+ }
437
+
438
+ diffView(desired, current, key) {
439
+ const changes = [];
440
+
441
+ const props = [
442
+ 'definition',
443
+ 'checkOption',
444
+ 'securityBarrier',
445
+ 'securityInvoker',
446
+ 'columns',
447
+ 'privileges',
448
+ 'owner',
449
+ 'comment',
450
+ 'isRecursive',
451
+ ];
452
+
453
+ for (const prop of props) {
454
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
455
+ changes.push(this.createPropertyChange('view', key, prop, current[prop], desired[prop]));
456
+ }
457
+ }
458
+
459
+ return changes;
460
+ }
461
+
462
+ diffMaterializedView(desired, current, key) {
463
+ const changes = [];
464
+
465
+ const props = [
466
+ 'definition',
467
+ 'isWithData',
468
+ 'isPopulated',
469
+ 'refreshMethod',
470
+ 'tablespace',
471
+ 'storageParameters',
472
+ 'columns',
473
+ 'privileges',
474
+ 'owner',
475
+ 'comment',
476
+ ];
477
+
478
+ for (const prop of props) {
479
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
480
+ changes.push(this.createPropertyChange('materializedView', key, prop, current[prop], desired[prop]));
481
+ }
482
+ }
483
+
484
+ if (changes.length > 0) {
485
+ changes[0].changeType = 'ALTER_MATERIALZIED_VIEW';
486
+ changes[0].requiresRecreation = true;
487
+ this.warnings.push({
488
+ code: 'MATERIALIZED_VIEW_DEFINITION_CHANGE',
489
+ message: `Materialized view ${key} requires DROP and CREATE`,
490
+ changeKey: key,
491
+ severity: 'medium',
492
+ reason: 'cannot_replace_materialized_view',
493
+ suggestions: ['Ensure REFRESH is run after recreation'],
494
+ });
495
+ }
496
+
497
+ return changes;
498
+ }
499
+
500
+ diffFunction(desired, current, key) {
501
+ const changes = [];
502
+
503
+ // Check for parameter type changes (requires drop+create)
504
+ if (JSON.stringify(desired.argumentTypes) !== JSON.stringify(current.argumentTypes)) {
505
+ changes.push(this.createPropertyChange('function', key, 'argumentTypes', current.argumentTypes, desired.argumentTypes));
506
+ }
507
+
508
+ const props = [
509
+ 'source',
510
+ 'language',
511
+ 'volatility',
512
+ 'isStrict',
513
+ 'security',
514
+ 'cost',
515
+ 'rows',
516
+ 'parallel',
517
+ 'isLeakproof',
518
+ 'returnType',
519
+ 'returnSet',
520
+ 'argumentNames',
521
+ 'argumentDefaults',
522
+ 'argumentModes',
523
+ 'precompiledBody',
524
+ 'configuration',
525
+ 'supportFunction',
526
+ 'owner',
527
+ 'comment',
528
+ 'privileges',
529
+ ];
530
+
531
+ for (const prop of props) {
532
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
533
+ changes.push(this.createPropertyChange('function', key, prop, current[prop], desired[prop]));
534
+ }
535
+ }
536
+
537
+ return changes;
538
+ }
539
+
540
+ diffAggregate(desired, current, key) {
541
+ const changes = [];
542
+
543
+ const props = [
544
+ 'sfunc',
545
+ 'stype',
546
+ 'finalfunc',
547
+ 'combinefunc',
548
+ 'initcond',
549
+ 'sspace',
550
+ 'finalfuncExtra',
551
+ 'finalfuncModify',
552
+ 'serialfunc',
553
+ 'deserialfunc',
554
+ 'sortop',
555
+ 'hypothetical',
556
+ 'owner',
557
+ 'comment',
558
+ ];
559
+
560
+ for (const prop of props) {
561
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
562
+ changes.push(this.createPropertyChange('aggregate', key, prop, current[prop], desired[prop]));
563
+ }
564
+ }
565
+
566
+ return changes;
567
+ }
568
+
569
+ diffTrigger(desired, current, key) {
570
+ const changes = [];
571
+
572
+ const props = [
573
+ 'function',
574
+ 'events',
575
+ 'timing',
576
+ 'enabled',
577
+ 'level',
578
+ 'whenCondition',
579
+ 'isConstraint',
580
+ 'isDeferrable',
581
+ 'isDeferred',
582
+ 'updateOfColumns',
583
+ 'oldTableName',
584
+ 'newTableName',
585
+ 'isForEachRow',
586
+ 'functionCall',
587
+ 'comment',
588
+ ];
589
+
590
+ for (const prop of props) {
591
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
592
+ changes.push(this.createPropertyChange('trigger', key, prop, current[prop], desired[prop]));
593
+ }
594
+ }
595
+
596
+ return changes;
597
+ }
598
+
599
+ diffPolicy(desired, current, key) {
600
+ const changes = [];
601
+
602
+ const props = [
603
+ 'command',
604
+ 'isPermissive',
605
+ 'roles',
606
+ 'using',
607
+ 'withCheck',
608
+ 'comment',
609
+ ];
610
+
611
+ for (const prop of props) {
612
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
613
+ changes.push(this.createPropertyChange('policy', key, prop, current[prop], desired[prop]));
614
+ }
615
+ }
616
+
617
+ return changes;
618
+ }
619
+
620
+ diffSequence(desired, current, key) {
621
+ const changes = [];
622
+
623
+ const props = [
624
+ 'startValue',
625
+ 'increment',
626
+ 'minValue',
627
+ 'maxValue',
628
+ 'cache',
629
+ 'cycle',
630
+ 'ownedBy',
631
+ 'owner',
632
+ 'dataType',
633
+ 'tablespace',
634
+ 'comment',
635
+ 'currentValue',
636
+ ];
637
+
638
+ for (const prop of props) {
639
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
640
+ changes.push(this.createPropertyChange('sequence', key, prop, current[prop], desired[prop]));
641
+ }
642
+ }
643
+
644
+ return changes;
645
+ }
646
+
647
+ diffType(desired, current, key) {
648
+ const changes = [];
649
+
650
+ // Handle ENUM type
651
+ if (desired.kind === 'ENUM' || current.kind === 'ENUM') {
652
+ const desiredValues = Array.isArray(desired?.enumValues) ? desired.enumValues : [];
653
+ const currentValues = Array.isArray(current?.enumValues) ? current.enumValues : [];
654
+
655
+ const desiredNormalized = desiredValues.map(v => typeof v === 'object' ? v.value : v).sort();
656
+ const currentNormalized = currentValues.map(v => typeof v === 'object' ? v.value : v).sort();
657
+
658
+ if (JSON.stringify(desiredNormalized) !== JSON.stringify(currentNormalized)) {
659
+ const added = desiredNormalized.filter(dv => !currentNormalized.includes(dv));
660
+ const removed = currentNormalized.filter(cv => !desiredNormalized.includes(cv));
661
+
662
+ changes.push(this.createPropertyChange('type', key, 'enumValues', { added: [], removed }, { added, removed }));
663
+
664
+ if (removed.length > 0) {
665
+ changes[changes.length - 1].changeType = 'REMOVE_ENUM_VALUES';
666
+ changes[changes.length - 1].requiresRecreation = true;
667
+ } else if (added.length > 0) {
668
+ changes[changes.length - 1].changeType = 'ADD_ENUM_VALUES';
669
+ }
670
+ }
671
+
672
+ const props = ['owner', 'comment', 'privileges'];
673
+ for (const prop of props) {
674
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
675
+ changes.push(this.createPropertyChange('type', key, prop, current[prop], desired[prop]));
676
+ }
677
+ }
678
+ }
679
+
680
+ // Handle COMPOSITE type
681
+ if (desired.kind === 'COMPOSITE' || current.kind === 'COMPOSITE') {
682
+ const desiredAttrs = Array.isArray(desired?.attributes) ? desired.attributes : [];
683
+ const currentAttrs = Array.isArray(current?.attributes) ? current.attributes : [];
684
+
685
+ if (JSON.stringify(desiredAttrs) !== JSON.stringify(currentAttrs)) {
686
+ changes.push(this.createPropertyChange('type', key, 'attributes', currentAttrs, desiredAttrs));
687
+ changes[changes.length - 1].changeType = 'COMPOSITE_ATTRIBUTES_CHANGE';
688
+ changes[changes.length - 1].requiresRecreation = true;
689
+ }
690
+
691
+ const props = ['owner', 'comment', 'privileges'];
692
+ for (const prop of props) {
693
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
694
+ changes.push(this.createPropertyChange('type', key, prop, current[prop], desired[prop]));
695
+ }
696
+ }
697
+ }
698
+
699
+ // Handle DOMAIN type
700
+ if (desired.kind === 'DOMAIN' || current.kind === 'DOMAIN') {
701
+ const props = ['baseType', 'baseTypeSchema', 'notNull', 'defaultValue', 'checkConstraint', 'collation', 'owner', 'comment', 'length', 'typmod', 'privileges'];
702
+ for (const prop of props) {
703
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
704
+ changes.push(this.createPropertyChange('type', key, prop, current[prop], desired[prop]));
705
+ }
706
+ }
707
+ // Base type changes require recreation
708
+ if (changes.some(c => c.property === 'baseType' || c.property === 'baseTypeSchema')) {
709
+ for (const change of changes) {
710
+ change.requiresRecreation = true;
711
+ }
712
+ }
713
+ }
714
+
715
+ // Handle RANGE type
716
+ if (desired.kind === 'RANGE' || current.kind === 'RANGE') {
717
+ const props = ['subtype', 'subtypeSchema', 'collation', 'subtypeOpclass', 'subtypeDiff', 'canonicalFunction', 'owner', 'comment', 'privileges'];
718
+ for (const prop of props) {
719
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
720
+ changes.push(this.createPropertyChange('type', key, prop, current[prop], desired[prop]));
721
+ }
722
+ }
723
+ // Subtype changes require recreation
724
+ if (changes.some(c => c.property === 'subtype' || c.property === 'subtypeSchema' || c.property === 'subtypeOpclass')) {
725
+ for (const change of changes) {
726
+ change.requiresRecreation = true;
727
+ }
728
+ }
729
+ }
730
+
731
+ return changes;
732
+ }
733
+
734
+ diffRule(desired, current, key) {
735
+ const changes = [];
736
+
737
+ const props = [
738
+ 'event',
739
+ 'isInstead',
740
+ 'isEnabled',
741
+ 'condition',
742
+ 'comment',
743
+ ];
744
+
745
+ for (const prop of props) {
746
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
747
+ changes.push(this.createPropertyChange('rule', key, prop, current[prop], desired[prop]));
748
+ }
749
+ }
750
+
751
+ if (desired.definition && current.definition && desired.definition !== current.definition) {
752
+ changes.push(this.createPropertyChange('rule', key, 'definition', current.definition, desired.definition));
753
+ }
754
+
755
+ return changes;
756
+ }
757
+
758
+ diffEventTrigger(desired, current, key) {
759
+ const changes = [];
760
+
761
+ const props = [
762
+ 'event',
763
+ 'tags',
764
+ 'function',
765
+ 'enabled',
766
+ 'owner',
767
+ 'comment',
768
+ ];
769
+
770
+ for (const prop of props) {
771
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
772
+ changes.push(this.createPropertyChange('eventTrigger', key, prop, current[prop], desired[prop]));
773
+ }
774
+ }
775
+
776
+ return changes;
777
+ }
778
+
779
+ diffOperator(desired, current, key) {
780
+ const changes = [];
781
+
782
+ const props = [
783
+ 'leftType',
784
+ 'rightType',
785
+ 'proc',
786
+ 'commutator',
787
+ 'negator',
788
+ 'restrictFunction',
789
+ 'joinFunction',
790
+ 'canHash',
791
+ 'canMerge',
792
+ 'owner',
793
+ 'comment',
794
+ ];
795
+
796
+ for (const prop of props) {
797
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
798
+ changes.push(this.createPropertyChange('operator', key, prop, current[prop], desired[prop]));
799
+ }
800
+ }
801
+
802
+ return changes;
803
+ }
804
+
805
+ diffOperatorClass(desired, current, key, objectType) {
806
+ const changes = [];
807
+
808
+ const props = [
809
+ 'isDefault',
810
+ 'family',
811
+ 'inputType',
812
+ 'accessMethod',
813
+ 'storageType',
814
+ 'operators',
815
+ 'functions',
816
+ 'owner',
817
+ 'comment',
818
+ ];
819
+
820
+ for (const prop of props) {
821
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
822
+ changes.push(this.createPropertyChange(objectType, key, prop, current[prop], desired[prop]));
823
+ }
824
+ }
825
+
826
+ return changes;
827
+ }
828
+
829
+ diffTextSearch(desired, current, key, objectType) {
830
+ const changes = [];
831
+
832
+ const propsByType = {
833
+ textSearchConfig: ['parser', 'tokenMappings', 'owner', 'comment'],
834
+ textSearchDict: ['template', 'options', 'owner', 'comment'],
835
+ textSearchParser: ['start', 'getToken', 'end', 'lextypes', 'headline', 'owner', 'comment'],
836
+ textSearchTemplate: ['lexize', 'init', 'owner', 'comment'],
837
+ };
838
+
839
+ const props = propsByType[objectType] || [];
840
+
841
+ for (const prop of props) {
842
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
843
+ changes.push(this.createPropertyChange(objectType, key, prop, current[prop], desired[prop]));
844
+ }
845
+ }
846
+
847
+ return changes;
848
+ }
849
+
850
+ diffReplication(desired, current, key, objectType) {
851
+ const changes = [];
852
+
853
+ const propsByType = {
854
+ publication: ['tables', 'allTables', 'insert', 'update', 'delete', 'truncate', 'viaRoot', 'schemas', 'owner', 'comment'],
855
+ subscription: ['conninfo', 'publications', 'enabled', 'slotName', 'syncCommit', 'binaryTransfer', 'streaming', 'twoPhase', 'disableOnError', 'origin', 'owner', 'comment'],
856
+ };
857
+
858
+ const props = propsByType[objectType] || [];
859
+
860
+ for (const prop of props) {
861
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
862
+ changes.push(this.createPropertyChange(objectType, key, prop, current[prop], desired[prop]));
863
+ }
864
+ }
865
+
866
+ return changes;
867
+ }
868
+
869
+ diffStatistics(desired, current, key) {
870
+ const changes = [];
871
+
872
+ const props = [
873
+ 'columns',
874
+ 'kinds',
875
+ 'table',
876
+ 'definition',
877
+ 'owner',
878
+ 'statisticsTarget',
879
+ 'comment',
880
+ ];
881
+
882
+ for (const prop of props) {
883
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
884
+ changes.push(this.createPropertyChange('statistics', key, prop, current[prop], desired[prop]));
885
+ }
886
+ }
887
+
888
+ return changes;
889
+ }
890
+
891
+ diffCollation(desired, current, key) {
892
+ const changes = [];
893
+
894
+ const props = [
895
+ 'locale',
896
+ 'lcCollate',
897
+ 'lcCtype',
898
+ 'provider',
899
+ 'isDeterministic',
900
+ 'version',
901
+ 'encoding',
902
+ 'owner',
903
+ 'comment',
904
+ ];
905
+
906
+ for (const prop of props) {
907
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
908
+ changes.push(this.createPropertyChange('collation', key, prop, current[prop], desired[prop]));
909
+ }
910
+ }
911
+
912
+ return changes;
913
+ }
914
+
915
+ diffCast(desired, current, key) {
916
+ const changes = [];
917
+
918
+ const props = [
919
+ 'sourceType',
920
+ 'targetType',
921
+ 'function',
922
+ 'context',
923
+ 'method',
924
+ 'comment',
925
+ ];
926
+
927
+ for (const prop of props) {
928
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
929
+ changes.push(this.createPropertyChange('cast', key, prop, current[prop], desired[prop]));
930
+ }
931
+ }
932
+
933
+ return changes;
934
+ }
935
+
936
+ diffExtension(desired, current, key) {
937
+ const changes = [];
938
+
939
+ const props = [
940
+ 'version',
941
+ 'owner',
942
+ 'isRelocatable',
943
+ 'isAvailable',
944
+ 'comment',
945
+ ];
946
+
947
+ for (const prop of props) {
948
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
949
+ changes.push(this.createPropertyChange('extension', key, prop, current[prop], desired[prop]));
950
+ }
951
+ }
952
+
953
+ return changes;
954
+ }
955
+
956
+ diffForeignObject(desired, current, key, objectType) {
957
+ const changes = [];
958
+
959
+ const propsByType = {
960
+ foreignServer: ['type', 'version', 'fdw', 'options', 'owner', 'privileges', 'comment'],
961
+ foreignDataWrapper: ['handler', 'validator', 'options', 'owner', 'privileges', 'comment'],
962
+ userMapping: ['user', 'server', 'options'],
963
+ };
964
+
965
+ const props = propsByType[objectType] || [];
966
+
967
+ for (const prop of props) {
968
+ if (prop === 'options') {
969
+ const desiredOpts = desired.options || {};
970
+ const currentOpts = current.options || {};
971
+ const desiredOptsRedacted = Object.fromEntries(Object.entries(desiredOpts).map(([k, v]) => [k, k === 'password' ? '[REDACTED]' : v]));
972
+ const currentOptsRedacted = Object.fromEntries(Object.entries(currentOpts).map(([k, v]) => [k, k === 'password' ? '[REDACTED]' : v]));
973
+ if (JSON.stringify(desiredOptsRedacted) !== JSON.stringify(currentOptsRedacted)) {
974
+ changes.push(this.createPropertyChange(objectType, key, 'options', currentOpts, desiredOpts));
975
+ }
976
+ } else if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
977
+ changes.push(this.createPropertyChange(objectType, key, prop, current[prop], desired[prop]));
978
+ }
979
+ }
980
+
981
+ return changes;
982
+ }
983
+
984
+ diffConversion(desired, current, key) {
985
+ const changes = [];
986
+
987
+ const props = [
988
+ 'sourceEncoding',
989
+ 'targetEncoding',
990
+ 'proc',
991
+ 'isDefault',
992
+ 'owner',
993
+ 'comment',
994
+ ];
995
+
996
+ for (const prop of props) {
997
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
998
+ changes.push(this.createPropertyChange('conversion', key, prop, current[prop], desired[prop]));
999
+ }
1000
+ }
1001
+
1002
+ return changes;
1003
+ }
1004
+
1005
+ diffDefaultPrivileges(desired, current, key) {
1006
+ const changes = [];
1007
+
1008
+ const props = [
1009
+ 'schema',
1010
+ 'forRole',
1011
+ 'privileges',
1012
+ 'objectType',
1013
+ 'grantees',
1014
+ 'withGrantOption',
1015
+ ];
1016
+
1017
+ for (const prop of props) {
1018
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
1019
+ changes.push(this.createPropertyChange('defaultPrivileges', key, prop, current[prop], desired[prop]));
1020
+ }
1021
+ }
1022
+
1023
+ return changes;
1024
+ }
1025
+
1026
+ diffAccessMethod(desired, current, key) {
1027
+ const changes = [];
1028
+
1029
+ const props = [
1030
+ 'type',
1031
+ 'handler',
1032
+ ];
1033
+
1034
+ for (const prop of props) {
1035
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
1036
+ changes.push(this.createPropertyChange('accessMethod', key, prop, current[prop], desired[prop]));
1037
+ }
1038
+ }
1039
+
1040
+ return changes;
1041
+ }
1042
+
1043
+ diffForeignTable(desired, current, key) {
1044
+ const changes = [];
1045
+
1046
+ const props = [
1047
+ 'columns',
1048
+ 'server',
1049
+ 'options',
1050
+ 'owner',
1051
+ 'comment',
1052
+ 'privileges',
1053
+ ];
1054
+
1055
+ for (const prop of props) {
1056
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
1057
+ changes.push(this.createPropertyChange('foreignTable', key, prop, current[prop], desired[prop]));
1058
+ }
1059
+ }
1060
+
1061
+ // Check column foreign options
1062
+ const desiredCols = desired.columns || [];
1063
+ const currentCols = current.columns || [];
1064
+ for (let i = 0; i < Math.min(desiredCols.length, currentCols.length); i++) {
1065
+ if (JSON.stringify(desiredCols[i].foreignOptions) !== JSON.stringify(currentCols[i].foreignOptions)) {
1066
+ changes.push(this.createPropertyChange('foreignTable', key, 'columnOptions', currentCols[i].foreignOptions, desiredCols[i].foreignOptions));
1067
+ }
1068
+ }
1069
+
1070
+ return changes;
1071
+ }
1072
+
1073
+ diffSchema(desired, current, key) {
1074
+ const changes = [];
1075
+
1076
+ const props = ['owner', 'comment'];
1077
+
1078
+ for (const prop of props) {
1079
+ if (desired[prop] !== current[prop] && desired[prop] !== undefined) {
1080
+ changes.push(this.createPropertyChange('schema', key, prop, current[prop], desired[prop]));
1081
+ }
1082
+ }
1083
+
1084
+ if (JSON.stringify(desired.privileges) !== JSON.stringify(current.privileges) && desired.privileges !== undefined) {
1085
+ changes.push(this.createPropertyChange('schema', key, 'privileges', current.privileges, desired.privileges));
1086
+ }
1087
+
1088
+ return changes;
1089
+ }
1090
+
1091
+ diffGeneric(desired, current, key, objectType) {
1092
+ const changes = [];
1093
+
1094
+ const allKeys = new Set([...Object.keys(desired), ...Object.keys(current)]);
1095
+
1096
+ for (const prop of allKeys) {
1097
+ if (prop === 'name' || prop === 'schema' || prop === 'objectType') continue;
1098
+
1099
+ if (JSON.stringify(desired[prop]) !== JSON.stringify(current[prop]) && desired[prop] !== undefined) {
1100
+ changes.push(this.createPropertyChange(objectType, key, prop, current[prop], desired[prop]));
1101
+ }
1102
+ }
1103
+
1104
+ return changes;
1105
+ }
1106
+
1107
+ createPropertyChange(objectType, path, property, currentValue, desiredValue) {
1108
+ return {
1109
+ changeType: 'ALTER',
1110
+ objectType,
1111
+ path,
1112
+ property,
1113
+ currentValue,
1114
+ desiredValue,
1115
+ key: `${path}.${property}`,
1116
+ targetKeys: [path],
1117
+ requiresRecreation: false,
1118
+ changePlan: [],
1119
+ collateralDamage: [],
1120
+ pgVersionMinimum: this.getVersionMinimum(objectType, property, currentValue, desiredValue),
1121
+ };
1122
+ }
1123
+
1124
+ getVersionMinimum(objectType, property, currentValue, desiredValue) {
1125
+ if (property === 'isGenerated' && desiredValue) return 12;
1126
+ if (property === 'parallelWorkers') return 9.6;
1127
+ if (objectType === 'policy') return 9.5;
1128
+ if (objectType === 'procedure') return 11;
1129
+ if (objectType === 'enumValues' && currentValue?.added?.length > 0) {
1130
+ return 8;
1131
+ }
1132
+ return null;
1133
+ }
1134
+ }