From 3eeb2833e40756ce4ffb3cd318db5c729eabd42b Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Fri, 24 Apr 2026 03:08:46 +0530 Subject: [PATCH] fix(fts): try local LOAD before INSTALL to avoid network failures (#726) --- gitnexus/src/core/lbug/lbug-adapter.ts | 41 +++++++++++++------ gitnexus/src/core/lbug/pool-adapter.ts | 15 ++----- .../integration/lbug-core-adapter.test.ts | 25 +++++++++++ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/gitnexus/src/core/lbug/lbug-adapter.ts b/gitnexus/src/core/lbug/lbug-adapter.ts index e4190c323..63d1b3db1 100644 --- a/gitnexus/src/core/lbug/lbug-adapter.ts +++ b/gitnexus/src/core/lbug/lbug-adapter.ts @@ -1144,23 +1144,40 @@ export const getEmbeddingTableName = (): string => EMBEDDING_TABLE_NAME; /** * Load the FTS extension (required before using FTS functions). - * Safe to call multiple times — tracks loaded state via module-level ftsLoaded. + * + * Safe to call multiple times — when invoked without arguments, tracks loaded + * state via module-level `ftsLoaded`. When invoked with an explicit + * connection, loads on that connection and returns whether the load + * succeeded — letting callers (e.g. the pool adapter) track their own state. + * + * Tries `LOAD EXTENSION fts` first so previously-cached installs skip the + * network entirely; falls back to `INSTALL` + `LOAD` only when the extension + * hasn't been cached yet. */ -export const loadFTSExtension = async (): Promise => { - if (ftsLoaded) return; - if (!conn) { +export const loadFTSExtension = async (targetConn?: lbug.Connection): Promise => { + const useModuleState = targetConn === undefined; + if (useModuleState && ftsLoaded) return true; + + const c: lbug.Connection | null = targetConn ?? conn; + if (!c) { throw new Error('LadybugDB not initialized. Call initLbug first.'); } + + const markLoaded = (): true => { + if (useModuleState) ftsLoaded = true; + return true; + }; + try { // Try loading locally first (no network required) - await conn.query('LOAD EXTENSION fts'); - ftsLoaded = true; + await c.query('LOAD EXTENSION fts'); + return markLoaded(); } catch { // Fall back to install + load (requires network) try { - await conn.query('INSTALL fts'); - await conn.query('LOAD EXTENSION fts'); - ftsLoaded = true; + await c.query('INSTALL fts'); + await c.query('LOAD EXTENSION fts'); + return markLoaded(); } catch (err: any) { const msg = err?.message || ''; if ( @@ -1168,10 +1185,10 @@ export const loadFTSExtension = async (): Promise => { msg.includes('already installed') || msg.includes('already exists') ) { - ftsLoaded = true; - } else { - console.error('GitNexus: FTS extension load failed:', msg); + return markLoaded(); } + console.error('GitNexus: FTS extension load failed:', msg); + return false; } } }; diff --git a/gitnexus/src/core/lbug/pool-adapter.ts b/gitnexus/src/core/lbug/pool-adapter.ts index bc7402637..90f3f1f11 100644 --- a/gitnexus/src/core/lbug/pool-adapter.ts +++ b/gitnexus/src/core/lbug/pool-adapter.ts @@ -17,6 +17,7 @@ import fs from 'fs/promises'; import lbug from '@ladybugdb/core'; +import { loadFTSExtension } from './lbug-adapter.js'; /** Per-repo pool: one Database, many Connections */ interface PoolEntry { @@ -354,12 +355,7 @@ async function doInitLbug(repoId: string, dbPath: string): Promise { // Done BEFORE pool registration so no concurrent checkout can grab // the connection while the async FTS load is in progress. if (!shared.ftsLoaded) { - try { - await available[0].query('LOAD EXTENSION fts'); - shared.ftsLoaded = true; - } catch { - // Extension may not be installed — FTS queries will fail gracefully - } + shared.ftsLoaded = await loadFTSExtension(available[0]); } // Load VECTOR extension once per shared Database for semantic search support. @@ -433,12 +429,7 @@ export async function initLbugWithDb( // Load FTS extension if not already loaded on this Database if (!shared.ftsLoaded) { - try { - await available[0].query('LOAD EXTENSION fts'); - shared.ftsLoaded = true; - } catch { - // Extension may already be loaded or not installed - } + shared.ftsLoaded = await loadFTSExtension(available[0]); } // Load VECTOR extension for semantic search support diff --git a/gitnexus/test/integration/lbug-core-adapter.test.ts b/gitnexus/test/integration/lbug-core-adapter.test.ts index 1930597bb..543dbb8bf 100644 --- a/gitnexus/test/integration/lbug-core-adapter.test.ts +++ b/gitnexus/test/integration/lbug-core-adapter.test.ts @@ -45,6 +45,31 @@ withTestLbugDB( ).resolves.toBeUndefined(); }); + it('loadFTSExtension(conn): loads on an explicit connection and returns true', async () => { + const lbug = (await import('@ladybugdb/core')).default; + const { loadFTSExtension, getDatabase } = + await import('../../src/core/lbug/lbug-adapter.js'); + + const db = getDatabase(); + expect(db).not.toBeNull(); + + // Fresh Connection on the same Database — simulates the pool adapter's + // path where loadFTSExtension is called with an explicit connection + // rather than the module-level singleton. + const freshConn = new lbug.Connection(db!); + try { + const loaded = await loadFTSExtension(freshConn); + expect(loaded).toBe(true); + + // Idempotent on the same connection — calling again still returns true + // (exercises the "already loaded" catch branch in the fallback path). + const loadedAgain = await loadFTSExtension(freshConn); + expect(loadedAgain).toBe(true); + } finally { + await freshConn.close().catch(() => {}); + } + }); + it('getLbugStats: returns correct node and edge counts for seeded data', async () => { const { getLbugStats } = await import('../../src/core/lbug/lbug-adapter.js');