120 lines
8.5 KiB
TypeScript
120 lines
8.5 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { randomUUID } from "node:crypto"
|
|
import { basename, dirname, resolve } from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
|
|
assert.equal(process.env.MMAPP_OWN_MIGRATION_SCHEMA_INVENTORY, "true")
|
|
const databaseUrl = new URL(process.env.DATABASE_URL ?? "")
|
|
assert.equal(databaseUrl.hostname, "127.0.0.1"); assert.equal(databaseUrl.pathname, "/postgres")
|
|
assert(["postgres:", "postgresql:"].includes(databaseUrl.protocol))
|
|
assert(process.env.MCP_STATE_DIR && basename(dirname(resolve(process.env.MCP_STATE_DIR))).startsWith("mm-mcp-test-"))
|
|
|
|
const { Pool } = await import("pg")
|
|
const { pool, closePool } = await import("./index.js")
|
|
const { applySqlMigrations } = await import("./migrate.js")
|
|
const { inventoryMigrationDirectory, migrationManifestDigest, MIGRATION_MANIFEST } = await import("./migration-manifest.js")
|
|
const { captureMigrationSchema, compareMigrationSchemas, MigrationSchemaInventoryError } = await import("./migration-schema-inventory.js")
|
|
const suffix = randomUUID().replaceAll("-", "").slice(0, 12), names: string[] = []
|
|
const identifier = (value: string) => { assert(/^mm_schema_[a-z0-9_]+$/.test(value)); return '"' + value + '"' }
|
|
const database = async (label: string, template?: string) => {
|
|
const name = `mm_schema_${suffix}_${label}`; identifier(name)
|
|
await pool.query(`CREATE DATABASE ${identifier(name)} TEMPLATE ${template ? identifier(template) : "template0"}`)
|
|
names.push(name)
|
|
const url = new URL(databaseUrl); url.pathname = "/" + name
|
|
return { name, connection: new Pool({ connectionString: url.toString(), max: 2 }) }
|
|
}
|
|
async function capture(connection: InstanceType<typeof Pool>, options?: Parameters<typeof captureMigrationSchema>[1]) {
|
|
const client = await connection.connect()
|
|
try {
|
|
await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY")
|
|
await client.query("SET LOCAL search_path=pg_catalog")
|
|
await client.query("SET LOCAL TimeZone='UTC'")
|
|
await client.query("SET LOCAL DateStyle='ISO, YMD'")
|
|
await client.query("SET LOCAL statement_timeout='5s'")
|
|
const result = await captureMigrationSchema(client, options)
|
|
await client.query("ROLLBACK")
|
|
return result
|
|
} catch (error) { await client.query("ROLLBACK").catch(() => {}); throw error }
|
|
finally { client.release() }
|
|
}
|
|
const cases: string[] = []
|
|
try {
|
|
await applySqlMigrations(pool)
|
|
const inventory = await inventoryMigrationDirectory(fileURLToPath(new URL("../../drizzle/", import.meta.url)))
|
|
assert.equal(inventory.length, MIGRATION_MANIFEST.length)
|
|
const reference = await database("reference")
|
|
let baseline: Awaited<ReturnType<typeof capture>>
|
|
try {
|
|
await applySqlMigrations(reference.connection)
|
|
baseline = await capture(reference.connection)
|
|
assert.equal(baseline.format, "pg18-application-schema-v3")
|
|
assert(baseline.sections.relations.length >= 89)
|
|
assert(baseline.sections.columns.length > 500)
|
|
assert(baseline.sections.constraints.length > 100)
|
|
assert(baseline.sections.functions.length > 0)
|
|
assert.deepEqual(compareMigrationSchemas(baseline, await capture(reference.connection)), { equal: true, evidenceOnly: true, enrollmentAuthorized: false, sha256: baseline.sha256 })
|
|
cases.push("all36 pinned migrations produce repeatable complete PG18 catalog evidence on an actual checked-out read-only backend")
|
|
} finally { await reference.connection.end() }
|
|
const independent = await database("independent")
|
|
try {
|
|
await applySqlMigrations(independent.connection)
|
|
const observed = await capture(independent.connection)
|
|
try { compareMigrationSchemas(baseline!, observed) } catch (error) {
|
|
// This reference is built exclusively from the pinned repository SQL;
|
|
// report field names, not function bodies/default values or user data.
|
|
const differences = Object.keys(baseline!.sections).flatMap(key => {
|
|
const section = key as keyof typeof baseline.sections, before = baseline!.sections[section], after = observed.sections[section]
|
|
if (JSON.stringify(before) === JSON.stringify(after)) return []
|
|
return [{ section, beforeRows: before.length, afterRows: after.length, fields: [...new Set(before.flatMap((row, i) => Object.keys(row).filter(field => JSON.stringify(row[field]) !== JSON.stringify(after[i]?.[field]))))] }]
|
|
})
|
|
console.error("Independent schema differences:", JSON.stringify(differences)); throw error
|
|
}
|
|
cases.push("independently migrated database with different physical OIDs matches the same exact logical schema")
|
|
} finally { await independent.connection.end() }
|
|
|
|
const mismatches: [string, string][] = [
|
|
["table", "CREATE TABLE public.mm_schema_extra(id integer)"],
|
|
["column-default", "ALTER TABLE public.servers ALTER COLUMN name SET DEFAULT 'owned schema drift'"],
|
|
["column-type", "ALTER TABLE public.servers ALTER COLUMN name TYPE varchar(999)"],
|
|
["index", "CREATE INDEX mm_schema_index ON public.servers(host)"],
|
|
["constraint", "ALTER TABLE public.servers ADD CONSTRAINT mm_schema_port CHECK(port>0) NOT VALID"],
|
|
["foreign-key", "ALTER TABLE public.servers ADD CONSTRAINT mm_schema_self FOREIGN KEY(id) REFERENCES public.servers(id) DEFERRABLE INITIALLY DEFERRED"],
|
|
["sequence", "ALTER SEQUENCE public.servers_id_seq INCREMENT BY 2"],
|
|
["comment", "COMMENT ON TABLE public.servers IS 'owned schema drift'"],
|
|
["function", "CREATE FUNCTION public.mm_schema_function() RETURNS integer LANGUAGE sql IMMUTABLE AS 'SELECT 7'"],
|
|
["view", "CREATE VIEW public.mm_schema_view AS SELECT id FROM public.servers"],
|
|
["partition", "CREATE TABLE public.mm_schema_partitioned(id integer) PARTITION BY RANGE(id); CREATE TABLE public.mm_schema_leaf PARTITION OF public.mm_schema_partitioned FOR VALUES FROM(0) TO(10)"],
|
|
["trigger", "CREATE FUNCTION public.mm_schema_trigger_fn() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RETURN NEW; END'; CREATE TRIGGER mm_schema_trigger BEFORE UPDATE ON public.servers FOR EACH ROW EXECUTE FUNCTION public.mm_schema_trigger_fn()"],
|
|
]
|
|
for (const [label, sql] of mismatches) {
|
|
const target = await database(label.replaceAll("-", "_"), reference.name)
|
|
try {
|
|
await target.connection.query(sql)
|
|
const observed = await capture(target.connection)
|
|
assert.throws(() => compareMigrationSchemas(baseline!, observed), MigrationSchemaInventoryError, label)
|
|
cases.push("actual " + label + " drift differs from the complete captured baseline")
|
|
} finally { await target.connection.end() }
|
|
}
|
|
const unsupported = await database("unsupported", reference.name)
|
|
try {
|
|
await unsupported.connection.query("ALTER TABLE public.servers ENABLE ROW LEVEL SECURITY; CREATE POLICY mm_schema_policy ON public.servers USING(true)")
|
|
await assert.rejects(capture(unsupported.connection), MigrationSchemaInventoryError)
|
|
cases.push("actual unsupported RLS policy refuses capture rather than disappearing from equality")
|
|
} finally { await unsupported.connection.end() }
|
|
const control = await database("control", reference.name)
|
|
try {
|
|
await control.connection.query("CREATE SCHEMA mm_control; CREATE TABLE mm_control.mm_schema_private(id integer)")
|
|
await assert.rejects(capture(control.connection), MigrationSchemaInventoryError)
|
|
const scoped = await capture(control.connection, { scope: { applicationNamespaces: ["public"], excludedControlNamespaces: ["mm_control"] } })
|
|
assert(scoped.sections.namespaces.some(row => row.namespace === "mm_control"))
|
|
assert(!scoped.sections.relations.some(row => row.namespace === "mm_control"))
|
|
await control.connection.query("CREATE VIEW public.mm_schema_external_view AS SELECT id FROM mm_control.mm_schema_private")
|
|
await assert.rejects(capture(control.connection, { scope: { applicationNamespaces: ["public"], excludedControlNamespaces: ["mm_control"] } }), MigrationSchemaInventoryError)
|
|
cases.push("extra namespace refuses by default; explicit control exclusion is inventoried and application cross-dependency still refuses")
|
|
} finally { await control.connection.end() }
|
|
const client = await pool.connect()
|
|
try { await assert.rejects(captureMigrationSchema(client), MigrationSchemaInventoryError); cases.push("autocommit/read-write connection cannot claim a stable read-only catalog snapshot") }
|
|
finally { client.release() }
|
|
console.log("Owned migration schema inventory SQL:", JSON.stringify({ cases: cases.length, names: cases, databases: names.length, actualSql: true, pinnedFiles: inventory.length, manifestDigest: migrationManifestDigest(), logicalSchemaDigest: baseline!.sha256, dispatcherChanged: false, enrollmentAuthorized: false, productionDatabaseTouched: false }))
|
|
} finally { await closePool() }
|