Task: Compound-key is_uuid gating drops trailing columns in Qt/service templates
Table of Contents
This page documents a task in the Fix codegen template drift story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
cpp_protocol.hpp.mustache and cpp_service.cpp.mustache gate their delete/history/save is_uuid branch on primary_key.is_uuid alone, which reflects only the first key column. A future compound key whose leading column is UUID would silently drop every subsequent key column from these structs/validations. Not exercised today (subject_area's key is two text columns). Raised in PR #1691 review round 1. Fix: make the is_uuid branches loop primary_key.columns like cpp_domain_type_mapper.cpp.mustache already does.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Fix codegen template drift |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-04 |
3. Acceptance
- Both org archetypes (
ores.cpp.protocol.protocol_header.org,ores.cpp.service.service_impl.org) iterateprimary_key.columnsin the{{#domain_entity}}delete/history/ save branches, with per-columnis_uuidselecting the member name (idsfor a uuid column) or the emptiness check (is_nil()vs.empty()), so a compound key whose leading column is uuid no longer drops its trailing key columns from the structs or the validations. - The
{{#entity}}(lookup-entity) delete gate and the{{#junction}}section are unchanged: schema primary keys carry nocolumnslist (single-column shape only), so the loop form cannot apply there, and the junction section has no primary-key gates. - Re-tangling changes only the two mustaches. Regenerating the 8 components is idempotent: a second regeneration is byte-identical to the first.
- The regenerated output is confined to the intended fix: protocol
delete/history and service save-single output is byte-identical for
every entity (uuid-keyed entities keep the
idsmember and the column-named history member), and the only expected churn is the braces the batch-save validation loop gains in uuid-keyed*_service.cppfiles (the loop must contain every key column's check; the old uuid shortcut emitted one unbracedif).
4. Plan
- 2026-09-04: task started; surveyed the blast radius. The two
templates render only for
domain_entitymodels (static mappingget_cpp_domain_entity_template_mappingsand the physical-space graph agree), exceptcpp_protocol.hpp.mustachealso renders for junctions (ores.cpp.protocoladmitsjunction) and lookup entities (schema), firing the mustache's{{#junction}}and{{#entity}}sections — so all three sections of the protocol mustache are live, one per model kind, and the fix must not touch the two non-domain sections. - The is_uuid shortcut lives in exactly four branches across the two
orgs: protocol delete request and history request
(
{{#domain_entity}}), and service save-single and save-batch validations. The{{#entity}}delete branch gates onprimary_key.is_textinstead, with the mirror scalar as fallback; schema (lookup) primary keys carry nocolumnslist — the loader produces a single-column dict (column=/=type=/=is_text), so a loop form would emit nothing for every lookup entity. That branch stays as-is and is out of scope: schema keys are single-column today and cannot express the compound shape the task repairs. - Key census (122 domain-entity orgs across the 8 components): every
primary key is a single uuid column (
id, orinstrument_idfor the trading instrument family), a single text column, or a compound text key (subject_area'sname=+=domain_name). The member-name convention is type-driven for uuid keys — the delete request member is literallyidswhatever the column name (checked in the generated instrument-family protocol headers), and the handler template readsreq->{{delete_request_id_field}}, whichcore.py(lines 2411-2413) derives asidsfor a uuid key — so a column-driven loop would rename the instrument family's delete member toinstrument_idsand break its generated handlers. The per-columnis_uuidform keepsidsfor uuid columns and{{column}}sfor text columns, matching the derivation layer for every viable shape (uuid-leading compound keys are the only mixed shape the service validation can express today; text-leading keys with a uuid trailing column would already fail the.empty()check the old branch emitted). - Fix shape per branch, in the org sources only (no core.py change):
delete
{{#primary_key.columns}}loop with per-column{{#is_uuid}}emittingidsand{{^is_uuid}}emitting{{column}}s; history plain loop over{{column}}; service save-single and save-batch per-column loops with{{#is_uuid}}.is_nil(){{/is_uuid}}{{^is_uuid}}.empty(){{/is_uuid}}and the per-columnis_identity_group_columnprefix. The batch-save loop takes the braced shape unconditionally (multipleifstatements in one loop require braces); text-keyed and compound-keyed entities already emit the braced shape, so only uuid-keyed*_service.cppfiles gain the braces — the intended, visible churn. - Baseline regen before the org edit, edit, re-tangle, per-component regen, idempotency check, component drift checks, then close out.
- 2026-09-04: implementation. The baseline full-ores regeneration
dirtied 48 tracked files outside the CI-checked components (dq 24,
synthetic 10, qt 9, iam 6, sql 11 file list overlaps, marketdata 1,
refdata 1) plus emitted untracked additions (dq eventing integration
tests for 9 entities, a monolithic account_party source tree and
component stubs under
projects/ores.iam/{include,src,tests}):main's checked-in generated tree does not reproduce from its own templates at full-ores regeneration in the non-CI components, while refdata/reporting/marketdata (the drift-checked three) are clean. All of it reverted or excluded from this PR and recorded here as unrecorded pre-existing drift, input for the per-component drift stories. The org edits themselves changed only the two mustaches on re-tangle (4-file diff: 2 orgs + 2 mustaches). - 2026-09-04: regeneration evidence. Regenerating the 8 components
after the edit is confined to the intended output: 36 uuid-keyed
*_service.cppfiles gained the braced batch-save loop, andfolder_service.cpp(synthetic) the same change on top of its recorded include drift, which was reverted out. No*_protocol.hppchurned anywhere (protocol delete/history output byte-identical for every entity: uuid keys keep theidsmember and column-named history member) and no save-single hunk exists in any diff (save-single byte-identical for every entity). Idempotency: three full regenerations; runs 2 and 3 are byte-identical at the content level (full-diff hash 224600e5), run 1 has the identical changed-file set, and the untracked additions are byte-identical across all runs. The verification tree carries exactly the fix diff: 4 template files, 37 service cpps whose every changed line is a batch-save for-loop brace, and the 2 agile docs.
5. Notes
6. 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 |
|---|---|---|
7. PRs
| PR | Title |
|---|---|
| #2005 | [ores.codegen] Protocol/service templates iterate primary_key.columns for key gates |
8. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
9. Result
Delivered 2026-09-04 on branch
feature/compound-key-is-uuid-gating-drops-trailing-columns, commit
28d36860dc.
- Both org sources (
ores.cpp.protocol.protocol_header.org,ores.cpp.service.service_impl.org) iterateprimary_key.columnsin the{{#domain_entity}}delete/history/save branches, with per-columnis_uuidselecting the member name (idsfor a uuid column,{{column}}sfor a text column) or the emptiness check (is_nil()vs.empty()). The{{#entity}}delete gate and the{{#junction}}section are untouched: schema primary keys carry nocolumnslist, and the junction section has no key gates. - Re-tangling changed only the two mustaches (4-file diff: the two orgs + the two mustaches). Regenerating the 8 components is idempotent: three full regenerations, runs 2 and 3 byte-identical at the content level (full-diff hash 224600e5), run 1 with the identical changed-file set, and the untracked additions byte-identical across all runs.
- The regenerated output is confined to the intended churn: 36
uuid-keyed
*_service.cppfiles gained the braced batch-save validation loop, andfolder_service.cpp(synthetic) the same change on top of its recorded include drift, which was reverted out. No*_protocol.hppchurned anywhere and no save-single hunk exists in any diff: protocol delete/history and save-single output is byte-identical for every entity. - The baseline regeneration (before the edit) dirtied 48 tracked
files in the non-CI components plus 42 untracked additions, none
of which reproduce from
main's own templates: unrecorded pre-existing drift (dq/synthetic/qt/iam/sql, plus the recorded marketdata/refdata pair), reverted or excluded from this PR and recorded in the Plan as input for the per-component drift stories. The 42 untracked regen byproducts stay in the working tree (deletion not authorised); they are inert, byte-identical across regenerations, not referenced by any build, and excluded from all commits. - Verification (change class: code + ci + docs): site build clean;
full system build clean (linux-clang-debug-make); ctest 71/71
passed against a recreated database — the CI-equivalent state,
because the repository write tests are not repeatable against a
persistent database (the first run's
ores.iam.core.testsduplicate-key failures were the known stale-DB artifact recorded in task 3F3559A2's Result); component drift checks (refdata, reporting, marketdata) clean except the three recorded pre-existing files, which were reverted after the check; ore domain roundtrip report unchanged from its recorded pre-existing state (fidelity gaps against upstream ORE, untouched by this diff).
Acceptance: all four bullets met.