Skip to content

Commit 5e76d73

Browse files
committed
refactor(cli): extract pg_upgrade functions to separate module with tests
- Move pg_upgrade utilities to cli/lib/pg-upgrade.ts for testability - Add comprehensive unit tests for version parsing and upgrade logic - Tests cover: parsePostgresVersionFromCompose, buildUpgradeScript, needsPostgresUpgrade functions
1 parent 50122e4 commit 5e76d73

3 files changed

Lines changed: 301 additions & 128 deletions

File tree

cli/bin/postgres-ai.ts

Lines changed: 5 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { createInterface } from "readline";
2020
import * as childProcess from "child_process";
2121
import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
2222
import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
23+
import { getRunningPostgresVersion, getTargetPostgresVersion, runPgUpgrade, needsPostgresUpgrade } from "../lib/pg-upgrade";
2324

2425
// Singleton readline interface for stdin prompts
2526
let rl: ReturnType<typeof createInterface> | null = null;
@@ -1069,130 +1070,6 @@ function isDockerRunning(): boolean {
10691070
}
10701071
}
10711072

1072-
/**
1073-
* Get PostgreSQL major version from a running container
1074-
* @returns Major version number (e.g., 15, 17, 18) or null if container not running
1075-
*/
1076-
function getRunningPostgresVersion(containerName: string): number | null {
1077-
try {
1078-
const result = spawnSync(
1079-
"docker",
1080-
["exec", containerName, "psql", "-U", "postgres", "-t", "-c", "SHOW server_version_num"],
1081-
{ stdio: "pipe", encoding: "utf8" }
1082-
);
1083-
if (result.status === 0 && result.stdout) {
1084-
const versionNum = parseInt(result.stdout.trim(), 10);
1085-
if (!isNaN(versionNum)) {
1086-
return Math.floor(versionNum / 10000); // e.g., 150000 -> 15
1087-
}
1088-
}
1089-
return null;
1090-
} catch {
1091-
return null;
1092-
}
1093-
}
1094-
1095-
/**
1096-
* Get target PostgreSQL major version from docker-compose.yml
1097-
* @returns Major version number or null if not found
1098-
*/
1099-
function getTargetPostgresVersion(composeFilePath: string): number | null {
1100-
try {
1101-
const content = fs.readFileSync(composeFilePath, "utf8");
1102-
// Match postgres:XX image tag for sink-postgres service
1103-
const match = content.match(/sink-postgres:[\s\S]*?image:\s*postgres:(\d+)/);
1104-
if (match && match[1]) {
1105-
return parseInt(match[1], 10);
1106-
}
1107-
return null;
1108-
} catch {
1109-
return null;
1110-
}
1111-
}
1112-
1113-
/**
1114-
* Run pg_upgrade to migrate PostgreSQL data between major versions
1115-
* Uses the new postgres image with old binaries installed
1116-
*/
1117-
async function runPgUpgrade(
1118-
oldVersion: number,
1119-
newVersion: number,
1120-
projectDir: string
1121-
): Promise<boolean> {
1122-
console.log(`\nMigrating PostgreSQL data from version ${oldVersion} to ${newVersion}...`);
1123-
1124-
const volumeName = "postgres_ai_sink_postgres_data";
1125-
const containerName = "postgres-ai-pg-upgrade";
1126-
1127-
// Build the upgrade script that runs inside the container
1128-
const upgradeScript = `
1129-
set -e
1130-
1131-
echo "Installing PostgreSQL ${oldVersion} binaries..."
1132-
apt-get update -qq
1133-
apt-get install -y -qq postgresql-${oldVersion} >/dev/null 2>&1
1134-
1135-
echo "Preparing data directories..."
1136-
mkdir -p /var/lib/postgresql/${newVersion}/data
1137-
chown postgres:postgres /var/lib/postgresql/${newVersion}/data
1138-
chmod 700 /var/lib/postgresql/${newVersion}/data
1139-
1140-
# Initialize new data directory
1141-
echo "Initializing new PostgreSQL ${newVersion} cluster..."
1142-
su postgres -c "/usr/lib/postgresql/${newVersion}/bin/initdb -D /var/lib/postgresql/${newVersion}/data"
1143-
1144-
# Run pg_upgrade
1145-
echo "Running pg_upgrade..."
1146-
cd /var/lib/postgresql
1147-
su postgres -c "/usr/lib/postgresql/${newVersion}/bin/pg_upgrade \\
1148-
--old-datadir=/var/lib/postgresql/data \\
1149-
--new-datadir=/var/lib/postgresql/${newVersion}/data \\
1150-
--old-bindir=/usr/lib/postgresql/${oldVersion}/bin \\
1151-
--new-bindir=/usr/lib/postgresql/${newVersion}/bin \\
1152-
--link"
1153-
1154-
# Replace old data with upgraded data
1155-
echo "Finalizing upgrade..."
1156-
rm -rf /var/lib/postgresql/data.old 2>/dev/null || true
1157-
mv /var/lib/postgresql/data /var/lib/postgresql/data.old
1158-
mv /var/lib/postgresql/${newVersion}/data /var/lib/postgresql/data
1159-
1160-
echo "PostgreSQL upgrade completed successfully!"
1161-
`;
1162-
1163-
try {
1164-
// Remove any existing upgrade container
1165-
spawnSync("docker", ["rm", "-f", containerName], { stdio: "ignore" });
1166-
1167-
// Run upgrade in a temporary container
1168-
console.log("Starting upgrade container...");
1169-
const result = spawnSync(
1170-
"docker",
1171-
[
1172-
"run",
1173-
"--rm",
1174-
"--name", containerName,
1175-
"-v", `${volumeName}:/var/lib/postgresql/data`,
1176-
`postgres:${newVersion}`,
1177-
"bash", "-c", upgradeScript
1178-
],
1179-
{ stdio: "inherit" }
1180-
);
1181-
1182-
if (result.status === 0) {
1183-
console.log("✓ PostgreSQL upgrade completed successfully\n");
1184-
return true;
1185-
} else {
1186-
console.error("✗ PostgreSQL upgrade failed");
1187-
return false;
1188-
}
1189-
} catch (error) {
1190-
const message = error instanceof Error ? error.message : String(error);
1191-
console.error(`PostgreSQL upgrade failed: ${message}`);
1192-
return false;
1193-
}
1194-
}
1195-
11961073
/**
11971074
* Get docker compose command
11981075
*/
@@ -1837,17 +1714,17 @@ mon
18371714
}
18381715

18391716
// Check if PostgreSQL major version upgrade is needed
1840-
const needsPgUpgrade = currentPgVersion && targetPgVersion && currentPgVersion !== targetPgVersion;
1717+
const pgUpgradeNeeded = needsPostgresUpgrade(currentPgVersion, targetPgVersion);
18411718

1842-
if (needsPgUpgrade) {
1719+
if (pgUpgradeNeeded && currentPgVersion && targetPgVersion) {
18431720
console.log(`\n⚠ PostgreSQL major version change detected: ${currentPgVersion}${targetPgVersion}`);
18441721

18451722
// Stop services before upgrade
18461723
console.log("\nStopping services for PostgreSQL upgrade...");
18471724
await runCompose(["stop"]);
18481725

18491726
// Run pg_upgrade
1850-
const upgradeSuccess = await runPgUpgrade(currentPgVersion, targetPgVersion, projectDir);
1727+
const upgradeSuccess = await runPgUpgrade(currentPgVersion, targetPgVersion);
18511728

18521729
if (!upgradeSuccess) {
18531730
console.error("\n✗ PostgreSQL upgrade failed");
@@ -1876,7 +1753,7 @@ mon
18761753

18771754
if (restartCode === 0) {
18781755
console.log("\n✓ Update completed successfully");
1879-
if (needsPgUpgrade) {
1756+
if (pgUpgradeNeeded) {
18801757
console.log(`✓ PostgreSQL upgraded from ${currentPgVersion} to ${targetPgVersion}`);
18811758
}
18821759
} else {

cli/lib/pg-upgrade.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* PostgreSQL upgrade utilities for handling major version migrations
3+
*/
4+
5+
import * as fs from "fs";
6+
import * as childProcess from "child_process";
7+
8+
/**
9+
* Spawn sync helper for Docker commands
10+
*/
11+
function spawnSync(
12+
cmd: string,
13+
args: string[],
14+
options?: { stdio?: "pipe" | "ignore" | "inherit"; encoding?: string }
15+
): { status: number | null; stdout: string; stderr: string } {
16+
const result = childProcess.spawnSync(cmd, args, {
17+
stdio: options?.stdio === "inherit" ? "inherit" : "pipe",
18+
encoding: "utf8",
19+
});
20+
return {
21+
status: result.status,
22+
stdout: typeof result.stdout === "string" ? result.stdout : "",
23+
stderr: typeof result.stderr === "string" ? result.stderr : "",
24+
};
25+
}
26+
27+
/**
28+
* Get PostgreSQL major version from a running container
29+
* @returns Major version number (e.g., 15, 17, 18) or null if container not running
30+
*/
31+
export function getRunningPostgresVersion(containerName: string): number | null {
32+
try {
33+
const result = spawnSync(
34+
"docker",
35+
["exec", containerName, "psql", "-U", "postgres", "-t", "-c", "SHOW server_version_num"],
36+
{ stdio: "pipe", encoding: "utf8" }
37+
);
38+
if (result.status === 0 && result.stdout) {
39+
const versionNum = parseInt(result.stdout.trim(), 10);
40+
if (!isNaN(versionNum)) {
41+
return Math.floor(versionNum / 10000); // e.g., 150000 -> 15
42+
}
43+
}
44+
return null;
45+
} catch {
46+
return null;
47+
}
48+
}
49+
50+
/**
51+
* Get target PostgreSQL major version from docker-compose.yml
52+
* @returns Major version number or null if not found
53+
*/
54+
export function getTargetPostgresVersion(composeFilePath: string): number | null {
55+
try {
56+
const content = fs.readFileSync(composeFilePath, "utf8");
57+
return parsePostgresVersionFromCompose(content);
58+
} catch {
59+
return null;
60+
}
61+
}
62+
63+
/**
64+
* Parse PostgreSQL version from docker-compose.yml content
65+
* Exported for testing purposes
66+
*/
67+
export function parsePostgresVersionFromCompose(content: string): number | null {
68+
// Match postgres:XX image tag for sink-postgres service
69+
const match = content.match(/sink-postgres:[\s\S]*?image:\s*postgres:(\d+)/);
70+
if (match && match[1]) {
71+
return parseInt(match[1], 10);
72+
}
73+
return null;
74+
}
75+
76+
/**
77+
* Build the shell script for running pg_upgrade inside a container
78+
*/
79+
export function buildUpgradeScript(oldVersion: number, newVersion: number): string {
80+
return `
81+
set -e
82+
83+
echo "Installing PostgreSQL ${oldVersion} binaries..."
84+
apt-get update -qq
85+
apt-get install -y -qq postgresql-${oldVersion} >/dev/null 2>&1
86+
87+
echo "Preparing data directories..."
88+
mkdir -p /var/lib/postgresql/${newVersion}/data
89+
chown postgres:postgres /var/lib/postgresql/${newVersion}/data
90+
chmod 700 /var/lib/postgresql/${newVersion}/data
91+
92+
# Initialize new data directory
93+
echo "Initializing new PostgreSQL ${newVersion} cluster..."
94+
su postgres -c "/usr/lib/postgresql/${newVersion}/bin/initdb -D /var/lib/postgresql/${newVersion}/data"
95+
96+
# Run pg_upgrade
97+
echo "Running pg_upgrade..."
98+
cd /var/lib/postgresql
99+
su postgres -c "/usr/lib/postgresql/${newVersion}/bin/pg_upgrade \\
100+
--old-datadir=/var/lib/postgresql/data \\
101+
--new-datadir=/var/lib/postgresql/${newVersion}/data \\
102+
--old-bindir=/usr/lib/postgresql/${oldVersion}/bin \\
103+
--new-bindir=/usr/lib/postgresql/${newVersion}/bin \\
104+
--link"
105+
106+
# Replace old data with upgraded data
107+
echo "Finalizing upgrade..."
108+
rm -rf /var/lib/postgresql/data.old 2>/dev/null || true
109+
mv /var/lib/postgresql/data /var/lib/postgresql/data.old
110+
mv /var/lib/postgresql/${newVersion}/data /var/lib/postgresql/data
111+
112+
echo "PostgreSQL upgrade completed successfully!"
113+
`;
114+
}
115+
116+
/**
117+
* Run pg_upgrade to migrate PostgreSQL data between major versions
118+
* Uses the new postgres image with old binaries installed
119+
*/
120+
export async function runPgUpgrade(
121+
oldVersion: number,
122+
newVersion: number,
123+
volumeName: string = "postgres_ai_sink_postgres_data"
124+
): Promise<boolean> {
125+
console.log(`\nMigrating PostgreSQL data from version ${oldVersion} to ${newVersion}...`);
126+
127+
const containerName = "postgres-ai-pg-upgrade";
128+
const upgradeScript = buildUpgradeScript(oldVersion, newVersion);
129+
130+
try {
131+
// Remove any existing upgrade container
132+
spawnSync("docker", ["rm", "-f", containerName], { stdio: "ignore" });
133+
134+
// Run upgrade in a temporary container
135+
console.log("Starting upgrade container...");
136+
const result = spawnSync(
137+
"docker",
138+
[
139+
"run",
140+
"--rm",
141+
"--name", containerName,
142+
"-v", `${volumeName}:/var/lib/postgresql/data`,
143+
`postgres:${newVersion}`,
144+
"bash", "-c", upgradeScript
145+
],
146+
{ stdio: "inherit" }
147+
);
148+
149+
if (result.status === 0) {
150+
console.log("✓ PostgreSQL upgrade completed successfully\n");
151+
return true;
152+
} else {
153+
console.error("✗ PostgreSQL upgrade failed");
154+
return false;
155+
}
156+
} catch (error) {
157+
const message = error instanceof Error ? error.message : String(error);
158+
console.error(`PostgreSQL upgrade failed: ${message}`);
159+
return false;
160+
}
161+
}
162+
163+
/**
164+
* Check if a PostgreSQL major version upgrade is needed
165+
*/
166+
export function needsPostgresUpgrade(
167+
currentVersion: number | null,
168+
targetVersion: number | null
169+
): boolean {
170+
return !!(currentVersion && targetVersion && currentVersion !== targetVersion);
171+
}

0 commit comments

Comments
 (0)