fix(search): contain the abstract-prefix strip to expressions that mean it - #639
Merged
Conversation
…an it #534 stripped the `Resource.`/`DomainResource.` prefix from every union member that carried one, so `_source` would finally index. But the strip activates on the prefix alone, not on the audited meta set, and the R5/R6 spec bundle ships two more `Resource.`-prefixed parameters. `_in` was the damaging one. Its expression is `Resource.id`, but the parameter means "this resource is a member of the referenced List or Group" — the id is a placeholder, not a filter target. Stripped and evaluated, it wrote one self-referential reference row per resource, so `GET /Patient?_in=42` matched `Patient/42` through the ordinary bare-id reference branch: a membership question answered with an identity test, plus a junk row per resource per reindex. It is now skipped at extraction (`NON_INDEXABLE_PARAM_CODES`) and rejected at the REST layer alongside `_query`, because it cannot safely fall through — on R5/R6 it *is* a registered `reference` parameter, so lenient handling would not drop it. Implementing it properly is #638. `_language` was newly indexed and correct on PostgreSQL, but SQLite routed it to `build_special_parameter_condition`'s `_ => None` arm, which drops the filter rather than narrowing it, and returned every resource of the type — the #474 failure mode, on a parameter #534 had just switched on. It joins the exemption list with the meta set. Conditional writes typed `_source` as String while the extractor writes Uri rows, so `If-None-Exist: Patient?_source=…` queried `value_string`, never matched, and created a duplicate on every request. The three hand-copied fallback tables (composite, postgres, sqlite) collapse into one `fallback_param_type`. Fixing `_source` surfaced the same latent mismatch in `_profile`: the tables said Token, but the embedded definition that wins registration — and so the extractor — says Uri on every version, including R5/R6 where the spec's own copy is `reference`. Also fixed, from the same review: - `split_union_members` splits `|` only at paren depth 0 and outside string/backtick literals. The old `split('|')` cut inside literals, and the unbalanced fragment used to be harmless (it matched no prefix and was dropped) but the abstract strip accepts a fragment on its prefix alone, so one member's parse error aborted extraction for every member of that parameter, concrete ones included. - Leading parens carry across the strip, so `(Resource.meta.source)` yields `(meta.source)` instead of silently extracting nothing. - `extract()` iterates `ABSTRACT_BASE_TYPES` instead of looking up "Resource" alone, which left the `DomainResource` registry bucket unreachable and half the constant dead. - `ABSTRACT_BASE_TYPES` is hoisted to `helios_fhir::search` and consumed by `applies_to`, the extractor, and `ui/editor.rs` — it was a third hand-copy. - `strip_abstract_base_prefix` returns `Option<Cow<'_, str>>`, borrowing on the common path. - The loader and seeding bounds derive from `load_embedded().len()` and assert exactly one definition per code. `<= 10` passed if a fallback was *lost*, which is the direction that breaks indexing, and `.find()` accepted a feature-gated duplicate. - `$reindex` docs note that pre-upgrade resources have no `_source` or `_language` rows and under-match until a reindex runs. Both blocking fixes are pinned by tests confirmed to fail without them: reverting the `_language` exemption fails with "produced no condition — the filter would be dropped", and reverting the `_in` skip fails showing the junk row (`param_name: "_in", value: Reference { reference: "p1" }`). Fixes #535 Claude-Session: https://claude.ai/code/session_018FhD4aQRNHkPJ5HXvDSJps
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #535 — all ten findings from the #534 review.
Root cause
#534's prefix strip activates on the prefix alone, not on the audited meta set. That set turned out to be the smaller half of what carries a
Resource.prefix: R5/R6 ship two more such parameters, and both broke.It is also worth recording what actually fixed #523, because it was not the strip.
SearchParameterRegistry::registerrejects a duplicate canonical URL outright, and the embedded fallbacks load before the spec bundle — so the spec'sResource.-prefixed_id/_lastUpdated/_tag/_profile/_security/_sourcenever register at all once a fallback exists. #534's other half, adding the_sourcefallback, is what made_sourceindex. In a server with a spec bundle the strip's entire net effect was to switch on_inand_language.Blocking-grade
1.
_inindexed the resource's own id. Its expression isResource.id, but the parameter means "this resource is a member of the referenced List or Group" — the id is a placeholder, not a filter target. Stripped and evaluated it wrote one self-referential reference row per resource, soGET /Patient?_in=42matchedPatient/42through the ordinary bare-id reference branch, and every$reindexadded a junk row per resource.It is now skipped at extraction (
NON_INDEXABLE_PARAM_CODES) and rejected at the REST layer alongside_query. Both halves are needed: un-indexing alone is not safe, because on R5/R6_inis a registeredreferenceparameter, soPrefer: handling=lenientwould not drop it as unknown and the self link would claim it had been applied — while SQLite would answer it with the whole resource type. Implementing it properly is #638.2.
_languagediverged by backend. Newly indexed, correct on PostgreSQL, but SQLite routed it tobuild_special_parameter_condition's_ => Nonearm, which drops the filter rather than narrowing it, and returned every resource of the type. That is the #474 failure mode, reintroduced on a parameter #534 had just switched on. It joins the exemption list with the meta set.3. Conditional writes duplicated on
_source. Typed as String while the extractor writes Uri rows, soIf-None-Exist: Patient?_source=…queriedvalue_string, never matched, and created a duplicate on every request — the exact opposite of what the idempotency guard exists for. The three hand-copied fallback tables collapse into onefallback_param_type.Fixing
_sourcesurfaced the same latent mismatch in_profile: the tables said Token, but the embedded definition that wins registration — and so the extractor — says Uri on every version, including R5/R6 where the spec's own copy isreference. Corrected in the same helper.Correctness, lower severity
4.
split_union_memberssplits|only at paren depth 0 and outside'…'/ backtick literals. The oldsplit('|')cut inside literals too; the unbalanced fragment used to be harmless (it matched no resource-type prefix and was dropped), but the abstract strip accepts a fragment on its prefix alone, so one member's parse error aborted extraction for every member of that parameter, concrete ones included.5. Leading parens carry across the strip, so
(Resource.meta.source)yields(meta.source)rather than falling through and silently extracting nothing.6.
extract()iteratesABSTRACT_BASE_TYPESinstead of looking up"Resource"alone. The registry buckets a definition under each declaredbase, sobase: ["DomainResource"]landed in a bucket nothing consulted.7.
$reindexdocs note that pre-upgrade resources have no_sourceor_languagerows and under-match until a reindex runs — a failure mode that returns no error, only fewer results.Robustness
8. Loader and seeding bounds derive from
load_embedded().len()and assert exactly one definition per code.<= 10passed if a fallback was lost — the direction that breaks indexing — and.find()accepted a feature-gated duplicate.9.
ABSTRACT_BASE_TYPEShoisted tohelios_fhir::search, consumed byapplies_to, the extractor, andui/editor.rs.10.
strip_abstract_base_prefixreturnsOption<Cow<'_, str>>, borrowing on the common path. The deeper root cause is untouched: the FHIRPath evaluator still resolves a leading type identifier by exact match with no subsumption, and this remains a patch in one consumer.Coverage
Both blocking fixes are pinned by tests confirmed to fail without them. Reverting the
_languageexemption:Reverting the
_inskip, showing the junk row itself:New tests:
r5_membership_parameter_is_not_indexed_but_language_is(R5-gated, so it runs under CI's--all-features),indexed_meta_parameters_are_not_dropped,membership_parameter_has_no_backend_condition,union_split_respects_literals_and_parens,literal_pipe_in_abstract_member_does_not_break_extraction,domain_resource_based_parameters_are_extracted,test_membership_parameter_in_is_rejected, plus parenthesized cases added to the existing strip test.Verification
sqlite,postgres,R4,R4B,R5,R6)sqlite_tests85 /search_suite103 /crud_suite82 /search_param_seeding6search_integration111; full rest/fhir/ui suites greencargo build --workspace --all-features,cargo fmt --check, and clippy under the CI flag set with-D warnings: cleanNot run locally: the PostgreSQL, Elasticsearch and MongoDB container-backed integration tests need Docker; CI covers them.
https://claude.ai/code/session_018FhD4aQRNHkPJ5HXvDSJps