diff --git a/apps/server/src/core/base/query-cache/postgres-extension.service.spec.ts b/apps/server/src/core/base/query-cache/postgres-extension.service.spec.ts index dfa27b8d..e94037bd 100644 --- a/apps/server/src/core/base/query-cache/postgres-extension.service.spec.ts +++ b/apps/server/src/core/base/query-cache/postgres-extension.service.spec.ts @@ -57,8 +57,8 @@ describe('PostgresExtensionService', () => { expect(Number(row.c)).toBeGreaterThan(0); await svc.detach(conn); } finally { - await conn.closeSync(); - await instance.closeSync(); + conn.closeSync(); + instance.closeSync(); } }); @@ -73,8 +73,8 @@ describe('PostgresExtensionService', () => { await svc.detach(conn); await expect(svc.detach(conn)).resolves.toBeUndefined(); } finally { - await conn.closeSync(); - await instance.closeSync(); + conn.closeSync(); + instance.closeSync(); } }); @@ -86,8 +86,44 @@ describe('PostgresExtensionService', () => { try { await expect(svc.configureOnConnection(conn)).rejects.toThrow(/not ready/i); } finally { - await conn.closeSync(); - await instance.closeSync(); + conn.closeSync(); + instance.closeSync(); + } + }); + + it('includes the bootstrap failure reason in the not-ready error', async () => { + // Force bootstrap to fail by giving the service a broken DB URL so that + // LOAD postgres still succeeds but something in the bootstrap path throws. + // Simplest reliable failure: monkey-patch the service so its bootstrap + // runs a SQL statement that cannot succeed. We accept a small amount of + // test-only access by subclassing. + + class BreakingService extends PostgresExtensionService { + async onApplicationBootstrap(): Promise { + // Call super to keep the gate logic, but sabotage inside by + // running INSTALL on a closed connection via a try-wrapper that + // throws synchronously and is captured by the parent catch. + // Simplest approach: directly set the failure and leave ready=false. + (this as any).ready = false; + (this as any).bootstrapFailure = 'simulated boot failure XYZ'; + } + } + + const svc = new BreakingService( + makeConfig(), + makeEnv() as any, + ); + await svc.onApplicationBootstrap(); + + const instance = await DuckDBInstance.create(':memory:'); + const conn = await instance.connect(); + try { + await expect(svc.configureOnConnection(conn)).rejects.toThrow( + /simulated boot failure XYZ/, + ); + } finally { + conn.closeSync(); + instance.closeSync(); } }); }); diff --git a/apps/server/src/core/base/query-cache/postgres-extension.service.ts b/apps/server/src/core/base/query-cache/postgres-extension.service.ts index 45813f25..51ed0af3 100644 --- a/apps/server/src/core/base/query-cache/postgres-extension.service.ts +++ b/apps/server/src/core/base/query-cache/postgres-extension.service.ts @@ -32,6 +32,7 @@ import { EnvironmentService } from '../../../integrations/environment/environmen export class PostgresExtensionService implements OnApplicationBootstrap { private readonly logger = new Logger(PostgresExtensionService.name); private ready = false; + private bootstrapFailure: string | null = null; constructor( private readonly config: QueryCacheConfigProvider, @@ -64,9 +65,10 @@ export class PostgresExtensionService implements OnApplicationBootstrap { // app: the cache service handles this by falling through to Postgres // when `isReady()` returns false (see `CollectionLoader.load`). this.ready = false; + this.bootstrapFailure = error.message; } finally { - await conn.closeSync(); - await bootstrap.closeSync(); + conn.closeSync(); + bootstrap.closeSync(); } } @@ -83,8 +85,11 @@ export class PostgresExtensionService implements OnApplicationBootstrap { */ async configureOnConnection(conn: DuckDBConnection): Promise { if (!this.ready) { + const reason = this.bootstrapFailure + ? `: ${this.bootstrapFailure}` + : ''; throw new Error( - 'PostgresExtensionService not ready — check bootstrap logs', + `PostgresExtensionService not ready${reason}. Check bootstrap logs.`, ); }