Task: Find why so much SQL is hand-written and generate what codegen can
Table of Contents
This page documents a task in the Model the ORE report configuration as entities story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
A new entity reaches the database without a hand-edited file. Every hand-written SQL file has a recorded reason to be hand-written. What codegen can own, it generates. What stays hand-written is checked by a gate that fails the build.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Model the ORE report configuration as entities |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-02 |
3. Acceptance
- Every hand-written file under
projects/ores.sql/create,dropandpopulateis classified by why it is hand-written, and the classification and the command that produced it are in anevidence/file. - Each class has a decision: generate it, keep it hand-written with a reason, or delete it.
- The component create and drop aggregates are generated from the models, or the task records why they cannot be.
- The
WIRE_001wiring check, and the other parser warnings, fail CI rather than print. - Generated drop files carry the same generated marker as the create files, so a count of hand-written files is possible. Both templates write it now, and all 537 generated drop files carry it. A marker alone still cannot count exactly, because 44 files keep one after their generator's input was deleted, so the census script renders the generator's output as well.
4. Plan
No single method fits. The census is an investigation, the CI gate is a feature, and generating the aggregates would be a refactoring whose evidence is an unchanged recreate. This plan sequences them.
| Phase | Produces | Check |
|---|---|---|
| 1. Census | every SQL file under create, drop and populate, labelled entity codegen, other generator, or hand-written |
the labels sum to the tree's file count; every file codegen renders exists in the tree |
| 2. Reasons | for each hand-written file, why no generator produces it | every file lands in exactly one class; one file per class is read to confirm |
| 3. Aggregates | whether a component's create and drop order can be derived from its models | a derived order for one component satisfies every dependency the hand order encodes, or the gap is named |
| 4. Gates | the parser fails CI on a warning; generated drop files carry a marker | the parser passes --strict on main; drift is clean after the template change |
| 5. Decisions | a decision per class, an evidence file, and follow-up tasks | each class has generate, keep with a reason, or delete |
The census uses the drift checker's own rendering, not file markers: it renders every catalogue component into a scratch root, and a file is entity codegen output exactly when that root contains it.
Stop and ask before generating any aggregate or moving a hand-written file to codegen. Phase 5 decides, and the operator approves, before that work starts. Stop phase 3 at a recommendation if the hand order encodes dependencies the models do not hold.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
| SQL files by origin: entity codegen, other generator, hand-written | 1119, 164, 517 of 1800 | render_all.py then classify.py, in the census |
8107e1f421 |
| Create files that are not entity codegen, by reason class | 192 in 12 classes | reasons.py |
8107e1f421 |
| Files no script includes, any suffix | create 16, drop 7, populate 5 | reach.py |
8107e1f421 |
Dead legacy notify triggers invisible to WIRE_001 |
15 | reach.py plus a code search |
8107e1f421 |
Stale WIRE_001 ignore entries |
4 of 9 | reach.py against validation_ignore.txt |
8107e1f421 |
| Tables whose soft-FK parent is created later | 88 of 372 | order.py |
8107e1f421 |
| Hand-written RLS policies on a table with a model but no flag, plain tenant isolation | about 129 of 251 | keyword scan, rough | 8107e1f421 |
Parser warnings with --strict after removing stale entries |
0 | plantuml_er_parse_sql.py ... --strict |
working tree |
Files WIRE_001 reports once widened to every suffix and the populate tree, before the fixes |
22 | plantuml_er_parse_sql.py ... --warn |
working tree |
Parser warnings with --strict after the fixes |
0 | plantuml_er_parse_sql.py ... --strict |
working tree |
Full drop/drop.sql against a recreated database |
completes, no error; 0 tables and 115 functions left | compass sql -- -v ON_ERROR_STOP=1 -f projects/ores.sql/drop/drop.sql |
working tree |
| Database recreate after the deletions and the wiring | succeeds in 27 s, 372 tables | compass db recreate -y -k |
working tree |
| Generated drop files with the generated marker | 537 of 537, from 0 | regenerate --all --address ores.sql.schema then the census |
working tree |
| Census at the end of the task | 1119 codegen, 148 other generator, 517 hand-written of 1784 | census_sql_origin.py |
working tree |
The marker-based counts this task was scaffolded with (144 of 754 create scripts, 741 of 741 drop files) are superseded: a marker is missing from every generated drop file and present on 44 files whose generator input no longer exists.
6. Notes
Found while reviewing PR #2500 (today's market round trip). Adding five entities passed the build, the model drift check and every test, and the entities still did not reach the database until two aggregate files were edited by hand.
The aggregates are hand-written in every component. All 22
create/<component>/<component>_create.sql files and their drop twins are
written by hand. They hold the \ir includes in dependency order, with prose
per block. This is a project-wide convention, not something PR #2500 started.
A check exists and does not fail. validate_component_wiring in
projects/ores.codegen/src/plantuml_er_parse_sql.py reports rule WIRE_001 for
any create or drop file the root script does not reach. The
codegen-drift.yml step runs the parser with --warn and without --strict,
so the report only prints. With the five today's market RLS_001 warnings
fixed, the tree has zero warnings, so --strict costs nothing today.
Generation looks possible for the aggregates. The models already hold the foreign keys, so a dependency order can be computed by a topological sort. The open questions:
- where the prose per block goes;
- how hand-written files (functions, RLS, service bundles) sit in the same order;
- whether one generated fragment per component is better than generating the whole aggregate.
Generated drop files carry no marker. For example,
drop/analytics/analytics_todays_market_configs_drop.sql is generated but has
no "generated" line. So the drop count below cannot tell generated from
hand-written.
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
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 |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2515 | [sql] Census the hand-written SQL, gate the schema parser, and mark generated drops |
9. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Local review: the WIRE_001 docstring narrated history and wrapped a sentence badly |
plantuml_er_parse_sql.py |
Accepted | It now states the reason only. |
| 2 | Local review: the CI comment narrated history | codegen-drift.yml |
Accepted | It now states what fails the step and where an exception goes. |
| 3 | PR checks: the ores.analytics.service source list missed the five today's market event registrars from PR #2500 |
component_files.cmake |
Accepted | Regenerated; the five sources now compile. Like the other analytics registrars, nothing calls them yet. |
| 4 | PR checks: after the rebase, main's 36 commits added no generated SQL; the ores.sql.schema regeneration changed nothing and every generated drop file carries the marker |
SQL tree | No change | Verified with the census. |
| 5 | PR review: reachability is not scoped to a tree; a drop file included only from create.sql counts as wired | plantuml_er_parse_sql.py |
Accepted | The walk from the bootstrap scripts stops at another tree's root. Test added. |
| 6 | PR review: every top-level script counts as a bootstrap root | plantuml_er_parse_sql.py |
Accepted, as a comment | Deliberate, because setup_schema.sql and recreate_database.sql include files directly; the comment now says so. |
| 7 | PR review: a commented-out \ir counts as wiring |
plantuml_er_parse_sql.py |
Accepted | Only an \ir at the start of a line counts. Test added. The RLS reachability check shares the collector and gains the same fix. The strict parser still reports no warning. |
| 8 | PR review: the census marker matches anywhere in the first 3000 bytes | census_sql_origin.py |
Accepted | It matches on comment lines only. The census counts are unchanged. |
| 9 | PR review: the 115 leftover functions and the two unwired populate scripts | captures | No change | Already filed. |
| 10 | PR review: local clang-format mismatch on trade_service.cpp |
— | No change | Local only; CI passes. |
10. Result
The hand-written SQL is measured, every class has a decision, and the checks that let two defects reach main are now gates.
The census. Of 1800 SQL files, 1119 are entity codegen output, 164 come from other generators and 517 are hand-written. The method renders the generator's own output rather than trusting markers. Every class, its reason and its decision are in the evidence file, with the scripts.
Decisions.
| Class | Decision | Owner |
|---|---|---|
| Component create and drop aggregates | Generate the entity part per component; hand-written files stay listed by hand | Generate the entity part of each component's SQL aggregates |
| FpML coded-scheme tables, artefacts and triggers (44 + 15) | Model them so codegen owns them | Model the FpML coded-scheme tables so codegen owns them |
| Hand-written RLS policies (251) | Generate from model flags into a late per-component file | Generate the hand-written RLS policies from model flags |
| Tables with no model (25) | Model those a codegen shape covers | Model the hand-written tables that have a codegen shape |
Procedural code, views, alter scripts, uncatalogued components, populate seed data, data generators |
Keep; each reason is in the evidence | — |
| Dead legacy triggers (15), broken FpML populate (1) | Deleted | this task |
Why aggregates can be generated. Soft foreign keys impose no creation order: 88 tables are already created before the parent their trigger checks, and every recreate succeeds. Only a table before its trigger, functions from earlier components and hand files that name tables at creation need order.
Gates. The schema parser fails CI on any warning. WIRE_001 now covers
every .sql file in the three trees, reached from their roots and from the
bootstrap scripts, which found fifteen dead files and two populate scripts that
never ran. Both drop templates now write the generated marker the create
templates write, so the 537 generated drop files no longer look hand-written. The five real drop gaps are wired and the full drop script runs
clean. The census is committed as a tool.
Recorded, not fixed here. Three captures: ores_iam_auth_events_tbl has a
tenant_id and no RLS policy despite an ignore reason calling it tenant-scoped;
two populate scripts were never wired, and wiring them changes what a recreate
seeds; the drop script leaves 115 functions behind.
Component audit. The task touched ores.codegen and ores.sql. The
automated half, validate_docs.sh, passes for all 41 components. It added one
script to ores.codegen/scripts beside its peers, deleted sixteen dead SQL
files and changed no folder, type or component, so the shape, morphology,
naming and registry checks have nothing new to judge.
One operational note. The first recreate hung for minutes in DROP
DATABASE on ProcSignalBarrier, with this environment's services still
reconnecting. Stopping the services, cancelling the drop and retrying worked.