Skip to content

Filter context & evaluation

Understanding one idea — the effective filter context — explains everything about how measures behave.

When a chart asks for a measure, the value is computed under filters layered in a fixed order:

  1. The model’s join tree (relationships).
  2. Row-level security — structural, applied beneath everything; no measure can remove it.
  3. Dashboard, page, and chart filters — what the viewer has applied.
  4. The chart’s grouping (its grain: the buckets on its axes).
  5. The measure’s own modifiers, per Calculate, in category order: RemoveFilters first, then added predicates, then DateShift.

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 filter
WestRevenue := 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 argumentSum(Sales.Amount);
  • inside a Calculate predicateSales.Status = "Open";
  • as a Fixed or modifier argumentFixed(Customer.Id, …), RemoveFilters(Sales.Region);
  • as a column-typed parameter — the dateCol of YTD/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.

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.

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.