FN-8051: ensure PostgreSQL schemas initialize before plugin hooks
Ensure required PostgreSQL namespaces exist before plugin initialization on every boot. - Create project, central, and archive schemas under the schema advisory lock before hooks run - Cover marker-present databases with a plugin-hook schema availability regression test - Add a patch changeset for the reliability fix Files changed: .changeset/fn-8051-schema-init.md | 7 ++++ .../src/__tests__/postgres/schema-applier.test.ts | 43 ++++++++++++++++++++++ packages/core/src/postgres/schema-applier.ts | 12 ++++++ 3 files changed, 62 insertions(+) Fusion-Task-Id: FN-8051 Fusion-Task-Lineage: a3b20683-a742-4a8c-9cfc-fbf316c5649b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8051-schema-init.md
Normal file
7
.changeset/fn-8051-schema-init.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Ensure required database schemas always initialize before plugin tables on boot.
|
||||||
|
category: fix
|
||||||
|
dev: applySchemaBaseline now runs CREATE SCHEMA IF NOT EXISTS project/central/archive unconditionally before plugin schema-init hooks (FN-8051).
|
||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
SQLITE_SCHEMA_PARITY_VERSION,
|
SQLITE_SCHEMA_PARITY_VERSION,
|
||||||
} from "../../postgres/schema-applier.js";
|
} from "../../postgres/schema-applier.js";
|
||||||
import { rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
|
import { rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
|
||||||
|
import type { PluginSchemaInitHook } from "../../postgres/plugin-schema-hook.js";
|
||||||
|
|
||||||
const PG_ADMIN_URL =
|
const PG_ADMIN_URL =
|
||||||
process.env.FUSION_PG_TEST_ADMIN_URL ?? "postgresql://localhost:5432/postgres";
|
process.env.FUSION_PG_TEST_ADMIN_URL ?? "postgresql://localhost:5432/postgres";
|
||||||
@@ -372,6 +373,48 @@ pgDescribe("schema-applier: VAL-SCHEMA-008 three-database topology", () => {
|
|||||||
`)) as unknown as Array<{ schema_name: string }>;
|
`)) as unknown as Array<{ schema_name: string }>;
|
||||||
expect(rows.map((r) => r.schema_name)).toEqual(["archive", "central", "project"]);
|
expect(rows.map((r) => r.schema_name)).toEqual(["archive", "central", "project"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ensures schemas before hooks when all migration markers are already recorded", async () => {
|
||||||
|
ctx = await setupFreshDb();
|
||||||
|
await ctx.db.execute(sql.raw(`
|
||||||
|
CREATE TABLE public.fusion_schema_migrations (
|
||||||
|
version text PRIMARY KEY,
|
||||||
|
applied_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
INSERT INTO public.fusion_schema_migrations (version)
|
||||||
|
SELECT lpad(n::text, 4, '0')
|
||||||
|
FROM generate_series(0, ${Number(SCHEMA_BASELINE_VERSION)}) AS migration(n);
|
||||||
|
`));
|
||||||
|
|
||||||
|
const observedSchemas: string[] = [];
|
||||||
|
const assertSchemasHook: PluginSchemaInitHook = {
|
||||||
|
pluginId: "assert-required-schemas",
|
||||||
|
async init(db) {
|
||||||
|
const rows = (await db.execute(sql`
|
||||||
|
SELECT schema_name FROM information_schema.schemata
|
||||||
|
WHERE schema_name IN ('project', 'central', 'archive')
|
||||||
|
ORDER BY schema_name
|
||||||
|
`)) as unknown as Array<{ schema_name: string }>;
|
||||||
|
observedSchemas.push(...rows.map(({ schema_name }) => schema_name));
|
||||||
|
if (rows.length !== 3) {
|
||||||
|
throw new Error(`Required schemas missing at plugin hook time: ${rows.map(({ schema_name }) => schema_name).join(", ")}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(applySchemaBaseline(ctx.db, { pluginHooks: [assertSchemasHook] })).resolves.toEqual({
|
||||||
|
applied: false,
|
||||||
|
pluginHooksRun: 1,
|
||||||
|
});
|
||||||
|
expect(observedSchemas).toEqual(["archive", "central", "project"]);
|
||||||
|
|
||||||
|
const schemas = (await ctx.db.execute(sql`
|
||||||
|
SELECT schema_name FROM information_schema.schemata
|
||||||
|
WHERE schema_name IN ('project', 'central', 'archive')
|
||||||
|
ORDER BY schema_name
|
||||||
|
`)) as unknown as Array<{ schema_name: string }>;
|
||||||
|
expect(schemas.map(({ schema_name }) => schema_name)).toEqual(["archive", "central", "project"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
pgDescribe("schema-applier: VAL-SCHEMA-001 final-schema parity (table counts)", () => {
|
pgDescribe("schema-applier: VAL-SCHEMA-001 final-schema parity (table counts)", () => {
|
||||||
|
|||||||
@@ -182,6 +182,18 @@ export async function applySchemaBaseline(
|
|||||||
return db.transaction(async (tx) => {
|
return db.transaction(async (tx) => {
|
||||||
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('fusion:schema-applier'))`);
|
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('fusion:schema-applier'))`);
|
||||||
await ensureBookkeepingTable(tx);
|
await ensureBookkeepingTable(tx);
|
||||||
|
/*
|
||||||
|
FNXC:PostgresSchema 2026-07-16-00:55:
|
||||||
|
FN-8051 requires project, central, and archive to exist before plugin schema-init hooks run.
|
||||||
|
Hooks run even when migration markers are already recorded and target project tables, so
|
||||||
|
ensure the namespaces unconditionally inside the advisory-locked transaction rather than
|
||||||
|
relying on the baseline batch that a marker-present database skips.
|
||||||
|
*/
|
||||||
|
await tx.execute(sql.raw(`
|
||||||
|
CREATE SCHEMA IF NOT EXISTS project;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS central;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS archive;
|
||||||
|
`));
|
||||||
const applied = await getAppliedMigrations(tx);
|
const applied = await getAppliedMigrations(tx);
|
||||||
const baselineAlreadyApplied = applied.includes(INITIAL_SCHEMA_VERSION);
|
const baselineAlreadyApplied = applied.includes(INITIAL_SCHEMA_VERSION);
|
||||||
const automationIsolationAlreadyApplied = applied.includes(AUTOMATION_ISOLATION_SCHEMA_VERSION);
|
const automationIsolationAlreadyApplied = applied.includes(AUTOMATION_ISOLATION_SCHEMA_VERSION);
|
||||||
|
|||||||
Reference in New Issue
Block a user