fix(lbug): stop dropFTSIndex from swallowing genuine engine failures

dropFTSIndex previously caught and discarded every DROP_FTS_INDEX error
unconditionally. Extract isBenignDropFtsIndexError, a pure classifier
for the two legitimate "nothing to drop" cases (Binder/Catalog
exceptions: index never created, or the FTS function isn't registered)
verified end-to-end against @ladybugdb/core 0.18.x's real conn.query()
error text. Anything else -- e.g. the Runtime exception "FTS index is
inconsistent" class from #2589 -- now rethrows instead of being masked,
so a corrupted index can no longer persist across analyze runs
undetected.
This commit is contained in:
Gergo Magyar 2026-07-21 05:56:04 +00:00
parent 1fd1f14cee
commit c548652eca
2 changed files with 83 additions and 3 deletions

View file

@ -2904,7 +2904,26 @@ export const queryFTS = async (
};
/**
* Drop an FTS index
* True for the two benign "nothing to drop" `DROP_FTS_INDEX` failures
* both catalog/binder exceptions, LadybugDB's classes for "this name isn't
* bound to anything right now" (probe-verified end-to-end through
* `dropFTSIndex`'s real `conn.query()` path against @ladybugdb/core
* 0.18.x): the named index was never created (`Binder exception: Table <T>
* doesn't have an index with name <name>.`), or the FTS extension/function
* isn't registered at all (`Catalog exception: function DROP_FTS_INDEX is
* not defined...`). A real engine failure — e.g. the `Runtime exception:
* FTS index '<name>' is inconsistent: ...` class from #2589 — is a
* DIFFERENT exception class (an execution-time failure, not a catalog/bind
* lookup miss), so this returns false for it. Pure string logic so it is
* unit-testable without a native LadybugDB connection.
*/
export const isBenignDropFtsIndexError = (message: string): boolean =>
message.includes('Binder exception:') || message.includes('Catalog exception:');
/**
* Drop an FTS index. Tolerates only {@link isBenignDropFtsIndexError}
* anything else rethrows instead of being silently masked, which previously
* let a corrupted index persist across analyze runs undetected.
*/
export const dropFTSIndex = async (tableName: string, indexName: string): Promise<void> => {
if (!conn) {
@ -2913,8 +2932,11 @@ export const dropFTSIndex = async (tableName: string, indexName: string): Promis
try {
await queryAndDrain(conn, `CALL DROP_FTS_INDEX('${tableName}', '${indexName}')`);
} catch {
// Index may not exist
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (!isBenignDropFtsIndexError(msg)) {
throw e;
}
} finally {
ensuredFTSIndexes.delete(ftsIndexKey(tableName, indexName));
}

View file

@ -0,0 +1,58 @@
/**
* #2589: `dropFTSIndex` must tolerate only benign "nothing to drop"
* `DROP_FTS_INDEX` failures and rethrow everything else previously it
* swallowed every error unconditionally, which could mask a genuinely
* corrupted FTS index across analyze runs.
*
* `isBenignDropFtsIndexError` is pure string logic (no native connection
* needed), so the classification itself is unit-tested directly, including
* against the exact reported #2589 error text a native repro of that
* specific engine failure was not achieved during investigation, but the
* classifier's behavior for it is still provable from the message alone.
*/
import { describe, expect, it } from 'vitest';
import { isBenignDropFtsIndexError, dropFTSIndex } from '../../src/core/lbug/lbug-adapter.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
describe('isBenignDropFtsIndexError', () => {
it('is true for the FTS-extension/function-not-registered catalog error (probe-verified text)', () => {
expect(
isBenignDropFtsIndexError(
"Catalog exception: function DROP_FTS_INDEX is not defined. This function exists in the FTS extension. You can install and load the extension by running 'INSTALL FTS; LOAD EXTENSION FTS;'.",
),
).toBe(true);
});
it('is true for the index-never-created binder error (probe-verified against the real dropFTSIndex path)', () => {
expect(
isBenignDropFtsIndexError(
"Binder exception: Table File doesn't have an index with name file_fts.",
),
).toBe(true);
});
it('is false for the #2589 runtime inconsistency error (must surface, not be swallowed)', () => {
expect(
isBenignDropFtsIndexError(
"Runtime exception: FTS index 'file_fts' is inconsistent: term 'wiki' is missing during delete.",
),
).toBe(false);
});
it('is false for an unrelated failure', () => {
expect(isBenignDropFtsIndexError('Connection Exception: database is closed')).toBe(false);
});
});
withTestLbugDB('drop-fts-index-benign-cases', (handle) => {
describe('dropFTSIndex end-to-end benign cases (#2589)', () => {
it('resolves cleanly when the named index was never created', async () => {
void handle;
const { executeQuery } = await import('../../src/core/lbug/lbug-adapter.js');
await executeQuery(
`CREATE NODE TABLE IF NOT EXISTS DropProbe (id STRING PRIMARY KEY, content STRING)`,
);
await expect(dropFTSIndex('DropProbe', 'drop_probe_never_created')).resolves.toBeUndefined();
}, 120_000);
});
});