Skip to content

feat(intent): report structure moves to a generated .view - the .report keeps a thin parameterized SELECT (#6938) - #6941

Merged
delchev merged 2 commits into
masterfrom
report-views-6938
Aug 25, 2026
Merged

feat(intent): report structure moves to a generated .view - the .report keeps a thin parameterized SELECT (#6938)#6941
delchev merged 2 commits into
masterfrom
report-views-6938

Conversation

@delchev

@delchev delchev commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #6938.

The intent report generators baked the whole query into the generated repository as one single-line Java constant - a 16-line kind: statement = a 10.2 KB literal, a correspondence: balance = 6.3 KB. That is the shape that overflowed JavaSourceParser and killed synchronization instance-wide (#6936), it hid the query from the Database perspective, and it re-shipped fully static SQL inside every consumer.

What changed

Every heavy ledger query is split at the parameter boundary (the design agreed in the issue): the parameter-free structure becomes a generated .view artifact next to the .report; the .report keeps a thin SELECT binding the named parameters - which a database view cannot hold, so the boundary sits exactly there, never a parameterized-view emulation.

  • kind: statement<REPORT>_LINES (<Report>Lines.view). Per declared line: one parameter-free HEAD arm (TERM_SIGN 0, NULL measure/account - what keeps a line whose selector matches nothing rendering with 0, and the count wrap seeing every line) plus one SELECT DISTINCT arm per flattened term, classifying the account codes its selector matches, enumerated from the nomenclature the account: path points at (DISTINCT so a duplicated code cannot double a balance). The thin query LEFT JOINs the view to the WITH "ACCOUNT_BALANCES" CTE - which keeps the windowed sums, filter:/scope: and the authored parameters - and decodes each row's TERM_MEASURE through a CASE carrying only the measures the report uses.
  • correspondence:<REPORT>_CORRESPONDENCE (<Report>Correspondence.view) of allocated line-level rows: the counter-side self-join, the proportional allocation with its correlated document totals, and the static filter: + lifecycle scope live in the view, with the entry date as a plain ENTRY_DATE column. The .report sets table = the view and is assembled by the same buildQuery as every plain report - so a correspondence report now round-trips in the report editor's visual builder (it reads the view like any table). A translated dimension exposes its base value plus a <DIM>_LANGUAGE_KEY column and the .report re-applies the :language overlay over them; an authored parameter's whole static left side (CAST + COALESCE included) is exposed as a PARAM_<NAME> column and compared in a plain builder-owned condition row.
  • The views ride the existing lifecycle. ViewsSynchronizer provisions them per tenant, SynchronizersOrder (SCHEMA → TABLE → VIEW) already orders them after the tables they read, and .view is an intent-owned extension - they regenerate with the report and are scrubbed with it. The unresolvable-document correspondence case falls back to the flat emission (which warns), and plain aggregations / plain kind: balance are deliberately unchanged - small literals, and the flat balance emission is what the editor's builder owns today.
  • Belt-and-braces, the other half of JavaSourceParser's STRING_LITERAL regex overflows the stack on a long literal - and the StackOverflowError kills the SynchronizationJob for good #6936: whatever SQL still reaches the generated repository is never one single-line literal - ModelTemplateAdapters pre-escapes the query as per-line literals and the repository template joins them with String.join("\n", ...). Complementary to fix(engine-java): one long string literal must not turn synchronization off platform-wide (#6936) #6939 (the parser-side fix): this stops the generators producing the trigger input; that one fixes the parser and contains the blast radius.

Tests

  • StatementReportSqlTest / CorrespondenceReconciliationTest install the emitted views on H2 and RUN the thin queries, asserting figures, never strings - all pre-existing assertions pass verbatim (netting-before-sum, flattened subtotals, allocation reconciling with the plain balance, the count/LIMIT wraps), plus a new one: a line whose selector matches no account still renders, with 0.
  • ReportEditorRoundTripTest: the correspondence document round-trips over the view; the statement stays deliberately free-style.
  • IntentEmissionCoverageIT (the outermost layer): the fixture gains a statement report over a seeded ledger - asserts the .view/.report/repository tokens, then reads the statement's figures over REST from the published app, proving the whole publish → ViewsSynchronizer → thin-query lifecycle including the netting arithmetic (100 debit vs 40 credit → 60).
  • ModelGenerationIT covers the per-line-literal template rendering (idempotent, no unresolved placeholders).

Verified locally: full engine-intent unit suite (926 tests), ide-template (85), IntentEmissionCoverageIT, ModelGenerationIT, formatter:validate on all changed modules.

No authoring change - the intent DSL is untouched, so no specification change rides along.

🤖 Generated with Claude Code

delchev and others added 2 commits August 25, 2026 11:25
…ted .view - the .report keeps a thin parameterized SELECT (#6938)

Split the generated report query at the parameter boundary: everything
that does not depend on runtime input is emitted as a generated .view
artifact next to the .report, and the query the generated repository
carries binds only the named parameters - which a database view cannot
hold, so the boundary is exactly there.

- kind: statement -> <REPORT>_LINES: one parameter-free head arm per
  declared line (a selector matching nothing still renders, with 0)
  plus one SELECT DISTINCT arm per flattened term, enumerating account
  codes from the nomenclature the account path points at. The thin
  query LEFT JOINs the view to the ACCOUNT_BALANCES CTE (windowed sums,
  filter/scope, authored parameters) and decodes each row's measure.
- correspondence: -> <REPORT>_CORRESPONDENCE: allocated line-level rows
  with the entry date as a plain column; the counter-side self-join,
  the proportional allocation and the static filter/scope live in the
  view. The .report reads the view as its base table through the same
  buildQuery as every plain report, so it now round-trips in the report
  editor's visual builder. A translated dimension exposes its base
  value plus a language key, and the .report re-applies the :language
  overlay; an authored parameter's static left side is exposed as a
  PARAM_* column.
- The views ride the existing lifecycle: ViewsSynchronizer provisions
  them per tenant after the tables (SynchronizersOrder), and .view is
  an intent-owned extension, so they regenerate and scrub with the
  report. Plain aggregations and plain kind: balance are unchanged.
- Belt-and-braces for whatever SQL still reaches generated Java: the
  repository QUERY is joined from per-line literals instead of one
  single-line literal - the #6936 trigger shape stops being emitted
  at all, from either end.

StatementReportSqlTest and CorrespondenceReconciliationTest install the
emitted views on H2 and run the thin queries, asserting figures (incl.
the matches-nothing line and the buckets-reconcile property);
ReportEditorRoundTripTest proves the correspondence round-trip;
IntentEmissionCoverageIT seeds a little ledger and reads the statement
figures over REST from the published app - the whole publish ->
ViewsSynchronizer -> thin-query lifecycle.

Fixes #6938

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rated .view, not in the .report query (#6938)

The smoke suite's IntentEngineIT still pinned the pre-split emission:
the correspondence self-join, the allocation and the statement line
arms inside the .report query. Those now live in the generated
OrderStatementLines.view / OrderItemCorrespondenceCorrespondence.view
artifacts - the assertions move there, the thin-query side gains its
own checks (view as base table, windowed ENTRY_DATE/ALLOCATED_* sums,
LINE_ORDINAL ordering, no re-shipped structure), and the two .view
files are pinned in the generate response's written list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@delchev

delchev commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

The smoke-tests failure was IntentEngineIT.generate_writes_all_model_files_into_the_workspace_project — the one report-emission test this PR had missed: it still pinned the pre-split query (the correspondence self-join, the allocation, the statement line arms inside the .report). dd1cadd moves those assertions to the generated .view artifacts, adds thin-query checks (view as base table, windowed ENTRY_DATE/ALLOCATED_* sums, LINE_ORDINAL ordering, no re-shipped structure), and pins the two .view files in the generate response's written list. Verified locally: all 59 IntentEngineIT tests green.

@delchev
delchev merged commit 94a9be4 into master Aug 25, 2026
9 checks passed
@delchev
delchev deleted the report-views-6938 branch August 25, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant