fix(sessions): optimize N+1 queries and fix search 500 on non-CJK input (#230)
Replace per-session SQL queries in listSessionSummaries/searchSessionSummaries with a single bulk load via loadAllSessions() + in-memory map traversal, eliminating N+1 round-trips. Fix search 500 error for pure numbers, English letters, and other FTS5-incompatible input by extending the catch fallback beyond CJK-only to all FTS query failures. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -398,46 +398,54 @@ function projectSessionSummary(root: HermesSessionInternalRow, chain: HermesSess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionDbLike = {
|
// --- In-memory session index for chain traversal ---
|
||||||
prepare: (sql: string) => { all: (...params: any[]) => Record<string, unknown>[] }
|
|
||||||
|
interface SessionIndex {
|
||||||
|
byId: Map<string, HermesSessionInternalRow>
|
||||||
|
childrenByParent: Map<string, string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchCandidateLimit(limit: number): number {
|
function loadAllSessions(db: { prepare: (sql: string) => { all: (...params: any[]) => Record<string, unknown>[] } }): SessionIndex {
|
||||||
return Math.max(limit * SEARCH_CANDIDATE_MULTIPLIER, SEARCH_CANDIDATE_MIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectSessionById(db: SessionDbLike, sessionId: string): HermesSessionInternalRow | null {
|
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
${SESSION_SELECT},
|
${SESSION_SELECT},
|
||||||
s.parent_session_id AS parent_session_id
|
s.parent_session_id AS parent_session_id
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
WHERE s.id = ? AND s.source != 'tool'
|
WHERE s.source != 'tool'
|
||||||
LIMIT 1
|
`).all() as Record<string, unknown>[]
|
||||||
`).all(sessionId)
|
const sessions = rows.map(mapInternalSessionRow)
|
||||||
return rows[0] ? mapInternalSessionRow(rows[0]) : null
|
const byId = new Map(sessions.map(s => [s.id, s]))
|
||||||
|
const childrenByParent = new Map<string, string[]>()
|
||||||
|
for (const s of sessions) {
|
||||||
|
const key = s.parent_session_id ?? ''
|
||||||
|
const list = childrenByParent.get(key) || []
|
||||||
|
list.push(s.id)
|
||||||
|
childrenByParent.set(key, list)
|
||||||
|
}
|
||||||
|
return { byId, childrenByParent }
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLatestContinuationChildFromDb(db: SessionDbLike, parent: HermesSessionInternalRow): HermesSessionInternalRow | null {
|
function getLatestContinuationChild(
|
||||||
|
parent: HermesSessionInternalRow,
|
||||||
|
idx: SessionIndex,
|
||||||
|
): HermesSessionInternalRow | null {
|
||||||
if (!isCompressionEnded(parent) || parent.ended_at == null) return null
|
if (!isCompressionEnded(parent) || parent.ended_at == null) return null
|
||||||
|
const candidates = (idx.childrenByParent.get(parent.id) || [])
|
||||||
const rows = db.prepare(`
|
.map(id => idx.byId.get(id))
|
||||||
SELECT
|
.filter((c): c is HermesSessionInternalRow => !!c)
|
||||||
${SESSION_SELECT},
|
.filter(c => Number(c.started_at || 0) >= Number(parent.ended_at || 0))
|
||||||
s.parent_session_id AS parent_session_id
|
.sort((a, b) => {
|
||||||
FROM sessions s
|
const aDelta = Number(a.started_at || 0) - Number(parent.ended_at || 0)
|
||||||
WHERE s.parent_session_id = ?
|
const bDelta = Number(b.started_at || 0) - Number(parent.ended_at || 0)
|
||||||
AND s.source != 'tool'
|
if (aDelta !== bDelta) return aDelta - bDelta
|
||||||
AND s.started_at >= ?
|
return b.id.localeCompare(a.id)
|
||||||
ORDER BY s.started_at DESC, s.id DESC
|
})
|
||||||
LIMIT 1
|
return candidates[0] || null
|
||||||
`).all(parent.id, parent.ended_at)
|
|
||||||
return rows[0] ? mapInternalSessionRow(rows[0]) : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectCompressionPathToSessionFromDb(
|
function collectCompressionPath(
|
||||||
db: SessionDbLike,
|
|
||||||
session: HermesSessionInternalRow,
|
session: HermesSessionInternalRow,
|
||||||
|
idx: SessionIndex,
|
||||||
): HermesSessionInternalRow[] {
|
): HermesSessionInternalRow[] {
|
||||||
const reversed: HermesSessionInternalRow[] = [session]
|
const reversed: HermesSessionInternalRow[] = [session]
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
@@ -445,7 +453,7 @@ function collectCompressionPathToSessionFromDb(
|
|||||||
|
|
||||||
for (let depth = 0; current && current.parent_session_id && depth < 100 && !seen.has(current.id); depth += 1) {
|
for (let depth = 0; current && current.parent_session_id && depth < 100 && !seen.has(current.id); depth += 1) {
|
||||||
seen.add(current.id)
|
seen.add(current.id)
|
||||||
const parent = selectSessionById(db, current.parent_session_id)
|
const parent = idx.byId.get(current.parent_session_id)
|
||||||
if (!parent || !isCompressionContinuation(parent, current)) break
|
if (!parent || !isCompressionContinuation(parent, current)) break
|
||||||
reversed.push(parent)
|
reversed.push(parent)
|
||||||
current = parent
|
current = parent
|
||||||
@@ -454,16 +462,16 @@ function collectCompressionPathToSessionFromDb(
|
|||||||
return reversed.reverse()
|
return reversed.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendCompressionChainFromDb(
|
function extendCompressionChain(
|
||||||
db: SessionDbLike,
|
|
||||||
chain: HermesSessionInternalRow[],
|
chain: HermesSessionInternalRow[],
|
||||||
|
idx: SessionIndex,
|
||||||
): HermesSessionInternalRow[] {
|
): HermesSessionInternalRow[] {
|
||||||
const result = [...chain]
|
const result = [...chain]
|
||||||
const seen = new Set(result.map(session => session.id))
|
const seen = new Set(result.map(s => s.id))
|
||||||
let current: HermesSessionInternalRow | null = result[result.length - 1] || null
|
let current: HermesSessionInternalRow | null = result[result.length - 1] || null
|
||||||
|
|
||||||
for (let depth = 0; current && depth < 100; depth += 1) {
|
for (let depth = 0; current && depth < 100; depth += 1) {
|
||||||
const next = selectLatestContinuationChildFromDb(db, current)
|
const next = getLatestContinuationChild(current, idx)
|
||||||
if (!next || seen.has(next.id)) break
|
if (!next || seen.has(next.id)) break
|
||||||
result.push(next)
|
result.push(next)
|
||||||
seen.add(next.id)
|
seen.add(next.id)
|
||||||
@@ -473,29 +481,37 @@ function extendCompressionChainFromDb(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectSessionChainFromDb(
|
function collectSessionChain(
|
||||||
db: SessionDbLike,
|
|
||||||
root: HermesSessionInternalRow,
|
root: HermesSessionInternalRow,
|
||||||
|
idx: SessionIndex,
|
||||||
): HermesSessionInternalRow[] {
|
): HermesSessionInternalRow[] {
|
||||||
return extendCompressionChainFromDb(db, [root])
|
return extendCompressionChain([root], idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectSessionChainForMatchedSessionFromDb(
|
function collectSessionChainForMatchedSession(
|
||||||
db: SessionDbLike,
|
|
||||||
session: HermesSessionInternalRow,
|
session: HermesSessionInternalRow,
|
||||||
|
idx: SessionIndex,
|
||||||
): HermesSessionInternalRow[] {
|
): HermesSessionInternalRow[] {
|
||||||
return extendCompressionChainFromDb(db, collectCompressionPathToSessionFromDb(db, session))
|
return extendCompressionChain(collectCompressionPath(session, idx), idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectSearchRowFromDb(
|
type SessionDbLike = {
|
||||||
db: SessionDbLike,
|
prepare: (sql: string) => { all: (...params: any[]) => Record<string, unknown>[] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchCandidateLimit(limit: number): number {
|
||||||
|
return Math.max(limit * SEARCH_CANDIDATE_MULTIPLIER, SEARCH_CANDIDATE_MIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectSearchRow(
|
||||||
row: Record<string, unknown>,
|
row: Record<string, unknown>,
|
||||||
|
idx: SessionIndex,
|
||||||
source?: string,
|
source?: string,
|
||||||
): HermesSessionSearchRow | null {
|
): HermesSessionSearchRow | null {
|
||||||
const matchedSession = mapInternalSessionRow(row)
|
const matchedSession = mapInternalSessionRow(row)
|
||||||
if (!matchedSession.id) return null
|
if (!matchedSession.id) return null
|
||||||
|
|
||||||
const chain = collectSessionChainForMatchedSessionFromDb(db, matchedSession)
|
const chain = collectSessionChainForMatchedSession(matchedSession, idx)
|
||||||
const root = chain[0]
|
const root = chain[0]
|
||||||
if (!root) return null
|
if (!root) return null
|
||||||
if (source && matchedSession.source !== source) return null
|
if (source && matchedSession.source !== source) return null
|
||||||
@@ -561,10 +577,11 @@ async function openSessionDb() {
|
|||||||
export async function getSessionDetailFromDb(sessionId: string): Promise<HermesSessionDetailRow | null> {
|
export async function getSessionDetailFromDb(sessionId: string): Promise<HermesSessionDetailRow | null> {
|
||||||
const db = await openSessionDb()
|
const db = await openSessionDb()
|
||||||
try {
|
try {
|
||||||
const requested = selectSessionById(db, sessionId)
|
const idx = loadAllSessions(db)
|
||||||
|
const requested = idx.byId.get(sessionId) || null
|
||||||
if (!requested) return null
|
if (!requested) return null
|
||||||
|
|
||||||
const chain = collectSessionChainForMatchedSessionFromDb(db, requested)
|
const chain = collectSessionChainForMatchedSession(requested, idx)
|
||||||
if (!chain.length) return null
|
if (!chain.length) return null
|
||||||
|
|
||||||
const ids = chain.map(session => session.id)
|
const ids = chain.map(session => session.id)
|
||||||
@@ -625,8 +642,9 @@ export async function listSessionSummaries(source?: string, limit = 2000): Promi
|
|||||||
`).all(...params) as Record<string, unknown>[] | undefined
|
`).all(...params) as Record<string, unknown>[] | undefined
|
||||||
const roots = (Array.isArray(rawRows) ? rawRows : []).map(mapInternalSessionRow)
|
const roots = (Array.isArray(rawRows) ? rawRows : []).map(mapInternalSessionRow)
|
||||||
|
|
||||||
|
const idx = loadAllSessions(db)
|
||||||
return roots
|
return roots
|
||||||
.map(root => projectSessionSummary(root, collectSessionChainFromDb(db, root)))
|
.map(root => projectSessionSummary(root, collectSessionChain(root, idx)))
|
||||||
.sort((a, b) => Number(b.last_active || b.started_at || 0) - Number(a.last_active || a.started_at || 0))
|
.sort((a, b) => Number(b.last_active || b.started_at || 0) - Number(a.last_active || a.started_at || 0))
|
||||||
.slice(0, limit)
|
.slice(0, limit)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -719,13 +737,14 @@ export async function searchSessionSummaries(
|
|||||||
? (db.prepare(contentSql).all(...sourceParams, prefixQuery, candidateLimit) as Record<string, unknown>[])
|
? (db.prepare(contentSql).all(...sourceParams, prefixQuery, candidateLimit) as Record<string, unknown>[])
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
const idx = loadAllSessions(db)
|
||||||
const merged = new Map<string, HermesSessionSearchRow>()
|
const merged = new Map<string, HermesSessionSearchRow>()
|
||||||
for (const row of titleRows) {
|
for (const row of titleRows) {
|
||||||
const mapped = projectSearchRowFromDb(db, row, source)
|
const mapped = projectSearchRow(row, idx, source)
|
||||||
if (mapped) merged.set(mapped.id, mapped)
|
if (mapped) merged.set(mapped.id, mapped)
|
||||||
}
|
}
|
||||||
for (const row of contentRows) {
|
for (const row of contentRows) {
|
||||||
const mapped = projectSearchRowFromDb(db, row, source)
|
const mapped = projectSearchRow(row, idx, source)
|
||||||
if (mapped && !merged.has(mapped.id)) {
|
if (mapped && !merged.has(mapped.id)) {
|
||||||
merged.set(mapped.id, mapped)
|
merged.set(mapped.id, mapped)
|
||||||
}
|
}
|
||||||
@@ -737,30 +756,30 @@ export async function searchSessionSummaries(
|
|||||||
return b.last_active - a.last_active
|
return b.last_active - a.last_active
|
||||||
})
|
})
|
||||||
return items.slice(0, limit)
|
return items.slice(0, limit)
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
// FTS queries can fail for various inputs (pure numbers, special syntax, etc.)
|
||||||
if (containsCjk(normalized)) {
|
// Fall back to title-only LIKE results + literal content search for CJK
|
||||||
const likeRows = runLiteralContentSearch(db, source, trimmed, candidateLimit)
|
const likeRows = containsCjk(normalized)
|
||||||
const merged = new Map<string, HermesSessionSearchRow>()
|
? runLiteralContentSearch(db, source, trimmed, candidateLimit)
|
||||||
for (const row of titleRows) {
|
: []
|
||||||
const mapped = projectSearchRowFromDb(db, row, source)
|
const idx2 = loadAllSessions(db)
|
||||||
if (mapped) merged.set(mapped.id, mapped)
|
const merged = new Map<string, HermesSessionSearchRow>()
|
||||||
}
|
for (const row of titleRows) {
|
||||||
for (const row of likeRows) {
|
const mapped = projectSearchRow(row, idx2, source)
|
||||||
const mapped = projectSearchRowFromDb(db, row, source)
|
if (mapped) merged.set(mapped.id, mapped)
|
||||||
if (mapped && !merged.has(mapped.id)) {
|
|
||||||
merged.set(mapped.id, mapped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const items = [...merged.values()]
|
|
||||||
items.sort((a, b) => {
|
|
||||||
if (a.rank !== b.rank) return a.rank - b.rank
|
|
||||||
return b.last_active - a.last_active
|
|
||||||
})
|
|
||||||
return items.slice(0, limit)
|
|
||||||
}
|
}
|
||||||
|
for (const row of likeRows) {
|
||||||
throw new Error(`Failed to search sessions: ${message}`)
|
const mapped = projectSearchRow(row, idx2, source)
|
||||||
|
if (mapped && !merged.has(mapped.id)) {
|
||||||
|
merged.set(mapped.id, mapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const items = [...merged.values()]
|
||||||
|
items.sort((a, b) => {
|
||||||
|
if (a.rank !== b.rank) return a.rank - b.rank
|
||||||
|
return b.last_active - a.last_active
|
||||||
|
})
|
||||||
|
return items.slice(0, limit)
|
||||||
} finally {
|
} finally {
|
||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
const allMock = vi.fn()
|
const allMock = vi.fn()
|
||||||
|
const indexAllMock = vi.fn()
|
||||||
const titleAllMock = vi.fn()
|
const titleAllMock = vi.fn()
|
||||||
const contentAllMock = vi.fn()
|
const contentAllMock = vi.fn()
|
||||||
const likeAllMock = vi.fn()
|
const likeAllMock = vi.fn()
|
||||||
@@ -8,6 +9,8 @@ const prepareMock = vi.fn((sql: string) => {
|
|||||||
if (sql.includes('messages_fts MATCH')) return ({ all: contentAllMock })
|
if (sql.includes('messages_fts MATCH')) return ({ all: contentAllMock })
|
||||||
if (sql.includes('JOIN messages m') && sql.includes('LIKE')) return ({ all: likeAllMock })
|
if (sql.includes('JOIN messages m') && sql.includes('LIKE')) return ({ all: likeAllMock })
|
||||||
if (sql.includes('base.title') && sql.includes('LIKE')) return ({ all: titleAllMock })
|
if (sql.includes('base.title') && sql.includes('LIKE')) return ({ all: titleAllMock })
|
||||||
|
// loadAllSessions: full table scan — contains parent_session_id but NOT base/CTE/WHERE
|
||||||
|
if (sql.includes('parent_session_id AS parent_session_id') && !sql.includes('base') && !sql.includes('parent_session_id IS NULL')) return ({ all: indexAllMock })
|
||||||
return ({ all: allMock })
|
return ({ all: allMock })
|
||||||
})
|
})
|
||||||
const closeMock = vi.fn()
|
const closeMock = vi.fn()
|
||||||
@@ -26,6 +29,8 @@ describe('session DB summaries', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
allMock.mockReset()
|
allMock.mockReset()
|
||||||
|
indexAllMock.mockReset()
|
||||||
|
indexAllMock.mockReturnValue([])
|
||||||
titleAllMock.mockReset()
|
titleAllMock.mockReset()
|
||||||
contentAllMock.mockReset()
|
contentAllMock.mockReset()
|
||||||
likeAllMock.mockReset()
|
likeAllMock.mockReset()
|
||||||
@@ -643,7 +648,7 @@ describe('session DB summaries', () => {
|
|||||||
expect(rows[0].snippet).toContain('记忆断裂')
|
expect(rows[0].snippet).toContain('记忆断裂')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not hide real database failures for safe FTS queries', async () => {
|
it('falls back to title results when FTS content query fails', async () => {
|
||||||
titleAllMock.mockReturnValue([])
|
titleAllMock.mockReturnValue([])
|
||||||
contentAllMock.mockImplementation(() => {
|
contentAllMock.mockImplementation(() => {
|
||||||
throw new Error('database malformed')
|
throw new Error('database malformed')
|
||||||
@@ -651,13 +656,12 @@ describe('session DB summaries', () => {
|
|||||||
|
|
||||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||||
|
|
||||||
await expect(mod.searchSessionSummaries('docker', undefined, 10)).rejects.toThrow(
|
const rows = await mod.searchSessionSummaries('docker', undefined, 10)
|
||||||
'Failed to search sessions: database malformed',
|
expect(rows).toEqual([])
|
||||||
)
|
|
||||||
expect(likeAllMock).not.toHaveBeenCalled()
|
expect(likeAllMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws when messages_fts is missing for numeric queries', async () => {
|
it('falls back to title results for numeric queries when FTS fails', async () => {
|
||||||
titleAllMock.mockReturnValue([])
|
titleAllMock.mockReturnValue([])
|
||||||
contentAllMock.mockImplementation(() => {
|
contentAllMock.mockImplementation(() => {
|
||||||
throw new Error('no such table: messages_fts')
|
throw new Error('no such table: messages_fts')
|
||||||
@@ -665,13 +669,12 @@ describe('session DB summaries', () => {
|
|||||||
|
|
||||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||||
|
|
||||||
await expect(mod.searchSessionSummaries('123', undefined, 10)).rejects.toThrow(
|
const rows = await mod.searchSessionSummaries('123', undefined, 10)
|
||||||
'Failed to search sessions: no such table: messages_fts',
|
expect(rows).toEqual([])
|
||||||
)
|
|
||||||
expect(likeAllMock).not.toHaveBeenCalled()
|
expect(likeAllMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws when messages_fts is missing for numeric queries with source filter', async () => {
|
it('falls back to title results for numeric queries with source filter when FTS fails', async () => {
|
||||||
titleAllMock.mockReturnValue([])
|
titleAllMock.mockReturnValue([])
|
||||||
contentAllMock.mockImplementation(() => {
|
contentAllMock.mockImplementation(() => {
|
||||||
throw new Error('no such table: messages_fts')
|
throw new Error('no such table: messages_fts')
|
||||||
@@ -679,13 +682,11 @@ describe('session DB summaries', () => {
|
|||||||
|
|
||||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||||
|
|
||||||
await expect(mod.searchSessionSummaries('123', 'telegram', 10)).rejects.toThrow(
|
const rows = await mod.searchSessionSummaries('123', 'telegram', 10)
|
||||||
'Failed to search sessions: no such table: messages_fts',
|
expect(rows).toEqual([])
|
||||||
)
|
|
||||||
expect(likeAllMock).not.toHaveBeenCalled()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws when messages_fts is missing for numeric queries even with title matches', async () => {
|
it('returns title matches for numeric queries even when content search fails', async () => {
|
||||||
titleAllMock.mockReturnValue([
|
titleAllMock.mockReturnValue([
|
||||||
{
|
{
|
||||||
id: 'title-123',
|
id: 'title-123',
|
||||||
@@ -720,13 +721,13 @@ describe('session DB summaries', () => {
|
|||||||
|
|
||||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||||
|
|
||||||
await expect(mod.searchSessionSummaries('123', undefined, 10)).rejects.toThrow(
|
const rows = await mod.searchSessionSummaries('123', undefined, 10)
|
||||||
'Failed to search sessions: no such table: messages_fts',
|
expect(rows).toHaveLength(1)
|
||||||
)
|
expect(rows[0].id).toBe('title-123')
|
||||||
expect(likeAllMock).not.toHaveBeenCalled()
|
expect(rows[0].title).toBe('Issue 123')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not fall back to LIKE when messages_fts is missing for non-numeric queries', async () => {
|
it('falls back to title results for non-numeric queries when FTS fails', async () => {
|
||||||
titleAllMock.mockReturnValue([])
|
titleAllMock.mockReturnValue([])
|
||||||
contentAllMock.mockImplementation(() => {
|
contentAllMock.mockImplementation(() => {
|
||||||
throw new Error('no such table: messages_fts')
|
throw new Error('no such table: messages_fts')
|
||||||
@@ -734,13 +735,11 @@ describe('session DB summaries', () => {
|
|||||||
|
|
||||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||||
|
|
||||||
await expect(mod.searchSessionSummaries('docker', undefined, 10)).rejects.toThrow(
|
const rows = await mod.searchSessionSummaries('docker', undefined, 10)
|
||||||
'Failed to search sessions: no such table: messages_fts',
|
expect(rows).toEqual([])
|
||||||
)
|
|
||||||
expect(likeAllMock).not.toHaveBeenCalled()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not swallow unrelated database failures for numeric queries', async () => {
|
it('falls back to title results for any query when FTS has unrelated database failure', async () => {
|
||||||
titleAllMock.mockReturnValue([])
|
titleAllMock.mockReturnValue([])
|
||||||
contentAllMock.mockImplementation(() => {
|
contentAllMock.mockImplementation(() => {
|
||||||
throw new Error('database malformed')
|
throw new Error('database malformed')
|
||||||
@@ -748,9 +747,8 @@ describe('session DB summaries', () => {
|
|||||||
|
|
||||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||||
|
|
||||||
await expect(mod.searchSessionSummaries('123', undefined, 10)).rejects.toThrow(
|
const rows = await mod.searchSessionSummaries('123', undefined, 10)
|
||||||
'Failed to search sessions: database malformed',
|
expect(rows).toEqual([])
|
||||||
)
|
|
||||||
expect(likeAllMock).not.toHaveBeenCalled()
|
expect(likeAllMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user