fix(session-query): guard FTS predicate planning (round 7)

This commit is contained in:
Hypatia May
2026-07-17 11:13:03 +08:00
parent f401528941
commit e8abfd6482
6 changed files with 119 additions and 9 deletions

View File

@@ -11,6 +11,7 @@ import {
normalizeSessionRequest,
quoteFtsData,
requestFingerprint,
SQLITE_FTS5_OUTER_PREDICATE_LIMIT,
SQLITE_MAX_PAGE_LIMIT,
type NormalizedEventRequest,
type NormalizedSessionRequest,
@@ -111,24 +112,36 @@ describe('SQLite search request normalization', () => {
describe('SQLite search predicate compilation', () => {
it('compiles all logical-session clauses including empty and nullable values', () => {
expect(buildSessionWhere([])).toEqual({ sql: '', params: [] })
expect(buildSessionWhere([{ kind: 'id', values: [] }])).toEqual({ sql: '0', params: [] })
expect(buildSessionWhere([])).toEqual({ sql: '', params: [], predicateCount: 0 })
expect(buildSessionWhere([{ kind: 'id', values: [] }])).toEqual({
sql: '0',
params: [],
predicateCount: 1,
})
expect(buildSessionWhere([{ kind: 'id', values: [SessionId('a'), SessionId('b')] }])).toEqual({
sql: 'session_id IN (?, ?)',
params: [SessionId('a'), SessionId('b')],
predicateCount: 1,
})
expect(buildSessionWhere([{ kind: 'cwd', values: [] }])).toEqual({
sql: '0',
params: [],
predicateCount: 1,
})
expect(buildSessionWhere([{ kind: 'cwd', values: [] }])).toEqual({ sql: '0', params: [] })
expect(buildSessionWhere([{ kind: 'cwd', values: [null] }])).toEqual({
sql: '(cwd IS NULL)',
params: [],
predicateCount: 1,
})
expect(buildSessionWhere([{ kind: 'cwd', values: ['/a'] }])).toEqual({
sql: '(cwd IN (?))',
params: ['/a'],
predicateCount: 1,
})
expect(buildSessionWhere([{ kind: 'parent', values: [SessionId('p'), null] }])).toEqual({
sql: '(parent_session IN (?) OR parent_session IS NULL)',
params: [SessionId('p')],
predicateCount: 1,
})
expect(buildSessionWhere([
{ kind: 'created-at', from: 1, to: 2 },
@@ -138,8 +151,13 @@ describe('SQLite search predicate compilation', () => {
])).toEqual({
sql: 'CAST(created_at AS INTEGER) >= ? AND CAST(created_at AS INTEGER) <= ? AND 0 AND live = 1',
params: [1, 2],
predicateCount: 4,
})
expect(buildSessionWhere([{ kind: 'created-at' }])).toEqual({
sql: '',
params: [],
predicateCount: 0,
})
expect(buildSessionWhere([{ kind: 'created-at' }])).toEqual({ sql: '', params: [] })
})
it('compiles every event clause and empty lists', () => {
@@ -151,11 +169,25 @@ describe('SQLite search predicate compilation', () => {
])).toEqual({
sql: 'CAST(seq AS INTEGER) >= ? AND CAST(time AS INTEGER) <= ? AND type IN (?) AND surface IN (?, ?)',
params: [1, 9, 'user/message', 'current', 'log-only'],
predicateCount: 4,
})
expect(buildEventWhere([
{ kind: 'type', values: [] },
{ kind: 'surface', values: [] },
])).toEqual({ sql: '0 AND 0', params: [] })
])).toEqual({ sql: '0 AND 0', params: [], predicateCount: 2 })
})
it('rejects predicate builders above the supported FTS5 outer budget', () => {
const filters = Array.from(
{ length: SQLITE_FTS5_OUTER_PREDICATE_LIMIT },
() => ({ kind: 'id' as const, values: [SessionId('safe')] }),
)
expect(buildSessionWhere(filters).predicateCount).toBe(SQLITE_FTS5_OUTER_PREDICATE_LIMIT)
expect(() => buildSessionWhere([
...filters,
{ kind: 'id', values: [SessionId('over')] },
])).toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
})
it('rejects runtime-unknown filter discriminants in both SQL builders', () => {