Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@
Frontmatter is now classified once, by the parser, as present, absent, or
malformed, and only the first two are ever written to.

### Internal

- **#1558**: Search filter compilation now runs over an explicit `ProjectScope` instead of
a repository-bound `project_id`. FTS term preparation and filter compilation moved out
of the SQLite and Postgres repositories into `sqlite_search_query` and
`postgres_search_query` as pure functions returning a `CompiledFilter`, and the filters
both backends share (scope, permalink, directory, item type, category, note type,
`after_date`, valid time, candidate keys) are compiled once in `search_filters`. The
note-type and valid-time predicates match search rows on their full
`(project_id, ...)` identity. Project repositories call the compilers with a scope of
one; no query behavior changes. First step of the shared single/multi-project reader.


## v0.23.2 (2026-08-25)

Expand Down
13 changes: 9 additions & 4 deletions src/basic_memory/repository/note_type_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

from typing import Any, Sequence

from basic_memory.repository.search_scope import ProjectScope
from basic_memory.schemas.search import SearchItemType

SEARCH_TABLE = "search_index"
Expand All @@ -49,6 +50,7 @@ def build_note_type_predicate(
note_types: Sequence[str],
params: dict[str, Any],
*,
scope: ProjectScope,
note_type_value: str,
) -> str:
"""Build the WHERE-clause fragment restricting rows to notes of the given types.
Expand All @@ -57,19 +59,22 @@ def build_note_type_predicate(
documented case-insensitive, so both sides are folded to lowercase.

Binds are added to `params` in place, following the convention the surrounding FTS
query builders already use. `project_id` is bound by the caller for the whole query.
query builders already use. The owning note is matched on `(project_id, id)`: the
search row's identity is composite, and the subquery is restricted to `scope` so an
owner outside the caller's projects can never admit a row.
"""
placeholders = []
for index, note_type in enumerate(note_types):
name = f"note_type_{index}"
params[name] = note_type.lower()
placeholders.append(f":{name}")
owner_scope = scope.predicate(f"{_OWNER}.project_id", params)

return (
f"{SEARCH_TABLE}.entity_id IN (\n"
f" SELECT {_OWNER}.id\n"
f"({SEARCH_TABLE}.project_id, {SEARCH_TABLE}.entity_id) IN (\n"
f" SELECT {_OWNER}.project_id, {_OWNER}.id\n"
f" FROM {SEARCH_TABLE} AS {_OWNER}\n"
f" WHERE {_OWNER}.type = '{SearchItemType.ENTITY.value}'\n"
f" AND {_OWNER}.project_id = :project_id\n"
f" AND {owner_scope}\n"
f" AND LOWER({note_type_value}) IN ({', '.join(placeholders)}))"
)
Loading
Loading