@pgpmjs/core 3.0.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 (140) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +99 -0
  3. package/core/boilerplate-scanner.d.ts +41 -0
  4. package/core/boilerplate-scanner.js +106 -0
  5. package/core/boilerplate-types.d.ts +52 -0
  6. package/core/boilerplate-types.js +6 -0
  7. package/core/class/pgpm.d.ts +150 -0
  8. package/core/class/pgpm.js +1470 -0
  9. package/core/template-scaffold.d.ts +29 -0
  10. package/core/template-scaffold.js +168 -0
  11. package/esm/core/boilerplate-scanner.js +96 -0
  12. package/esm/core/boilerplate-types.js +5 -0
  13. package/esm/core/class/pgpm.js +1430 -0
  14. package/esm/core/template-scaffold.js +161 -0
  15. package/esm/export/export-meta.js +240 -0
  16. package/esm/export/export-migrations.js +180 -0
  17. package/esm/extensions/extensions.js +31 -0
  18. package/esm/files/extension/index.js +3 -0
  19. package/esm/files/extension/reader.js +79 -0
  20. package/esm/files/extension/writer.js +63 -0
  21. package/esm/files/index.js +6 -0
  22. package/esm/files/plan/generator.js +49 -0
  23. package/esm/files/plan/index.js +5 -0
  24. package/esm/files/plan/parser.js +296 -0
  25. package/esm/files/plan/validators.js +181 -0
  26. package/esm/files/plan/writer.js +114 -0
  27. package/esm/files/sql/index.js +1 -0
  28. package/esm/files/sql/writer.js +107 -0
  29. package/esm/files/sql-scripts/index.js +2 -0
  30. package/esm/files/sql-scripts/reader.js +19 -0
  31. package/esm/files/types/index.js +1 -0
  32. package/esm/files/types/package.js +1 -0
  33. package/esm/index.js +21 -0
  34. package/esm/init/client.js +144 -0
  35. package/esm/init/sql/bootstrap-roles.sql +55 -0
  36. package/esm/init/sql/bootstrap-test-roles.sql +72 -0
  37. package/esm/migrate/clean.js +23 -0
  38. package/esm/migrate/client.js +551 -0
  39. package/esm/migrate/index.js +5 -0
  40. package/esm/migrate/sql/procedures.sql +258 -0
  41. package/esm/migrate/sql/schema.sql +37 -0
  42. package/esm/migrate/types.js +1 -0
  43. package/esm/migrate/utils/event-logger.js +28 -0
  44. package/esm/migrate/utils/hash.js +27 -0
  45. package/esm/migrate/utils/transaction.js +125 -0
  46. package/esm/modules/modules.js +49 -0
  47. package/esm/packaging/package.js +96 -0
  48. package/esm/packaging/transform.js +70 -0
  49. package/esm/projects/deploy.js +123 -0
  50. package/esm/projects/revert.js +75 -0
  51. package/esm/projects/verify.js +61 -0
  52. package/esm/resolution/deps.js +526 -0
  53. package/esm/resolution/resolve.js +101 -0
  54. package/esm/utils/debug.js +147 -0
  55. package/esm/utils/target-utils.js +37 -0
  56. package/esm/workspace/paths.js +43 -0
  57. package/esm/workspace/utils.js +31 -0
  58. package/export/export-meta.d.ts +8 -0
  59. package/export/export-meta.js +244 -0
  60. package/export/export-migrations.d.ts +17 -0
  61. package/export/export-migrations.js +187 -0
  62. package/extensions/extensions.d.ts +5 -0
  63. package/extensions/extensions.js +35 -0
  64. package/files/extension/index.d.ts +2 -0
  65. package/files/extension/index.js +19 -0
  66. package/files/extension/reader.d.ts +24 -0
  67. package/files/extension/reader.js +86 -0
  68. package/files/extension/writer.d.ts +39 -0
  69. package/files/extension/writer.js +70 -0
  70. package/files/index.d.ts +5 -0
  71. package/files/index.js +22 -0
  72. package/files/plan/generator.d.ts +22 -0
  73. package/files/plan/generator.js +57 -0
  74. package/files/plan/index.d.ts +4 -0
  75. package/files/plan/index.js +21 -0
  76. package/files/plan/parser.d.ts +27 -0
  77. package/files/plan/parser.js +303 -0
  78. package/files/plan/validators.d.ts +52 -0
  79. package/files/plan/validators.js +187 -0
  80. package/files/plan/writer.d.ts +27 -0
  81. package/files/plan/writer.js +124 -0
  82. package/files/sql/index.d.ts +1 -0
  83. package/files/sql/index.js +17 -0
  84. package/files/sql/writer.d.ts +12 -0
  85. package/files/sql/writer.js +114 -0
  86. package/files/sql-scripts/index.d.ts +1 -0
  87. package/files/sql-scripts/index.js +18 -0
  88. package/files/sql-scripts/reader.d.ts +8 -0
  89. package/files/sql-scripts/reader.js +23 -0
  90. package/files/types/index.d.ts +46 -0
  91. package/files/types/index.js +17 -0
  92. package/files/types/package.d.ts +20 -0
  93. package/files/types/package.js +2 -0
  94. package/index.d.ts +21 -0
  95. package/index.js +45 -0
  96. package/init/client.d.ts +26 -0
  97. package/init/client.js +148 -0
  98. package/init/sql/bootstrap-roles.sql +55 -0
  99. package/init/sql/bootstrap-test-roles.sql +72 -0
  100. package/migrate/clean.d.ts +1 -0
  101. package/migrate/clean.js +27 -0
  102. package/migrate/client.d.ts +80 -0
  103. package/migrate/client.js +555 -0
  104. package/migrate/index.d.ts +5 -0
  105. package/migrate/index.js +21 -0
  106. package/migrate/sql/procedures.sql +258 -0
  107. package/migrate/sql/schema.sql +37 -0
  108. package/migrate/types.d.ts +67 -0
  109. package/migrate/types.js +2 -0
  110. package/migrate/utils/event-logger.d.ts +13 -0
  111. package/migrate/utils/event-logger.js +32 -0
  112. package/migrate/utils/hash.d.ts +12 -0
  113. package/migrate/utils/hash.js +32 -0
  114. package/migrate/utils/transaction.d.ts +27 -0
  115. package/migrate/utils/transaction.js +129 -0
  116. package/modules/modules.d.ts +31 -0
  117. package/modules/modules.js +56 -0
  118. package/package.json +70 -0
  119. package/packaging/package.d.ts +19 -0
  120. package/packaging/package.js +102 -0
  121. package/packaging/transform.d.ts +22 -0
  122. package/packaging/transform.js +75 -0
  123. package/projects/deploy.d.ts +8 -0
  124. package/projects/deploy.js +160 -0
  125. package/projects/revert.d.ts +15 -0
  126. package/projects/revert.js +112 -0
  127. package/projects/verify.d.ts +8 -0
  128. package/projects/verify.js +98 -0
  129. package/resolution/deps.d.ts +57 -0
  130. package/resolution/deps.js +531 -0
  131. package/resolution/resolve.d.ts +37 -0
  132. package/resolution/resolve.js +107 -0
  133. package/utils/debug.d.ts +21 -0
  134. package/utils/debug.js +153 -0
  135. package/utils/target-utils.d.ts +5 -0
  136. package/utils/target-utils.js +40 -0
  137. package/workspace/paths.d.ts +14 -0
  138. package/workspace/paths.js +50 -0
  139. package/workspace/utils.d.ts +8 -0
  140. package/workspace/utils.js +36 -0
@@ -0,0 +1,551 @@
1
+ import { Logger } from '@pgpmjs/logger';
2
+ import { errors } from '@pgpmjs/types';
3
+ import { readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { getPgPool } from 'pg-cache';
6
+ import { parsePlanFile, parsePlanFileSimple, readScript } from '../files';
7
+ import { resolveDependencies } from '../resolution/deps';
8
+ import { resolveTagToChangeName } from '../resolution/resolve';
9
+ import { cleanSql } from './clean';
10
+ import { EventLogger } from './utils/event-logger';
11
+ import { hashFile, hashSqlFile } from './utils/hash';
12
+ import { executeQuery, withTransaction } from './utils/transaction';
13
+ // Helper function to get changes in order
14
+ function getChangesInOrder(planPath, reverse = false) {
15
+ const plan = parsePlanFileSimple(planPath);
16
+ return reverse ? [...plan.changes].reverse() : plan.changes;
17
+ }
18
+ const log = new Logger('migrate');
19
+ export class PgpmMigrate {
20
+ pool;
21
+ pgConfig;
22
+ hashMethod;
23
+ eventLogger;
24
+ initialized = false;
25
+ toUnqualifiedLocal(pkg, nm) {
26
+ if (!nm.includes(':'))
27
+ return nm;
28
+ const [p, local] = nm.split(':', 2);
29
+ if (p === pkg)
30
+ return local;
31
+ throw new Error(`Cross-package change encountered in local tracking: ${nm} (current package: ${pkg})`);
32
+ }
33
+ constructor(config, options = {}) {
34
+ this.pgConfig = config;
35
+ // Use environment variable DEPLOYMENT_HASH_METHOD if available, otherwise use options or default to 'content'
36
+ const envHashMethod = process.env.DEPLOYMENT_HASH_METHOD;
37
+ this.hashMethod = options.hashMethod || envHashMethod || 'content';
38
+ this.pool = getPgPool(this.pgConfig);
39
+ this.eventLogger = new EventLogger(this.pgConfig);
40
+ }
41
+ /**
42
+ * Calculate script hash using the configured method
43
+ */
44
+ async calculateScriptHash(filePath) {
45
+ if (this.hashMethod === 'ast') {
46
+ return await hashSqlFile(filePath);
47
+ }
48
+ else {
49
+ return await hashFile(filePath);
50
+ }
51
+ }
52
+ /**
53
+ * Initialize the migration schema
54
+ */
55
+ async initialize() {
56
+ if (this.initialized)
57
+ return;
58
+ try {
59
+ log.info('Checking LaunchQL migration schema...');
60
+ // Check if pgpm_migrate schema exists
61
+ const result = await this.pool.query(`
62
+ SELECT schema_name
63
+ FROM information_schema.schemata
64
+ WHERE schema_name = 'pgpm_migrate'
65
+ `);
66
+ if (result.rows.length === 0) {
67
+ log.info('Schema not found, creating migration schema...');
68
+ // Read and execute schema SQL to create schema and tables
69
+ const schemaPath = join(__dirname, 'sql', 'schema.sql');
70
+ const proceduresPath = join(__dirname, 'sql', 'procedures.sql');
71
+ const schemaSql = readFileSync(schemaPath, 'utf-8');
72
+ const proceduresSql = readFileSync(proceduresPath, 'utf-8');
73
+ await this.pool.query(schemaSql);
74
+ await this.pool.query(proceduresSql);
75
+ log.success('Migration schema created successfully');
76
+ }
77
+ else {
78
+ log.success('Migration schema found and ready');
79
+ }
80
+ this.initialized = true;
81
+ }
82
+ catch (error) {
83
+ log.error('Failed to initialize migration schema:', error);
84
+ throw error;
85
+ }
86
+ }
87
+ /**
88
+ * Resolve toChange parameter, handling tag resolution if needed
89
+ */
90
+ resolveToChange(toChange, planPath, packageName) {
91
+ return toChange && toChange.includes('@') ? resolveTagToChangeName(planPath, toChange, packageName) : toChange;
92
+ }
93
+ /**
94
+ * Deploy changes according to plan file
95
+ */
96
+ async deploy(options) {
97
+ await this.initialize();
98
+ const { modulePath, toChange, useTransaction = true, debug = false, logOnly = false } = options;
99
+ const planPath = join(modulePath, 'pgpm.plan');
100
+ const plan = parsePlanFileSimple(planPath);
101
+ const resolvedToChange = this.resolveToChange(toChange, planPath, plan.package);
102
+ const changes = getChangesInOrder(planPath);
103
+ const fullPlanResult = parsePlanFile(planPath);
104
+ const packageDir = dirname(planPath);
105
+ const resolvedDeps = resolveDependencies(packageDir, fullPlanResult.data?.package || plan.package, {
106
+ tagResolution: 'resolve',
107
+ loadPlanFiles: true,
108
+ source: options.usePlan === false ? 'sql' : 'plan'
109
+ });
110
+ const deployed = [];
111
+ const skipped = [];
112
+ let failed;
113
+ // Use a separate pool for the target database
114
+ const targetPool = getPgPool(this.pgConfig);
115
+ // Execute deployment with or without transaction
116
+ await withTransaction(targetPool, { useTransaction }, async (context) => {
117
+ for (const change of changes) {
118
+ // Stop if we've reached the target change
119
+ if (resolvedToChange && deployed.includes(resolvedToChange)) {
120
+ break;
121
+ }
122
+ const isDeployed = await this.isDeployed(plan.package, change.name);
123
+ if (isDeployed) {
124
+ log.info(`Skipping already deployed change: ${change.name}`);
125
+ const unqualified = this.toUnqualifiedLocal(plan.package, change.name);
126
+ skipped.push(unqualified);
127
+ continue;
128
+ }
129
+ // Read deploy script
130
+ const deployScript = readScript(dirname(planPath), 'deploy', change.name);
131
+ if (!deployScript) {
132
+ log.error(`Deploy script not found for change: ${change.name}`);
133
+ failed = change.name;
134
+ break;
135
+ }
136
+ const cleanDeploySql = await cleanSql(deployScript, false, '$EOFCODE$');
137
+ // Calculate script hash
138
+ const scriptHash = await this.calculateScriptHash(join(dirname(planPath), 'deploy', `${change.name}.sql`));
139
+ const changeKey = `/deploy/${change.name}.sql`;
140
+ const resolvedFromDeps = resolvedDeps?.deps[changeKey];
141
+ const resolvedChangeDeps = (resolvedFromDeps !== undefined) ? resolvedFromDeps : change.dependencies;
142
+ const qualifiedDeps = (resolvedChangeDeps && resolvedChangeDeps.length > 0)
143
+ ? Array.from(new Set(resolvedChangeDeps.map((dep) => (dep.includes(':') ? dep : `${plan.package}:${dep}`))))
144
+ : resolvedChangeDeps;
145
+ try {
146
+ // Call the deploy stored procedure
147
+ await executeQuery(context, 'CALL pgpm_migrate.deploy($1::TEXT, $2::TEXT, $3::TEXT, $4::TEXT[], $5::TEXT, $6::BOOLEAN)', [
148
+ plan.package,
149
+ change.name,
150
+ scriptHash,
151
+ qualifiedDeps && qualifiedDeps.length > 0 ? qualifiedDeps : null,
152
+ cleanDeploySql,
153
+ logOnly
154
+ ]);
155
+ const unqualified = this.toUnqualifiedLocal(plan.package, change.name);
156
+ deployed.push(unqualified);
157
+ log.success(`Successfully ${logOnly ? 'logged' : 'deployed'}: ${change.name}`);
158
+ }
159
+ catch (error) {
160
+ // Log failure event outside of transaction
161
+ await this.eventLogger.logEvent({
162
+ eventType: 'deploy',
163
+ changeName: change.name,
164
+ package: plan.package,
165
+ errorMessage: error.message || 'Unknown error',
166
+ errorCode: error.code || null
167
+ });
168
+ // Build comprehensive error message
169
+ const errorLines = [];
170
+ errorLines.push(`Failed to deploy ${change.name}:`);
171
+ errorLines.push(` Change: ${change.name}`);
172
+ errorLines.push(` Package: ${plan.package}`);
173
+ errorLines.push(` Script Hash: ${scriptHash}`);
174
+ errorLines.push(` Dependencies: ${qualifiedDeps && qualifiedDeps.length > 0 ? qualifiedDeps.join(', ') : 'none'}`);
175
+ errorLines.push(` Error Code: ${error.code || 'N/A'}`);
176
+ errorLines.push(` Error Message: ${error.message || 'N/A'}`);
177
+ // Show SQL script preview for debugging
178
+ if (cleanDeploySql) {
179
+ const sqlLines = cleanDeploySql.split('\n');
180
+ const previewLines = debug ? sqlLines : sqlLines.slice(0, 10);
181
+ if (debug) {
182
+ errorLines.push(` Full SQL Script (${sqlLines.length} lines):`);
183
+ previewLines.forEach((line, index) => {
184
+ errorLines.push(` ${index + 1}: ${line}`);
185
+ });
186
+ }
187
+ else {
188
+ errorLines.push(` SQL Preview (first 10 lines):`);
189
+ previewLines.forEach((line, index) => {
190
+ errorLines.push(` ${index + 1}: ${line}`);
191
+ });
192
+ if (sqlLines.length > 10) {
193
+ errorLines.push(` ... and ${sqlLines.length - 10} more lines`);
194
+ errorLines.push(` 💡 Use debug mode to see full SQL script`);
195
+ }
196
+ }
197
+ }
198
+ // Provide debugging hints based on error code
199
+ if (error.code === '25P02') {
200
+ errorLines.push(`🔍 Debug Info: This error means a previous command in the transaction failed.`);
201
+ errorLines.push(` The SQL script above may contain the failing command.`);
202
+ errorLines.push(` Check the transaction query history for more details.`);
203
+ }
204
+ else if (error.code === '42P01') {
205
+ errorLines.push(`💡 Hint: A table or view referenced in the SQL script does not exist.`);
206
+ errorLines.push(` Check if dependencies are applied in the correct order.`);
207
+ }
208
+ else if (error.code === '42883') {
209
+ errorLines.push(`💡 Hint: A function referenced in the SQL script does not exist.`);
210
+ errorLines.push(` Check if required extensions or previous migrations are applied.`);
211
+ }
212
+ // Log the consolidated error message
213
+ log.error(errorLines.join('\n'));
214
+ failed = change.name;
215
+ throw error; // Re-throw to trigger rollback if in transaction
216
+ }
217
+ // Stop if this was the target change
218
+ if (toChange && change.name === toChange) {
219
+ break;
220
+ }
221
+ }
222
+ });
223
+ return { deployed, skipped, failed };
224
+ }
225
+ /**
226
+ * Revert changes according to plan file
227
+ */
228
+ async revert(options) {
229
+ await this.initialize();
230
+ const { modulePath, toChange, useTransaction = true } = options;
231
+ const planPath = join(modulePath, 'pgpm.plan');
232
+ const plan = parsePlanFileSimple(planPath);
233
+ const resolvedToChange = this.resolveToChange(toChange, planPath, plan.package);
234
+ const changes = getChangesInOrder(planPath, true); // Reverse order for revert
235
+ const reverted = [];
236
+ const skipped = [];
237
+ let failed;
238
+ // Use a separate pool for the target database
239
+ const targetPool = getPgPool(this.pgConfig);
240
+ // Execute revert with or without transaction
241
+ await withTransaction(targetPool, { useTransaction }, async (context) => {
242
+ for (const change of changes) {
243
+ // Stop if we've reached the target change
244
+ if (resolvedToChange && change.name === resolvedToChange) {
245
+ break;
246
+ }
247
+ // Check if deployed
248
+ const isDeployed = await this.isDeployed(plan.package, change.name);
249
+ if (!isDeployed) {
250
+ log.info(`Skipping not deployed change: ${change.name}`);
251
+ const unqualified = this.toUnqualifiedLocal(plan.package, change.name);
252
+ skipped.push(unqualified);
253
+ continue;
254
+ }
255
+ // Read revert script
256
+ const revertScript = readScript(dirname(planPath), 'revert', change.name);
257
+ if (!revertScript) {
258
+ log.error(`Revert script not found for change: ${change.name}`);
259
+ failed = change.name;
260
+ break;
261
+ }
262
+ const cleanRevertSql = await cleanSql(revertScript, false, '$EOFCODE$');
263
+ try {
264
+ // Call the revert stored procedure
265
+ await executeQuery(context, 'CALL pgpm_migrate.revert($1, $2, $3)', [plan.package, change.name, cleanRevertSql]);
266
+ reverted.push(change.name);
267
+ log.success(`Successfully reverted: ${change.name}`);
268
+ }
269
+ catch (error) {
270
+ // Log failure event outside of transaction
271
+ await this.eventLogger.logEvent({
272
+ eventType: 'revert',
273
+ changeName: change.name,
274
+ package: plan.package,
275
+ errorMessage: error.message || 'Unknown error',
276
+ errorCode: error.code || null
277
+ });
278
+ log.error(`Failed to revert ${change.name}:`, error);
279
+ failed = change.name;
280
+ throw error; // Re-throw to trigger rollback if in transaction
281
+ }
282
+ }
283
+ });
284
+ return { reverted, skipped, failed };
285
+ }
286
+ /**
287
+ * Verify deployed changes
288
+ */
289
+ async verify(options) {
290
+ await this.initialize();
291
+ const { modulePath, toChange } = options;
292
+ const planPath = join(modulePath, 'pgpm.plan');
293
+ const plan = parsePlanFileSimple(planPath);
294
+ const resolvedToChange = this.resolveToChange(toChange, planPath, plan.package);
295
+ const changes = getChangesInOrder(planPath);
296
+ const verified = [];
297
+ const failed = [];
298
+ // Use a separate pool for the target database
299
+ const targetPool = getPgPool(this.pgConfig);
300
+ try {
301
+ for (const change of changes) {
302
+ // Stop if we've reached the target change
303
+ if (resolvedToChange && change.name === resolvedToChange) {
304
+ break;
305
+ }
306
+ // Check if deployed
307
+ const isDeployed = await this.isDeployed(plan.package, change.name);
308
+ if (!isDeployed) {
309
+ continue;
310
+ }
311
+ // Read verify script
312
+ const verifyScript = readScript(dirname(planPath), 'verify', change.name);
313
+ if (!verifyScript) {
314
+ log.warn(`Verify script not found for change: ${change.name}`);
315
+ continue;
316
+ }
317
+ const cleanVerifySql = await cleanSql(verifyScript, false, '$EOFCODE$');
318
+ try {
319
+ // Call the verify function
320
+ const result = await targetPool.query('SELECT pgpm_migrate.verify($1, $2, $3) as verified', [plan.package, change.name, cleanVerifySql]);
321
+ if (result.rows[0].verified) {
322
+ verified.push(change.name);
323
+ log.success(`Successfully verified: ${change.name}`);
324
+ }
325
+ else {
326
+ const verificationError = new Error(`Verification failed for ${change.name}`);
327
+ verificationError.code = 'VERIFICATION_FAILED';
328
+ throw verificationError;
329
+ }
330
+ }
331
+ catch (error) {
332
+ // Log failure event with rich error information
333
+ await this.eventLogger.logEvent({
334
+ eventType: 'verify',
335
+ changeName: change.name,
336
+ package: plan.package,
337
+ errorMessage: error.message || 'Unknown error',
338
+ errorCode: error.code || null
339
+ });
340
+ log.error(`Failed to verify ${change.name}:`, error);
341
+ failed.push(change.name);
342
+ }
343
+ }
344
+ }
345
+ finally {
346
+ }
347
+ if (failed.length > 0) {
348
+ throw errors.OPERATION_FAILED({ operation: 'Verification', reason: `${failed.length} change(s): ${failed.join(', ')}` });
349
+ }
350
+ return { verified, failed };
351
+ }
352
+ /**
353
+ * Get deployment status
354
+ */
355
+ async status(packageName) {
356
+ await this.initialize();
357
+ const result = await this.pool.query('SELECT * FROM pgpm_migrate.status($1)', [packageName]);
358
+ return result.rows.map(row => ({
359
+ package: row.package,
360
+ totalDeployed: row.total_deployed,
361
+ lastChange: row.last_change,
362
+ lastDeployed: new Date(row.last_deployed)
363
+ }));
364
+ }
365
+ /**
366
+ * Check if a change is deployed
367
+ */
368
+ async isDeployed(packageName, changeName) {
369
+ const result = await this.pool.query('SELECT pgpm_migrate.is_deployed($1::TEXT, $2::TEXT) as is_deployed', [packageName, changeName]);
370
+ return result.rows[0].is_deployed;
371
+ }
372
+ /**
373
+ * Check if Sqitch tables exist in the database
374
+ */
375
+ async hasSqitchTables() {
376
+ const result = await this.pool.query(`
377
+ SELECT EXISTS (
378
+ SELECT 1 FROM information_schema.tables
379
+ WHERE table_schema = 'sqitch'
380
+ AND table_name IN ('projects', 'changes', 'tags', 'events')
381
+ )
382
+ `);
383
+ return result.rows[0].exists;
384
+ }
385
+ /**
386
+ * Import from existing Sqitch deployment
387
+ */
388
+ async importFromSqitch() {
389
+ await this.initialize();
390
+ try {
391
+ log.info('Checking for existing Sqitch tables...');
392
+ // Check if sqitch schema exists
393
+ const schemaResult = await this.pool.query("SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'sqitch')");
394
+ if (!schemaResult.rows[0].exists) {
395
+ log.info('No Sqitch schema found, nothing to import');
396
+ return;
397
+ }
398
+ // Import packages
399
+ log.info('Importing Sqitch packages...');
400
+ await this.pool.query(`
401
+ INSERT INTO pgpm_migrate.packages (package, created_at)
402
+ SELECT DISTINCT project, now()
403
+ FROM sqitch.projects
404
+ ON CONFLICT (package) DO NOTHING
405
+ `);
406
+ // Import changes with dependencies
407
+ log.info('Importing Sqitch changes...');
408
+ await this.pool.query(`
409
+ WITH change_data AS (
410
+ SELECT
411
+ c.project,
412
+ c.change,
413
+ c.change_id,
414
+ c.committed_at
415
+ FROM sqitch.changes c
416
+ WHERE c.change_id IN (
417
+ SELECT change_id FROM sqitch.tags
418
+ )
419
+ )
420
+ INSERT INTO pgpm_migrate.changes (change_id, change_name, package, script_hash, deployed_at)
421
+ SELECT
422
+ encode(sha256((cd.project || cd.change || cd.change_id)::bytea), 'hex'),
423
+ cd.change,
424
+ cd.project,
425
+ cd.change_id,
426
+ cd.committed_at
427
+ FROM change_data cd
428
+ ON CONFLICT (package, change_name) DO NOTHING
429
+ `);
430
+ // Import dependencies
431
+ log.info('Importing Sqitch dependencies...');
432
+ await this.pool.query(`
433
+ INSERT INTO pgpm_migrate.dependencies (change_id, requires)
434
+ SELECT
435
+ c.change_id,
436
+ d.dependency
437
+ FROM pgpm_migrate.changes c
438
+ JOIN sqitch.dependencies d ON d.change_id = c.script_hash
439
+ ON CONFLICT DO NOTHING
440
+ `);
441
+ log.success('Successfully imported Sqitch deployment history');
442
+ }
443
+ catch (error) {
444
+ log.error('Failed to import from Sqitch:', error);
445
+ throw error;
446
+ }
447
+ }
448
+ /**
449
+ * Get recent changes
450
+ */
451
+ async getRecentChanges(targetDatabase, limit = 10) {
452
+ const targetPool = getPgPool({
453
+ ...this.pgConfig,
454
+ database: targetDatabase
455
+ });
456
+ try {
457
+ const result = await targetPool.query(`
458
+ SELECT
459
+ c.change_name,
460
+ c.deployed_at,
461
+ c.package
462
+ FROM pgpm_migrate.changes c
463
+ ORDER BY c.deployed_at DESC NULLS LAST
464
+ LIMIT $1
465
+ `, [limit]);
466
+ return result.rows;
467
+ }
468
+ catch (error) {
469
+ log.error('Failed to get recent changes:', error);
470
+ throw error;
471
+ }
472
+ }
473
+ /**
474
+ * Get pending changes (in plan but not deployed)
475
+ */
476
+ async getPendingChanges(planPath, targetDatabase) {
477
+ const plan = parsePlanFileSimple(planPath);
478
+ const allChanges = getChangesInOrder(planPath);
479
+ const targetPool = getPgPool({
480
+ ...this.pgConfig,
481
+ database: targetDatabase
482
+ });
483
+ try {
484
+ const deployedResult = await targetPool.query(`
485
+ SELECT c.change_name
486
+ FROM pgpm_migrate.changes c
487
+ WHERE c.package = $1 AND c.deployed_at IS NOT NULL
488
+ `, [plan.package]);
489
+ const deployedSet = new Set(deployedResult.rows.map((r) => r.change_name));
490
+ return allChanges.filter(c => !deployedSet.has(c.name)).map(c => c.name);
491
+ }
492
+ catch (error) {
493
+ // If schema doesn't exist, all changes are pending
494
+ if (error.code === '42P01') { // undefined_table
495
+ return allChanges.map(c => c.name);
496
+ }
497
+ throw error;
498
+ }
499
+ }
500
+ /**
501
+ * Get all deployed changes for a project
502
+ */
503
+ async getDeployedChanges(targetDatabase, packageName) {
504
+ const targetPool = getPgPool({
505
+ ...this.pgConfig,
506
+ database: targetDatabase
507
+ });
508
+ try {
509
+ const result = await targetPool.query(`
510
+ SELECT
511
+ c.change_name,
512
+ c.deployed_at,
513
+ c.script_hash
514
+ FROM pgpm_migrate.changes c
515
+ WHERE c.package = $1 AND c.deployed_at IS NOT NULL
516
+ ORDER BY c.deployed_at ASC
517
+ `, [packageName]);
518
+ return result.rows;
519
+ }
520
+ catch (error) {
521
+ // If schema doesn't exist, no changes are deployed
522
+ if (error.code === '42P01') { // undefined_table
523
+ return [];
524
+ }
525
+ throw error;
526
+ }
527
+ }
528
+ /**
529
+ * Get dependencies for a change
530
+ */
531
+ async getDependencies(packageName, changeName) {
532
+ await this.initialize();
533
+ try {
534
+ const result = await this.pool.query(`SELECT d.requires
535
+ FROM pgpm_migrate.dependencies d
536
+ JOIN pgpm_migrate.changes c ON c.change_id = d.change_id
537
+ WHERE c.package = $1 AND c.change_name = $2`, [packageName, changeName]);
538
+ return result.rows.map(row => row.requires);
539
+ }
540
+ catch (error) {
541
+ log.error(`Failed to get dependencies for ${packageName}:${changeName}:`, error);
542
+ return [];
543
+ }
544
+ }
545
+ /**
546
+ * Close the database connection pool
547
+ */
548
+ async close() {
549
+ // Pool is managed by PgPoolCacheManager, no need to close
550
+ }
551
+ }
@@ -0,0 +1,5 @@
1
+ export * from './clean';
2
+ export * from './client';
3
+ export * from './types';
4
+ export * from './utils/hash';
5
+ export * from './utils/transaction';