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