Skip to content

Reports

Administrators generate operational reports, reuse private settings and download the resulting table as XLSX or CSV. Whole-school, full-academic-year attendance is a supported job shape. The contract uses settings schemaVersion: 1, definitionVersion: 2, and frozen-result schemaVersion: 2.

1. Ownership and entry points

Responsibility Source
HTTP, Swagger and flat aggregate DTOs src/reports/reports.controller.ts, reports.swagger.ts, dto/
Tenant-local owner, admin view and fresh access reports.policy.ts, reports.access.ts, reports.queries.ts
Finite field/preset catalogue and settings grammar catalogue/report.catalogue.ts, report.fields.ts, report.types.ts, report.validation.ts
Year/date/reference resolution and hashes catalogue/report.resolve.ts, report.hash.ts
Private settings with optimistic revisions saved/saved-reports.{service,queries}.ts
Source composition and immutable logical table runs/report.source.ts, report.table.ts
Durable generation, leases and cleanup runs/report-worker.{service,queries}.ts, reports.config.ts
Owned run detail, pages and export authorization runs/report-runs.{service,queries}.ts
CSV/XLSX encoding and renderer thread export/report-export.service.ts, report-csv.ts, report-xlsx.ts, report-xlsx.worker.ts
Module registration reports.module.ts, src/app.module.ts
Live reports (synchronous query, saved configurations, export) src/report-v2/ (see §9)

Reporting composes source-owned readers through module barrels. Every reader receives the explicit source transaction and resolved tenant/year/time context. It does not loop over internal HTTP endpoints or open an independent source transaction.

2. Admission and confidentiality

Every route requires the admin session view. A non-admin role with an injected reporting grant is still refused. Null active-view context is admitted only for an admin or platform administrator; another explicit view is refused. A platform administrator still needs an active tenant-local User and owns only that User's configurations and runs.

The entity is reports, flat scope configuration. Read routes require READ; update requires WRITE; create/duplicate require create with WRITE; delete requires delete with WRITE; enqueue/export require generate/export with READ. Only the admin preset receives these grants. Configurations and runs are private to their tenant-local owner.

Fresh authority comes from current User activity/invitation acceptance, tenant status, active admin assignment and compiled admin-view scopes/actions. PermissionsService.getUserPermissionsInTransaction uses named queries on the supplied transaction, without cached JWT grants. Reports also requires the source scope of every displayed, filtered, sorted and grouped field, plus each definition's base sources. A health filter requires health access for both its predicate and its mandatory visible column. Source restrictions are enforced explicitly before producing aggregate cells.

All configuration/run lookups bind tenant, owner User and ID. Cross-owner IDs return hidden 404s. Catalogue fields/presets omit unavailable choices. Saved-list and history entries that have become unavailable retain minimal identity/state metadata with unavailableReason; source labels/settings/counts are withheld as appropriate so one inaccessible entry does not break the list. Opening an inaccessible entry returns its explicit error. An owner with Reports delete authority can remove an obsolete private configuration without reading its unavailable source fields.

Generation checks authority at admission, claim execution, inside the source snapshot and before publication. Reads and both sides of export rendering check fresh authority again. Completeness freezes its captured readable source scopes in the manifest, since missing-field names themselves reveal source information. Responses and attachments use Cache-Control: private, no-store.

3. Catalogue and source meanings

Definition Row meaning Source seam
students One selected-year student; status-neutral list src/students/reporting.queries.ts
student_contacts One student/contact relationship; REFERENT and GUARDIAN union Same reader; student links and guardian mapping
student_memberships One dated HOMEROOM or COURSE membership src/homerooms/reporting.queries.ts, src/subject-groups/reporting.queries.ts
attendance_daily One student-day in the dated roster, including untaken days src/attendance/reporting/
attendance_summary One student or student/dated-cohort subtotal Same reader plus summary fold
lunch_daily Stored selected-year lunch participants with dated school attendance Student health projection + attendance reader
teachers One selected-year teacher src/teachers/reporting.queries.ts
staff One selected-year staff member src/staff/reporting.queries.ts
teacher_assignments One effective ordinary teacher/course pair src/subject-groups/reporting.queries.ts
teacher_commitments One teacher and official weekly-pattern context Teacher reader delegates to loadScheduledTeacherBudgets
completeness One person or guardian with current canonical completeness CompletenessService.loadReportItems

Each definition has a system preset. Additional presets are Student Status, Students by Class and Expiring Teacher Contracts: 11 definitions and 14 presets. Presets are code-owned starting settings, not shared editable templates.

The catalogue is the exhaustive field/operation allowlist. Native/custom model fields do not automatically become report fields. Contract-expiry ranges filter stored teacher contract dates. Staff has no contract dates or department relation. Commitments is an official weekly-pattern workload, not substitution availability or hours actually worked. Completeness reuses canonical required-field/status rules, including its current participating-student policy; it does not invent document/consent requirements.

Contact conditions apply to the same contact row. Multiple contacts produce multiple rows; students without contacts produce no contact row. REFERENT pickup authorization is null. Student relationship flags reuse the canonical inferred/pinned flag projection; sibling identities are not included. Lunch participation/diet/allergies are stored values, not historical service registrations or consumed meals.

4. Settings, selectors and time

Settings have schemaVersion: 1, reportType, definitionVersion: 2, academic year, time, ordered columns, AND filters, up to three ordered sorts, one nullable group and membership options where applicable. See dto/report-settings.dto.ts and catalogue/report.types.ts for the full union.

  • Missing academic year normalizes to {kind: ACTIVE}. FIXED selects a non-deleted tenant year; PARAMETER requests an ID at run time. Reports metadata can reference prior/draft years independently of source write gates.
  • Time is NONE, ON or INTERVAL according to the definition. Memberships requires options.membershipKind. A relative year does not fabricate school-wide term dates: departments own calendars.
  • Date values are fixed, parameterized or supported relative forms. TODAY has an integer offset in −366..366. CURRENT_MONTH returns its calendar bounds. PREVIOUS_N_DAYS is today−N through yesterday; NEXT_N_DAYS is tomorrow through today+N; N is 1..366. Range-valued relative forms belong to BETWEEN date filters; time.from and time.to each require a scalar date.
  • All relative values/year/clock resolve once at the successful source attempt. Admission validates current references; a queued run can resolve a newly active year. Retry attempts resolve afresh. context.resolvedAt, timezone, schoolToday, nowMinutes, requested dates and observedThrough describe the published attempt.
  • Academic-year, student, teacher, department, grade, homeroom and course lookup choices are tenant/year constrained. Dependent options accept departmentId and gradeId. Fixed/bound IDs are checked even when another parameter is missing; unresolved year parameters defer that check until bound. Membership group IDs use homeroom/course lookup according to membershipKind. An old-year selection requires re-selection, never name remapping or widening.
  • Names/text compare case-insensitively; BETWEEN is inclusive. Null is false for comparisons, including negative comparisons. Explicit empty operators select null/empty values. Only student.hasLunch EQ false includes unset values to preserve its existing source semantic.
  • Grouping ordinary lists orders rows and includes the group column; it does not aggregate. Sorting is stable with row-key ties and nulls last. Attendance grouping has subtotal semantics below.

Validation returns normalized settings, ordered effective column metadata, requiredColumns reasons, attendanceBasis, settingsHash, parameter descriptors, missingParameters, compatibility issues and compatible. Missing parameters alone do not make a reusable configuration incompatible; generation still requires their values. Invalid grammar/types return 422. Saved names, descriptions and preferred format are outside the settings hash. Revision is incremented on updates and must be supplied for update/delete. Request-key idempotency binds the normalized submitted settings or saved source, bindings and locale; array order in submitted bindings is significant. A deliberate new run uses a new UUID.

Effective columns

The finite mapping in catalogue/report.columns.ts appends missing readable filter, grouping and context columns in catalogue order; chosen order stays intact. The complete schema is authorized and counts against the 64-column limit. requiredColumns identifies FILTER + field, GROUPING, and CONTEXT + key reasons. Removing a condition removes its requirement but preserves the selected column until the user removes it.

Every report contains context.academicYearName. Dated reports add context.on; intervals add context.from and context.to. Attendance adds context.attendanceBasis, memberships add membership.kind, and lunch adds student.hasLunch. ID lookups map to readable names; person IDs map to first and last names. Empty results retain schema/context but have no synthetic row.

Language and saved compatibility

Labels, descriptions, unit labels and row meanings are bilingual objects with en_US and it_IT. English Class/Classes means Italian Corso/Corsi; Homeroom remains Classe. Result cells retain canonical enum/boolean/coded-list values. TEXT_LIST.listValueKind is CODE or TEXT; only CODE values use valueLabels. Names and user-authored text are never translated.

CompletenessService.loadReportItems captures readable missing-field labels within the same source transaction. Native keys retain their identifiers, indexed contact paths have translated labels, and custom keys use <personType>.<fieldKey> with the school's stored label in both languages. Labels for only the missing, authorized fields enter this report seam. The frozen dictionary is pruned to codes present in actual output.

The completeness label mapper includes stored document references (passportFileId, identityCardFileId) and indexed student relationships (students[n].relationshipType) as well as referent/guardian paths. These canonical completeness keys retain their codes even though document references are absent from the request-field label catalogue. person.type includes GUARDIAN ("Guardian" / "Tutore"): one row per guardian linked to at least one ENROLLED selected-year student the owner can read, keyed guardian:<id>, with the bare identityDocumentFileId code as its only missing field. Guardian completeness participates in the same person.type filter/grouping contract.

Version-1 saved configurations remain readable to authorized owners through a read-only grammar. Detail returns the original settings/hash, compatible:false, structured issues with bilingual messages, and proposedSettings when a structural upgrade is safe. Attendance settings require explicit selection of new measures and review of class filters; completeness missing-field filters also require review. Opening never writes or increments revision. Metadata-only edits and duplication retain version 1; an explicit settings save must use version 2. Old settings cannot enqueue or execute. Old result definitions/manifests fail with REPORT_RUN_STALE / DEFINITION_CHANGED before cells are served.

5. Attendance interpretation

AttendanceReportReader traverses department calendar dates and retained authored dates (found by a loose index scan of (tenant_id, academic_year_id, date), one probe per distinct date) and loads histories in 28-day horizons for whole-school reads and in 93-day horizons for narrowed reads (a longer horizon is a superset of every shorter one inside it, and each date still filters its rows through governsSlot). A read is two phases. prepare() loads what does not depend on the students once: dimensions, traversal dates, and the schedule range of ExpectedScheduleResolver.forRange, so each timetable version's geometry is read once (Attendance §3). The prepared read(studentIds) loads the students' facts and cells; the first read also computes the coverage gaps, which later reads copy, and the first narrowed read loads the course-level facts over the whole range. With reuseSchedules, each date's slots are built once and kept for later reads; without it, each read builds them again. Report v2 prepares once per request and reads per id chunk, keeping schedules only when there is more than one chunk; read() is one prepare without reuseSchedules plus one read, so a report-card cycle or an annual run keeps no per-date schedule. Whole-school reads page each date's cells by 2,000. Narrowed reads (studentIds) load their students' cells in 28-day windows, up to school today, one unpaged, unordered statement per window and per 1,000 students: attendance_cell_unique allows one cell per student, date and start tick, so row order cannot change the per-day dedupe. It reuses composeExpectedAttendance; Reports does not hydrate annual collections of register boards.

The optional studentIds range filter narrows the student-keyed fact rows (placements, homerooms, selections, course memberships) and the cell pages in SQL. Course, combination and teacher rows stay whole, so a listed student's per-day counts equal those of an unfiltered read. loadRegisterTakenDates (same module, attendance barrel) returns per department the distinct dates with at least one cell, keyed on the department recorded on the cell. Report cards combine the two for the card attendance block; see report cards.

Participation follows dated placements and the canonical history fence: PRE_ENROLLED is excluded; departed pupils retain historical days. End-of-day placement/homeroom supplies each day's cohort, as in the register. Expected slot eligibility and selected class membership use the tick of each lesson.

Field under attendance. DAYS meaning CLASS_SLOTS meaning
presentCount Register classifies the day as present Selected slot projects PRESENT
absentCount Every authored entry is ABSENT Selected slot projects ABSENT
lateEntryCount Register classifies the day as lateEntry Selected slot projects LATE_ENTRY
earlyExitCount Register classifies the day as earlyExit Selected slot projects EARLY_EXIT
tripCount Register classifies the day as trip Selected slot projects FIELD_TRIP or DAY_TRIP
suspendedCount Register classifies the day as suspended Selected slot projects SUSPENDED
notRecordedCount Eligible school day with no authored attendance Selected slot projects null
expectedCount Eligible observed school dates Eligible observed selected lesson slots

No class selector: attendanceBasis = DAYS, unit = DAYS. countReportDay calls the register's existing classifyStudentDay over authored cells, in both DAILY and PERIOD departments. Absent → Late → Present produces Late=1, Present=0, Absent=0. Late+Early contributes one to both; suspension precedes trip, and each excludes ordinary states. Repeated entries do not multiply dates. These columns are not a partition of expectedCount.

Class selector: one audience.courseIds HAS_ANY condition, with one or more fixed or parameter-bound class IDs, selects attendanceBasis = CLASS_SLOTS and unit = SLOTS. Empty, negative and multiple class selectors are rejected. Project the full eligible day with the register's projectDayStatuses and the actual department mode, then narrow to selected eligible LESSON slots. An authored DAILY status in another class can supply the projection. PERIOD missing slots stay unknown. Activities and classes without matching lessons do not contribute.

Ordinary and combined-class attribution follows membership at the slot tick. Collect all selected contributing class names before deduplicating physical student/date/startTick occurrences. Each occurrence counts once regardless of duration. A class-selected row requires at least one matching slot; future matching slots establish row/context but every observed count remains zero.

Future dates contribute zero in either basis. A non-school date without evidence contributes no state. Retained authored evidence can contribute daily states without reconstructed expected slots; it cannot invent class attribution. Missing schedule coverage remains in coverageGaps. Not recorded is neither absence nor an overdue registration count. registrationsDue separately serves the register's owed-entry calculation.

Ungrouped summaries produce one row per student. Explicit department/grade/homeroom groups produce student/cohort subtotals; measure conditions apply after each subtotal is accumulated. Population conditions run before accumulation. A filtered historical dimension requires its scalar group name when grouped, otherwise its output-only history.departmentNames, history.gradeNames or history.homeroomNames list. Current cohort names never label ungrouped annual totals. Independent lists do not imply pairwise relationships.

audience.courseNames contains the selected contributors actually represented by that row, including future zero-count context. Historical lists are unique and deterministically ordered, with bounded accumulation. Adding filter context never introduces an implicit group.

6. Durable generation and frozen result

Three required-tenant models: SavedReport (private reusable settings), ReportRun (durable submission/lifecycle) and ReportRunChunk (attempt-private rows). RLS uses the normal tenant GUC. The run's savedReportId is a soft source reference: deleting settings makes an existing sourced run stale, while owner deletion cascades settings/runs/chunks. Full tenant reset explicitly deletes runs/settings because the administrator may survive reset.

The registered worker sweeps every 5 seconds unless NODE_ENV=test or REPORT_WORKER_ENABLED=false. It scans ordinary tenant IDs in rotating batches of 25, enters each tenant GUC, performs maintenance and claims the oldest eligible job. One job generates per process. An advisory transaction lock plus report_runs_one_running_tenant enforces one RUNNING row per tenant across replicas.

Claims increment attemptCount and claimVersion, carry a 60-second lease, renew every 10 seconds and have a 360-second attempt deadline. The source read/staging phase has a 300-second REPEATABLE READ transaction. Every source query receives that handle. Checkpoints yield between batches; heartbeat runs separately. The source transaction inserts versioned chunks but never updates the parent run, avoiding conflict with the heartbeat.

Publication uses a separate short transaction: fresh access/source-settings check, complete manifest/chunk validation, version/lease/deadline compare-and-swap, READY and generated audit atomically. Only publishedClaimVersion can be served. A crash after staging leaves invisible chunks; a recovered attempt cannot publish through the old claim. Known transient connection/serialization errors and expired leases retry up to three attempts with 10/30-second backoff. Source/attempt timeout, invalid settings and exhausted attempts fail with safe codes. Queue lifetime is 24 hours.

Chunk zero holds the immutable schemaVersion-2 manifest, including bilingual presentation metadata, attendanceBasis and requiredColumns: resolved context, ordered schema, source-scope evidence, counts, snapshot time, configuration hash and coverage gaps. Rows retain stable keys and positional values. Preview reads exact ordinal windows; exporting reads all published rows. Source edits never rewrite a READY table. A substantive saved-source edit/deletion or mismatching expectedConfigurationHash invalidates delivery; rename/description/preferred-format edits do not.

READY is available for 30 minutes from publication. Access enforces expiresAt independently of cleanup. Tenant maintenance changes expired runs to EXPIRED, clears settings/bindings/actor/context/schema/source pointers and deletes chunks. Failed payloads and abandoned versions are also scrubbed. Minimal terminal metadata remains for 24 hours, then rows are deleted. Saved reusable configurations persist until explicitly deleted. Disabling all workers also disables automatic cleanup and generation; access expiry still applies.

7. Budgets and export

Budget Configured value
Columns / filters / explicit sorts 64 / 40 / 3
Selected IDs / settings plus bindings 5,000 / 256 KiB
Complete result 100,000 rows and 32 MiB serialized rows plus manifest metadata
Chunk Up to 500 rows and 512 KiB
Preview page 50 default; 200 maximum
Source / attempt 300 seconds / 360 seconds
XLSX rendering 120 seconds; 256 MiB worker old-generation heap
Export output / materialized concurrency 64 MiB / one per process

Limits fail the job/export; no truncation. Ordinary people readers use 500-row batches. Output is materialized for final sorting/chunking. Attendance stores daily working facts and summary accumulators, not an annual cell matrix. Canonical completeness and teacher budget readers still materialize their school cohort; 28-day history batches scale with cohort/churn. Account for these materialized cohorts and history batches when sizing representative workloads.

Export accepts optional locale (en_US/it_IT), defaulting to the run preference. It localizes only the frozen metadata and canonical rows; switching language does not change hash, ordering, source snapshot or expiry. Export uses @NoTenantTx: read and authorize under GUC, close transaction, render, re-authorize expiry/ownership/saved hash/current grants and audit under a new GUC transaction, then return the attachment. A busy process responds 429 with Retry-After: 5. Export failure leaves READY data available for another attempt or CSV.

CSV uses UTF-8 BOM, commas, CRLF, escaped quotes/newlines, ISO dates, localized Yes/No and newline-separated lists. Null becomes an empty cell. Dangerous text/control/formula prefixes receive an apostrophe; numeric negative values remain numbers. CSV cannot guarantee spreadsheet type inference or distinguish null from empty text. XLSX uses the pinned SheetJS dependency, one Results sheet and typed dates/numbers/text IDs; boolean display uses localized text; no formulas or hyperlinks are generated.

The local renderer thread receives plain columns/rows only, env: {}, no inherited execArgv and its heap/output/time limits. The backend artifact must contain dist/src/reports/export/report-xlsx.worker.js beside the compiled exporter. No database transaction or connection spans encoding or client transfer. The delivery audit records approved file delivery, not proof that a browser completed saving bytes.

8. Extending and operating Reports

Add a definition/field through the explicit catalogue, owning reader, source grants, DTO/labels and relevant coverage. Preserve old keys/semantics; semantic changes require a new definition version and a stored-settings compatibility decision. Do not expose Prisma reflection, new joins or custom-field expansion implicitly.

Persisted settings and results are versioned contracts. Coordinate incompatible worker/client changes across replicas: pause admission, drain or stop incompatible workers, deploy matching readers/writers and resume supported workers. Never reinterpret stored chunks by changing only their version number. Preserve reusable settings through an explicit compatibility decision; unsupported results require regeneration.

The renderer's compiled worker must ship with the backend. Worker enablement controls both generation and automatic cleanup, while expiry checks remain enforced by the API. Source transaction budgets, result limits and export limits live in reports.config.ts and src/common/constants/tx-budgets.ts; adjust them only with representative source and rendering workloads.

9. Live reports (src/report-v2)

Live reports run beside the run-based module and share only the reports entity, its configuration scope, its create/delete/export actions and the saved_reports table. Nothing in src/report-v2 imports src/reports. A live report is a synchronous query over one population of the active year: no run, no frozen result, no worker lease.

Concern Owner
Admission, fresh authority, scope fingerprint report-v2.access.ts
Populations, fields, presets populations/ (registry.ts, students.population.ts, students-attendance.ts, teachers.population.ts, staff.population.ts, presets.ts)
Query engine, validation, guards, read cache engine/
Catalogue and facet options catalogue.ts, facets.service.ts
Saved configurations report-v2.queries.ts, saved/report-v2-saved.service.ts
Export export/

Routes. All live under /report-v2 and need the admin view. GET catalogue, GET :population/facets/:facet, POST query, GET saved and GET saved/:id need reports.configuration READ; PATCH saved/:id needs reports.configuration WRITE; POST saved and POST saved/:id/duplicate need the create action; DELETE saved/:id needs delete; POST export needs export. Responses carry Cache-Control: private, no-store.

Admission. ReportV2AccessService.resolve re-reads the active user, the admin assignment and the compiled admin permissions on every request, so a cache hit never skips authorization. A field is readable only when the caller holds every one of its source scopes; unreadable fields are absent from the catalogue and refused in a query, a saved configuration or an export. Health fields (lunch, diet) need students.health for the column and for the header filter alike. In the referents field, the guardian and the referent email and mobile need referents.contacts, and the referent home phone needs referents.private_data; without them the contact keeps its name and relationship and the missing values are null. The guardian home phone is never exposed.

Populations. students, teachers and staff are registered in PopulationRegistry. A population is a base(ctx) reader that returns the rows of the active year and an enrich(ctx, rows, query) reader that adds the expensive fields (referents, siblings, attendance, completeness) only when they are visible, filtered or sorted, and only for the rows left after hierarchy narrowing. Hierarchy (department, curriculum, grade, homeroom) is evaluated on the reference date: the placement and the homeroom membership effective that day. A student without one leaves the list as soon as any hierarchy level is selected. The reference date is the attendance day (day mode), the period end (period mode), the explicit referenceDate, or the school today, in that order. Facet options (departments, curricula, grades, homerooms, nationalities) cascade on the selected parents but are year-scoped and take no referenceDate; a facet is readable exactly when the field it serves is.

Population Fields and source scopes
students name, dateOfBirth: students.identity; nationality: students.private_data; code, department, curriculum, grade, homeroom, status, enrolment date: students.assignment (curriculum also homerooms.composition); childOfEmployee, hasSiblings: students.school_relationships; referents: students.referents_and_guardians and referents.identity, contact values as above; lunch, diet: students.health; attendance fields: attendance.register; missingFields: no source scope
teachers identity: teachers.identity; employment, workload, contract dates and availableDays: teachers.employment; subjects: subject_groups.composition; homerooms taught: subject_groups.composition and homerooms.composition
staff identity: staff.identity; role, workload and contract: staff.employment

The authoritative list is the catalogue (GET catalogue), which already omits every field the caller cannot read. availableDays stores day codes in school-week order with localized labels and accepts a single EQ day. missingFields counts the required fields still empty with the command-center completeness rules, only for enrolled students, and only over the data the caller can read.

Attendance. Day codes follow the register: dayCodes() (src/attendance/reporting/day-codes.ts) maps classifyStudentDay to P, A, R, U, D, G, S, telling a field trip (D) from a day trip (G) through the authored cells; a late entry with an early exit is {R, U}; suspension and trips replace every other code. A day that was not taken has no code: it is the empty set, never null. Justification is a separate flag and the register's own value, read from the live AttendanceDayEvent of the day: ACKNOWLEDGED and NOT_REQUIRED are justified; UNDER_REVIEW (also within grace), NOTIFIED and FOLLOW_UP are to justify; with no live event (none, or only CANCELLED and SUPERSEDED), and on U, P, D, G and S days, it is null. Operators on the code set: EQ contains the code, NE does not contain it, IN contains at least one; IN with an empty code list means no code filter. A not-taken day matches NE and never EQ or IN. In period mode the range is at most 93 days, both ends included, and each student gets the att.P to att.S counts, att.justified, att.toJustify and att.days (school days from expectedCount, not calendar days); a student matches when at least one day carries a selected code. The att.* columns are sortable and present only in period mode.

Presets. A preset is a pre-filled configuration of the same population (attendance block, filters, columns, sort), not a report type. Relative dates resolve on the school clock when the catalogue is built (today, or the 14 days ending today). A preset is offered only when the caller could run it unchanged under the same validation as a query, so a missing scope hides it.

Operational design. POST query and POST export hold no request transaction (@NoTenantTx()). Each read runs in a short tenant-GUC transaction under TX_BUDGETS.REPORT_V2 with a statement timeout, in three phases: fresh authority and school clock on every request; on a read-cache miss, base rows; then enrichments for the narrowed rows. Filtering, sorting, paging and rendering happen after the transactions close. A 10 s deadline is checked between batches. A statement cancelled by the statement timeout or a phase transaction that expires answers REPORT_V2_LIMIT_EXCEEDED DEADLINE; a phase that cannot get a connection within its maxWait answers BUSY. Rows after narrowing and enrichment are cached per user for 60 s, keyed by tenant, year, population, reference date, hierarchy, attendance block, needed fields and the caller's scope fingerprint, so header filters, sort and page reuse them; a newer request supersedes an older one, and cached arrays are copied before sorting. A cache miss takes one of three heavy-read slots per process, and a full set answers 429 with Retry-After. REPORT_V2_MAX_ROWS (default 10000) caps the narrowed population. POST query and the facet route allow 300 requests per minute per client: the throttler tracks the request IP, so callers behind one address share the budget; a page holds at most 500 rows.

Errors. Live reports use two codes of their own; the run-based codes keep their behaviour. REPORT_V2_SETTINGS_INVALID (422) carries one leaf per issue under data.issues, each with the setting path as field and the localized messages chosen by its reason (unknown or unreadable field, health field without students.health, wrong operator or value, invalid date, day column without an attendance day, unordered or too long period). REPORT_V2_LIMIT_EXCEEDED picks its sentence from reason: ROWS and DEADLINE answer 422, BUSY answers 429 with Retry-After. Saved revision conflicts reuse REPORT_CONFIGURATION_CONFLICT (409), a busy export slot REPORT_EXPORT_BUSY, a failed render REPORT_EXPORT_FAILED.

Saved configurations. Rows use reportType v2:<population>, definitionVersion 1 and preferredFormat XLSX; settings is the query body without population and page. With attendance.dateIsParam true the configuration is stored without its dates, which are asked each time it runs. They are private to their owner, updated and deleted at an expected revision (a stale revision answers 409 REPORT_CONFIGURATION_CONFLICT), and audited as saved_report.created, saved_report.updated and saved_report.deleted. Opening a configuration that refers to a field the caller can no longer read returns it with issues instead of failing. A population can list fields it removed on purpose in retiredFields (students: siblingNames): opening drops them from columns, filters and sort before validation, without an issue, and the migration that retires a field removes it from the stored rows; a query or export that still names one answers as for any unknown field. Both modules share the table, so each filters by prefix: the live queries include only v2: rows, and the run-based module reads and writes saved reports through legacySaved() in src/reports/saved/saved-reports.queries.ts, which excludes them. That guard covers list, get, update, duplicate and delete of the old routes; owned() stays plain because create and ReportRun also use it. The guard is removed with the run-based module.

Export. XLSX and CSV contain every row and the visible columns of the current configuration, rendered outside any transaction in a worker-thread renderer with the same heap and byte limits as §7. One 25 s budget, below the orchestrator proxy timeout, covers read, render and audit, and is measured from the moment the export takes its slot; an overrun answers REPORT_EXPORT_FAILED with reason TIMEOUT. Live exports have their own busy slot, one per process, separate from the run-based export; a second request answers 429 REPORT_EXPORT_BUSY. Each delivery re-checks authority and records report_v2.exported (entityType: 'report_v2') with population, format, row count, saved report id and the sensitive scope categories of the file, never names, settings, filter values or cells.