From 9f0b315ff67eb21ce18c75742d832c470df5fadb Mon Sep 17 00:00:00 2001 From: JAYA DILEEP Date: Sun, 6 Sep 2026 05:26:11 +0000 Subject: [PATCH] fix(indexing): escape apostrophes in LanceDB SQL path predicates Paths and cache keys with single quotes broke delete/retrieve predicates (Unterminated string literal). Double-quote escape them for Lance SQL. Fixes #13233 Signed-off-by: JAYA DILEEP --- core/indexing/LanceDbIndex.ts | 8 +++++--- core/indexing/escapeLanceSqlString.test.ts | 18 ++++++++++++++++++ core/indexing/escapeLanceSqlString.ts | 7 +++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 core/indexing/escapeLanceSqlString.test.ts create mode 100644 core/indexing/escapeLanceSqlString.ts diff --git a/core/indexing/LanceDbIndex.ts b/core/indexing/LanceDbIndex.ts index e01518629a9..46e573d722d 100644 --- a/core/indexing/LanceDbIndex.ts +++ b/core/indexing/LanceDbIndex.ts @@ -25,6 +25,7 @@ import { import type * as LanceType from "vectordb"; import { tagToString } from "./utils"; +import { escapeLanceSqlString } from "./escapeLanceSqlString"; interface LanceDbRow { uuid: string; @@ -38,6 +39,7 @@ type ItemWithChunks = { item: PathAndCacheKey; chunks: Chunk[] }; type ChunkMap = Map; + export class LanceDbIndex implements CodebaseIndex { private static lance: typeof LanceType | null = null; @@ -364,7 +366,7 @@ export class LanceDbIndex implements CodebaseIndex { for (const { path, cacheKey } of toDel) { await lanceTable.delete( - `cachekey = '${cacheKey}' AND path = '${path}'`, + `cachekey = '${escapeLanceSqlString(cacheKey)}' AND path = '${escapeLanceSqlString(path)}'`, ); accumulatedProgress += 1 / toDel.length / 3; @@ -419,7 +421,7 @@ export class LanceDbIndex implements CodebaseIndex { const table = await db.openTable(tableName); let query = table.search(vector); if (directory) { - query = query.where(`path LIKE '${directory}%'`).limit(300); + query = query.where(`path LIKE '${escapeLanceSqlString(directory)}%'`).limit(300); } else { query = query.limit(n); } @@ -477,7 +479,7 @@ export class LanceDbIndex implements CodebaseIndex { const sqliteDb = await SqliteDb.get(); const data = await sqliteDb.all( `SELECT * FROM lance_db_cache WHERE uuid in (${allResults - .map((r) => `'${r.uuid}'`) + .map((r) => `'${escapeLanceSqlString(r.uuid)}'`) .join(",")})`, ); diff --git a/core/indexing/escapeLanceSqlString.test.ts b/core/indexing/escapeLanceSqlString.test.ts new file mode 100644 index 00000000000..cc93b1521c4 --- /dev/null +++ b/core/indexing/escapeLanceSqlString.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; + +import { escapeLanceSqlString } from "./escapeLanceSqlString"; + +describe("escapeLanceSqlString", () => { + it("leaves strings without quotes unchanged", () => { + expect(escapeLanceSqlString("src/normal.ts")).toBe("src/normal.ts"); + }); + + it("doubles single quotes so LanceDB predicates stay valid", () => { + expect(escapeLanceSqlString("src/don't.ts")).toBe("src/don''t.ts"); + expect(escapeLanceSqlString("it's a test/")).toBe("it''s a test/"); + }); + + it("escapes multiple apostrophes", () => { + expect(escapeLanceSqlString("a'b'c")).toBe("a''b''c"); + }); +}); diff --git a/core/indexing/escapeLanceSqlString.ts b/core/indexing/escapeLanceSqlString.ts new file mode 100644 index 00000000000..3bb8d3b201b --- /dev/null +++ b/core/indexing/escapeLanceSqlString.ts @@ -0,0 +1,7 @@ +/** + * Escape a value for use inside a single-quoted LanceDB SQL string literal. + * LanceDB / SQL standard escaping doubles single quotes. + */ +export function escapeLanceSqlString(value: string): string { + return value.replace(/'/g, "''"); +}