Skip to content

Commit 86bbadb

Browse files
authored
test: cover untested FR-044 reporting features across conformance corpora (#423)
* test(reporting): cover more FR-044 features in the canonical test model and every port Adds 12 reports to the canonical fitness model (and a Session entity) and grows the api-contract report corpus to four entities and nine reports, so the report corpora exercise vocabulary they did not before: multi-hop and self-referencing dimensions, day/quarter/year/hour/date time grains, min/max/avg/count-distinct over more column types, negative and decimal measure defaults over an empty table, measure filters on the remaining operators, segment and report filters combining like/in/or/ne, a two-hop @spine, and a TPH base as @from. - persistence-conformance: 10 new report query scenarios, all five ports - api-contract-conformance/report: 6 new scenarios (22 in all), all five ports - cube-model goldens and the live Cube lane cover the new reports - committed derived artifacts regenerated with the repo's own tools - test-harness fixes only: date and uuid operand coercion in the Java and C# runners, `like` and uuid in the seam lanes' in-memory report repositories, and the SQLite convergence test's known inet residue - removes a stray generated temp directory committed in #420, which turned the ts build + typecheck gate red on main No product port defect was found, so no CHANGELOG entry. * test(reporting): run the new report scenarios on SQLite, MySQL and D1 and the MySQL Cube lane The engine lanes added in #421 now run all 19 report scenarios. The MySQL adopter tables gain the nodes, sessions, measurements, auths and all_types tables, the api-contract MySQL schema gains the disputed/category/sku columns, and the MySQL Cube lane covers all 21 served reports. A naive timestamp keeps its wall clock without a zone suffix on the SQLite and D1 wire form; two scenarios no longer depend on engine-specific NULL ordering or on a Postgres-only clock expression. * test(integration-tests-kotlin): coerce a date filter operand on SQLite and read min/max timestamps there The Kotlin SQLite lane spells a DATE column's SQL type as TEXT, so the string operand of a date filter reached Exposed uncoerced; key the coercion on the column type as well. The report-time-min-max seed now spells its timestamps in the form the SQLite driver's date_string_format parses (fixture seed only; expect blocks unchanged).
1 parent 7daa8e6 commit 86bbadb

124 files changed

Lines changed: 4285 additions & 324 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/CONFORMANCE.md‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
3333
| [`fixtures/render-conformance/`](../fixtures/render-conformance/) | 15 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
3434
| [`fixtures/extract-conformance/`](../fixtures/extract-conformance/) | 48 | ✓ | ✓ | inherits the shared JVM engine | ✓ | ✓ |
3535
| [`fixtures/output-prompt-conformance/`](../fixtures/output-prompt-conformance/) | 17 | ✓ | ✓ | ✓ | ✓ | ✓ |
36-
| [`fixtures/persistence-conformance/`](../fixtures/persistence-conformance/) | 42 (36 query + 6 migration) | all 42 (the 9 `report-*` also on SQLite, MySQL and D1) | 36 query (migrations TS-only, ADR-0015); the 9 `report-*` also on MySQL | 36 query (via Exposed); the 9 `report-*` also on SQLite and MySQL | 36 query; the 9 `report-*` also on SQLite and MySQL | 36 query (Postgres only: pg8000) |
37-
| [`fixtures/api-contract-conformance/`](../fixtures/api-contract-conformance/) | 82 (31 core + 10 tph + 9 m2m + 2 jsonb + 2 write-through + 12 projection + 16 report) | ✓ (Fastify reference + generated lane); `report/` also on SQLite, MySQL and D1 | ✓ (embedded HTTP + JDBC) | ✓ (embedded HTTP + Exposed) | ✓ (HttpListener + Npgsql); `report/` also on SQLite and MySQL | ✓ (FastAPI + pg8000) |
36+
| [`fixtures/persistence-conformance/`](../fixtures/persistence-conformance/) | 52 (46 query + 6 migration) | all 52 (the 19 `report-*` also on SQLite, MySQL and D1) | 46 query (migrations TS-only, ADR-0015); the 19 `report-*` also on MySQL | 46 query (via Exposed); the 19 `report-*` also on SQLite and MySQL | 46 query; the 19 `report-*` also on SQLite and MySQL | 46 query (Postgres only: pg8000) |
37+
| [`fixtures/api-contract-conformance/`](../fixtures/api-contract-conformance/) | 88 (31 core + 10 tph + 9 m2m + 2 jsonb + 2 write-through + 12 projection + 22 report) | ✓ (Fastify reference + generated lane); `report/` also on SQLite, MySQL and D1 | ✓ (embedded HTTP + JDBC) | ✓ (embedded HTTP + Exposed) | ✓ (HttpListener + Npgsql); `report/` also on SQLite and MySQL | ✓ (FastAPI + pg8000) |
3838
| [`fixtures/validation-conformance/`](../fixtures/validation-conformance/) | 42 cases | ✓ (generated Zod + run-time `runValidators`) | ✓ | ✓ | ✓ | ✓ (generated Pydantic + run-time `run_validators`) |
3939
| [`fixtures/registry-conformance/`](../fixtures/registry-conformance/) | 1 canonical manifest | ✓ (reference emitter) | ✓ | ✓ | ✓ | ✓ |
4040
| [`fixtures/object-model-conformance/`](../fixtures/object-model-conformance/) | 1 shared metadata fixture (per-port scenarios) | ✓ | ✓ | ✓ | ✓ | ✓ |
@@ -136,13 +136,13 @@ manifest contains without moving these boundaries.
136136
(`scripts/ci-local.sh --only cube`, which needs Docker) loads the canonical golden and the 34
137137
cases that hold a tree into a pinned Cube (`cubejs/cube:v1.7.43`) and requires each to
138138
compile, the two MySQL cases included: compiling a model runs no SQL, so the lane's Postgres
139-
data source serves them too. For the nine canonical reports it compares the Cube query with the
139+
data source serves them too. For the canonical reports it compares the Cube query with the
140140
report view (a `@spine` report through its Cube view, its empty spine rows included), and for
141141
the `escaping` and `measure-default` cases it reads the SQL back from Cube's `/v1/sql`.
142142
A second live file (`cube-model-mysql.live.ts`) runs the MySQL path: the canonical golden
143143
generated with `dialect: "mysql"` is loaded into the same pinned Cube over a real MySQL 8.4
144144
holding the report views `buildReportViews` lowers (`fixtures/cube-model/canonical/seed.mysql.sql`),
145-
and each of the nine reports' Cube query is compared with `SELECT *` from its view, with the
145+
and each of the twenty-one served reports' Cube query is compared with `SELECT *` from its view, with the
146146
pre-aggregation used asserted. That is the smaller of the two honest options: the two MySQL
147147
*cases* (escaping, a quoted identifier) are still compiled, not executed, and the goldens hold
148148
their SQL. The lane runs Cube in development mode, so Cube's production mode with a separate
@@ -298,12 +298,12 @@ trailing-newline preservation, and unicode multibyte handling.
298298
All 31 fixtures → [features/migrations-and-drift.md](features/migrations-and-drift.md)
299299
(template drift section — `Renderer.verify`).
300300

301-
### `fixtures/persistence-conformance/` (42 — 36 query + 6 migration)
301+
### `fixtures/persistence-conformance/` (52 — 46 query + 6 migration)
302302

303303
- `migrations/*` (6) → [features/migrations-and-drift.md](features/migrations-and-drift.md) (schema migration section)
304-
- `queries/*` (36) → [features/source-kinds.md](features/source-kinds.md) (query semantics against `source.rdb`)
304+
- `queries/*` (46) → [features/source-kinds.md](features/source-kinds.md) (query semantics against `source.rdb`)
305305

306-
**Engines beyond Postgres (FR-044 reports).** The nine `queries/report-*.yaml` scenarios also
306+
**Engines beyond Postgres (FR-044 reports).** The nineteen `queries/report-*.yaml` scenarios also
307307
run on SQLite and MySQL 8.4 wherever a port's runtime has the engine, and on D1's local runtime
308308
in TypeScript (no cloud account): TypeScript on all four, C# and Kotlin on SQLite and MySQL, Java
309309
on MySQL. **Stated limitations, not gaps:** Java's OMDB has no SQLite driver, and Python's runtime
@@ -317,9 +317,9 @@ REAL, a MySQL `DECIMAL` keeps its scale, a MySQL `DATETIME` is the UTC wall cloc
317317
`report-relative-date` carries a per-engine seed (`seed-data-engine`). `op: relate` and the
318318
write ops stay Postgres-only. See [features/reporting.md](features/reporting.md#what-the-corpus-gates).
319319

320-
### `fixtures/api-contract-conformance/` (82)
320+
### `fixtures/api-contract-conformance/` (88)
321321

322-
All 82 scenarios → [features/api-contract.md](features/api-contract.md) (cross-port
322+
All 88 scenarios → [features/api-contract.md](features/api-contract.md) (cross-port
323323
REST API URL grammar + JSON wire format). Verifies every backend's emitted CRUD
324324
routes answer identically over HTTP — list / get / create / patch+put / delete,
325325
plus pagination (`limit`/`offset`), sort (`sort=field:dir`), the `withCount=1`
@@ -337,7 +337,7 @@ each corpus alone could not reach, and 1 pinning the collection-URL spelling),
337337
`write-through/` (2, table-write + view-read entities), `projection/` (12, a
338338
read-only view answers reads and refuses writes with 405; a projection with no declared
339339
identity has no item route; a projection keyed on a field not named `id` is addressed by
340-
it, whether its identity names `@fields` or derives them; decimal and float fields filter) and `report/` (16,
340+
it, whether its identity names `@fields` or derives them; decimal and float fields filter) and `report/` (22,
341341
FR-044: a view-backed `object.report` is listed, filtered, sorted and paged on
342342
its derived fields, answers `POST` with 405 and mounts no `/{id}`; a `@spine` report keeps
343343
a row with no facts, and a measure with `@default` filters and sorts as its default). All 5 ports — TS, Java,
@@ -523,7 +523,7 @@ files into a real Cube.
523523
## Orphaned fixtures (tested but not yet documented)
524524

525525
The fixtures in the nine corpora mapped above (metamodel 374 + yaml 16 + verify 31
526-
+ render 15 + persistence 42 + api-contract 82 + source-resolution 25 + scope 10 +
526+
+ render 15 + persistence 52 + api-contract 88 + source-resolution 25 + scope 10 +
527527
dependency 23) each map to a feature doc, and so do the two requirement corpora and the
528528
`cube-model` corpus, whose case lists live in their own READMEs. None are orphaned today. The remaining
529529
corpora in the totals table gate tooling contracts (registry manifests, provider

‎docs/features/api-contract.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ the empty rows, `isNull=true` matches nothing, and a sort on it orders the same
480480
engine.
481481

482482
Gated by [`fixtures/api-contract-conformance/report/`](../../fixtures/api-contract-conformance/report/)
483-
(16 scenarios), **generated lane only, on all five ports**, for the reason `projection/`
483+
(22 scenarios), **generated lane only, on all five ports**, for the reason `projection/`
484484
gives. The columns, their types and the per-port generated files are in
485485
[reporting.md](reporting.md#how-a-report-is-served).
486486

‎docs/features/cube-export.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@ there.
556556
| Grains | Cube offers every granularity on a time dimension. | `@grains` is carried as `meta.grains`, not enforced. |
557557
| Ad-hoc queries | Cube lets a caller pick any members. | Nothing here checks the numbers for a combination no report declares. |
558558
| Rollup freshness | Cube refreshes a rollup on its refresh key, and the exporter writes none, so Cube's default applies. | A rollup can trail the table until it refreshes. The view is never stale. A report with a relative date gets no rollup for this reason. |
559+
| Empty scope | A report with no dimension whose scope reads no rows returns one row from the view: the count `0` and each measure's `@default`. Cube answers from the rollup, which is empty, and sums it to null for every measure, the count and the default included. | `ProgramsEmptyScope` is the one canonical report where the rows differ; the live check requires both answers, so the difference stays an executed check. |
560+
| Several grains | A report that lists one time dimension at several grains gets a rollup naming that dimension once per grain. A Cube query names a time dimension once, at one granularity, so Cube 1.7.43 matches no query to that rollup. | The rollup is declared and never used; Cube answers from the source table and the rows equal the view's. `ProgramsOverTime` is the canonical case. |
559561
| Encodings | Cube's REST API sends numbers as strings and time buckets as `YYYY-MM-DDTHH:MM:SS.sss`. Where a rollup sends `60`, the view sends `60.0000000000000000`, and where Cube sends `2026-05-01T00:00:00.000`, the view sends `2026-05-01`. | Clients normalize them. The live check does, before it compares rows. |
560562

561563
## Checking it
@@ -584,13 +586,13 @@ container and the network are removed on every exit path. It checks six things o
584586

585587
1. The generated canonical model is the reviewed golden.
586588
2. Cube compiles it and lists every cube and member the files declare.
587-
3. For each of the nine canonical reports, the Cube query above, answered from the report's
589+
3. For each of the canonical reports, the Cube query above, answered from the report's
588590
rollup when the model has one (the lane asserts the rollup name in `usedPreAggregations`),
589591
returns the rows of `SELECT * FROM <view>`, after the normalization the Encodings row above
590-
describes. The two `@spine` reports, `ProgramRoster` and `ProgramLongWeeks`, are queried
591-
through their Cube views, and their 7 rows each include the programs with no weeks (and, for
592-
`ProgramLongWeeks`, the two whose weeks are all short); `FitnessTotalsFilled`, a report of
593-
defaulted measures, is answered from its rollup.
592+
describes. The three `@spine` reports, `ProgramRoster`, `ProgramLongWeeks` and
593+
`ProgramSessions`, are queried through their Cube views, and their 11 rows each include the
594+
programs with no weeks (and, for `ProgramLongWeeks`, the two whose weeks are all short);
595+
`FitnessTotalsFilled`, a report of defaulted measures, is answered from its rollup.
594596
4. Every case of the mapping corpus that has an expected tree (34 of the 44: 32 Postgres and
595597
2 MySQL) compiles in the same Cube, and each `title` and `description` it declares comes back
596598
from `/v1/meta` as declared. Compiling runs no SQL, so the MySQL cases compile against the

‎docs/features/reporting.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -621,19 +621,19 @@ the anti-join by hand, and make two reports.
621621

622622
### What the corpus gates
623623

624-
Nine shared scenarios under `fixtures/persistence-conformance/queries/report-*.yaml` read the
624+
Nineteen shared scenarios under `fixtures/persistence-conformance/queries/report-*.yaml` read the
625625
canonical reports through every port's runtime (list and count, filter, sort, an empty table,
626626
the Monday boundary, an hour bucket, a relative window, a `@spine` report with a program that
627627
has no weeks, a `@spine` report whose segment leaves a program no facts, and defaulted measures
628-
over an empty table). The derived columns are pinned by
628+
over an empty table; and, beyond those, every `@agg` kind over int, long, float, double, decimal, currency, date and instant columns, a nullable int-backed enum, a boolean and a uuid as dimensions, `day`, `quarter` and `year` grains together, every filter operator on a measure, a report `@segment` and `@filter` together, a two-hop `@via`, a self-referencing dimension, a report declared from a TPH base, and a negative `@default`). The derived columns are pinned by
629629
`fixtures/persistence-conformance/report-shapes.json`, produced by TypeScript and byte-matched
630630
by every port. The SQL is produced by TypeScript only, so the other ports read the view the
631631
TypeScript migrate engine produced and never lower a report themselves. TypeScript's own value
632632
tests read the `@spine` and `@default` views and their column types on Postgres, SQLite and
633633
MySQL, and on Postgres and SQLite also a fact whose reference is null, an `isNull` condition
634634
and a two-hop spine.
635635

636-
The nine scenarios run on more than Postgres, wherever a port's runtime has the engine:
636+
The nineteen report scenarios run on more than Postgres, wherever a port's runtime has the engine:
637637

638638
| | Postgres | SQLite | MySQL 8.4 | D1 (local runtime) |
639639
|---|---|---|---|---|
@@ -651,21 +651,23 @@ stay the Postgres wire ones, never loosened: the engine's own spelling (a REAL r
651651
a scale-keeping `DECIMAL` and a zoneless `DATETIME` on MySQL) is mapped on the actual side by
652652
each runner, and `report-relative-date` spells its seed per engine (`seed-data-engine`). D1
653653
has no separate corpus: it is SQLite behind Cloudflare's binding, so TypeScript runs the same
654-
scenarios and the same sixteen REST scenarios through Miniflare's local D1 (`meta migrate
654+
scenarios and the same twenty-two REST scenarios through Miniflare's local D1 (`meta migrate
655655
--dialect d1`), with no cloud account.
656656

657657
A port without the engine is a stated limitation, not a gap to fill by building a driver:
658658
Java's OMDB and Python's `ObjectManager` ship the drivers they ship, and a view-reading test
659659
does not justify a new one.
660660

661-
The REST surface is gated by sixteen scenarios under
661+
The REST surface is gated by twenty-two scenarios under
662662
[`fixtures/api-contract-conformance/report/`](../../fixtures/api-contract-conformance/report/),
663663
run in the **generated lane on all five ports**: list (a dimension with a segment-scoped sum
664664
that is null for one group), a time dimension at a grain (`YYYY-MM-DD`), a no-dimension
665665
totals report with the `withCount` envelope, a filter on a dimension and on a measure, a sort
666666
on a measure and an enum dimension, paging over groups, the three field-naming `400` envelopes,
667667
`405` on `POST`, `404` on every verb at `/{id}`, and a `@spine` report whose product with no
668-
sale keeps its row, with a filter and a sort on its defaulted measure. The corpus model
668+
sale keeps its row, with a filter and a sort on its defaulted measure, a dimension reached through
669+
two references, one time dimension listed at two grains, a filter on a string (`like`), a boolean and a
670+
uuid dimension, and a filter on a measure that carries its own `@filter`. The corpus model
669671
carries one sourceless report, so a port that serves every report it finds fails. No scenario asserts a decimal's
670672
spelling or a timestamp literal. TypeScript and C# run the scenarios against the real views on
671673
Postgres, SQLite and MySQL (`report/schema.sqlite.sql`, `report/schema.mysql.sql`), and

0 commit comments

Comments
 (0)