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,561 @@
1
+ /**
2
+ * Risk tagger - assesses and tags every change with risk information.
3
+ */
4
+
5
+ import { isWideningCast, isSafeCast } from './utils/type-compatibility.js';
6
+
7
+ const ALL_KNOWN_OBJECT_TYPES = new Set([
8
+ 'table', 'column', 'index', 'constraint', 'sequence', 'extension', 'schema',
9
+ 'type', 'statistics', 'collation', 'operator', 'operatorClass', 'operatorFamily',
10
+ 'cast', 'foreignTable', 'accessMethod', 'defaultPrivileges',
11
+ 'view', 'materializedView', 'function', 'procedure', 'trigger', 'eventTrigger',
12
+ 'policy', 'rule', 'aggregate', 'textSearchConfig', 'textSearchDict', 'conversion',
13
+ 'textSearchParser', 'textSearchTemplate', 'foreignDataWrapper', 'foreignServer',
14
+ 'userMapping', 'publication', 'subscription', 'language',
15
+ 'database', 'domain', 'role', 'tablespace'
16
+ ]);
17
+
18
+ export class RiskTagger {
19
+ constructor(pgVersion = 150000) {
20
+ this.pgVersion = pgVersion;
21
+ }
22
+
23
+ /**
24
+ * Tag all changes with risk information.
25
+ */
26
+ tag(changes) {
27
+ for (const change of changes) {
28
+ change.risk = this.assessRisk(change);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Assess risk for a single change.
34
+ */
35
+ assessRisk(change) {
36
+ const risk = {
37
+ level: 'none',
38
+ categories: [],
39
+ warnings: [],
40
+ safePatternAvailable: false,
41
+ requiresDowntime: false,
42
+ estimatedDuration: null,
43
+ };
44
+
45
+ // Check NONE risk first (zero risk)
46
+ if (this.isNoneRisk(change)) {
47
+ return risk;
48
+ }
49
+
50
+ if (this.isCriticalRisk(change)) {
51
+ risk.level = 'critical';
52
+ this.addRiskCategories(risk, ['data_loss', 'irreversible']);
53
+ risk.warnings.push(this.getCriticalWarning(change));
54
+ }
55
+
56
+ else if (this.isHighRisk(change)) {
57
+ risk.level = this.upgradeRisk(risk.level, 'high');
58
+ this.addRiskCategories(risk, ['irreversible']);
59
+ risk.warnings.push(this.getHighWarning(change));
60
+ }
61
+
62
+ else if (this.isMediumRisk(change)) {
63
+ risk.level = this.upgradeRisk(risk.level, 'medium');
64
+ risk.warnings.push(this.getMediumWarning(change));
65
+ }
66
+
67
+ else if (this.isLowRisk(change)) {
68
+ risk.level = this.upgradeRisk(risk.level, 'low');
69
+ }
70
+
71
+ if (this.isLockHazard(change)) {
72
+ risk.level = this.upgradeRisk(risk.level, 'medium');
73
+ this.addRiskCategories(risk, ['lock_hazard']);
74
+ risk.safePatternAvailable = this.hasSafePattern(change);
75
+ if (!risk.safePatternAvailable) {
76
+ risk.requiresDowntime = true;
77
+ }
78
+ }
79
+
80
+ if (change.pgVersionMinimum && this.pgVersion < change.pgVersionMinimum * 10000) {
81
+ risk.level = this.upgradeRisk(risk.level, 'critical');
82
+ this.addRiskCategories(risk, ['version_incompatible']);
83
+ risk.warnings.push(`Requires PostgreSQL ${change.pgVersionMinimum}+. Current: ${Math.floor(this.pgVersion / 10000)}`);
84
+ }
85
+
86
+ if (change.property === 'dataType' && change.changeType !== 'widening') {
87
+ if (change.changeType === 'impossible') {
88
+ risk.level = 'critical';
89
+ this.addRiskCategories(risk, ['impossible_cast']);
90
+ risk.warnings.push(`No cast path from ${change.currentValue} to ${change.desiredValue}`);
91
+ } else if (change.changeType === 'unsafe_cast' || change.changeType === 'NARROWING_CAST') {
92
+ risk.level = this.upgradeRisk(risk.level, 'high');
93
+ this.addRiskCategories(risk, ['unsafe_cast']);
94
+ risk.warnings.push(`Type change ${change.currentValue} → ${change.desiredValue} may fail or lose data`);
95
+ }
96
+ }
97
+
98
+ if (change.property === 'isNullable' && change.desiredValue === false && change.currentValue !== false) {
99
+ risk.level = this.upgradeRisk(risk.level, 'medium');
100
+ this.addRiskCategories(risk, ['constraint_violation']);
101
+ risk.safePatternAvailable = true;
102
+ risk.warnings.push('Adding NOT NULL may fail if NULL values exist');
103
+ }
104
+
105
+ if (change.objectType === 'constraint' && change.constraintType === 'FOREIGN_KEY') {
106
+ risk.level = this.upgradeRisk(risk.level, 'medium');
107
+ this.addRiskCategories(risk, ['lock_hazard']);
108
+ risk.safePatternAvailable = true;
109
+ }
110
+
111
+ if (change.objectType === 'index' && change.changeType === 'CREATE') {
112
+ if (!change.isConcurrent) {
113
+ risk.level = this.upgradeRisk(risk.level, 'medium');
114
+ this.addRiskCategories(risk, ['lock_hazard']);
115
+ risk.safePatternAvailable = true;
116
+ risk.warnings.push('Non-concurrent index creation locks table');
117
+ }
118
+ }
119
+
120
+ if (change.changeType === 'REMOVE_ENUM_VALUES') {
121
+ risk.level = 'critical';
122
+ this.addRiskCategories(risk, ['data_loss', 'irreversible']);
123
+ risk.warnings.push(`Cannot remove enum values without DROP + CREATE type`);
124
+ }
125
+
126
+ if (change.changeType === 'RENAME' && !change.confirmed) {
127
+ risk.level = this.upgradeRisk(risk.level, 'medium');
128
+ this.addRiskCategories(risk, ['rename_unconfirmed']);
129
+ risk.warnings.push('Rename detected but not confirmed. Will be treated as drop+add if not confirmed.');
130
+ }
131
+
132
+ if (change.collateralDamage && change.collateralDamage.length > 0) {
133
+ risk.level = this.upgradeRisk(risk.level, 'high');
134
+ this.addRiskCategories(risk, ['collateral_damage']);
135
+ risk.warnings.push(`Affects: ${change.collateralDamage.join(', ')}`);
136
+ }
137
+
138
+ // FIX RISK-002: ALTER TABLE SET ACCESS METHOD rewrites entire table
139
+ if (change.objectType === 'table' && change.changeType === 'ALTER') {
140
+ const changedProperties = change.changedProperties || (change.property ? [change.property] : []);
141
+ if (changedProperties.includes('accessMethod')) {
142
+ risk.level = 'critical';
143
+ this.addRiskCategories(risk, ['table_rewrite', 'data_loss']);
144
+ risk.warnings.push('ALTER TABLE SET ACCESS METHOD rewrites the entire table. This operation may take a long time on large tables and requires sufficient disk space.');
145
+ risk.safePatternAvailable = false;
146
+ }
147
+ }
148
+
149
+ // FIX RISK-003: DROP EXTENSION logic was inverted
150
+ if (change.objectType === 'extension' && change.changeType === 'DROP') {
151
+ if (change.isUsed) {
152
+ risk.level = this.upgradeRisk(risk.level, 'high');
153
+ this.addRiskCategories(risk, ['collateral_damage']);
154
+ risk.warnings.push('Extension is used by other objects. Dropping will CASCADE to dependents.');
155
+ risk.safePatternAvailable = false;
156
+ } else {
157
+ risk.level = 'low';
158
+ risk.warnings = risk.warnings.filter(w => !w.includes('Extension'));
159
+ risk.warnings.push('Extension is not used by any objects.');
160
+ }
161
+ }
162
+
163
+ // FIX RISK-004: CASCADE DROP is always critical
164
+ if (change.changeType === 'DROP' && change.cascade === true) {
165
+ risk.level = 'critical';
166
+ if (!risk.categories.includes('cascade_risk')) {
167
+ this.addRiskCategories(risk, ['cascade_risk']);
168
+ }
169
+ const objectName = change.objectKey || change.name || 'unknown';
170
+ risk.warnings.push(
171
+ `CASCADE DROP on ${change.objectType} '${objectName}' may destroy dependent objects. ` +
172
+ `Review dependent objects before proceeding.`
173
+ );
174
+ risk.safePatternAvailable = false;
175
+ }
176
+
177
+ // FIX RISK-007: Add missing risk categories
178
+
179
+ // dependency_impact — objects that others depend on
180
+ if (change.changeType === 'DROP' && change.isUsed) {
181
+ if (!risk.categories.includes('dependency_impact')) {
182
+ this.addRiskCategories(risk, ['dependency_impact']);
183
+ }
184
+ const depWarning = change.objectType === 'type'
185
+ ? 'Type is used by columns or functions. Dropping will break dependent objects.'
186
+ : change.objectType === 'function'
187
+ ? 'Function is used by triggers or views. Dropping will break dependent objects.'
188
+ : `${change.objectType} is used by other objects. Dropping will break dependents.`;
189
+ if (!risk.warnings.some(w => w.includes('used by'))) {
190
+ risk.warnings.push(depWarning);
191
+ }
192
+ }
193
+
194
+ // data_validation — ADD NOT NULL validated
195
+ if (change.objectType === 'column' && change.changeType === 'ALTER') {
196
+ const changedProperties = change.changedProperties || (change.property ? [change.property] : []);
197
+ if (changedProperties.includes('isNullable') || change.property === 'isNullable') {
198
+ if (change.desiredValue === false && change.currentValue !== false) {
199
+ if (!risk.categories.includes('data_validation')) {
200
+ this.addRiskCategories(risk, ['data_validation']);
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ // performance_impact — operations that scan/rewrite full table
207
+ if (change.objectType === 'index' && change.changeType === 'CREATE' && !change.isConcurrent) {
208
+ if (!risk.categories.includes('performance_impact')) {
209
+ this.addRiskCategories(risk, ['performance_impact']);
210
+ }
211
+ }
212
+ if (change.objectType === 'column' && change.changeType === 'ALTER') {
213
+ const changedProperties = change.changedProperties || (change.property ? [change.property] : []);
214
+ if (changedProperties.includes('dataType') || change.property === 'dataType') {
215
+ if (!risk.categories.includes('performance_impact')) {
216
+ this.addRiskCategories(risk, ['performance_impact']);
217
+ }
218
+ }
219
+ }
220
+ // ACCESS METHOD already adds table_rewrite, also add performance_impact
221
+ if (change.objectType === 'table' && change.changeType === 'ALTER') {
222
+ const changedProperties = change.changedProperties || (change.property ? [change.property] : []);
223
+ if (changedProperties.includes('accessMethod')) {
224
+ if (!risk.categories.includes('performance_impact')) {
225
+ this.addRiskCategories(risk, ['performance_impact']);
226
+ }
227
+ }
228
+ }
229
+
230
+ // reversibility — all DROP operations
231
+ if (change.changeType === 'DROP' || change.changeType?.startsWith('DROP')) {
232
+ const hardReverse = ['table', 'column', 'sequence'];
233
+ const easyReverse = ['view', 'materializedView', 'function', 'procedure', 'trigger',
234
+ 'eventTrigger', 'policy', 'rule', 'aggregate', 'index',
235
+ 'textSearchConfig', 'textSearchDict', 'textSearchParser', 'textSearchTemplate',
236
+ 'conversion', 'operator', 'operatorClass', 'operatorFamily', 'cast'];
237
+
238
+ if (!risk.categories.includes('reversibility')) {
239
+ this.addRiskCategories(risk, ['reversibility']);
240
+ }
241
+
242
+ if (hardReverse.includes(change.objectType)) {
243
+ if (!risk.warnings.some(w => w.includes('irreversible'))) {
244
+ risk.warnings.push(`${change.objectType} drop is irreversible — data cannot be recovered.`);
245
+ }
246
+ }
247
+ }
248
+
249
+ // FIX RISK-001: Unknown object types default to "high" risk
250
+ if (!ALL_KNOWN_OBJECT_TYPES.has(change.objectType)) {
251
+ risk.level = this.upgradeRisk(risk.level, 'high');
252
+ risk.warnings.push(`Unknown object type '${change.objectType}' — risk assessment may be inaccurate`);
253
+ risk.safePatternAvailable = false;
254
+ }
255
+
256
+ return risk;
257
+ }
258
+
259
+ /**
260
+ * Determine if this is a CRITICAL risk change.
261
+ * Critical = data loss with no recovery path, or irreversible structural damage.
262
+ */
263
+ isCriticalRisk(change) {
264
+ if (change.changeType === 'DROP' || change.changeType?.startsWith('DROP')) {
265
+ if (change.objectType === 'table' && change.hasData === true) {
266
+ return true;
267
+ }
268
+ if (change.objectType === 'column' && change.isReferenced === true) {
269
+ return true;
270
+ }
271
+ if (change.objectType === 'type' && change.isUsed === true) {
272
+ return true;
273
+ }
274
+ }
275
+
276
+ if (change.changeType === 'REMOVE_ENUM_VALUES') {
277
+ return true;
278
+ }
279
+
280
+ if (change.property === 'dataType') {
281
+ if (change.changeType === 'impossible') {
282
+ return true;
283
+ }
284
+ const info = this.getTypeChangeInfo(change.currentValue, change.desiredValue);
285
+ if (info && info.dataLossRisk === 'critical') {
286
+ return true;
287
+ }
288
+ }
289
+
290
+ return false;
291
+ }
292
+
293
+ /**
294
+ * Determine if this is a HIGH risk change.
295
+ * High = potential data loss, but may be recoverable or has safe patterns.
296
+ */
297
+ isHighRisk(change) {
298
+ if (change.changeType === 'DROP' || change.changeType?.startsWith('DROP')) {
299
+ if (change.objectType === 'table') {
300
+ return true;
301
+ }
302
+ if (change.objectType === 'column') {
303
+ return true;
304
+ }
305
+ if (change.objectType === 'type') {
306
+ return true;
307
+ }
308
+ if (change.objectType === 'constraint') {
309
+ if (change.constraintType === 'FOREIGN_KEY') {
310
+ return true;
311
+ }
312
+ return true;
313
+ }
314
+ if (change.objectType === 'sequence') {
315
+ return true;
316
+ }
317
+ if (change.objectType === 'view' || change.objectType === 'materializedView') {
318
+ return true;
319
+ }
320
+ }
321
+
322
+ if (change.changeType?.includes('RECREATE') && change.requiresRecreation) {
323
+ return true;
324
+ }
325
+
326
+ if (change.property === 'dataType') {
327
+ const info = this.getTypeChangeInfo(change.currentValue, change.desiredValue);
328
+ if (info && info.dataLossRisk === 'truncation') {
329
+ return true;
330
+ }
331
+ }
332
+
333
+ if (change.changeType === 'DROP' && change.objectType === 'index') {
334
+ const isConcurrent = change.isConcurrent || change.ddlStrategy === 'DROP_INDEX_CONCURRENTLY';
335
+ if (!isConcurrent) {
336
+ return true;
337
+ }
338
+ }
339
+
340
+ return false;
341
+ }
342
+
343
+ /**
344
+ * Determine if this is a MEDIUM risk change.
345
+ * Medium = functional impact, application breaking, but no direct data loss.
346
+ */
347
+ isMediumRisk(change) {
348
+ if (change.changeType === 'DROP' || change.changeType?.startsWith('DROP')) {
349
+ if (change.objectType === 'function' || change.objectType === 'procedure') {
350
+ return true;
351
+ }
352
+ if (change.objectType === 'trigger' || change.objectType === 'eventTrigger') {
353
+ return true;
354
+ }
355
+ if (change.objectType === 'policy' || change.objectType === 'rule') {
356
+ return true;
357
+ }
358
+ if (change.objectType === 'index' && change.isConcurrent) {
359
+ return true;
360
+ }
361
+ if (change.objectType === 'publication' || change.objectType === 'subscription') {
362
+ return true;
363
+ }
364
+ if (change.objectType === 'foreignServer' || change.objectType === 'foreignTable') {
365
+ return true;
366
+ }
367
+ }
368
+
369
+ if (change.property === 'dataType') {
370
+ return !isWideningCast(change.currentValue, change.desiredValue);
371
+ }
372
+
373
+ if (change.property === 'isNullable' && change.desiredValue === false) {
374
+ return true;
375
+ }
376
+
377
+ if (change.changeType === 'ALTER' && change.objectType === 'function') {
378
+ return true;
379
+ }
380
+
381
+ return false;
382
+ }
383
+
384
+ /**
385
+ * Determine if this is a LOW risk change.
386
+ * Low = cosmetic changes, no data or application impact.
387
+ */
388
+ isLowRisk(change) {
389
+ if (change.changeType === 'DROP' || change.changeType?.startsWith('DROP')) {
390
+ if (change.objectType === 'cast') return true;
391
+ if (change.objectType === 'operator') return true;
392
+ if (change.objectType === 'operatorClass' || change.objectType === 'operatorFamily') return true;
393
+ if (change.objectType === 'conversion') return true;
394
+ if (change.objectType === 'statistics') return true;
395
+ if (change.objectType === 'collation') return true;
396
+ if (change.objectType === 'textSearchConfig' || change.objectType === 'textSearchDict') return true;
397
+ if (change.objectType === 'textSearchParser' || change.objectType === 'textSearchTemplate') return true;
398
+ if (change.objectType === 'extension' && !change.isUsed) return true;
399
+ }
400
+
401
+ if (change.property === 'defaultValue') {
402
+ return true;
403
+ }
404
+
405
+ if (change.property === 'comment') {
406
+ return true;
407
+ }
408
+
409
+ if (change.property === 'owner') {
410
+ return true;
411
+ }
412
+
413
+ if (change.changeType === 'RENAME' && change.confirmed) {
414
+ return true;
415
+ }
416
+
417
+ return false;
418
+ }
419
+
420
+ /**
421
+ * Determine if this is a NONE risk change (zero risk).
422
+ * None = no impact on data or application behavior.
423
+ */
424
+ isNoneRisk(change) {
425
+ if (change.property === 'comment') {
426
+ return true;
427
+ }
428
+
429
+ if (change.property === 'owner') {
430
+ return true;
431
+ }
432
+
433
+ if (change.changeType === 'CREATE' || change.changeType?.startsWith('ADD')) {
434
+ if (change.objectType === 'function' || change.objectType === 'procedure') {
435
+ return true;
436
+ }
437
+ if (change.objectType === 'view') {
438
+ return true;
439
+ }
440
+ if (change.objectType === 'trigger') {
441
+ return true;
442
+ }
443
+ if (change.objectType === 'index' && change.isConcurrent) {
444
+ return true;
445
+ }
446
+ if (change.objectType === 'column' && change.isNullable !== false) {
447
+ return true;
448
+ }
449
+ }
450
+
451
+ if (change.objectType === 'index' && change.isConcurrent) {
452
+ return true;
453
+ }
454
+
455
+ if (change.objectType === 'constraint' && change.isValidated === false) {
456
+ return true;
457
+ }
458
+
459
+ return false;
460
+ }
461
+
462
+ addRiskCategories(risk, categories) {
463
+ for (const cat of categories) {
464
+ if (!risk.categories.includes(cat)) {
465
+ risk.categories.push(cat);
466
+ }
467
+ }
468
+ }
469
+
470
+ getCriticalWarning(change) {
471
+ if (change.objectType === 'table' && change.changeType === 'DROP') {
472
+ return `Dropping table ${change.objectKey} will permanently delete all data`;
473
+ }
474
+ if (change.objectType === 'column' && change.changeType === 'DROP') {
475
+ return `Dropping column ${change.objectKey} will lose all data in that column`;
476
+ }
477
+ if (change.objectType === 'type' && change.changeType === 'DROP') {
478
+ return `Dropping type ${change.objectKey} will cascade to all columns using it`;
479
+ }
480
+ if (change.changeType === 'REMOVE_ENUM_VALUES') {
481
+ return `Removing enum values will require DROP + CREATE type, affecting all columns using it`;
482
+ }
483
+ return 'This change may cause irreversible data loss';
484
+ }
485
+
486
+ getHighWarning(change) {
487
+ if (change.objectType === 'constraint') {
488
+ return `Dropping constraint ${change.objectKey} removes data integrity protection`;
489
+ }
490
+ if (change.objectType === 'index' && change.changeType === 'DROP') {
491
+ return `Dropping index ${change.objectKey} non-blockingly will impact performance`;
492
+ }
493
+ if (change.objectType === 'sequence') {
494
+ return `Dropping sequence ${change.objectKey} may break auto-generated values`;
495
+ }
496
+ if (change.objectType === 'view' || change.objectType === 'materializedView') {
497
+ return `Dropping ${change.objectType} ${change.objectKey} removes query abstraction`;
498
+ }
499
+ if (change.property === 'dataType') {
500
+ return `Type change ${change.currentValue} → ${change.desiredValue} may truncate data`;
501
+ }
502
+ return 'This change may impact data integrity';
503
+ }
504
+
505
+ getMediumWarning(change) {
506
+ if (change.objectType === 'function' || change.objectType === 'procedure') {
507
+ return `Dropping ${change.objectType} ${change.objectKey} may break application code`;
508
+ }
509
+ if (change.objectType === 'trigger' || change.objectType === 'eventTrigger') {
510
+ return `Dropping trigger ${change.objectKey} disables automated behavior`;
511
+ }
512
+ if (change.objectType === 'policy') {
513
+ return `Dropping policy ${change.objectKey} changes row-level security`;
514
+ }
515
+ if (change.objectType === 'rule') {
516
+ return `Dropping rule ${change.objectKey} changes query behavior`;
517
+ }
518
+ if (change.objectType === 'index' && change.changeType === 'DROP') {
519
+ return `Dropping index ${change.objectKey} concurrently is safer but slower`;
520
+ }
521
+ return 'This change may have unintended side effects';
522
+ }
523
+
524
+ getTypeChangeInfo(fromType, toType) {
525
+ if (!fromType || !toType) return null;
526
+
527
+ try {
528
+ const { getCastInfo } = require('./utils/type-compatibility.js');
529
+ return getCastInfo(fromType, toType);
530
+ } catch {
531
+ return null;
532
+ }
533
+ }
534
+
535
+ isLockHazard(change) {
536
+ if (change.objectType === 'index' && !change.isConcurrent && change.ddlStrategy === 'CREATE_INDEX') {
537
+ return true;
538
+ }
539
+ if (change.objectType === 'constraint' && change.constraintType === 'FOREIGN_KEY') {
540
+ return true;
541
+ }
542
+ if (change.property === 'dataType') {
543
+ return true;
544
+ }
545
+ return false;
546
+ }
547
+
548
+ hasSafePattern(change) {
549
+ if (change.objectType === 'index' && !change.isConcurrent) return true;
550
+ if (change.property === 'isNullable' && change.desiredValue === false) return true;
551
+ if (change.objectType === 'constraint' && change.constraintType === 'FOREIGN_KEY') return true;
552
+ return false;
553
+ }
554
+
555
+ upgradeRisk(current, newLevel) {
556
+ const levels = ['none', 'low', 'medium', 'high', 'critical'];
557
+ const currentIdx = levels.indexOf(current);
558
+ const newIdx = levels.indexOf(newLevel);
559
+ return levels[Math.max(currentIdx, newIdx)];
560
+ }
561
+ }