Repository navigation
fix(persistence): a transaction Bundle PUT to a deleted id restores it - #2227
Merged
Merged
Conversation
A PUT entry whose id was soft-deleted read no current representation, fell through to a create, and collided with the tombstone's row, so the whole Bundle failed 400 "resource already exists". The same PUT outside a Bundle restores the resource (FHIR http.html#delete). The issue reported this for MongoDB; SQLite and PostgreSQL had the same gap (their transaction `read` hides the tombstone and `create` refuses its row). Each backend's Bundle PUT now restores a tombstone inside the transaction, continuing its version chain and keeping its FHIR version, before it falls back to a create: - MongoDB: `read_deleted_in_bundle_transaction` + `restore_resource_in_bundle_transaction`, sharing one versioned write with the Bundle update. - SQLite: `SqliteTransaction::restore`. - PostgreSQL: `PostgresTransaction::restore`, sharing the update's compare-and-swap statement via `write_next_version`. POST with a reused (live or deleted) id still fails with AlreadyExists. Closes #2145
Contributor
Author
|
PostgreSQL results, run locally (testcontainers, postgres:16-alpine):
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
FHIR/fhir-test-cases merged oldrobot's PRs #316-#320 on 2026-10-23 and the "Test FHIRPath" gate (which runs the upstream corpus) went red on every PR with 10 undeclared failures. Fix the engine rather than declare them: - Delimited identifiers: `none_of("`")` consumed the backslash before the escape parser ran, so Patient.`name` never resolved. - unescape('json'): decode \uXXXX (pairing UTF-16 surrogates) and \/. - Indexer: a negative index is out of range, so the result is empty instead of an error. - skip({}) / take({}): an empty count returns empty. - DateTime + / - calendar duration: accept hour- and minute-precision values with a timezone (@2015-02-04T10Z, @2015-02-04T10:00Z) and keep the input precision in the result, so `= @2015-02-04T11:00Z` compares. Vendor the new cases into the local r4/r5 corpora. testLessThan28 then exposed that extension(url) on a FHIR primitive only worked through a hard-coded fabricated birthTime extension; read the primitive's own extensions instead and drop the fabrication. The parse-all test now requires invalid="syntax" expressions to be rejected. (cherry picked from commit 7e2e6de)
main gained its own transaction-level restore (read_including_deleted + restore(&deleted, body) over a shared write_revision) on SQLite and Postgres, which overlapped this branch's restore(type, id, &body) and the Postgres write_next_version refactor. Take main's transaction.rs on both backends and route the bundle PUT-to-deleted-id path through main's API.
mauripunzueta
marked this pull request as ready for review
October 27, 2026 13:01
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
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.
Closes #2145.
Problem
A transaction Bundle
PUT Patient/<id>where<id>is soft-deleted failed the whole Bundle with400 … resource already exists, while the samePUToutside a Bundle restores the resource as a new version (FHIR http.html#delete).This was not MongoDB-only, as the issue suggested checking: SQLite and PostgreSQL had the same gap. Their transaction
readreturnsNonefor a tombstone, so the entry fell through tocreate, which collides with the tombstone's row (SQLite's existence probe / PG'sON CONFLICT DO NOTHING). Before the fix, the new SQLite test failed with exactly the issue's message:Fix
On every backend, the Bundle PUT's "no current representation" arm now looks for a tombstone first and restores it inside the transaction. The restore continues the version chain (create 1, delete 2, restore 3), keeps the FHIR version the resource was stored under, rebuilds its search rows, and answers
201. Only an id that was never stored creates.read_deleted_in_bundle_transaction+restore_resource_in_bundle_transaction. The Bundle update and the restore sharewrite_next_version_in_bundle_transaction(CAS on the stored document, then the history row and search rows).SqliteTransaction::restore.PostgresTransaction::restore. The update's single compare-and-swap CTE moved intowrite_next_version, whichupdateandrestoreboth call. The large diff intransaction.rsis mostly that move.ifMatchbehavior is unchanged: a suppliedifMatchstill fails against a deleted resource, because there is no current representation.Out of scope, unchanged: POST with a reused id (live or deleted) still fails with
AlreadyExists(a_body_id_of_a_deleted_resource_fails_at_its_entrystill passes). Conditional PUT (PUT Patient?…) that matches nothing still creates.Tests
bundle_identity_suite::put_over_a_deleted_id_restores_it, wired into SQLite, PostgreSQL and MongoDB.a_put_that_creates_over_a_deleted_id_fails_as_before(from perf(mongodb): stage transaction Bundle creates and send them in chunks #2144), which pinned the old answer, is replaced bya_put_over_a_deleted_id_restores_it. It runs in both the inline and deferred search-row modes and checks document and history counts plus searchability of the restored body.Run locally:
transactions_suite: 89 passed. The new scenario fails on the unfixed code with the message above.bundle_create_chunks(live MongoDB via testcontainers): 14 passed.