Party Codename and Queue Isolation Design
Table of Contents
Overview
Introduce a human-readable codename field on the party domain entity.
Codenames are globally unique, immutable, and composed of two words
(adjective + noun, e.g. willow_flower) generated by the faker-cxx library
at party provisioning time.
The codename serves two immediate purposes:
- Queue isolation — each party owns a dedicated pgmq queue named
{codename}_report_events. Because pgmq tables have notenant_idcolumn, RLS cannot isolate messages between parties. The per-party queue is the primary mechanism that prevents one party's report events from being visible to another party's queue listener. - Internal identification — the codename is a stable, pronounceable handle that appears in log files, pg_cron job names, and operator tooling without exposing UUIDs. End-user facing UI hides the codename by default.
Background: Why not RLS, hashes, or short codes?
pgmq creates tables of the form pgmq.q_{queue_name} with no tenant_id or
party_id column, so RLS cannot filter by tenant. Sharing a single queue for
all parties would require consumers to filter by tenant_id embedded in the
message body — this breaks isolation guarantees.
Alternative naming schemes were considered and rejected:
- Per-tenant queue (
report_events_{tenant.code}): still collapses all parties of a tenant together. - UUID-based names (tenant UUID + party UUID): 36 + 36 + separator = 73 chars; pgmq queue names are capped at 47 characters.
- Hash names (MD5 of UUIDs): technically feasible but unreadable in logs
and operator tooling. Neither
uuid-osspnorpgcryptoare installed; MD5 is available but produces opaque strings. - Short code composites (
tenant.code_party.short_code): brittle — codes can contain underscores, making the separator ambiguous; also tight on length with codes likebarclays_plc_system(19 chars).
The whimsical-name approach produces names like willow_flower_report_events
(28 chars), which are globally unique, human-readable, within the pgmq limit,
and pleasant to read in logs.
Goals
- Add
codenameto thepartySQL table, domain type, repository, protocol, and Qt UI. - Generate codenames automatically using
faker-cxxat creation time, with collision avoidance. - Enforce global uniqueness and immutability at the database level.
- Create a per-party pgmq queue (
{codename}_report_events) during party provisioning. - Update the report scheduling handler to use the party's codename when
constructing the queue name passed to
pgmq.send()and pg_cron. - Provide a backfill SQL migration for the ~85 existing parties.
Non-Goals
- Consumer-side queue listener implementation — the report event consumer is out of scope for this design.
- Multi-word codenames — two words provide sufficient entropy and readability.
- Exposing codenames to end-users — the column is hidden by default in the Qt party list; only operators and developers see it.
- Automated queue cleanup when a party is deactivated — dropping the queue requires an explicit operator action to avoid discarding unprocessed messages.
Design Decisions
Word-pair format
The codename follows the pattern {adjective}_{noun},
e.g. willow_flower, swift_river, golden_maple. Constraints:
- All lowercase ASCII letters and underscores only.
- Maximum 32 characters (leaves room for the
_report_eventssuffix within pgmq's 47-character queue name limit: 32 + 14 = 46 chars ✓). - Globally unique across all parties and all tenants.
- Regex:
^[a-z][a-z_]+$.
Collision handling: generate a candidate from a random adjective + noun pair.
If a unique-constraint violation occurs, retry with a different pair (up to 10
attempts). If all retries fail, append _2, _3, … until a unique name is
found.
Immutability
The codename is assigned once and never changes. The INSERT trigger on
ores_refdata_parties_tbl enforces this at the database level: when updating
an existing party, the trigger reads the current codename from the active row
and writes it back unchanged, ignoring the caller's supplied value.
Partial unique index
Because the table is bitemporal, a plain UNIQUE (codename) column constraint
would conflict when the same party is versioned (multiple rows with the same
codename for successive versions of the same party). A partial unique index on
active records solves this:
create unique index if not exists ores_refdata_parties_codename_uniq_idx on "ores_refdata_parties_tbl" (codename) where valid_to = ores_utility_infinity_timestamp_fn();
Historical rows for the same party retain the same codename without violating the constraint.
Queue lifecycle
The pgmq queue {codename}_report_events is created via pgmq.create() when
the party is first saved (version = 1). The queue is never dropped
automatically — it must be removed by an operator if needed.
Queue name in the report scheduling handler
The scheduling handler currently passes the hardcoded string 'report_events'
to pgmq.send() and to the pg_cron command. After this change, the handler
will:
- Fetch the report definition from the repository to get its
party_id. - Look up the party from
party_serviceusing thatparty_id. - Construct the queue name as
party.codename + "_report_events". - Use this queue name in both the
pgmq.send()payload embedded in the pg_cron command string, and in any log messages.
Implementation Plan
Phase 1 — SQL Schema
Task 1.1 — Add codename column and constraints
File: projects/ores.sql/create/refdata/refdata_parties_create.sql
Add the column definition after
short_code:"codename" text not null default '',
The empty-string default is required for the initial
CREATE TABLEto succeed for databases being recreated from scratch. The backfill migration (Task 1.3) will assign real values; the trigger will reject empty codenames on any subsequent insert.Add a check constraint within the table definition:
check (length("codename") <= 32), check ("codename" = '' or "codename" ~ '^[a-z][a-z_]+$')
The second check allows the empty-string default only before backfill; the trigger (Task 1.2) prevents empty strings from reaching active rows after provisioning.
Add partial unique index after the existing index block:
create unique index if not exists ores_refdata_parties_codename_uniq_idx on "ores_refdata_parties_tbl" (codename) where valid_to = ores_utility_infinity_timestamp_fn() and codename <> '';
Task 1.2 — Update INSERT trigger for immutability
File: same as above (ores_refdata_parties_insert_fn())
In the if found then branch (updating an existing party), before the
update ores_refdata_parties_tbl set valid_to = ... statement, add:
-- Codename is immutable: restore from the active row, ignore caller's value. select codename into NEW.codename from "ores_refdata_parties_tbl" where tenant_id = NEW.tenant_id and id = NEW.id and valid_to = ores_utility_infinity_timestamp_fn();
In the else branch (new party), validate the supplied codename:
-- New party must supply a valid codename. if NEW.codename = '' or NEW.codename is null then raise exception 'codename is required for new parties' using errcode = '23502'; end if; if NEW.codename !~ '^[a-z][a-z_]+$' then raise exception 'codename must match ^[a-z][a-z_]+$ got: %', NEW.codename using errcode = '23514'; end if;
Task 1.3 — Backfill migration script
File: projects/ores.sql/migrate/backfill_party_codenames.sql (new file)
A PL/pgSQL anonymous block with an embedded vocabulary of ~40 adjectives and
~60 nouns. The block iterates over all active parties with codename = '',
generates a random word-pair, checks uniqueness, retries on collision, and
updates the row directly via SQL (bypassing the immutability trigger by
operating on rows that have codename = '').
Run this script once, immediately after deploying the schema change, before starting the updated application binary.
Task 1.4 — Recreate database script
Verify that projects/ores.sql/recreate_database.sh includes the new backfill
script after the main schema creation, so fresh installations are also
populated correctly.
Phase 2 — C++ Domain and Repository
Task 2.1 — Domain type
File: projects/ores.refdata/include/ores.refdata/domain/party.hpp
Add after the short_code field:
/** * @brief Globally unique human-readable codename for this party. * * Assigned once at creation using an adjective_noun pattern (e.g. * "willow_flower"). Used as the prefix for pgmq queue names and in * operator tooling. Never exposed to end-users in the UI. */ std::string codename;
Task 2.2 — Repository entity
File: projects/ores.refdata/include/ores.refdata/repository/party_entity.hpp
Add after short_code:
std::string codename;
Task 2.3 — Mapper
File: projects/ores.refdata/src/repository/party_mapper.cpp
In both map() overloads, add r.codename = v.codename; after the
short_code mapping line.
Task 2.4 — Domain I/O
projects/ores.refdata/src/domain/party_json_io.cpp— includecodenamein the JSON serialization (if manually implemented) or verify it is picked up automatically ifrfl::jsonis used directly on the domain struct.projects/ores.refdata/src/domain/party_table_io.cpp— addcodenameto theoperator<<display, e.g.<< " codename" << v.codename=.projects/ores.refdata/src/repository/party_entity.cpp— addcodenameto the entityoperator<<output.
Task 2.5 — Generator
File: projects/ores.refdata/src/generators/party_generator.cpp
Add codename generation using the existing faker-cxx dependency:
#include <faker-cxx/Word.h> // In generate_synthetic_party(): r.codename = std::string(faker::word::adjective()) + "_" + std::string(faker::word::noun()) + "_" + std::to_string(idx);
The idx suffix guarantees uniqueness in generated test data sets without
needing collision-check loops.
Phase 3 — Party Service: Generation and Queue Provisioning
Task 3.1 — Codename generation with collision avoidance
File: projects/ores.refdata/src/service/party_service.cpp
In the save() path, when saving a new party (party.codename.empty()):
- Loop up to 10 times:
a. Generate candidate =
faker::word::adjective() + "_" + faker::word::noun(). b. Attempt to save. If a unique-constraint violation (SQLSTATE 23505) is thrown, continue to the next iteration. c. On success, break. - If all 10 plain attempts fail (extremely unlikely), append
_2,_3, … until success.
When saving an existing party (party.codename is non-empty), pass it through
unchanged — the trigger will preserve it anyway.
Task 3.2 — pgmq queue creation
File: same (party_service.cpp)
After a successful first-save (version = 1= after save), create the queue:
#include "ores.mq/pgmq/client.hpp" if (saved_party.version == 1) { mq::pgmq::client mq; const auto queue_name = saved_party.codename + "_report_events"; mq.create(ctx, queue_name); BOOST_LOG_SEV(lg(), info) << "Created queue: " << queue_name; }
pgmq::client::create() is a no-op if the queue already exists, so this is
idempotent.
Task 3.3 — CMakeLists.txt
File: projects/ores.refdata/src/CMakeLists.txt
Add to PRIVATE link libraries:
ores.mq.lib
Phase 4 — Protocol
The protocol structs for party messages (get_parties_response,
save_party_request, etc.) embed the full domain::party struct. Since the
serialization uses rfl::json on the domain type, adding codename to
domain::party is sufficient — no explicit protocol file changes are required.
Verify this assumption by checking party_protocol.cpp serialize/deserialize
implementations. If they call rfl::json::write(party) / rfl::json::read<party>()
directly, no further changes are needed.
Phase 5 — Report Scheduling Handler
Task 5.1 — Resolve queue name from party codename
File: projects/ores.reporting/src/messaging/reporting_message_handler.cpp
In handle_schedule_report_definitions_request, after fetching the report
definition, add:
#include "ores.refdata/service/party_service.hpp" // Resolve the owning party's codename to construct the queue name. refdata::service::party_service party_svc; const auto parties = party_svc.get(ctx_, {report_def.party_id}); if (parties.empty()) { BOOST_LOG_SEV(lg(), error) << "Party not found for report definition"; co_return std::unexpected(error_code::not_found); } const std::string queue_name = parties.front().codename + "_report_events";
Replace the hardcoded 'report_events' string in both the pgmq.send() SQL
command and any log output with queue_name.
Task 5.2 — Add ores.refdata dependency
File: projects/ores.reporting/src/CMakeLists.txt
Add ores.refdata.lib to PRIVATE link libraries if not already present.
Phase 6 — Qt UI
Task 6.1 — ClientPartyModel: add Codename column
File: projects/ores.qt/include/ores.qt/ClientPartyModel.hpp
Add Codename to the Column enum between ShortCode and FullName:
enum Column { BusinessCenterCode, ShortCode, Codename, // new FullName, TransliteratedName, PartyCategory, PartyType, Status, Version, ModifiedBy, RecordedAt, ColumnCount };
File: corresponding .cpp (ClientPartyModel.cpp):
headerData(): return"Codename"for the new column.data(): returnQString::fromStdString(party.codename)forQt::DisplayRole.
Task 6.2 — PartyMdiWindow: hide Codename column by default
File: projects/ores.qt/src/PartyMdiWindow.cpp
After setting the model on the table view, hide the column:
tableView_->horizontalHeader()->hideSection(ClientPartyModel::Codename);
Operators can right-click the header to show it when needed.
Task 6.3 — Party detail dialog: read-only Codename field
Locate the party detail dialog UI file and implementation. Add a read-only
QLineEdit (or QLabel) labelled "Codename" populated from party.codename.
The field must be non-editable (codenames are immutable after creation).
File Summary
| File | Change |
|---|---|
projects/ores.sql/create/refdata/refdata_parties_create.sql |
Add codename column, check constraints, partial unique index, trigger immutability |
projects/ores.sql/migrate/backfill_party_codenames.sql |
NEW — assign codenames to the ~85 existing parties |
projects/ores.refdata/include/ores.refdata/domain/party.hpp |
Add codename field |
projects/ores.refdata/include/ores.refdata/repository/party_entity.hpp |
Add codename field |
projects/ores.refdata/src/repository/party_mapper.cpp |
Map codename in both directions |
projects/ores.refdata/src/domain/party_json_io.cpp |
Include codename in serialization (if manual) |
projects/ores.refdata/src/domain/party_table_io.cpp |
Include codename in display output |
projects/ores.refdata/src/repository/party_entity.cpp |
Include codename in entity display output |
projects/ores.refdata/src/generators/party_generator.cpp |
Generate codename using faker-cxx |
projects/ores.refdata/src/service/party_service.cpp |
Codename generation with collision avoidance + pgmq queue creation |
projects/ores.refdata/src/CMakeLists.txt |
Add ores.mq.lib to PRIVATE deps |
projects/ores.reporting/src/messaging/reporting_message_handler.cpp |
Look up party codename; use {codename}_report_events as queue name |
projects/ores.reporting/src/CMakeLists.txt |
Add ores.refdata.lib if not already present |
projects/ores.qt/include/ores.qt/ClientPartyModel.hpp |
Add Codename column to enum |
projects/ores.qt/src/ClientPartyModel.cpp |
Implement Codename column data and header |
projects/ores.qt/src/PartyMdiWindow.cpp |
Hide Codename column by default |
| Party detail dialog UI + implementation | Add read-only Codename field |
No new CMakeLists.txt GLOB configuration is needed — both ores.refdata and
ores.reporting use file(GLOB_RECURSE).
Verification
- Run
cmake --preset linux-clang-debugandcmake --build --preset linux-clang-debug. - Run
./projects/ores.sql/recreate_database.sh— this must run the backfill script so existing parties receive codenames. - Start the server and client; open the Party list — Codename column must be hidden by default.
- Right-click the Party list header → show Codename column — all parties must
display a codename in
adjective_nounformat. - Open the party detail dialog — Codename field visible but read-only.
- Create a new party via the UI; verify a codename is auto-assigned and the
pgmq queue
ores.q_{codename}_report_eventsexists in the database. - Schedule a report definition; verify the pg_cron job uses the correct per-party queue name in its SQL command.
- Verify
pgmq.q_{codename}_report_eventsreceives a message when the scheduled time arrives.