You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
searchsetBundle, 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
Patientborn1950-04-12with oneObservationreferencing them:/Patient?birthdate=le1956-07-14/Observation?patient.birthdate=1950-04-12/Observation?patient.gender=female/Observation?patient.family=Rivera/Observation?patient.birthdate=le1956-07-14/Observation?patient.birthdate=gt1900-01-01gt1900-01-01returning 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, inChainedSearchProvider::resolve_chain:It forces an
Eqprefix 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::parseand 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 mapsLe→<=,Gt→>, etc.resolve_terminal_type(same file, :299) resolves the terminal param's type from the registry, andPatient.birthdateis aDatethere.So the remaining suspect is between those two: either the terminal type is not landing as
Dateat runtime (which would fall through to the_arm,value_string LIKE '%…%'— and that would explain exact-match succeeding while comparators fail), orvalue_dateis not populated insearch_indexfor 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
200with zero entries and noOperationOutcome:FHIR's default (
Prefer: handling=lenient) is to ignore the unknown parameter and return the unfiltered result;handling=strictis to error. Returning zero is neither, and it is the same silent-empty trap as above.Impact
le/ge/gt/lt/sa/ebon a date or number, on SQLite — which is the default backend.