Filter context & evaluation
Understanding one idea — the effective filter context — explains everything about how measures behave.
The effective context
Section titled “The effective context”When a chart asks for a measure, the value is computed under filters layered in a fixed order:
- The model’s join tree (relationships).
- Row-level security — structural, applied beneath everything; no measure can remove it.
- Dashboard, page, and chart filters — what the viewer has applied.
- The chart’s grouping (its grain: the buckets on its axes).
- The measure’s own modifiers, per
Calculate, in category order:RemoveFiltersfirst, then added predicates, thenDateShift.
Steps 1–4 are why one measure definition works everywhere: the context comes from wherever it’s used. Step 5 is the part you control in the language.
Removals before additions: the override idiom
Section titled “Removals before additions: the override idiom”Inside one Calculate, the written order of modifiers doesn’t matter —
removals always apply first. That’s a feature:
// West revenue regardless of any dashboard Region filterWestRevenue := Calculate([Revenue], Sales.Region = "West", RemoveFilters(Sales.Region))The outer Region filter is lifted, then Region = "West" is added. (If
additions ran first, the RemoveFilters would delete the filter you just
wrote.) Without the RemoveFilters, the added predicate intersects:
an outer Region = "East" ∩ Region = "West" is empty ⇒ blank. Both
behaviors are deliberate — intersect by default, override only when you
explicitly remove.
Nested Calculate composes inside-out: inner modifiers see the context the
outer ones produced. A Fixed inside a Calculate sees the modified filter
set, and Fixed’s own dimensions are structural — a RemoveFilters inside a
Fixed body can’t collapse the Fixed grain.
Aggregate closure — why there’s no row context
Section titled “Aggregate closure — why there’s no row context”A measure must reduce to one value per group. A bare Table.Column may
appear only:
- as an aggregation argument —
Sum(Sales.Amount); - inside a
Calculatepredicate —Sales.Status = "Open"; - as a
Fixedor modifier argument —Fixed(Customer.Id, …),RemoveFilters(Sales.Region); - as a column-typed parameter — the
dateColofYTD/RollingSum.
Anywhere else, a bare column is row-level logic and fails with SQX014, pointing you to calculated fields. This is the deliberate removal of DAX’s row context / context transition — the single hardest thing to learn there simply doesn’t exist here.
NULL & type semantics
Section titled “NULL & type semantics”SquareX uses SQL semantics, not invented ones:
| Situation | Result |
|---|---|
| Aggregate over NULLs | NULLs skipped; Sum/Avg/Min/Max over zero rows ⇒ blank; Count/CountDistinct ⇒ 0 |
| Arithmetic with a blank | blank (NULL + 5 is NULL — DAX’s BLANK + 5 = 5 is deliberately not replicated) |
| Comparison with a blank | never matches a filter; and/or/not follow SQL three-valued logic |
Blank in & concat |
blank, on every database |
| Division by zero | error with /; blank (or fallback) with Divide |
"5" + 1 |
type error (SQX006) — no implicit coercion |
| Want zeros instead of blanks? | Coalesce([Measure], 0) |
A blank measure value renders as an empty cell; number formatting may display it as 0, but the underlying value stays blank through the API.
Security guarantee
Section titled “Security guarantee”RLS predicates are injected beneath every part of a compiled measure —
Calculate siblings, Fixed groups, DateShift windows all build on
RLS-filtered data, and RemoveFilters can only see user filters. This is
pinned by tests in the engine, not just policy.