Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Report AST

The normalized shape every report source format parses into — PBIR (definition/ folders) and PBIR-Legacy (report.json Layout). Power BI facts the type definitions cannot state on their own; read this before changing the AST or adding an ingestion format.

What is modeled, and what is not

Modeled: report identity and its dataset link, pages (filters, drillthrough/tooltip bindings, visuals), visuals (field wells, filters, sort-by, conditional formatting, tooltip-page references), bookmarks (saved filters and active projections), report-level measures (reportExtensions.json), and the phone layout's pages (definition.mobile/, issue #49).

Deliberately absent: page order and the active/landing page (pages.json — the one exception, pageOrder, is read at ingest as a bookmark-section liveness authority, but never modeled), themes and resource packages, semanticModelDiagramLayout.json, and every literal value a filter or slicer selection persists.

The phone layout is not in that list, but it is a near miss worth spelling out. Report/definition.mobile/ mirrors definition/ page for page and binds the same model — phone users see those visuals — so its pages are parsed into ReportModel::mobile_pages and enumerate as roots exactly like desktop pages. Only the page/visual tree is read there: no report anchor, report measures, or bookmarks. What stays absent is the per-visual mobile.json inside the desktop tree (visualContainerMobileState): a visual's phone position and styling. It carries no field projections of its own — its objects selectors, where they occur at all, name fields the same visual's visual.json already binds (verified across the sample corpus) — so it is skipped silently. Themes, resource packages, and the diagram layout never reference model objects either; none of the absent artifacts can keep one alive. Adding any of them later is additive — but do not add them speculatively.

Power BI semantics the types don't show

One ReportModel per report, not one per project. A semantic model is often shared by several reports (thin reports, deployment pipelines). Reachability takes the union of all reports' bindings, so the graph core does not care — but provenance does. A BindingRef carries page/visual/bookmark/kind; which report is answered by the ReportModel the binding was enumerated from, and that report's name completes the "used by" explanation.

A slicer is not a kind of binding. A slicer is a visual with visual_type: "slicer"; its field well is the binding. Saved slicer selections are literal values (data), not references.

Bindings are written-form, never resolved. PBIR binds structured JSON entity trees (Column/Measure/HierarchyLevel/Aggregation); legacy Layout binds written names. Both normalize into FieldTarget, which keeps the column-vs-measure discrimination the entity states outright. Resolution against ModelIndex is the graph layer's job; a Measure target should resolve against report measures first — within its report, a report measure shadows a model measure of the same name — then the model.

Query aliases are the parser's problem. PBIR filter condition trees introduce aliases (From: [{Name: "p", Entity: "Product"}], then SourceRef: {Source: "p"}). By the time a filter reaches this AST, Filter::references holds alias-resolved targets. Anything the parser cannot resolve lands in FieldTarget::Written — kept rather than dropped, because a binding we cannot read is still a binding.

Hidden is not dead. Hidden pages and locked/hidden filters still apply; every flag here is display-only, never liveness. Inactive projections bind too — they are one toggle away from live.

Bookmarks are roots. Applying a bookmark re-applies its saved filters and projections, so a field kept alive only by a bookmark is still alive. Bookmark bindings enumerate with bookmark set, alongside the page and visual they captured. One bound: a section whose page the report no longer defines is not a binding — Power BI leaves deleted pages' sections inside bookmarks forever, and a filter on a page nobody can reach would keep its columns alive with no way to re-apply it. Those sections are dropped at ingest (see the bookmark staleness rule in formats.md); they never reach this AST.

Report measures bridge both directions. A report-level measure's DAX body references model objects (so ReportModel::dax_expressions is an expression source on top of TabularDatabase::dax_expressions), and visuals reference it by name. Its graph identity is ObjectId::ReportMeasure — deliberately not ObjectId::Measure, so a report measure can never be conflated with a model measure of the same name. It is a graph node, not a root: a report measure no visual binds and no other DAX names is dead, like any other object (see graph.md).

Tooltip pages are report-internal references. A visual's tooltip_page keeps a page reachable, not a model object, so it lives on the AST but never enumerates as a binding. The page's own visuals bind fields like any other page's.

Bindings hide in unobvious places

Mirroring the model side's rule for expressions: missing one binding site means the objects it references get no roots and are reported unused — a false positive the scan design forbids. ReportModel::bindings is the single enumeration; a new binding-bearing field must be added there or it is silently invisible to reachability. Beyond visual field wells, report-side roots also live in:

  • report-, page-, and visual-level filters (filterConfig) — the declared field and the fields inside the condition tree
  • drillthrough parameters (pageBinding.parameters[].fieldExpr)
  • sort definitions (sortDefinition.sort[].field)
  • conditional-formatting rule keys
  • bookmarks' saved filters — at all three levels, mirroring a live report: report-level (explorationState.filters), per page section, and per visual — and their saved projections
  • report-level measures, whose bodies consume model objects even though the measure itself is report-owned

Provenance vocabulary

A binding's site answers "where does this come from", which later powers explanations like 'Sales'[Amount] ← visual Card 1 on Sales overview (page 2) and per-report slicing:

FieldNone means
pagereport-level (report filter, or a report measure reference)
visualpage-level or report-level
bookmarka live binding, not saved state

The kind (FieldWell { role }, Filter, Sort, Drillthrough, ConditionalFormatting) says what the binding does; the role string preserves the well's name as written ("Category", "Y", "Tooltips"). The mobile flag says which surface the binding lives on — the desktop tree or the phone layout (definition.mobile/); both bind identically, the flag exists so an audit of a survivor names the right surface (issue #49).