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 derivedpair_codecollides 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.