app.storage.postgres.search¶
Full-text search and the SQL approval-state expression.
approval_join() mirrors app.domain.approval.evaluate() rule
by rule (see that module’s docstring) as a set-based SQL expression
over decision_event, so the “needs approval” filter is applied
before pagination:
no valid policy ->
policy_missing;policy not required ->
not_required;the event for (identity, current digest) is accepted ->
accepted;that event is rejected ->
rejected;an accepted event for the identity exists (necessarily for another digest, because rules 3/4 did not apply) ->
changed_since_acceptance;otherwise
pending.
Rules 1 and 2 depend only on the policy and are resolved in Python to a constant. A parity test compares this expression with the domain function on a fixture set.
Rules 3/4 join decision_event on identity and digest directly: the
unique constraint uq_decision_event_identity_digest (one formal
decision per content hash, see
docs/developer/decisions/0006-content-digest-approval.rst) guarantees at
most one event, so that event is the domain’s “latest event for the
current digest” and the join never duplicates a record row. Rule 5
is an EXISTS over the identity’s accepted events, which
PostgreSQL plans as a hashed subplan (one scan of the accepted events)
when many rows are evaluated and as per-row probes of
uq_decision_event_identity_digest (identity prefix) for a single
page. This replaced two correlated ORDER BY ... LIMIT 1 subqueries
that ran ~20,000 index probes per statement at 20,000 records and
whose cost estimate pushed PostgreSQL into ~0.4-0.8 s of JIT
compilation per statement (see docs/developer/performance.rst); the new
statements cost far below jit_above_cost, so JIT stays off without
any setting.
Without the “needs approval” filter the state does not restrict the
rows, so the page is selected first and only its rows are joined to
the decision events; with the filter the join precedes LIMIT.
The record_kind filter (decisions / errata) is a plain WHERE
condition shared by both paths, so it combines with every other filter
including “needs approval”.
Order. SearchSort picks a descending
leading key (text rank for relevance with a query, updated_at
for recent, none for title) followed by the lower-cased title
and the id, so equal leading keys still give stable pages. Both page
paths (with and without “needs approval”) use the same key list.
Text queries use websearch_to_tsquery with the simple
configuration (exact tokens: identifiers, UUIDs, paths) OR’ed with the
english configuration (stemmed body words), matching how the
record.search vector is built by the migration trigger.
Public API: NEEDS_APPROVAL_STATES, approval_join(),
search().
- app.storage.postgres.search.NEEDS_APPROVAL_STATES: frozenset[ApprovalStateCode] = frozenset({ApprovalStateCode.CHANGED_SINCE_ACCEPTANCE, ApprovalStateCode.PENDING, ApprovalStateCode.POLICY_MISSING, ApprovalStateCode.REJECTED})¶
States for which
app.domain.approval.evaluate()reportsneeds_approval.
- app.storage.postgres.search.approval_join(base, policy)¶
Join record rows to the evidence their approval state depends on.
- Parameters:
- Return type:
- Returns:
The relation to select from (base itself when the policy alone decides the state) and the state expression yielding an
ApprovalStateCodevalue per row.
- app.storage.postgres.search.search(session, query, filters, page, page_size, policy, sort=SearchSort.RELEVANCE)¶
Run a filtered, paginated full-text search.
- Parameters:
session (
Session) – Unit-of-work session.query (
str|None) – Free text, orNone/blank for all records.filters (
SearchFilters) – Filters.page (
int) – 1-based page number.page_size (
int) – Page size, 1 toMAX_PAGE_SIZE.sort (
SearchSort) – Result order (seeSearchSort).
- Return type:
- Returns:
The page.
- Raises:
ValueError – If page or page_size is out of bounds.