Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/core/pglite-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ export class PGLiteEngine implements BrainEngine {
* - `content_chunks.symbol_name` column (indexed by `idx_chunks_symbol_name`) — v0.19
* - `content_chunks.language` column (indexed by `idx_chunks_language`) — v0.19
* - `pages.deleted_at` column (indexed by `pages_deleted_at_purge_idx`) — v0.26.5
* - `mcp_request_log.agent_name` column (indexed by `idx_mcp_log_agent_time`) — v0.26.3
* - `mcp_request_log.params` column (referenced by SCHEMA_SQL replay) — v0.26.3
* - `mcp_request_log.error_message` column (referenced by SCHEMA_SQL replay) — v0.26.3
*
* **Maintenance contract:** when a future migration adds a column-with-index
* or new-table-with-FK referenced by PGLITE_SCHEMA_SQL, extend this method
Expand Down Expand Up @@ -240,7 +243,15 @@ export class PGLiteEngine implements BrainEngine {
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema='public' AND table_name='content_chunks' AND column_name='symbol_name') AS symbol_name_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema='public' AND table_name='content_chunks' AND column_name='language') AS language_exists
WHERE table_schema='public' AND table_name='content_chunks' AND column_name='language') AS language_exists,
EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema='public' AND table_name='mcp_request_log') AS mcp_log_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema='public' AND table_name='mcp_request_log' AND column_name='agent_name') AS mcp_log_agent_name_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema='public' AND table_name='mcp_request_log' AND column_name='params') AS mcp_log_params_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema='public' AND table_name='mcp_request_log' AND column_name='error_message') AS mcp_log_error_message_exists
`);
const probe = rows[0] as {
pages_exists: boolean;
Expand All @@ -252,6 +263,10 @@ export class PGLiteEngine implements BrainEngine {
chunks_exists: boolean;
symbol_name_exists: boolean;
language_exists: boolean;
mcp_log_exists: boolean;
mcp_log_agent_name_exists: boolean;
mcp_log_params_exists: boolean;
mcp_log_error_message_exists: boolean;
};

const needsPagesBootstrap = probe.pages_exists && !probe.source_id_exists;
Expand All @@ -260,9 +275,22 @@ export class PGLiteEngine implements BrainEngine {
const needsChunksBootstrap = probe.chunks_exists
&& (!probe.symbol_name_exists || !probe.language_exists);
const needsPagesDeletedAt = probe.pages_exists && !probe.deleted_at_exists;
// v0.26.3: idx_mcp_log_agent_time in PGLITE_SCHEMA_SQL crashes if
// agent_name doesn't exist yet. v33 (admin_dashboard_columns_v0_26_3)
// also adds these but bootstrap runs first.
const needsMcpLogBootstrap = probe.mcp_log_exists
&& (!probe.mcp_log_agent_name_exists
|| !probe.mcp_log_params_exists
|| !probe.mcp_log_error_message_exists);

// Fresh installs (no tables yet) and modern brains both no-op.
if (!needsPagesBootstrap && !needsLinksBootstrap && !needsChunksBootstrap && !needsPagesDeletedAt) return;
if (
!needsPagesBootstrap
&& !needsLinksBootstrap
&& !needsChunksBootstrap
&& !needsPagesDeletedAt
&& !needsMcpLogBootstrap
) return;

console.log(' Pre-v0.21 brain detected, applying forward-reference bootstrap');

Expand Down Expand Up @@ -320,6 +348,20 @@ export class PGLiteEngine implements BrainEngine {
ALTER TABLE pages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
`);
}

if (needsMcpLogBootstrap) {
// v33 (admin_dashboard_columns_v0_26_3) adds these three columns to
// mcp_request_log AND backfills agent_name. Bootstrap only adds the
// column shells so PGLITE_SCHEMA_SQL's
// `CREATE INDEX idx_mcp_log_agent_time ON mcp_request_log(agent_name, ...)`
// doesn't crash on a v0.22.7-shape mcp_request_log. v33 runs later via
// runMigrations and is idempotent.
await this.db.exec(`
ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS agent_name TEXT;
ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS params JSONB;
ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS error_message TEXT;
`);
}
}

async withReservedConnection<T>(fn: (conn: ReservedConnection) => Promise<T>): Promise<T> {
Expand Down
51 changes: 48 additions & 3 deletions src/core/postgres-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ export class PostgresEngine implements BrainEngine {
* - `content_chunks.symbol_name` column (indexed by `idx_chunks_symbol_name`) — v0.19
* - `content_chunks.language` column (indexed by `idx_chunks_language`) — v0.19
* - `pages.deleted_at` column (indexed by `pages_deleted_at_purge_idx`) — v0.26.5
* - `mcp_request_log.agent_name` column (indexed by `idx_mcp_log_agent_time`) — v0.26.3
* - `mcp_request_log.params` column (referenced by SCHEMA_SQL replay) — v0.26.3
* - `mcp_request_log.error_message` column (referenced by SCHEMA_SQL replay) — v0.26.3
*
* Keep this in sync with the PGLite version; covered by
* `test/schema-bootstrap-coverage.test.ts` (PGLite side) and
Expand All @@ -174,6 +177,10 @@ export class PostgresEngine implements BrainEngine {
chunks_exists: boolean;
symbol_name_exists: boolean;
language_exists: boolean;
mcp_log_exists: boolean;
mcp_log_agent_name_exists: boolean;
mcp_log_params_exists: boolean;
mcp_log_error_message_exists: boolean;
}[]>`
SELECT
EXISTS (SELECT 1 FROM information_schema.tables
Expand All @@ -193,7 +200,15 @@ export class PostgresEngine implements BrainEngine {
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'content_chunks' AND column_name = 'symbol_name') AS symbol_name_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'content_chunks' AND column_name = 'language') AS language_exists
WHERE table_schema = current_schema() AND table_name = 'content_chunks' AND column_name = 'language') AS language_exists,
EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema() AND table_name = 'mcp_request_log') AS mcp_log_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'mcp_request_log' AND column_name = 'agent_name') AS mcp_log_agent_name_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'mcp_request_log' AND column_name = 'params') AS mcp_log_params_exists,
EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'mcp_request_log' AND column_name = 'error_message') AS mcp_log_error_message_exists
`;
const probe = probeRows[0]!;

Expand All @@ -205,8 +220,22 @@ export class PostgresEngine implements BrainEngine {
// v0.26.5: pages_deleted_at_purge_idx in SCHEMA_SQL crashes if the column
// doesn't exist yet. Migration v34 also adds it, but bootstrap runs first.
const needsPagesDeletedAt = probe.pages_exists && !probe.deleted_at_exists;

if (!needsPagesBootstrap && !needsLinksBootstrap && !needsChunksBootstrap && !needsPagesDeletedAt) return;
// v0.26.3: idx_mcp_log_agent_time in SCHEMA_SQL crashes if agent_name
// doesn't exist yet. Migration v33 also adds it, but bootstrap runs first.
// We bootstrap params + error_message together because v33's UPDATE/SELECT
// statements that backfill agent_name reference all three columns.
const needsMcpLogBootstrap = probe.mcp_log_exists
&& (!probe.mcp_log_agent_name_exists
|| !probe.mcp_log_params_exists
|| !probe.mcp_log_error_message_exists);

if (
!needsPagesBootstrap
&& !needsLinksBootstrap
&& !needsChunksBootstrap
&& !needsPagesDeletedAt
&& !needsMcpLogBootstrap
) return;

console.log(' Pre-v0.21 brain detected, applying forward-reference bootstrap');

Expand Down Expand Up @@ -263,6 +292,22 @@ export class PostgresEngine implements BrainEngine {
ALTER TABLE pages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
`);
}

if (needsMcpLogBootstrap) {
// v33 (admin_dashboard_columns_v0_26_3) adds these three columns to
// mcp_request_log AND backfills agent_name via an UPDATE that joins on
// oauth_clients/access_tokens. Bootstrap only adds the column shells
// so SCHEMA_SQL's `CREATE INDEX idx_mcp_log_agent_time ON
// mcp_request_log(agent_name, ...)` doesn't crash on a v0.22.7-shape
// mcp_request_log. v33 runs later via runMigrations and is idempotent
// (its `ADD COLUMN IF NOT EXISTS` statements no-op, the backfill
// UPDATE finds NULL agent_name rows and fills them).
await conn.unsafe(`
ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS agent_name TEXT;
ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS params JSONB;
ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS error_message TEXT;
`);
}
}

async transaction<T>(fn: (engine: BrainEngine) => Promise<T>): Promise<T> {
Expand Down
35 changes: 35 additions & 0 deletions test/e2e/postgres-bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,39 @@ describe.skipIf(skip)('PostgresEngine forward-reference bootstrap (E2E)', () =>
await engine.initSchema();
expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));
});

test('PostgresEngine.initSchema bootstraps mcp_request_log v0.26.3 columns on a v31 brain', async () => {
// Regression for the v0.26.3 mcp_request_log forward-reference miss:
// a brain stuck at v31 has the v0.22.7-shape mcp_request_log (5 cols, no
// agent_name / params / error_message), but SCHEMA_SQL line ~420 declares
// `CREATE INDEX idx_mcp_log_agent_time ON mcp_request_log(agent_name, ...)`.
// Without bootstrap coverage, init crashes with `column "agent_name" does
// not exist` BEFORE migration v33 (admin_dashboard_columns_v0_26_3) runs.
await engine.initSchema();
const conn = (engine as any).sql;

// Mutate to pre-v0.26.3 mcp_request_log shape.
await conn.unsafe(`
DROP INDEX IF EXISTS idx_mcp_log_agent_time;
DROP INDEX IF EXISTS idx_mcp_log_time_agent;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS agent_name CASCADE;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS params CASCADE;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS error_message CASCADE;
`);
await engine.setConfig('version', '31');

// Bootstrap → SCHEMA_SQL → runMigrations chain. Must not crash.
await engine.initSchema();

expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));

// Verify the bootstrapped columns exist after upgrade.
const cols = await conn`
SELECT column_name FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'mcp_request_log'
AND column_name IN ('agent_name', 'params', 'error_message')
`;
expect(cols).toHaveLength(3);
});
});
19 changes: 19 additions & 0 deletions test/schema-bootstrap-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ const REQUIRED_BOOTSTRAP_COVERAGE: ForwardReference[] = [
// v0.26.5 — forward-referenced by `CREATE INDEX pages_deleted_at_purge_idx
// ON pages (deleted_at) WHERE deleted_at IS NOT NULL`.
{ kind: 'column', table: 'pages', column: 'deleted_at' },
// v0.26.3 — forward-referenced by `CREATE INDEX idx_mcp_log_agent_time
// ON mcp_request_log(agent_name, created_at DESC)`.
{ kind: 'column', table: 'mcp_request_log', column: 'agent_name' },
// v0.26.3 — admin dashboard request log columns. Not directly indexed, but
// referenced by the same v33 migration's UPDATE backfill, so the schema-blob
// replay assumes they're present alongside agent_name.
{ kind: 'column', table: 'mcp_request_log', column: 'params' },
{ kind: 'column', table: 'mcp_request_log', column: 'error_message' },
];

test('applyForwardReferenceBootstrap covers every forward reference declared in REQUIRED_BOOTSTRAP_COVERAGE', async () => {
Expand Down Expand Up @@ -95,6 +103,12 @@ test('applyForwardReferenceBootstrap covers every forward reference declared in

DROP INDEX IF EXISTS pages_deleted_at_purge_idx;
ALTER TABLE pages DROP COLUMN IF EXISTS deleted_at;

DROP INDEX IF EXISTS idx_mcp_log_agent_time;
DROP INDEX IF EXISTS idx_mcp_log_time_agent;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS agent_name;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS params;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS error_message;
`);

// Run bootstrap in isolation (NOT initSchema). This is what we're testing.
Expand Down Expand Up @@ -150,6 +164,11 @@ test('after bootstrap, PGLITE_SCHEMA_SQL replays without crashing on missing for
ALTER TABLE links DROP COLUMN IF EXISTS origin_page_id;
DROP INDEX IF EXISTS pages_deleted_at_purge_idx;
ALTER TABLE pages DROP COLUMN IF EXISTS deleted_at;
DROP INDEX IF EXISTS idx_mcp_log_agent_time;
DROP INDEX IF EXISTS idx_mcp_log_time_agent;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS agent_name;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS params;
ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS error_message;
`);

// Bootstrap, then schema replay. Either step crashing fails the test.
Expand Down