fix(fts): try local LOAD before INSTALL to avoid network failures (#726)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / scope-parity (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run

This commit is contained in:
Pratyush Sharma 2026-04-24 03:08:46 +05:30 • committed by GitHub
parent 2d7b15f18c
commit 3eeb2833e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 24 deletions

View file

@ -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<void> => {
if (ftsLoaded) return;
if (!conn) {
export const loadFTSExtension = async (targetConn?: lbug.Connection): Promise<boolean> => {
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<void> => {
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;
}
}
};

View file

@ -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<void> {
// 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

View file

@ -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');