Skip to content

Commit 5aaf3b5

Browse files
committed
Add documentation for user-defined functions in DAX, covering syntax, parameter types, optional parameters, model dependency, and comparison with calculation groups.
1 parent f90ef8b commit 5aaf3b5

7 files changed

Lines changed: 455 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
layout: page
3+
title: Returning CALCULATE modifiers
4+
published: true
5+
order: /04
6+
---
7+
8+
Because UDFs expand as macros, a function body can contain expressions that are only syntactically valid in a specific calling position. In particular, a function can "return" a CALCULATE modifier — provided the function is always called as a filter argument of `CALCULATE` or `CALCULATETABLE`.
9+
10+
## Mechanism
11+
12+
`REMOVEFILTERS`, `USERELATIONSHIP`, and other CALCULATE modifiers are not valid as standalone expressions; they are only valid inside `CALCULATE` or `CALCULATETABLE`. When a UDF whose body is a CALCULATE modifier is placed inside `CALCULATE` or `CALCULATETABLE`, macro-expansion substitutes the modifier directly into the correct position, producing a valid expression.
13+
14+
## Pattern 1: function returns a modifier directly
15+
16+
```dax
17+
DEFINE
18+
FUNCTION Gregorian.RemoveFilterKeepColumns = () =>
19+
REMOVEFILTERS (
20+
'Date'[Day of Week],
21+
'Date'[Day of Week Number],
22+
'Date'[Day of Week Short]
23+
)
24+
```
25+
26+
Called as a CALCULATE filter argument:
27+
28+
```dax
29+
CALCULATE (
30+
MAX ( 'Date'[Date] ),
31+
Gregorian.RemoveFilterKeepColumns ()
32+
)
33+
```
34+
35+
After macro-expansion this is equivalent to:
36+
37+
```dax
38+
CALCULATE (
39+
MAX ( 'Date'[Date] ),
40+
REMOVEFILTERS (
41+
'Date'[Day of Week],
42+
'Date'[Day of Week Number],
43+
'Date'[Day of Week Short]
44+
)
45+
)
46+
```
47+
48+
This function **cannot** be called anywhere except as a filter argument of `CALCULATE` or `CALCULATETABLE`. Its return is not a scalar or table, so any other calling position is invalid.
49+
50+
## Pattern 2: CALCULATE encapsulated inside the function
51+
52+
An alternative wraps the `CALCULATE` call inside the function and accepts the target expression as an `EXPR` parameter:
53+
54+
```dax
55+
DEFINE
56+
FUNCTION Gregorian.ComputeRemovingFilterKeepColumns = ( formulaExpr : EXPR ) =>
57+
CALCULATE (
58+
formulaExpr,
59+
REMOVEFILTERS (
60+
'Date'[Day of Week],
61+
'Date'[Day of Week Number],
62+
'Date'[Day of Week Short]
63+
)
64+
)
65+
```
66+
67+
Called as:
68+
69+
```dax
70+
Gregorian.ComputeRemovingFilterKeepColumns ( [Sales Amount] )
71+
```
72+
73+
This pattern returns a scalar and has no calling-position restriction. The choice between the two patterns is a matter of design preference; Pattern 1 is more composable when the same modifier needs to appear in many different `CALCULATE` calls with different expressions.
74+
75+
## Practical use case
76+
77+
A common application is centralizing the list of **filter-keep columns** — columns on a date table whose filters must be removed when computing a reference date, but preserved by time intelligence functions applied afterward. Placing the `REMOVEFILTERS` list in a function prevents it from being duplicated across many measures and simplifies maintenance when the column list changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
layout: page
3+
title: User-defined functions (UDF)
4+
published: true
5+
order: /
6+
next_reading: true
7+
next_reading_title: false
8+
---
9+
10+
A DAX user-defined function (UDF) is a named, reusable expression declared with the `DEFINE FUNCTION` statement. UDFs are not subroutines invoked on a call stack; they expand inline at the call site as macros. The function body is substituted wherever the function is called, which means that expressions only valid in certain positions — such as CALCULATE modifiers — can be encapsulated inside a function and remain valid after expansion.
11+
12+
UDFs can be defined at query level inside a `DEFINE` block or stored in a semantic model and shared across all measures and calculated columns in that model.
13+
14+
Related SQLBI articles:
15+
- [Introducing user-defined functions in DAX](https://www.sqlbi.com/articles/introducing-user-defined-functions-in-dax/)
16+
- [Model-dependent and model-independent user-defined functions in DAX](https://www.sqlbi.com/articles/model-dependent-and-model-independent-user-defined-functions-in-dax/)
17+
- [Understanding parameter types in DAX user-defined functions](https://www.sqlbi.com/articles/understanding-parameter-types-in-dax-user-defined-functions-udf/)
18+
- [UDFs vs. calculation groups](https://www.sqlbi.com/articles/dax-user-defined-functions-udf-vs-calculation-groups/)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
layout: page
3+
title: Model dependency
4+
published: true
5+
order: /05
6+
---
7+
8+
A UDF is **model-dependent** when its body references model objects directly — tables, columns, measures, or calendars — by name. It is **model-independent** when all such references arrive exclusively through parameters, making the function portable across semantic models without modification.
9+
10+
## Hidden dependencies
11+
12+
A function accepting a `TABLE` or `TABLE EXPR` parameter may appear model-independent while harboring implicit structural dependencies. If the function body accesses specific column names inside that table (e.g., `tableParam[Quantity]`), it implicitly requires those columns to exist in whatever table the caller passes. To make the function truly model-independent, expose each required column as a separate `COLUMNREF` or `ANYREF` parameter.
13+
14+
## Naming conventions
15+
16+
### Function names
17+
18+
Use dot notation to namespace functions and communicate their intended scope:
19+
20+
| Prefix | Meaning |
21+
|---|---|
22+
| `Local.` | Model-specific; not intended for sharing across models |
23+
| `<LibraryName>.` | Distributed library function (e.g., `DaxPatterns.LikeForLike.EntityStatus`) |
24+
25+
### Parameter names
26+
27+
Use camelCase with a type-indicating suffix when the parameter type carries semantic meaning:
28+
29+
| Suffix | Parameter type |
30+
|---|---|
31+
| `Expr` or `Measure` | EXPR scalar or `MEASUREREF` |
32+
| `Column` | `COLUMNREF` |
33+
| `Table` | `TABLEREF` or `TABLE EXPR` |
34+
| `Calendar` | `CALENDARREF` |
35+
36+
Example: `( salesMeasure : MEASUREREF, dateColumn : COLUMNREF, salesTable : TABLEREF )`
37+
38+
## Validating column parameters
39+
40+
When a function accepts multiple `COLUMNREF` parameters that must belong to the same table, use `TABLEOF` and `NAMEOF` to verify at runtime:
41+
42+
```dax
43+
IF (
44+
NAMEOF ( TABLEOF ( col1 ) ) <> NAMEOF ( TABLEOF ( col2 ) ),
45+
ERROR ( "col1 and col2 must belong to the same table" )
46+
)
47+
```
48+
49+
This pattern does not work reliably today: when the column parameters are used incorrectly in the function body, DAX generates its own internal error from that usage before the `IF`/`ERROR` validation code executes, hiding the custom error message. See the same limitation noted in [Parameter types / Introspection functions](parameter-types.md#introspection-functions).
50+
51+
## Portability pattern
52+
53+
Wrapping a model-independent function in a thin model-dependent function is a common pattern to simplify call sites without sacrificing the underlying function's portability. The model-independent function is shared (e.g., via a library); the model-dependent wrapper maps concrete model objects to the required parameters and exposes a shorter signature to measure authors.
54+
55+
Model-independent functions can be published to and consumed from shared DAX libraries such as [daxlib.org](https://daxlib.org).
56+
57+
See [Model-dependent and model-independent user-defined functions in DAX](https://www.sqlbi.com/articles/model-dependent-and-model-independent-user-defined-functions-in-dax/).
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
layout: page
3+
title: Optional parameters
4+
published: true
5+
order: /03
6+
---
7+
8+
A parameter is optional when its declaration includes a **default expression**. When the caller omits the argument, DAX evaluates the default expression and uses its result as the parameter value.
9+
10+
## Syntax
11+
12+
```
13+
<ParameterName> [ : <TypeHint> ] = <DefaultExpression>
14+
```
15+
16+
A parameter without `=` is mandatory; a parameter with `=` is optional.
17+
18+
```dax
19+
DEFINE
20+
FUNCTION Increment = ( x : NUMERIC, y : NUMERIC = 1 ) => x + y
21+
```
22+
23+
`x` is mandatory; `y` is optional with a default of `1`.
24+
25+
## Omitting arguments
26+
27+
### Trailing omission
28+
29+
Optional parameters at the end of the parameter list are omitted by stopping the argument list early:
30+
31+
```dax
32+
Increment ( 3 ) -- y uses default 1; returns 4
33+
Increment ( 10, 20 ) -- y is 20; returns 30
34+
```
35+
36+
### Skipping a middle parameter
37+
38+
To skip an optional parameter that is not the last one, write an **empty position** — a comma with no value before it:
39+
40+
```dax
41+
IncrementLimit ( 5, , 20 ) -- y uses its default; limit is 20
42+
```
43+
44+
## Position and arity
45+
46+
Optional parameters can appear in any position in the signature; required parameters can follow optional ones. Callers can always reach a required parameter by leaving an empty position for each optional parameter before it (`MyFunc ( 1, , 3 )` omits the second argument).
47+
48+
The minimum number of arguments a caller must supply — the function's arity — is determined by the **position of the rightmost required parameter**. If a function has three parameters and only the second is optional, callers must still supply at least three arguments: there is no way to omit the third, because it is required and comes last.
49+
50+
The recommended practice is: **once a parameter is optional, all following parameters should also be optional.** Placing a required parameter after an optional one forces callers to write empty positions just to reach it, which is harder to read and easy to get wrong.
51+
52+
## Detecting absent arguments with BLANK
53+
54+
When no fixed value is a natural default, use `BLANK()` as the default expression and test with `ISBLANK` inside the function body:
55+
56+
```dax
57+
DEFINE
58+
FUNCTION RoundDivision =
59+
(
60+
x : NUMERIC,
61+
y : NUMERIC,
62+
digits = BLANK ()
63+
) =>
64+
VAR Result = DIVIDE ( x, y )
65+
RETURN
66+
IF (
67+
ISBLANK ( digits ),
68+
Result,
69+
ROUND ( Result, digits )
70+
)
71+
```
72+
73+
**Limitation:** The function cannot distinguish an omitted argument from an explicitly passed `BLANK()`. `RoundDivision ( 2, 3, BLANK() )` is indistinguishable from `RoundDivision ( 2, 3 )`.
74+
75+
## Rules for default expressions
76+
77+
### Context
78+
79+
Like the function expression body, a default expression inherits the **filter context of the caller** but does **not** inherit any row context. It is evaluated as if written at the call site, outside any iterator.
80+
81+
### Scope
82+
83+
A default expression can only reference names — columns, tables, measures, variables, functions — that are **visible at the point where the UDF is defined**, not where it is called. It cannot reference another parameter of the same function.
84+
85+
### Type
86+
87+
Type checking of the default expression against the parameter's type hint is enforced only **when the default is used** (i.e., the caller omitted the argument). When the caller provides an explicit argument, the type hint is applied to that argument instead.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
layout: page
3+
title: Parameter types
4+
published: true
5+
order: /02
6+
---
7+
8+
Each parameter has two independent properties: a **data type** (what expressions it accepts) and a **passing mode** (how the argument is transferred from call site to function body).
9+
10+
## Passing modes
11+
12+
### VAL
13+
14+
The argument is evaluated once, in the evaluation context of the caller, before the function body runs. The parameter behaves like a `VAR` bound to that value. Changes in filter or row context inside the function body have no effect on the parameter's value.
15+
16+
Semantic equivalent for a call `F( SUM ( Sales[Quantity] ) )` with parameter `qty : VAL`:
17+
18+
```dax
19+
VAR qty = SUM ( Sales[Quantity] )
20+
RETURN <body>
21+
```
22+
23+
### EXPR
24+
25+
The argument expression is captured unevaluated and substituted inline at every reference to the parameter inside the function body. The expression is re-evaluated each time it is referenced, in the evaluation context present at that point in the body.
26+
27+
Semantic equivalent for a call `F( SUM ( Sales[Quantity] ) )` with parameter `qty : EXPR`:
28+
29+
```dax
30+
-- every reference to qty in the body becomes SUM ( Sales[Quantity] )
31+
```
32+
33+
**Context transition:** EXPR parameters do **not** receive automatic context transition in row contexts, unlike a measure reference. To ensure correct behavior inside iterators, wrap the parameter reference in CALCULATE:
34+
35+
```dax
36+
CALCULATE ( paramExpr )
37+
```
38+
39+
`MEASUREREF` (see below) is the only reference type that guarantees context transition automatically.
40+
41+
## Data types
42+
43+
| Type | Accepts | Passing mode | Notes |
44+
|---|---|---|---|
45+
| `ANYVAL` | Any scalar or table | VAL (default) | Default when no type hint is specified |
46+
| `SCALAR` | Scalar expressions only | VAL or EXPR | Accepts a subtype to restrict the data type |
47+
| `TABLE` | Table expressions only | VAL or EXPR | |
48+
| `ANYREF` | Any expression | EXPR (forced) | No semantic guarantee on the expression kind |
49+
| `MEASUREREF` | Measure references only | EXPR (forced) | Guarantees context transition in row contexts |
50+
| `COLUMNREF` | Column references only | EXPR (forced) | Enables model-independent column access |
51+
| `TABLEREF` | Model table references only | EXPR (forced) | Provides full column and relationship access |
52+
| `CALENDARREF` | Calendar references only | EXPR (forced) | Intended for time intelligence functions |
53+
54+
`ANYREF`, `MEASUREREF`, `COLUMNREF`, `TABLEREF`, and `CALENDARREF` force EXPR passing mode and cannot be declared as VAL.
55+
56+
### Scalar subtypes
57+
58+
`SCALAR` can be qualified with a subtype that restricts the accepted data type and enables automatic coercion:
59+
60+
| Subtype | Accepts |
61+
|---|---|
62+
| `VARIANT` (default) | Any scalar data type |
63+
| `INT64` | Integer |
64+
| `DECIMAL` | Fixed-decimal number |
65+
| `DOUBLE` | Floating-point number |
66+
| `NUMERIC` | Any numeric type (INT64, DECIMAL, DOUBLE) |
67+
| `STRING` | Text |
68+
| `DATETIME` | Date or timestamp |
69+
| `BOOLEAN` | True/False |
70+
71+
Coercion applies independently to each parameter; it does not propagate across parameters.
72+
73+
## Type declaration syntax
74+
75+
```
76+
<ParameterName> : <Type> [<Subtype>] [<PassingMode>]
77+
```
78+
79+
When only a passing mode is written without a type, the type defaults to `ANYVAL`:
80+
81+
```dax
82+
FUNCTION F = ( a : VAL, b : EXPR ) => ...
83+
```
84+
85+
## Type checking
86+
87+
Type checking and coercion apply differently depending on the parameter category:
88+
89+
**Scalar subtypes** (`INT64`, `DECIMAL`, `DOUBLE`, etc.) do not reject incompatible arguments — they coerce them. Each argument is independently converted to the declared type before the function body runs. No error is raised; see the coercion note under *Scalar subtypes* above.
90+
91+
**Reference types** (`MEASUREREF`, `COLUMNREF`, `TABLEREF`, `CALENDARREF`) perform genuine type checking at call time. Passing an incompatible expression produces an error that identifies the expected and received types, for example: *"An invalid argument type was passed into parameter 'amountMeasure' of the user-defined function. Expected 'MEASUREREF' but got 'SCALAR'."* There is a known limitation for `COLUMNREF`: when a column reference is invalid, the internal syntax error from the function body surfaces before any custom validation error the function author may have written.
92+
93+
**`ANYREF`** performs no type checking. An incompatible argument may produce confusing errors deep inside the function body or, in the worst case, incorrect results with no error at all. Functions using `ANYREF` must handle the general case defensively — for example, wrapping every reference to the parameter in `CALCULATE` to guarantee context transition regardless of what was passed.
94+
95+
## Introspection functions
96+
97+
Two functions are available for use inside a function body with `COLUMNREF` or `TABLEREF` parameters:
98+
99+
- **`TABLEOF ( columnRef )`** — returns the table in which the referenced column is defined.
100+
- **`NAMEOF ( columnRef )`** — returns the column's fully qualified name as a string.
101+
102+
These functions are intended to support runtime validation — for example, checking that two `COLUMNREF` parameters belong to the same table:
103+
104+
```dax
105+
IF (
106+
NAMEOF ( TABLEOF ( col1 ) ) <> NAMEOF ( TABLEOF ( col2 ) ),
107+
ERROR ( "col1 and col2 must belong to the same table" )
108+
)
109+
```
110+
111+
This pattern does not work reliably today: when the column parameters are used incorrectly in the function body, DAX generates its own internal error from that usage before the `IF`/`ERROR` validation code executes, hiding the custom error message. The intent is correct and the pattern is expected to work once the evaluation order is enforced.
112+
113+
See [Understanding parameter types in DAX user-defined functions](https://www.sqlbi.com/articles/understanding-parameter-types-in-dax-user-defined-functions-udf/).

0 commit comments

Comments
 (0)