mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
feat(db): move the bundled database to Postgres 18
The compose file runs Postgres 18. An install from v1.45 or earlier keeps its Postgres 16 data in `pgdata`, which 18 cannot open, so a one-shot `db-upgrade` service moves it on the first start: it starts Postgres 16 on the old volume inside its own container, streams a dump into 18 in one transaction, compares every table's row count, gathers planner statistics and marks the old volume as moved. The app waits for it to succeed. The old volume is never changed apart from the marker, so the way back is the previous image and volume lines. A move cut short leaves a "moving" marker and runs again from scratch; an emptied new volume after a finished move is refused rather than refilled with the old state. scripts/compose-db-upgrade-check.sh plays all of it against real containers from the v1.45.0 compose file, and runs in CI on every change to the compose file. Pull requests run the integration suite and e2e on Postgres 18; the nightly integration run keeps 16 covered for installs that bring their own database.
This commit is contained in:
1 parent
6b3c1cc433
commit
d4d91994e8
11 files changed
+445
-24
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
/**
|
||||
* The wiring around `db-upgrade` in docker-compose.yml, the one-time move of a
|
||||
* Postgres 16 database from v1.45 or earlier into Postgres 18.
|
||||
*
|
||||
* What the move does is proven against real containers by
|
||||
* `scripts/compose-db-upgrade-check.sh` (the "Compose database move"
|
||||
* workflow). That check starts `db-upgrade` by name, so it cannot see the
|
||||
* part that protects an operator's data on a normal `docker compose up`: the
|
||||
* app must wait for the move to succeed, or it starts on an empty Postgres 18
|
||||
* database and the instance looks wiped. This file freezes that and the
|
||||
* volume layout the move depends on.
|
||||
*/
|
||||
|
||||
type Service = {
|
||||
image?: string;
|
||||
restart?: string;
|
||||
volumes?: string[];
|
||||
depends_on?: Record<string, { condition?: string }>;
|
||||
command?: string[];
|
||||
};
|
||||
|
||||
const compose = parse(
|
||||
readFileSync(resolve(__dirname, "../../docker-compose.yml"), "utf8"),
|
||||
) as { services: Record<string, Service>; volumes: Record<string, unknown> };
|
||||
const { app, db } = compose.services;
|
||||
const upgrade = compose.services["db-upgrade"];
|
||||
|
||||
function major(image: string | undefined): number {
|
||||
const match = image?.match(/^postgres:(\d+)\./);
|
||||
if (!match) throw new Error(`not a pinned postgres image: ${image}`);
|
||||
return Number(match[1]);
|
||||
}
|
||||
|
||||
describe("the Postgres 16 -> 18 move in docker-compose.yml", () => {
|
||||
it("starts the app only after the move succeeded", () => {
|
||||
expect(app.depends_on?.["db-upgrade"]?.condition).toBe(
|
||||
"service_completed_successfully",
|
||||
);
|
||||
expect(app.depends_on?.db?.condition).toBe("service_healthy");
|
||||
});
|
||||
|
||||
it("moves into a running database", () => {
|
||||
expect(upgrade.depends_on?.db?.condition).toBe("service_healthy");
|
||||
});
|
||||
|
||||
it("reads the old data with the major that wrote it", () => {
|
||||
expect(major(upgrade.image)).toBe(16);
|
||||
expect(major(db.image)).toBe(18);
|
||||
expect(upgrade.volumes).toContain("pgdata:/var/lib/postgresql/data");
|
||||
});
|
||||
|
||||
it("keeps the new data where the Postgres 18 image expects it", () => {
|
||||
// Mounted at .../data, as 16 had it, the 18 image refuses to start.
|
||||
expect(db.volumes).toEqual(["postgres-data:/var/lib/postgresql"]);
|
||||
expect(Object.keys(compose.volumes).sort()).toEqual([
|
||||
"pgdata",
|
||||
"postgres-data",
|
||||
]);
|
||||
});
|
||||
|
||||
it("runs once per start instead of restarting", () => {
|
||||
// A restart policy would rerun a failed move in a loop; "no" also keeps
|
||||
// Coolify from counting the exited container as a failed service.
|
||||
expect(upgrade.restart).toBe("no");
|
||||
});
|
||||
|
||||
it("never fills a database that already holds data", () => {
|
||||
const script = upgrade.command?.join("\n") ?? "";
|
||||
// The checks run before the restore, in this order.
|
||||
const order = [
|
||||
"PG_VERSION",
|
||||
"$$MARK",
|
||||
"$$MOVING",
|
||||
'elif [ "$$TABLES" != 0 ]',
|
||||
"pg_restore",
|
||||
].map((needle) => script.indexOf(needle));
|
||||
expect(order.every((index) => index >= 0)).toBe(true);
|
||||
expect([...order].sort((a, b) => a - b)).toEqual(order);
|
||||
expect(script).toContain("--single-transaction");
|
||||
});
|
||||
});
|
||||
@@ -31,9 +31,13 @@ describe("the bundled database pin", () => {
|
||||
...read("docker-compose.yml").matchAll(/^\s*image:\s*(postgres:\S+)/gm),
|
||||
].map((match) => match[1]);
|
||||
|
||||
expect(images.length).toBe(1);
|
||||
// `postgres:16-alpine` or `postgres:16` would match a moving target.
|
||||
expect(images[0]).toMatch(/^postgres:\d+\.\d+-alpine$/);
|
||||
// The database itself and `db-upgrade`, which reads the Postgres 16 data
|
||||
// of an install from v1.45 or earlier.
|
||||
expect(images.length).toBe(2);
|
||||
for (const image of images) {
|
||||
// `postgres:18-alpine` or `postgres:18` would match a moving target.
|
||||
expect(image).toMatch(/^postgres:\d+\.\d+-alpine$/);
|
||||
}
|
||||
});
|
||||
|
||||
it("lets a major version through only as a decision, never as an update PR", () => {
|
||||
|
||||
Reference in new issue
Block a user