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:

  1. Queue isolation — each party owns a dedicated pgmq queue named {codename}_report_events. Because pgmq tables have no tenant_id column, 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.
  2. 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-ossp nor pgcrypto are 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 like barclays_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 codename to the party SQL table, domain type, repository, protocol, and Qt UI.
  • Generate codenames automatically using faker-cxx at 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_events suffix 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:

  1. Fetch the report definition from the repository to get its party_id.
  2. Look up the party from party_service using that party_id.
  3. Construct the queue name as party.codename + "_report_events".
  4. 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

  1. Add the column definition after short_code:

    "codename" text not null default '',
    

    The empty-string default is required for the initial CREATE TABLE to 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.

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

  3. 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 — include codename in the JSON serialization (if manually implemented) or verify it is picked up automatically if rfl::json is used directly on the domain struct.
  • projects/ores.refdata/src/domain/party_table_io.cpp — add codename to the operator<< display, e.g. << " codename" << v.codename=.
  • projects/ores.refdata/src/repository/party_entity.cpp — add codename to the entity operator<< 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()):

  1. 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.
  2. 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(): return QString::fromStdString(party.codename) for Qt::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

  1. Run cmake --preset linux-clang-debug and cmake --build --preset linux-clang-debug.
  2. Run ./projects/ores.sql/recreate_database.sh — this must run the backfill script so existing parties receive codenames.
  3. Start the server and client; open the Party list — Codename column must be hidden by default.
  4. Right-click the Party list header → show Codename column — all parties must display a codename in adjective_noun format.
  5. Open the party detail dialog — Codename field visible but read-only.
  6. Create a new party via the UI; verify a codename is auto-assigned and the pgmq queue ores.q_{codename}_report_events exists in the database.
  7. Schedule a report definition; verify the pg_cron job uses the correct per-party queue name in its SQL command.
  8. Verify pgmq.q_{codename}_report_events receives a message when the scheduled time arrives.