fix(rest): execute search-style GET bundle entries as searches - #481
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Flagging a data-corruption interaction found while implementing #503 (PR #512) — it lands squarely What this PR does// A query string is not part of the type/id path (`Patient?name=x` is a
// search on Patient, not a read of a resource named `Patient?name=x`).
let path = url.split('?').next().unwrap_or(url);That is correct, and it is exactly what #503 needs. The problem is what happens to the write The mechanismWith the query stripped,
So it writes a row whose id is the empty string, into the real Why it matters that it is this PRBefore the strip, the corruption is quarantined: the criteria ride along in the resource type, so the Verified empirically: with the empty-id guard disabled, a batch Options
Either is fine; option 2 needs no change here. Happy to do whichever you prefer — just flagging so Context
|
The comment justified exempting GET entries from the query guard by saying those URLs are "resolved by the REST layer rather than by a backend `parse_url`". That describes a state this branch does not have: nothing in `process_transaction` resolves a GET entry, and `parse_search_entry_url` does not exist here — it arrives with #478/PR #481, which is still open. A query-bearing GET entry still reaches the backend's `parse_url` and still fails there. The exemption itself is correct, for a different reason: it keeps this guard off the dispatch arm #478 is rewriting, so that work lands on an untouched path rather than merging against a refusal it is about to replace. Say that instead. Comment only; no behaviour change. `a_transaction_get_with_a_query_is_not_declined_by_the_query_guard` already pins the exemption, and it asserts only that the guard does not catch the entry — not that the entry succeeds. Tests: unchanged and re-run — 20 unit tests in handlers::batch pass. Refs #503
65f127a to
b48e39c
Compare
A GET bundle entry may carry a read URL (`Patient/123`) or a search URL (`Patient?name=x`, or bare `Patient` for an unfiltered type search) — the spec's "read or search" wording for bundle GETs. Only the read form was implemented; a search-style entry was dispatched as an instance read against an empty id. Adds `parse_search_entry_url` and `searchset_result`, dispatches type-level GET entries through `execute_search_bundle` in both arms, and lets a transaction's GET searches see the bundle's own committed writes. Rebased from `main` onto the #501/#489/#503/#502/#504 stack (originally opened against `main` as #481; base is now `fix/504-batch-entry-issue-codes`). Five conflict hunks in `batch.rs` and one keep-both append in `batch_conformance.rs`. The resolution, recorded because it is more than textual: - **The two new failure sites now render through `entry_failure`.** Both were written as `let (status, _, details) = e.client_response(); create_error_result( status.as_u16(), &details)` — the same code-discard #504 deleted from every other call site, which would have left search entries as the one path still answering `processing`. The transaction-arm site is the more consequential of the two: its loop bypasses the backend executor, making it the first *reachable* per-entry outcome on that arm, where #504 could accurately say none existed. - **`parse_request_url`'s query strip is dropped as redundant.** This commit added `url.split('?').next()`; #503 had already landed the same fix with the empty-id write guards that make it safe. #512 predicted this exact outcome: "if this lands first, #481's strip becomes a no-op on rebase." - **The GET arm keys off `BundleMethod::Get`**, not the raw `"GET"` string, since #502 replaced the string matcher with an exhaustive enum match. - **The rollback fan-out** keeps #504's status/code threading and gains this commit's `.chain(&search_entries)`. - **The batch unit tests' `DelayStorage` gains `SearchProvider`, `IncludeProvider` and `RevincludeProvider`**, and `run_batch`'s bound widens to match `process_batch`'s. Every method is `unimplemented!()`, the same lever the mock's write methods already use: no unit test drives a search entry, and one that started to would panic rather than silently exercise a stub. That pulls `parking_lot` in as a dev-dependency, because `search_param_registry` returns a `parking_lot::RwLock` and the crate's own code never names the lock type. Tests: 1114 pass (1109 on the base + this commit's 5). Its own five tests are happy-path only; the failure paths on both new call sites are covered by the follow-up commit. Refs #478
Rebased onto the batch stackHeads-up @angela-helios — I force-pushed this branch and retargeted its base from Why: this PR and the #501→#489→#503→#502→#504 stack both touch Resolution applied (5 hunks in
1114 tests pass (1109 on the base + your 5). One gap worth flagging: your five tests are all happy-path Nothing here changes what this PR does; shout if you'd rather own the resolution yourself and I'll hand it back. |
A GET bundle entry may carry a read URL (`Patient/123`) or a search URL (`Patient?name=x`, or bare `Patient` for an unfiltered type search) — the spec's "read or search" wording for bundle GETs. Only the read form was implemented; a search-style entry was dispatched as an instance read against an empty id. Adds `parse_search_entry_url` and `searchset_result`, dispatches type-level GET entries through `execute_search_bundle` in both arms, and lets a transaction's GET searches see the bundle's own committed writes. Rebased from `main` onto the #501/#489/#503/#502/#504 stack (originally opened against `main` as #481; base is now `fix/504-batch-entry-issue-codes`). Five conflict hunks in `batch.rs` and one keep-both append in `batch_conformance.rs`. The resolution, recorded because it is more than textual: - **The two new failure sites now render through `entry_failure`.** Both were written as `let (status, _, details) = e.client_response(); create_error_result( status.as_u16(), &details)` — the same code-discard #504 deleted from every other call site, which would have left search entries as the one path still answering `processing`. The transaction-arm site is the more consequential of the two: its loop bypasses the backend executor, making it the first *reachable* per-entry outcome on that arm, where #504 could accurately say none existed. - **`parse_request_url`'s query strip is dropped as redundant.** This commit added `url.split('?').next()`; #503 had already landed the same fix with the empty-id write guards that make it safe. #512 predicted this exact outcome: "if this lands first, #481's strip becomes a no-op on rebase." - **The GET arm keys off `BundleMethod::Get`**, not the raw `"GET"` string, since #502 replaced the string matcher with an exhaustive enum match. - **The rollback fan-out** keeps #504's status/code threading and gains this commit's `.chain(&search_entries)`. - **The batch unit tests' `DelayStorage` gains `SearchProvider`, `IncludeProvider` and `RevincludeProvider`**, and `run_batch`'s bound widens to match `process_batch`'s. Every method is `unimplemented!()`, the same lever the mock's write methods already use: no unit test drives a search entry, and one that started to would panic rather than silently exercise a stub. That pulls `parking_lot` in as a dev-dependency, because `search_param_registry` returns a `parking_lot::RwLock` and the crate's own code never names the lock type. Tests: 1114 pass (1109 on the base + this commit's 5). Its own five tests are happy-path only; the failure paths on both new call sites are covered by the follow-up commit. Refs #478
The comment justified exempting GET entries from the query guard by saying those URLs are "resolved by the REST layer rather than by a backend `parse_url`". That describes a state this branch does not have: nothing in `process_transaction` resolves a GET entry, and `parse_search_entry_url` does not exist here — it arrives with #478/PR #481, which is still open. A query-bearing GET entry still reaches the backend's `parse_url` and still fails there. The exemption itself is correct, for a different reason: it keeps this guard off the dispatch arm #478 is rewriting, so that work lands on an untouched path rather than merging against a refusal it is about to replace. Say that instead. Comment only; no behaviour change. `a_transaction_get_with_a_query_is_not_declined_by_the_query_guard` already pins the exemption, and it asserts only that the guard does not catch the entry — not that the entry succeeds. Tests: unchanged and re-run — 20 unit tests in handlers::batch pass. Refs #503
cc5962c to
076dbc4
Compare
b48e39c to
32b3766
Compare
The comment justified exempting GET entries from the query guard by saying those URLs are "resolved by the REST layer rather than by a backend `parse_url`". That describes a state this branch does not have: nothing in `process_transaction` resolves a GET entry, and `parse_search_entry_url` does not exist here — it arrives with #478/PR #481, which is still open. A query-bearing GET entry still reaches the backend's `parse_url` and still fails there. The exemption itself is correct, for a different reason: it keeps this guard off the dispatch arm #478 is rewriting, so that work lands on an untouched path rather than merging against a refusal it is about to replace. Say that instead. Comment only; no behaviour change. `a_transaction_get_with_a_query_is_not_declined_by_the_query_guard` already pins the exemption, and it asserts only that the guard does not catch the entry — not that the entry succeeds. Tests: unchanged and re-run — 20 unit tests in handlers::batch pass. Refs #503
A GET bundle entry may carry a read URL (`Patient/123`) or a search URL (`Patient?name=x`, or bare `Patient` for an unfiltered type search) — the spec's "read or search" wording for bundle GETs. Only the read form was implemented; a search-style entry was dispatched as an instance read against an empty id. Adds `parse_search_entry_url` and `searchset_result`, dispatches type-level GET entries through `execute_search_bundle` in both arms, and lets a transaction's GET searches see the bundle's own committed writes. Rebased from `main` onto the #501/#489/#503/#502/#504 stack (originally opened against `main` as #481; base is now `fix/504-batch-entry-issue-codes`). Five conflict hunks in `batch.rs` and one keep-both append in `batch_conformance.rs`. The resolution, recorded because it is more than textual: - **The two new failure sites now render through `entry_failure`.** Both were written as `let (status, _, details) = e.client_response(); create_error_result( status.as_u16(), &details)` — the same code-discard #504 deleted from every other call site, which would have left search entries as the one path still answering `processing`. The transaction-arm site is the more consequential of the two: its loop bypasses the backend executor, making it the first *reachable* per-entry outcome on that arm, where #504 could accurately say none existed. - **`parse_request_url`'s query strip is dropped as redundant.** This commit added `url.split('?').next()`; #503 had already landed the same fix with the empty-id write guards that make it safe. #512 predicted this exact outcome: "if this lands first, #481's strip becomes a no-op on rebase." - **The GET arm keys off `BundleMethod::Get`**, not the raw `"GET"` string, since #502 replaced the string matcher with an exhaustive enum match. - **The rollback fan-out** keeps #504's status/code threading and gains this commit's `.chain(&search_entries)`. - **The batch unit tests' `DelayStorage` gains `SearchProvider`, `IncludeProvider` and `RevincludeProvider`**, and `run_batch`'s bound widens to match `process_batch`'s. Every method is `unimplemented!()`, the same lever the mock's write methods already use: no unit test drives a search entry, and one that started to would panic rather than silently exercise a stub. That pulls `parking_lot` in as a dev-dependency, because `search_param_registry` returns a `parking_lot::RwLock` and the crate's own code never names the lock type. Tests: 1114 pass (1109 on the base + this commit's 5). Its own five tests are happy-path only; the failure paths on both new call sites are covered by the follow-up commit. Refs #478
Closes #478
The bug
Bundle GET entries were unconditionally parsed as
Type/idreads.GET Patient?name=Nguyenbecame a read of resource typePatient?name=Nguyen— a 404 in batch bundles, and a 400 rejecting the whole bundle in transactions — even though the spec's processing rules allow any read or search URL in a GET entry (this is what blocked the #476 screenshot bundle and got the bug filed).The fix
parse_request_urlstrips the query string before extracting type/id, so scope checks and reads see the real resource type.Patient?name=x, or barePatient) now runs through the same search pipeline as the HTTP endpoint (execute_search_bundle, factored out of the search handler): terminology expansion,_list/chain resolution,_include, paging clamps. The resulting searchset Bundle is embedded as the entry's resource with200 OK. Instance reads (Patient/123) are untouched.GET Patient?family=Tranin one transaction finds it). Their queries are validated up front, so a malformed search still rejects the whole bundle before anything executes; a post-commit execution failure surfaces as that entry's own error outcome rather than a misleading whole-bundle failure for writes that did commit. GET-by-id keeps running inside the storage transaction as before.Tests
New
search_entriesconformance tests: batch search entry returns a searchset, bare-type GET is a search, batch mixing read + search entries, transaction search seeing the bundle's own writes, transaction GET-by-id unchanged. Full helios-rest suite green (33 test binaries, incl. batch_conformance 44, search_integration 101, rest_conformance 84); clippy clean.