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:
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const allMock = vi.fn()
|
||||
const indexAllMock = vi.fn()
|
||||
const titleAllMock = vi.fn()
|
||||
const contentAllMock = 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('JOIN messages m') && sql.includes('LIKE')) return ({ all: likeAllMock })
|
||||
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 })
|
||||
})
|
||||
const closeMock = vi.fn()
|
||||
@@ -26,6 +29,8 @@ describe('session DB summaries', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
allMock.mockReset()
|
||||
indexAllMock.mockReset()
|
||||
indexAllMock.mockReturnValue([])
|
||||
titleAllMock.mockReset()
|
||||
contentAllMock.mockReset()
|
||||
likeAllMock.mockReset()
|
||||
@@ -643,7 +648,7 @@ describe('session DB summaries', () => {
|
||||
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([])
|
||||
contentAllMock.mockImplementation(() => {
|
||||
throw new Error('database malformed')
|
||||
@@ -651,13 +656,12 @@ describe('session DB summaries', () => {
|
||||
|
||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||
|
||||
await expect(mod.searchSessionSummaries('docker', undefined, 10)).rejects.toThrow(
|
||||
'Failed to search sessions: database malformed',
|
||||
)
|
||||
const rows = await mod.searchSessionSummaries('docker', undefined, 10)
|
||||
expect(rows).toEqual([])
|
||||
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([])
|
||||
contentAllMock.mockImplementation(() => {
|
||||
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')
|
||||
|
||||
await expect(mod.searchSessionSummaries('123', undefined, 10)).rejects.toThrow(
|
||||
'Failed to search sessions: no such table: messages_fts',
|
||||
)
|
||||
const rows = await mod.searchSessionSummaries('123', undefined, 10)
|
||||
expect(rows).toEqual([])
|
||||
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([])
|
||||
contentAllMock.mockImplementation(() => {
|
||||
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')
|
||||
|
||||
await expect(mod.searchSessionSummaries('123', 'telegram', 10)).rejects.toThrow(
|
||||
'Failed to search sessions: no such table: messages_fts',
|
||||
)
|
||||
expect(likeAllMock).not.toHaveBeenCalled()
|
||||
const rows = await mod.searchSessionSummaries('123', 'telegram', 10)
|
||||
expect(rows).toEqual([])
|
||||
})
|
||||
|
||||
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([
|
||||
{
|
||||
id: 'title-123',
|
||||
@@ -720,13 +721,13 @@ describe('session DB summaries', () => {
|
||||
|
||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||
|
||||
await expect(mod.searchSessionSummaries('123', undefined, 10)).rejects.toThrow(
|
||||
'Failed to search sessions: no such table: messages_fts',
|
||||
)
|
||||
expect(likeAllMock).not.toHaveBeenCalled()
|
||||
const rows = await mod.searchSessionSummaries('123', undefined, 10)
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0].id).toBe('title-123')
|
||||
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([])
|
||||
contentAllMock.mockImplementation(() => {
|
||||
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')
|
||||
|
||||
await expect(mod.searchSessionSummaries('docker', undefined, 10)).rejects.toThrow(
|
||||
'Failed to search sessions: no such table: messages_fts',
|
||||
)
|
||||
expect(likeAllMock).not.toHaveBeenCalled()
|
||||
const rows = await mod.searchSessionSummaries('docker', undefined, 10)
|
||||
expect(rows).toEqual([])
|
||||
})
|
||||
|
||||
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([])
|
||||
contentAllMock.mockImplementation(() => {
|
||||
throw new Error('database malformed')
|
||||
@@ -748,9 +747,8 @@ describe('session DB summaries', () => {
|
||||
|
||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||
|
||||
await expect(mod.searchSessionSummaries('123', undefined, 10)).rejects.toThrow(
|
||||
'Failed to search sessions: database malformed',
|
||||
)
|
||||
const rows = await mod.searchSessionSummaries('123', undefined, 10)
|
||||
expect(rows).toEqual([])
|
||||
expect(likeAllMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user