Repository navigation
OpenConceptLab/ocl_issues#3023 | Bulk import with update_if_exists versions a mapping by its ID when its target changes - #989
Conversation
…entifies a mapping by its ID, so changing the target versions it instead of failing as a duplicate MappingImporter.get_queryset also filtered on map_type, from/to concept and to_source_url. A changed target hid the existing mapping, the import took the create path and failed with "Mapping ID must be unique within a source.". An explicit ID now matches the mapping inside its source only. Mapping.populate_fields_from_relations kept the previously linked concept when only to_source_url/from_source_url changed, leaving a mapping whose concept belonged to the old repo. It now relinks through the new source URL and code.
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f47a662f5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…he target on a code change, drops the previous repo's source version and refreshes the old from concept Review findings on the first commit: - changing only to_concept_code (same to_source_url) kept the previously linked concept; the link is now re-resolved when the requested code differs - to_source_version/from_source_version recorded for the previous repo were carried over to the new one; they are now reset when the source changes - a mapping moved to another from concept left the old one's mapped codes stale in the index; the importer now refreshes it too
…actory sequence The test sorted references as strings, so it failed whenever the concept mnemonic counter crossed from 999 to 1000 between its two concepts. Look the expected references up by URI instead.
|
Follow-up on the Codex review (head
The CI failure was unrelated to the importer: the test sorted references as strings, so it broke when the factory's concept counter crossed 999 -> 1000. The new tests in this PR shifted that counter into the test. It now looks references up by URI. Each finding got a regression test that failed before the fix. Local run: 711 tests across Not covered: HTTP bulk-import route end to end, and real Elasticsearch indexing (mocked in the tests). |
snyaggarwal
left a comment
There was a problem hiding this comment.
Review: correctness and happy-path impact
Verdict: Request changes. The importer identity fix is right and works on real data (one P2 blocks). Changing a target by ID now versions the mapping. CIEL and LOINC replays behave exactly like master. But keep_linked_concept also changes populate_fields_from_relations for everything else that calls it, and it breaks cloned mappings on a normal edit (P2 below).
How I checked it
Real local data (CIEL with 324k mappings, two LOINC repos, consumer sources cloned from CIEL). I replayed the same lines through BulkImportInline(update_if_exists=True) and through PUT on master and on this branch, each inside a rolled-back transaction, and diffed the HEAD mapping fields after every line.
| Scenario | Lines | master | this PR |
|---|---|---|---|
CIEL mappings replayed as-is, to_source_url + to_concept_code |
511 | 511 unchanged | 511 unchanged, same fields |
Same, to_concept_url form |
511 | 510 unchanged, 1 failed (dup ID) | 511 unchanged |
CIEL lines with an edited extras |
511 | 484 updated / 27 unchanged | identical, no link changes |
LOINC 2.82 mappings.json sample (no ID, relationship path) |
400 | 400 unchanged | 400 unchanged |
| CIEL→LOINC mapping repointed to the other LOINC repo, same code | 20 | 20 failed (dup ID) | 20 updated, relinked to the new repo's concept, to_source_id updated ✅ |
DELETE line with a non-existent ID matching another mapping's target |
5 | not_found | not_found (sibling not retired) ✅ |
Cloned mappings, line with to_source_url + to_concept_code and an edited field |
75 | 75 updated, local link kept | 75 updated, to_concept_id dropped ❌ |
Same cloned mappings via PUT (to_source_url + to_concept_code) |
10 | 200, local link kept | 200, to_concept_id dropped ❌ |
P2: keep_linked_concept drops deliberate local links on cloned mappings
core/mappings/models.py (keep_linked_concept, used for both ends)
Source.clone_mappings (core/sources/models.py, used by clone/cascade-add) links a cloned mapping's to_concept (and from_concept) to the local copy in the consumer source. It keeps to_source_url pointing at the origin, e.g. /orgs/CIEL/sources/CIEL/. So "linked concept's parent ≠ to_source_url" is a normal, intended state. Locally, 3,849 HEAD mappings are in this state on the to side and 3,277 on the from side, mostly MSF, PIH and Kenya_SMART_Guidelines/CIEL.
The new check compares the linked concept's parent with the URL in the data. Any update that sends the mapping's own to_source_url + to_concept_code (no to_concept_url) therefore throws away the local link and re-resolves against the origin:
- where the origin has the code, the mapping is silently repointed from the local copy to CIEL's concept;
- where it doesn't (as with my local CIEL subset),
to_concept_idbecomes NULL.
On master both keep the local link. A line with nothing new is safe: the standard checksum ignores to_concept_id, so it comes back unchanged and nothing is saved. Any real edit in the same line (extras, map type, retired, comment) saves the new link. The oclweb3 mapping form isn't affected because it sends to_concept_url. Bulk/CSV-style lines and API clients that send source + code are affected.
Suggested fix: decide "the target moved" by comparing what the request asks for with what the mapping had (previous_to_source_url / previous to_concept_code), not with the linked concept's parent. That keeps clone links when the target is unchanged and still covers the repoint cases in this PR. Rough shape:
def keep_linked_concept(concept, requested_source_url, resolved_source_url, previous_source_url,
requested_code, previous_code):
if not concept:
return None
if requested_source_url and separate_version(resolved_source_url)[1] != previous_source_url:
return None
if requested_code and requested_code != previous_code:
return None
return conceptPlease add a regression test that clones a concept + mapping with Source.clone_mappings, then updates the clone (import line with to_source_url + to_concept_code + an extras change, and a PUT) and asserts to_concept_id still points at the local copy.
P3: to_source_version / from_source_version reset compares URL strings, not sources
core/mappings/models.py (if previous_to_source_url and to_source_url != previous_to_source_url)
The stored to_source_url can be a canonical or unresolved spelling (e.g. https://cielterminology.org/ with a pinned version) from when the target couldn't be resolved. Once that same source resolves, get_source rewrites it to the relative URI. The strings then differ and the pinned version is cleared, although the target repo hasn't changed. Locally, a few HEAD mappings store such a canonical URL with a version. I found this by reading the code; it isn't one of the replay scenarios. Suggest resetting only when the resolved source actually changes, e.g. compare to_source_id before and after when both are known, and fall back to the URL only when neither resolves.
P4: a canonical to_source_url now downgrades a resolved, ID'd mapping to unresolved
With the line's to_source_url set to http://loinc.org for 10 resolved CIEL→LOINC mappings: master fails them (dup ID). The PR updates them, and to_concept_id/to_source_id become NULL with to_source_url = http://loinc.org. That's because 10 local repos share that canonical, so it doesn't resolve. This matches what a new mapping would get, so not blocking. But a line that used to fail loudly now quietly unlinks a resolved mapping. Worth a line in the PR notes, or a test that pins the intended behaviour.
P5 / nits
core/importers/models.pyget_queryset: the ID early return sits after thefrom_concept_url/to_concept_url/to_source_urllocals, which it doesn't use. Move it to the top of the method.core/importers/models.pyprocess:replaced_from_concept_idis set on every same-source update, even when the from concept doesn't change. It's harmless because the set dedupes. Setting it only whenfrom_concept_idactually changed aftercreate_new_version_forwould make the intent clearer.- The
PUT/Conceptmappings-payload path (create_new_version_foroutside the importer) still doesn't refresh the old from concept's mapped codes when a mapping moves. It wasn't touched here and isn't required, but it's the same class of staleness the importer now handles.
Checks
pylint -j0 core/(as in CI): exit 0, 10.00/10.core.importers+core.mappings: 320 tests, 1 failure in an unrelated, pre-existing importer limits test (fails locally for environment reasons).- Integration suites:
tests_mappings/tests_concepts/tests_collections/tests_sourcesran, but the local Elasticsearch container went down mid-run. The ES-dependentsetUpClasserrors happen identically on master, so they're inconclusive locally rather than a regression; CI is the reference there.
…gs when an update repeats their target; compare repos by resolved URL Review findings (snyaggarwal): - P2: the linked concept was dropped whenever its parent differed from the requested to_source_url, which is the normal state of a cloned mapping (the clone links the local concept but keeps the origin URL). A target now counts as moved only when the URL or code differs from what the mapping already had. - P3: the pinned source version was reset by comparing URL spellings. Repos are now compared by their resolved versionless URL, so a canonical URL that starts to resolve to the same repo keeps its version. - P5: the explicit-ID lookup returns before the unused URL locals, and the previous from concept is only recorded when it actually changed.
|
Thanks @snyaggarwal, the clone case was a real regression in my
Checks run locally in a throwaway container with the API image (not the long-running API container, which was stopped): 1124 tests across |
PR Notes —
fix/bulk-import-mappings-url-update-if-existsTL;DR: Bulk import with
update_if_exists=truenow updates a mapping that has an explicit ID when its target changes, instead of failing withMapping ID must be unique within a source.Closes OpenConceptLab/ocl_issues#3023
Root cause
MappingImporter.get_queryset(core/importers/models.py) matched an existing mapping on the ID andmap_type, from/to concept andto_source_url. When the import changed the target, the existing mapping was not found, the importer took the create path, andMapping.persist_newrejected the duplicate ID.Mapping.populate_fields_from_relationskept the previously linked concept when onlyto_source_url(orfrom_source_url) changed, so the new version pointed at a concept of the old repo. The issue lists "resolved targets" as untested.Main Changes
core/importers/models.pyid,get_querysetmatches by owning source + ID only. Without anid, the existing relationship-based match is unchanged.core/mappings/models.pytoandfrom.core/importers/tests.pyBulkImportInlineTest.Key Decisions
ConceptImporteridentifies concepts. Changing target, map type or from-concept of an ID'd mapping now versions it.toandfrom)populate_fields_from_relations.get_querysetDELETElinesupdate_if_existspromises.Verification
failed=1(Mapping ID must be unique within a source.); the 2 control tests passedto_source_urlOld -> New): 1 updated, 0 failed, one HEAD mapping, 2 versions, previous version keeps the old URL,extras/external_id/to_concept_codepreservedto_concept_urlto another repo: relinked,to_source_idupdatedto_source_url+to_concept_code: relinked (failed before thepopulate_fields_from_relationschange)update_if_existsthe duplicate error is unchangedcore.importers+core.mappings: 317 tests, 1 failure (below)core.integration_tests.tests_mappings+tests_concepts: 226 tests OKgit diff --checkcleanBaseline failure (not caused by this PR):
BulkImportLimitsTest.test_grandfathered_npm_url_is_downloaded_and_handed_over_as_uploadfails identically onmasterin the local container (storage backend env), verified by stashing the change and re-running it.