Task: has_pagination knob is a silent-failure footgun in the Qt client model template
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_qt_client_model.cpp.mustache generates a fetch method whose
behaviour is a binary fork on the :has_pagination: Qt-facet flag:
set true, it sends real offset=/=limit to the server and reports
the server's true total_available_count; unset (the silent
default), it ignores offset/limit entirely, always fetches the
server's default page, and reports the fetched page's own size as if
it were the total — masking the truncation instead of surfacing it.
Every entity's MdiWindow unconditionally shows a full
PaginationWidget (Next/Previous/Load All/page size) regardless of
this flag, so an entity with the flag off looks identical to one
with it on right up until its row count exceeds 100, at which point
rows silently become unreachable with no error, warning, or visual
cue. 61 of the app's ~89 Qt list entities currently have the flag
unset — see the tracking
task for the full list and the fix (set the flag, regenerate).
The narrower fix (setting the flag everywhere) doesn't address the shape of the footgun: a knob that silently defaults to the broken behaviour, with no build-time or run-time signal distinguishing "deliberately unpaginated, all rows always fit" from "someone forgot to set the flag." Worth reconsidering the underlying design once the narrow fix lands:
- Generate the paginated fetch path unconditionally (drop the
has_paginationfork) and let every entity always send real offset/limit and report the real total — the branch exists presumably as an optimisation/simplification for small, truly bounded lookup tables, but the two code paths are barely different and the unconditional one is strictly more correct. - Or, if a genuinely unpaginated mode is still wanted for small fixed
lookup tables, gate the widget on the same flag too, so an
unpaginated entity's
MdiWindowdoesn't render Next/Previous/Load All controls that don't do anything — the mismatch between "looks paginated" and "is paginated" is what let this go unnoticed across 61 entities. - Either way, a codegen lint/validation pass that flags entities whose generated model and generated window disagree on pagination support would catch the next occurrence of this drift at generation time rather than in production data.
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
ores.cpp.qt.client_model_impl.org(and the tangedcpp_qt_client_model.cpp.mustache) generates the fetch method with nohas_paginationfork: the request always carriesoffset/limitand the model always takestotal_available_countfrom the response. The{{^}}truncating branch and both{{#}}wrappers are gone, and the narrative no longer documents a two-branch design.- Re-tangling changes only the two files (org archetype + mustache).
- The change is output-neutral for every entity that can regenerate today: regenerating the Qt client models (all flag-on) is byte-identical to the pre-edit output, so no model churn is attributable to the change beyond the recorded pre-existing drift baseline, and a second regeneration is byte-identical.
- No entity can emit the truncating shape from this template any more:
the 16 legacy-`* Primary key` entities cannot regenerate until their
unblocking task lands (836BB4CE), and when they do, and for every
new entity from now on, the generated fetch pages correctly with no
per-entity knob to set. The unconditional
PaginationWidgetis therefore accurate for every generated model — the model/window disagreement class is gone.
4. Plan
- 2026-09-04: survey. The flag's behavioural read surface is exactly
one template: grep of the templates directory shows
has_paginationconsumed only by the client model fetch function (two{{#}}sites and one{{^}}branch, lines 311-364 of the org). Everywhere else it is inert documentation: the doc entity org templates echo:has_pagination:back into regenerated entity doc pages when set, andcore.py(lines 3224-3225) loads the Qt drawer property with a silentFalsedefault to feed that same echo.doc_feature.orgmentions the property only as an example literal in a generic feature template. - The protocol side has no gate:
offset(default 0) /limit(default 100) andtotal_available_countare unconditional members of every generated list request and response class (ores.cpp.protocol.protocol_header.org, five request shapes, all with the same unconditional trio), so the paginated branch compiles for every entity when it can regenerate. The country-fix commit that created the fork (3f51aef9f4) changed only the model template, the country org, and the narrative — the flag never reached the protocol, the service, or the server. - Committed-model census: 30 entities carry the flag (the tracking
task 914B206F's 29 fixes in PR #1760 plus
calendar_type), all regenerable and byte-stable across the fixes' regeneration rounds. The only committed flag-off models belong to the 16 legacy-`* Primary key` heading entities (follow-up 836BB4CE) that cannot regenerate at all today —resolve_targetsfails on any address for them, so their truncating output stays in the tree until that unblocking task lands, whatever this task decides. - Design decision: capture option 1 — generate the paginated fetch
path unconditionally and delete the fork. The fork never was a
small-table optimisation worth preserving: the
{{^}}branch does not fetch "the whole set", it sends the request with its defaultoffset=0/limit=100and reports the returned page's size as the total, so both branches fetch at most the server's default page and differ only in whether the model can page onward and report a truthful total. The unconditional path is the one proven in production by the ~30 flag-on entities; for a table over 100 rows the flag-off path is strictly lossy. Deleting the fork makes the unconditionalPaginationWidgetcorrect for every generated model, which dissolves the model/window disagreement that let the bug go unnoticed across 61 entities — capture option 3 (a lint pass) is then unnecessary, and the flag itself survives only as the entity- org doc property it already is (loader and doc echo untouched; nothing behavioural reads it any more). - Implementation order: baseline full regeneration (drift record,
pre-edit), org edit (unbrace the two
{{#}}sites so the offset/limit pair and the response-total return are unconditional; delete the{{^}}branch; rewrite the narrative), re-tangle (only the one mustache should change), per-component regeneration (byte-identical to the baseline for all regenerable models), idempotency check (second regen byte-identical), then close out with the code+ci+docs verification union (site build, full build + ctest on a recreated database, drift checks, roundtrip). - 2026-09-04: regeneration evidence. The baseline full-ores
regeneration (run 1, pre-edit) churned 202 tracked generated files
— pre-existing drift whose composition (trading 95, sql 31, dq 15,
scheduler 24 incl. Qt, synthetic 17 incl. Qt, database 7, analytics
4, compute 4, qt/iam 2, refdata/marketdata/iam 1 each) differs from
task 453FA225's recorded baseline as main evolved; all of it
reverted or excluded from this PR. The attributable delta between
run 1 and run 2 (post-edit) is exactly 7 files: the two templates,
two
ores.databasecomponent_files.cmake(stub-byproduct churn, reverted), and three Qt client model flips (ClientBadgeSeverityModel,ClientReportInstanceModel— carried;ClientJobDefinitionModel— reverted, see Result). Runs 2 and 3 are byte-identical (identical md5 sets) — idempotent. The 166 untracked byproducts (42 inherited from task 453FA225's regens) are byte-identical across the runs.
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 |
|---|---|
| #2006 | [ores.codegen] Qt client model fetches paginated unconditionally |
8. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Four reporting client models and ClientDatasetBundleModel still compiled on the truncating shape under ores.qt/compute/src and ores.qt/api/src |
ores.qt/compute/src/Client{ReportInstance,ReportType,ReportDefinition,ConcurrencyPolicy}Model.cpp, ores.qt/api/src/ClientDatasetBundleModel.cpp |
Follow-up | Migration orphans: no generator writes to ores.qt/compute or ores.qt/api for these entities (canonical output goes to ores.qt/reporting and ores.qt/dq), so no regen can flip them; compiled via component_files.cmake and a duplicate-class (ODR) hazard while both libraries load — recorded as follow-up input for the migration/drift story, not foldable into this PR |
| 2 | ClientWorkspaceModel still on the truncating shape despite its protocol carrying offset/limit |
ores.qt/workspace/src/ClientWorkspaceModel.cpp |
Not changed | Its org (ores.workspace.workspace.org) is legacy-`* Primary key`: codegen validation fails for it in both regen runs, so it cannot regenerate until the unblocking task 836BB4CE lands — the acceptance-recorded class, not a "trivial flip" |
| 3 | ClientJobDefinitionModel (scheduler) not flipped |
ores.qt/scheduler/src/ClientJobDefinitionModel.cpp |
Not changed | Deferral recorded in Result and story Decisions: its regeneration bundles unrelated pre-existing drift (org evolved after its last regen), which never rides a foundation PR |
| 4 | variability_feature_has_pagination.org still documents the flag as a live behavioural toggle |
projects/modeling/variability_feature_has_pagination.org |
Fixed | Feature retired; page now records the retirement (task 9FF6FAD3) and the flag's documentation-only survival |
9. Result
Delivered 2026-09-04 on branch
feature/pagination-knob-fragility, commit 7287a46527.
- The fetch is unconditional in both the org source
(
ores.cpp.qt.client_model_impl.org) and the tangled mustache: the{{#domain_entity.qt.has_pagination}}wrappers around theoffset=/=limitpair and around the log + response-total return are gone, and the{{^}}truncating branch (defaults, page-size reported as total) is deleted. The request always carriesoffset=/=limitand the model always takestotal_available_countfrom the response, so no entity can emit the truncating shape from this template. The section (now "** Fetching a page") records the fork's history; the flag survives only as entity-org documentation (loader and doc echo untouched). - Re-tangling changed only the two template files (the org and its tangled mustache).
- Regeneration evidence (three full-ores regens, md5 per changed
file, in the Plan): run 1 (pre-edit) churned 202 tracked generated
files of pre-existing drift — all reverted or excluded from this PR
and input for the per-component drift stories. The attributable
delta between run 1 and run 2 is exactly 7 files: the two
templates, two
component_files.cmakefiles (stub-byproduct churn, reverted), and three Qt client model flips. Runs 2 and 3 are byte-identical (identical md5 sets) — idempotent. - Census correction: the survey census (30 flag-on entities, 16
legacy-`* Primary key` unregenerable) missed three regenerable
modern-key entities that carry the flag off: the regen delta flipped
ClientBadgeSeverityModel(dq) andClientReportInstanceModel(reporting), both carried in this PR as the intended repair — the tracking task 914B206F's census never listed them. The narrow fix (set the flag everywhere) was therefore incomplete exactly where this fix cannot be: there is no flag left to forget. The third flip,ClientJobDefinitionModel(scheduler), sits on top of stale committed drift (its org evolved after its last regeneration), so the file was reverted to its committed state and its regeneration is left to the scheduler per-component drift story; until then its committed model stays the old truncating shape, a historical artifact this template can no longer produce. - Output neutrality held for every flag-on entity: no Qt client model churn exists in any flag-on component across the runs beyond the three flips above, and the post-fix component drift check (refdata) wrote every flag-on Qt model byte-identical to the committed tree.
- 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; component drift checks (refdata, reporting,
marketdata) reproduce the committed tree except the three recorded
pre-existing files (
party_repository.cpp,feed_binding.hppand its create sql), reverted after the check; ore domain roundtrip report generated clean (rc 0), structurally unchanged — this diff touches no roundtrip input or parser. - The 166 untracked regen byproducts (42 inherited from task 453FA225's regen plus this unit's additions) stay in the working tree, byte-identical across runs 2 and 3, excluded from all commits (deletion not authorised).
Acceptance: bullets 1, 2, and 4 met as written. Bullet 3 holds for its flag-on scope with the correction recorded above: the two census-missed flag-off entities flip as the intended repair (which is the story's purpose — an entity with the flag off must not truncate), and no other model churn is attributable to the change.
10. Promoted from capture
Captured 2026-07-19 in the product backlog; promoted preserving the UUID.
10.1. What
cpp_qt_client_model.cpp.mustache generates a fetch method whose
behaviour is a binary fork on the :has_pagination: Qt-facet flag:
set true, it sends real offset=/=limit to the server and reports
the server's true total_available_count; unset (the silent
default), it ignores offset/limit entirely, always fetches the
server's default page, and reports the fetched page's own size as if
it were the total — masking the truncation instead of surfacing it.
Every entity's MdiWindow unconditionally shows a full
PaginationWidget (Next/Previous/Load All/page size) regardless of
this flag, so an entity with the flag off looks identical to one
with it on right up until its row count exceeds 100, at which point
rows silently become unreachable with no error, warning, or visual
cue. 61 of the app's ~89 Qt list entities currently have the flag
unset — see the tracking
task for the full list and the fix (set the flag, regenerate).
The narrower fix (setting the flag everywhere) doesn't address the shape of the footgun: a knob that silently defaults to the broken behaviour, with no build-time or run-time signal distinguishing "deliberately unpaginated, all rows always fit" from "someone forgot to set the flag." Worth reconsidering the underlying design once the narrow fix lands:
- Generate the paginated fetch path unconditionally (drop the
has_paginationfork) and let every entity always send real offset/limit and report the real total — the branch exists presumably as an optimisation/simplification for small, truly bounded lookup tables, but the two code paths are barely different and the unconditional one is strictly more correct. - Or, if a genuinely unpaginated mode is still wanted for small fixed
lookup tables, gate the widget on the same flag too, so an
unpaginated entity's
MdiWindowdoesn't render Next/Previous/Load All controls that don't do anything — the mismatch between "looks paginated" and "is paginated" is what let this go unnoticed across 61 entities. - Either way, a codegen lint/validation pass that flags entities whose generated model and generated window disagree on pagination support would catch the next occurrence of this drift at generation time rather than in production data.
10.2. Why
Discovered while investigating a real bug during manual QA of the calendar Qt screens task: the Currencies list showed "Showing all records (100)" with 68 currencies (including USD) silently unreachable. The immediate fix is per-entity (tracked here); this capture is about the generator-level anti-pattern that made the bug possible and easy to reintroduce on the next new entity.
10.3. References
- Task: Fix silent 100-row list truncation on entities missing has_pagination — the narrow fix.
projects/ores.codegen/library/templates/cpp_qt_client_model.cpp.mustache— the template with thehas_paginationfork.
10.4. See also
- Qt screens for calendars: list/add/edit, combo picker, flag display — where this was found.