@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.
- package/LICENSE +23 -0
- package/README.md +99 -0
- package/core/boilerplate-scanner.d.ts +41 -0
- package/core/boilerplate-scanner.js +106 -0
- package/core/boilerplate-types.d.ts +52 -0
- package/core/boilerplate-types.js +6 -0
- package/core/class/pgpm.d.ts +150 -0
- package/core/class/pgpm.js +1470 -0
- package/core/template-scaffold.d.ts +29 -0
- package/core/template-scaffold.js +168 -0
- package/esm/core/boilerplate-scanner.js +96 -0
- package/esm/core/boilerplate-types.js +5 -0
- package/esm/core/class/pgpm.js +1430 -0
- package/esm/core/template-scaffold.js +161 -0
- package/esm/export/export-meta.js +240 -0
- package/esm/export/export-migrations.js +180 -0
- package/esm/extensions/extensions.js +31 -0
- package/esm/files/extension/index.js +3 -0
- package/esm/files/extension/reader.js +79 -0
- package/esm/files/extension/writer.js +63 -0
- package/esm/files/index.js +6 -0
- package/esm/files/plan/generator.js +49 -0
- package/esm/files/plan/index.js +5 -0
- package/esm/files/plan/parser.js +296 -0
- package/esm/files/plan/validators.js +181 -0
- package/esm/files/plan/writer.js +114 -0
- package/esm/files/sql/index.js +1 -0
- package/esm/files/sql/writer.js +107 -0
- package/esm/files/sql-scripts/index.js +2 -0
- package/esm/files/sql-scripts/reader.js +19 -0
- package/esm/files/types/index.js +1 -0
- package/esm/files/types/package.js +1 -0
- package/esm/index.js +21 -0
- package/esm/init/client.js +144 -0
- package/esm/init/sql/bootstrap-roles.sql +55 -0
- package/esm/init/sql/bootstrap-test-roles.sql +72 -0
- package/esm/migrate/clean.js +23 -0
- package/esm/migrate/client.js +551 -0
- package/esm/migrate/index.js +5 -0
- package/esm/migrate/sql/procedures.sql +258 -0
- package/esm/migrate/sql/schema.sql +37 -0
- package/esm/migrate/types.js +1 -0
- package/esm/migrate/utils/event-logger.js +28 -0
- package/esm/migrate/utils/hash.js +27 -0
- package/esm/migrate/utils/transaction.js +125 -0
- package/esm/modules/modules.js +49 -0
- package/esm/packaging/package.js +96 -0
- package/esm/packaging/transform.js +70 -0
- package/esm/projects/deploy.js +123 -0
- package/esm/projects/revert.js +75 -0
- package/esm/projects/verify.js +61 -0
- package/esm/resolution/deps.js +526 -0
- package/esm/resolution/resolve.js +101 -0
- package/esm/utils/debug.js +147 -0
- package/esm/utils/target-utils.js +37 -0
- package/esm/workspace/paths.js +43 -0
- package/esm/workspace/utils.js +31 -0
- package/export/export-meta.d.ts +8 -0
- package/export/export-meta.js +244 -0
- package/export/export-migrations.d.ts +17 -0
- package/export/export-migrations.js +187 -0
- package/extensions/extensions.d.ts +5 -0
- package/extensions/extensions.js +35 -0
- package/files/extension/index.d.ts +2 -0
- package/files/extension/index.js +19 -0
- package/files/extension/reader.d.ts +24 -0
- package/files/extension/reader.js +86 -0
- package/files/extension/writer.d.ts +39 -0
- package/files/extension/writer.js +70 -0
- package/files/index.d.ts +5 -0
- package/files/index.js +22 -0
- package/files/plan/generator.d.ts +22 -0
- package/files/plan/generator.js +57 -0
- package/files/plan/index.d.ts +4 -0
- package/files/plan/index.js +21 -0
- package/files/plan/parser.d.ts +27 -0
- package/files/plan/parser.js +303 -0
- package/files/plan/validators.d.ts +52 -0
- package/files/plan/validators.js +187 -0
- package/files/plan/writer.d.ts +27 -0
- package/files/plan/writer.js +124 -0
- package/files/sql/index.d.ts +1 -0
- package/files/sql/index.js +17 -0
- package/files/sql/writer.d.ts +12 -0
- package/files/sql/writer.js +114 -0
- package/files/sql-scripts/index.d.ts +1 -0
- package/files/sql-scripts/index.js +18 -0
- package/files/sql-scripts/reader.d.ts +8 -0
- package/files/sql-scripts/reader.js +23 -0
- package/files/types/index.d.ts +46 -0
- package/files/types/index.js +17 -0
- package/files/types/package.d.ts +20 -0
- package/files/types/package.js +2 -0
- package/index.d.ts +21 -0
- package/index.js +45 -0
- package/init/client.d.ts +26 -0
- package/init/client.js +148 -0
- package/init/sql/bootstrap-roles.sql +55 -0
- package/init/sql/bootstrap-test-roles.sql +72 -0
- package/migrate/clean.d.ts +1 -0
- package/migrate/clean.js +27 -0
- package/migrate/client.d.ts +80 -0
- package/migrate/client.js +555 -0
- package/migrate/index.d.ts +5 -0
- package/migrate/index.js +21 -0
- package/migrate/sql/procedures.sql +258 -0
- package/migrate/sql/schema.sql +37 -0
- package/migrate/types.d.ts +67 -0
- package/migrate/types.js +2 -0
- package/migrate/utils/event-logger.d.ts +13 -0
- package/migrate/utils/event-logger.js +32 -0
- package/migrate/utils/hash.d.ts +12 -0
- package/migrate/utils/hash.js +32 -0
- package/migrate/utils/transaction.d.ts +27 -0
- package/migrate/utils/transaction.js +129 -0
- package/modules/modules.d.ts +31 -0
- package/modules/modules.js +56 -0
- package/package.json +70 -0
- package/packaging/package.d.ts +19 -0
- package/packaging/package.js +102 -0
- package/packaging/transform.d.ts +22 -0
- package/packaging/transform.js +75 -0
- package/projects/deploy.d.ts +8 -0
- package/projects/deploy.js +160 -0
- package/projects/revert.d.ts +15 -0
- package/projects/revert.js +112 -0
- package/projects/verify.d.ts +8 -0
- package/projects/verify.js +98 -0
- package/resolution/deps.d.ts +57 -0
- package/resolution/deps.js +531 -0
- package/resolution/resolve.d.ts +37 -0
- package/resolution/resolve.js +107 -0
- package/utils/debug.d.ts +21 -0
- package/utils/debug.js +153 -0
- package/utils/target-utils.d.ts +5 -0
- package/utils/target-utils.js +40 -0
- package/workspace/paths.d.ts +14 -0
- package/workspace/paths.js +50 -0
- package/workspace/utils.d.ts +8 -0
- 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
|
+
}
|