Task: Creating a Currency Pair with an already-used pair_code silently versions the existing row

Table of Contents

This page documents a task in the Currency pair support in reference data story. It captures the goal, current status, acceptance, and any notes or results.

Goal

Reproduced: creating a new Currency Pair (Add mode) with base/quote matching an already-existing pair silently succeeds and shows no error — the insert trigger (ores_refdata_currency_pairs_insert_fn, projects/ores.sql/create/refdata/refdata_currency_pairs_create.sql) finds an existing row for that pair_code, treats the new record as its next version (retiring the old one), rather than rejecting it as a duplicate create. This is the same create-vs-update-by-natural-key pattern every bitemporal entity in this codebase uses — not unique to currency_pair — but it's more likely to bite here now that pair_code is derived and immutable (see Pair code doesn't recompute when base/quote currency changes): a user has no way to type a different pair_code by mistake, but picking an already-used base/quote combination is easy to do accidentally, and the result is silently overwriting an unrelated existing pair's data with whatever else was in the "new" record's fields.

Needs a design decision before implementing, since a real fix likely means distinguishing create-intent from update-intent somewhere in the stack (client, protocol, or trigger), which could touch the broader natural-key-versioning pattern used by other entities too — scope that out before committing to an approach.

Status

Field Value
State DONE
Parent story Currency pair support in reference data
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-07-09

Acceptance

  • [X] Creating a new Currency Pair whose derived pair_code collides with an existing one shows a clear "Duplicate Pair" error dialog and blocks the save, rather than silently succeeding.
  • [X] Editing an existing pair is unaffected (check is create-mode only).
  • [X] Fix is scoped to the Qt client, not the shared SQL insert trigger — the trigger's upsert-by-natural-key semantics are relied on by the DQ→refdata publish flow and must not change.

Plan

Design decision: reject at the SQL trigger level was ruled out — ores_refdata_currency_pairs_insert_fn is shared with the DQ→refdata publish flow (refdata_publish_from_dq_create.sql), which relies on exactly this "insert with version=0 upserts/re-versions an existing row" behaviour for its own insert_only=/=replace_all modes. Tightening the trigger to reject on any duplicate would break that.

Fix instead lives entirely client-side: a new paste-block seam (4E83FA7A-742A-4102-90AF-D337F6FB1269) inside the shared onSaveClicked() template, right after validateInput() passes and before the change-reason prompt/save dispatch — synchronous (not QtConcurrent-wrapped, matching existing precedent e.g. SystemSettingController::onRevertClicked), fetches the current pair list (explicit high limit — the request's default of 100 would silently miss pairs beyond the first page) and checks the UI-derived pair_code against it.

Found and fixed a real bug while implementing: the first version compared against pair_.pair_code, which isn't synced from the UI until updatePairFromUi() runs later in onSaveClicked() — so the check was always comparing against an empty string and would never have caught an actual duplicate. Fixed by reading ui_->pairCodeEdit->text() directly instead.

Notes

Root cause is architectural, not currency_pair-specific — written up in full in Optimistic Concurrency Versioning: the version=0 Overload and Why It's a Problem. The client-side pre-check implemented here (paste seam 4E83FA7A-742A-4102-90AF-D337F6FB1269) is that document's mitigation (B) — a stopgap for this one entity/client, not a substitute for the proper DB-level fix (A), which is tracked as its own capture.

Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario, run through the QA Validation Runner panel) that verify this task. Link new ones here as they're created; the scenario doc itself links back via its "Verifies task" field.

Scenario State Notes
Currency Pair: duplicate pair_code rejected on create PENDING  

PRs

PR Title
   

Review

Comment summary File Decision Notes
       

Result

Shipped: exact-duplicate and inverted-duplicate (e.g. USD/EUR when EUR/USD exists) pair_code collisions are both rejected on create, client-side, with clear messages explaining why and (for the inverted case) how to proceed if the inverse is really wanted. Verified via Currency Pair: duplicate and inverted pair_code rejected on create — all 6 steps pass.

This is a mitigation, not the full fix — a proper DB-level fix needs a broader architectural change, written up separately in Optimistic Concurrency Versioning: the version=0 Overload and tracked as its own capture, Enforce optimistic concurrency version checks at the DB layer.

Emacs 29.3 (Org mode 9.6.15)