Summary
Conditional criteria that carry a modifier (identifier:exact=…, identifier:not=…) or a chain (organization.name=…) are accepted and silently match nothing. A conditional create or upsert then creates a duplicate, and a conditional delete answers 204 having deleted nothing. Discussion #28 models the sixteen modifiers as first-class search fragments and passes conditional criteria as typed SearchParameters.
Reproduction (batch_conformance harness, in-memory SQLite with spec parameters)
Seed one Patient with identifier http://example.org|12345, then batch PUT with each URL; every call answered 201 Created and the Patient count rose by one:
| criteria |
result |
Patient?identifier:exact=http://example.org|12345 |
201, duplicate |
Patient?identifier:not=http://example.org|99999 |
201, duplicate |
Patient?organization.name=Acme |
201, duplicate |
The resource endpoints take the same backend path (PUT /Patient?identifier:exact=…).
Cause
build_search_parameters (crates/persistence/src/backends/sqlite/storage.rs:2949-2977, backends/postgres/storage.rs:2881-2907) keeps the raw key as name and sets modifier: None, chain: vec![], components: vec![].
lookup_param_type misses for identifier:exact; crate::search::fallback_param_type (search/registry.rs:49-60) matches "identifier" exactly and falls through to SearchParamType::String.
- The query builder interpolates the raw name:
param_name = 'identifier:exact' (sqlite/search/query_builder.rs:459-464; postgres query_builder.rs:1054). No such index row exists, so zero rows and no error.
The ordinary search path parses name:modifier and chains before reaching the builder; the conditional path bypasses that parser.
Proposed fix
Parse conditional criteria with the same parser search uses (modifier, chain, components), or reject criteria the conditional path cannot evaluate with 400 rather than matching nothing. Tests per backend: :exact matching the seeded resource; a chain either resolving or being rejected; never a silent 201.
Related: #535 item 3 fixed the _source typing gap in the same fallbacks. Found while validating #511 (see PR #860).
Summary
Conditional criteria that carry a modifier (
identifier:exact=…,identifier:not=…) or a chain (organization.name=…) are accepted and silently match nothing. A conditional create or upsert then creates a duplicate, and a conditional delete answers 204 having deleted nothing. Discussion #28 models the sixteen modifiers as first-class search fragments and passes conditional criteria as typedSearchParameters.Reproduction (batch_conformance harness, in-memory SQLite with spec parameters)
Seed one Patient with
identifier http://example.org|12345, then batchPUTwith each URL; every call answered201 Createdand the Patient count rose by one:Patient?identifier:exact=http://example.org|12345Patient?identifier:not=http://example.org|99999Patient?organization.name=AcmeThe resource endpoints take the same backend path (
PUT /Patient?identifier:exact=…).Cause
build_search_parameters(crates/persistence/src/backends/sqlite/storage.rs:2949-2977,backends/postgres/storage.rs:2881-2907) keeps the raw key asnameand setsmodifier: None, chain: vec![], components: vec![].lookup_param_typemisses foridentifier:exact;crate::search::fallback_param_type(search/registry.rs:49-60) matches"identifier"exactly and falls through toSearchParamType::String.param_name = 'identifier:exact'(sqlite/search/query_builder.rs:459-464; postgresquery_builder.rs:1054). No such index row exists, so zero rows and no error.The ordinary search path parses
name:modifierand chains before reaching the builder; the conditional path bypasses that parser.Proposed fix
Parse conditional criteria with the same parser search uses (modifier, chain, components), or reject criteria the conditional path cannot evaluate with 400 rather than matching nothing. Tests per backend:
:exactmatching the seeded resource; a chain either resolving or being rejected; never a silent 201.Related: #535 item 3 fixed the
_sourcetyping gap in the same fallbacks. Found while validating #511 (see PR #860).