diff --git a/src/core/postgres-engine.ts b/src/core/postgres-engine.ts index 1ed51ce9a..4ad16bab4 100644 --- a/src/core/postgres-engine.ts +++ b/src/core/postgres-engine.ts @@ -1614,16 +1614,16 @@ export class PostgresEngine implements BrainEngine { params.push(symbolKind); symbolKindClause = `AND cc.symbol_type = $${params.length}`; } - // v0.27.0: date filtering support + // v0.29.1: since/until filter by effective date, with import-time fallback. let afterDateClause = ''; if (opts?.afterDate) { params.push(opts.afterDate); - afterDateClause = `AND COALESCE(p.updated_at, p.created_at) > $${params.length}::timestamptz`; + afterDateClause = `AND COALESCE(p.effective_date, p.updated_at, p.created_at) > $${params.length}::timestamptz`; } let beforeDateClause = ''; if (opts?.beforeDate) { params.push(opts.beforeDate); - beforeDateClause = `AND COALESCE(p.updated_at, p.created_at) < $${params.length}::timestamptz`; + beforeDateClause = `AND COALESCE(p.effective_date, p.updated_at, p.created_at) < $${params.length}::timestamptz`; } // v0.34.1 (#861 — P0 leak seal): source-isolation filter. When the // caller's auth scope is set, narrow the inner CTE candidate set so @@ -1763,16 +1763,16 @@ export class PostgresEngine implements BrainEngine { params.push(symbolKind); symbolKindClause = `AND cc.symbol_type = $${params.length}`; } - // v0.27.0: date filtering support + // v0.29.1: since/until filter by effective date, with import-time fallback. let afterDateClause = ''; if (opts?.afterDate) { params.push(opts.afterDate); - afterDateClause = `AND COALESCE(p.updated_at, p.created_at) > $${params.length}::timestamptz`; + afterDateClause = `AND COALESCE(p.effective_date, p.updated_at, p.created_at) > $${params.length}::timestamptz`; } let beforeDateClause = ''; if (opts?.beforeDate) { params.push(opts.beforeDate); - beforeDateClause = `AND COALESCE(p.updated_at, p.created_at) < $${params.length}::timestamptz`; + beforeDateClause = `AND COALESCE(p.effective_date, p.updated_at, p.created_at) < $${params.length}::timestamptz`; } // v0.34.1 (#861 — P0 leak seal): source-isolation. Anchor primitive // for two-pass retrieval, so cross-source anchors would let the walk @@ -1885,16 +1885,16 @@ export class PostgresEngine implements BrainEngine { params.push(symbolKind); symbolKindClause = `AND cc.symbol_type = $${params.length}`; } - // v0.27.0: date filtering support + // v0.29.1: since/until filter by effective date, with import-time fallback. let afterDateClause = ''; if (opts?.afterDate) { params.push(opts.afterDate); - afterDateClause = `AND COALESCE(p.updated_at, p.created_at) > $${params.length}::timestamptz`; + afterDateClause = `AND COALESCE(p.effective_date, p.updated_at, p.created_at) > $${params.length}::timestamptz`; } let beforeDateClause = ''; if (opts?.beforeDate) { params.push(opts.beforeDate); - beforeDateClause = `AND COALESCE(p.updated_at, p.created_at) < $${params.length}::timestamptz`; + beforeDateClause = `AND COALESCE(p.effective_date, p.updated_at, p.created_at) < $${params.length}::timestamptz`; } // v0.34.1 (#861, F2 — P0 leak seal): source-isolation in the INNER CTE // specifically. Pushing the filter inside narrows the HNSW candidate set diff --git a/test/postgres-engine.test.ts b/test/postgres-engine.test.ts index 0cf2afdfb..e750f9d5f 100644 --- a/test/postgres-engine.test.ts +++ b/test/postgres-engine.test.ts @@ -94,12 +94,30 @@ describe('postgres-engine / search path timeout isolation', () => { }); }); +describe('postgres-engine / search date filtering', () => { + test('search paths filter since/until on effective_date before import-time fallback', () => { + const expectedDateExpr = 'COALESCE(p.effective_date, p.updated_at, p.created_at)'; + const staleDatePredicate = /COALESCE\(p\.updated_at,\s*p\.created_at\)\s*[<>]\s*\$/; + + for (const methodName of ['searchKeyword', 'searchKeywordChunks', 'searchVector']) { + const fn = stripComments(extractMethod(SRC, methodName)); + + expect(countOccurrences(fn, expectedDateExpr)).toBe(2); + expect(fn).not.toMatch(staleDatePredicate); + } + }); +}); + function stripComments(s: string): string { return s .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/(^|\s)\/\/[^\n]*/g, '$1'); } +function countOccurrences(s: string, needle: string): number { + return s.split(needle).length - 1; +} + // extractMethod grabs the body of a class method by brace-matching from // its opening line. Returns the method body up to the matching closing // brace. Good enough for the small number of methods in this file.