Skip to content

Chained search with a comparator prefix silently returns zero results (SQLite) #258

Description

@angela-helios

Summary

On the SQLite backend, a chained search with a comparator prefix silently returns zero results instead of the matching resources. No error, no warning — an empty searchset Bundle, which reads as "there is no such data" when the truth is "this server did not run the query you asked for".

Surfaced while testing natural-language search (#255): an LLM asked for "blood pressure observations for patients over 70" naturally writes patient.birthdate=le1956-07-14, which is correct FHIR and returns nothing.

Silent-empty is the worst failure mode we can have on a clinical search surface. A user cannot tell it apart from a real negative.

Reproduce

One Patient born 1950-04-12 with one Observation referencing them:

Query Result Expected
/Patient?birthdate=le1956-07-14 4 ✅ comparator works on a direct search
/Observation?patient.birthdate=1950-04-12 1 ✅ chained search works, exact value
/Observation?patient.gender=female 1 ✅ chained token
/Observation?patient.family=Rivera 1 ✅ chained string
/Observation?patient.birthdate=le1956-07-14 0 ❌ should be 1
/Observation?patient.birthdate=gt1900-01-01 0 ❌ should be 1 — every patient is born after 1900

gt1900-01-01 returning nothing is the tell: this is not an off-by-one, the comparison is not happening at all.

Root cause (partly traced)

crates/persistence/src/backends/sqlite/search_impl.rs, in ChainedSearchProvider::resolve_chain:

let search_value = SearchValue::eq(value);   // value is still "le1956-07-14"

It forces an Eq prefix onto a value that still carries its comparator, so the comparator becomes part of the string being compared. Exact-value chains work precisely because they are equality.

The Postgres backend does not have this line — it calls SearchValue::parse(value) (backends/postgres/search_impl.rs:634), which extracts the prefix. Worth confirming whether Postgres is actually correct here or merely differently wrong; I only tested SQLite.

Necessary but not sufficient. I tried the one-line change to SearchValue::parse and the queries still return 0, so something downstream also drops the prefix or the type. The pieces I ruled out:

  • build_date_condition (sqlite/search/chain_builder.rs:742) is correct — it maps Le<=, Gt>, etc.
  • resolve_terminal_type (same file, :299) resolves the terminal param's type from the registry, and Patient.birthdate is a Date there.

So the remaining suspect is between those two: either the terminal type is not landing as Date at runtime (which would fall through to the _ arm, value_string LIKE '%…%' — and that would explain exact-match succeeding while comparators fail), or value_date is not populated in search_index for this parameter. Someone with the search-index internals in their head will see it in five minutes.

Also worth a look (separate, possibly its own issue)

Unknown search parameters return 200 with zero entries and no OperationOutcome:

/Patient?parametro-inventado=x   →  200, 0 entries   (there are 7 patients)

FHIR's default (Prefer: handling=lenient) is to ignore the unknown parameter and return the unfiltered result; handling=strict is to error. Returning zero is neither, and it is the same silent-empty trap as above.

Impact

  • Any chained search with le/ge/gt/lt/sa/eb on a date or number, on SQLite — which is the default backend.
  • Natural-language search (Natural Language / AI-assisted search: enable on back end and front end #255) makes this much easier to hit, because relative-age and time-window questions are exactly what people ask, and the model translates them into chained comparators correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions