Skip to content

Commit 2838635

Browse files
authored
docs: give macros an overview of SQL and Python macros, and a Python macros page (#1235)
1 parent 90970f2 commit 2838635

16 files changed

Lines changed: 645 additions & 507 deletions

File tree

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/CONTENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ Bundled copies of every page on the SQLBuild documentation site, matching the in
5252
- [Collections and Rendering](concepts/constants/collections-and-rendering.md) (`concepts/constants/collections-and-rendering`) - Define list, set, and object constants and control their adapter-specific SQL rendering.
5353
- [Enum Model Contracts](concepts/enums/model-contracts.md) (`concepts/enums/model-contracts`) - Use an enum as a portable model-column domain with generated accepted-value validation.
5454
- [Model-Private Values](concepts/model-private-values.md) (`concepts/model-private-values`) - Keep enums and constants inside one model when no other resource should use them.
55-
- [Writing Macros](concepts/macros.md) (`concepts/macros`) - Write Python functions that generate reusable SQL fragments at compile time.
56-
- [Composition and Context](concepts/macros/composition-and-context.md) (`concepts/macros/composition-and-context`) - Compose macros through Python, use compile context, and understand scoped imports.
55+
- [Macros](concepts/macros.md) (`concepts/macros`) - Reuse SQL with typed SQL macros, or generate it with Python macros when it needs logic.
5756
- [SQL Macros](concepts/macros/sql-macros.md) (`concepts/macros/sql-macros`) - Write typed, logic-free SQL fragments that the compiler checks and sees inside.
57+
- [Python Macros](concepts/macros/python-macros.md) (`concepts/macros/python-macros`) - Write Python functions that generate SQL at compile time when the SQL needs logic.
58+
- [Python Macro Composition and Context](concepts/macros/composition-and-context.md) (`concepts/macros/composition-and-context`) - Compose Python macros through imports, and use adapter, target, constants and enums.
5859
- [Interpolation](concepts/interpolation.md) (`concepts/interpolation`) - How SQLBuild processes variables, context, and dynamic content in SQL and config.
5960
- [Functions](concepts/functions.md) (`concepts/functions`) - User-defined functions and table functions managed as part of your project.
6061
- [Incremental Models](concepts/incremental.md) (`concepts/incremental`) - Cursor-based incremental strategies, microbatch execution, and backfill policies.

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/compile.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ files count only by presence.
129129

130130
Reuse assumes macros, hooks, providers, adapters, and custom rules are deterministic: the same inputs
131131
must produce the same output. Macro results are also reused within a compile and may be kept between
132-
compiles; see [Macros must be deterministic](../concepts/macros.md#macros-must-be-deterministic). A
132+
compiles; see [Macros must be deterministic](../concepts/macros/python-macros.md#macros-must-be-deterministic). A
133133
compile whose templates read `run.id` is never reused.
134134

135135
Compile always runs in full with `--no-cache`, `--manifest`, `--debug`, or a profiling flag, when
@@ -191,7 +191,7 @@ the whole project. Every model's SQL references are scanned again too.
191191
lexical rules, and the SQLBuild build; a damaged or unreadable entry is scanned again.
192192
- Rule results are reused for unchanged inputs.
193193
- Macro call results are reused as described in
194-
[Macros must be deterministic](../concepts/macros.md#macros-must-be-deterministic).
194+
[Macros must be deterministic](../concepts/macros/python-macros.md#macros-must-be-deterministic).
195195

196196
Either way, the output is byte-identical to `sqb compile --no-cache`. To turn compiling only the
197197
changed files off, set `delta_compile = false` under `[settings]` or set

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/declaration-scopes.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,6 @@ top-level roles as a genuine project API instead of a neutral dumping ground.
140140
Ask SQLBuild what a file can use and preview how moving it would change that answer.
141141

142142
Learn the features themselves in [Enums](enums.md), [Constants](constants.md),
143-
[Writing Macros](macros.md), [Audits](audits.md),
143+
[Macros](macros.md), [Audits](audits.md),
144144
[Reusable Schemas](models/schemas.md), and [Hooks](models/hooks.md). See [Interpolation](interpolation.md) for project,
145145
environment, and runtime context values, which are separate from declarations.
Lines changed: 55 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,266 +1,77 @@
11
<!-- generated-by: sqlbuild skills -->
22

3-
# Writing Macros
3+
# Macros
44

5-
> Write Python functions that generate reusable SQL fragments at compile time.
5+
> Reuse SQL with typed SQL macros, or generate it with Python macros when it needs logic.
66
77
Online: https://sqlbuild.com/docs/concepts/macros/
88

9-
## Contents
9+
A macro is a reusable piece of SQL that a model, test, hook or audit calls by name. SQLBuild
10+
expands each call at compile time, so the warehouse only ever sees plain SQL.
1011

11-
- Create a macro
12-
- Call a macro from SQL
13-
- Arguments
14-
- Typed reference arguments
15-
- Keep implementation details private
16-
- Use macros in tests
17-
- Use macros in hooks
18-
- Macros must be deterministic
19-
- Where macros work
20-
- Next steps
12+
There are two kinds:
2113

22-
Python macros are Python functions that generate SQL during compilation. They provide reusable,
23-
parameterized SQL without introducing a separate template language. When the SQL needs no logic,
24-
a typed [SQL macro](macros/sql-macros.md) is simpler and the compiler can see inside
25-
it.
14+
| | SQL macro | Python macro |
15+
|---|---|---|
16+
| Written as | A `.sql` file: a `MACRO` header and a SQL body | A Python function that returns SQL |
17+
| Called as | `@sql.name(...)` | `@py.name(...)` |
18+
| Lives in | `macros/sql/` | `macros/python/` |
19+
| Arguments | Typed SQL expressions, checked against the caller's column types | Python values: strings, numbers, lists, dictionaries, references |
20+
| Logic | None: the body is SQL | Any Python: loops, conditions, adapter and target context |
21+
| Checks and tools | Each macro is checked on its own; `sqb format`, Rules and lineage see inside every call | Rules and lineage see the SQL each call returns |
2622

27-
## Create a macro
23+
Prefer a SQL macro whenever no logic is needed: its calls stay readable, typed and checked, and
24+
every tool sees the SQL it produces.
2825

29-
Put project-wide Python macros in Python files under `macros/python/`. SQL macros live next to
30-
them in `macros/sql/`:
26+
Typed, logic-free expressions, CTE lists and column lists.
27+
Python functions that generate SQL when it needs logic.
28+
29+
## Where macros live
30+
31+
Each kind has its own folder:
3132

3233
```text
3334
my_project/
3435
├── macros/
3536
│ ├── python/
36-
│ │ └── currency.py
37+
│ │ └── labels.py
3738
│ └── sql/
39+
│ └── cents_to_dollars.sql
3840
├── models/
3941
└── sqlbuild_project.toml
4042
```
4143

4244
A file directly under a `macros/` folder fails with `D013`, which names the subfolder to move it
43-
to. Folder-scoped macros use the same layout, such as `models/marts/_sqlbuild/_macros/python/`.
44-
45-
Every public function defined by a macro file becomes callable from SQL:
46-
47-
```python
48-
# macros/python/currency.py
49-
def cents_to_dollars(expression: str) -> str:
50-
"""Convert a cents expression to dollars."""
51-
return f"ROUND(CAST({expression} AS DOUBLE) / 100, 2)"
52-
```
45+
to. A macro used by only part of the project can live in that folder instead, as
46+
`_sqlbuild/macros/sql/` or `_sqlbuild/_macros/python/`, and is then visible only there. See
47+
[Declarations and Scopes](declaration-scopes.md).
5348

54-
## Call a macro from SQL
49+
## Call a macro
5550

56-
Use `@py.macro_name(...)` in a model query. Every macro call says its kind: `@py.` calls a Python
57-
macro and `@sql.` a SQL macro. A bare `@macro_name(...)` fails with `P014`, which names both
58-
spellings, and calling a macro with the other kind's prefix fails with `P020`.
51+
Every call names the kind of macro it calls:
5952

6053
```sql
61-
MODEL (
62-
description "One row per order",
63-
materialized table,
64-
tags [marts],
65-
);
66-
6754
SELECT
6855
order_id,
69-
@py.cents_to_dollars("amount_cents") AS amount_dollars
56+
@sql.cents_to_dollars(amount_cents) AS amount_dollars,
57+
@py.status_label("status") AS status_label
7058
FROM __ref("stg_orders")
7159
```
7260

73-
During compilation, SQLBuild replaces the call with the function's returned SQL:
74-
75-
```sql
76-
ROUND(CAST(amount_cents AS DOUBLE) / 100, 2)
77-
```
78-
79-
A macro used directly in SQL must return a string.
80-
81-
## Arguments
82-
83-
Macro calls accept Python literal values:
84-
85-
- Strings: `"value"` or `'value'`
86-
- Numbers: `42`, `3.14`, `-1`
87-
- Booleans: `True`, `False`
88-
- Lists: `[1, 2, 3]`
89-
- Dictionaries: `{"key": "value"}`
90-
- `None`
91-
- The result of another macro call
92-
- `__ref("model")`, `__source("source")`, and `__seed("seed")` references, alone or inside lists
93-
and dictionaries
94-
95-
These inputs are rejected with an error. In a model body the error names the file line and
96-
column; elsewhere it names the position within the macro's arguments:
97-
98-
- bytes (`b"text"`), complex numbers (`2j`), f-strings, and `...`
99-
- a keyword argument passed twice, such as `@py.lit(a=1, a=2)`, which Python also rejects
100-
- lone surrogate escapes such as `"\ud800"`; write a character outside the Basic Multilingual
101-
Plane as one escape, such as `"\U0001F600"`, rather than as a surrogate pair
102-
- `\N{...}` naming a multi-character named sequence; `\N` escapes name one character
103-
- two values with no comma between them
104-
105-
Positional and keyword arguments are supported:
106-
107-
```sql
108-
@py.mock_orders(count=5, status="completed")
109-
```
110-
111-
Use quoted strings when passing SQL expressions such as column names. The macro decides how to
112-
place that text into its returned SQL.
113-
114-
## Typed reference arguments
115-
116-
Pass the models, sources, and seeds a macro reads as reference arguments:
117-
118-
```sql
119-
SELECT * FROM @py.union_all([__ref("orders_eu"), __ref("orders_us")])
120-
```
121-
122-
The macro receives each reference as a typed `SqlResourceRef` from `sqlbuild.refs`, the same type
123-
Python nodes use. It has `kind` and `name` attributes, and formatting it into SQL renders the
124-
relation with the adapter's quoting:
125-
126-
```python
127-
# models/marts/_sqlbuild/_macros/python/unions.py
128-
from sqlbuild.refs import SqlResourceRef
129-
130-
def union_all(relations: list[SqlResourceRef]) -> str:
131-
return " UNION ALL ".join(f"SELECT order_id FROM {relation}" for relation in relations)
132-
```
133-
134-
The dependency belongs to the model that makes the call. The graph, column lineage, and
135-
declaration scopes treat `@py.union_all([__ref("orders_eu"), __ref("orders_us")])` exactly like a
136-
model that wrote `__ref("orders_eu")` and `__ref("orders_us")` itself.
137-
138-
### References in macro output
139-
140-
A macro must not produce references on its own. Any `__ref()`, `__source()`, or `__seed()` in a
141-
macro's expanded output fails to compile with `P006`, whether the macro's Python code wrote it or
142-
it arrived as a quoted string argument such as `@py.wrap('__ref("stg_orders")')`:
143-
144-
```
145-
error[P006]: model:order_summary depends on model:stg_orders through macro orders_base()
146-
model: order_summary
147-
--> models/marts/_sqlbuild/_macros/python/orders.py
148-
= help: write the reference in the model, or pass it in: @py.orders_base(__ref("stg_orders"))
149-
= help: the current value is [references] enforce_explicit = true (from sqlbuild_project.toml or its default); while migrating a project, to allow macro-generated references, set this in sqlbuild_project.toml:
150-
[references]
151-
enforce_explicit = false
152-
```
153-
154-
A dependency hidden inside a macro is invisible to reviewers of the model file and bypasses
155-
declaration scopes. For projects migrating from other tools,
156-
[`[references] enforce_explicit = false`](project-configuration.md#references) allows
157-
macro-generated references project-wide.
158-
159-
## Keep implementation details private
160-
161-
Only public functions owned by the file are exported as macros. Prefix helpers, constants, classes,
162-
and type aliases with `_`:
163-
164-
```python
165-
# macros/python/orders.py
166-
_DEFAULT_STATUS = "completed"
167-
168-
def _status_filter(status: str) -> str:
169-
return f"status = '{status}'"
170-
171-
def completed_orders() -> str:
172-
return _status_filter(_DEFAULT_STATUS)
173-
```
61+
A bare `@name(...)` fails with `P014`, which names both spellings. Calling a macro with the other
62+
kind's prefix fails with `P020`, and a SQL macro and a Python macro may not share a visible name.
17463

175-
Here, SQL may call `@py.completed_orders()`. `_status_filter` and `_DEFAULT_STATUS` remain ordinary
176-
Python implementation details.
64+
## Use the two kinds together
17765

178-
Imported functions are not re-exported as new macros from the importing file.
179-
180-
## Use macros in tests
181-
182-
Tests are SQL, so they can use macros as reusable fixture generators. A macro that only tests use
183-
follows the usual [placement](declaration-scopes/placement.md) rule: it must be private
184-
to the narrowest folder that contains its uses, so the project-wide `macros/` directory rejects it
185-
with `S024`. Put it in `_sqlbuild/_macros/python/` in the folder of the tests that call it, such as
186-
`tests/unit/orders/_sqlbuild/_macros/python/`, or, when those tests define `__expected__` output
187-
for a model, in that model's `_sqlbuild/_macros/python/`. The `S024` error names the folder to use.
188-
`tests/unit/_sqlbuild/` itself is rejected with `D013`.
189-
190-
```python
191-
# tests/unit/orders/_sqlbuild/_macros/python/order_fixtures.py
192-
def mock_orders(count: int = 1) -> str:
193-
rows = [
194-
f"SELECT {i} AS id, {i * 100} AS customer_id, 'completed' AS status"
195-
for i in range(1, count + 1)
196-
]
197-
return " UNION ALL ".join(rows)
198-
```
66+
A SQL macro calls only SQL macros, and a Python macro composes other Python macros through Python
67+
imports. To combine the two, pass one call's result to the other in the calling SQL:
19968

20069
```sql
201-
TEST();
202-
203-
WITH
204-
__source__raw__orders AS (
205-
@py.mock_orders(3)
206-
),
207-
__expected__stg_orders AS (
208-
SELECT 1 AS order_id, 100 AS customer_id, 'completed' AS status
209-
UNION ALL
210-
SELECT 2 AS order_id, 200 AS customer_id, 'completed' AS status
211-
UNION ALL
212-
SELECT 3 AS order_id, 300 AS customer_id, 'completed' AS status
213-
)
70+
SELECT @py.bracket(@sql.cents_to_dollars(o.amount_cents)) AS amount_band
71+
FROM __ref("stg_orders") AS o
21472
```
21573

216-
## Use macros in hooks
217-
218-
Macros work inside inline and named SQL hooks:
219-
220-
```python
221-
# macros/python/permissions.py
222-
def grant_target(target: str) -> str:
223-
return f"GRANT SELECT ON {target} TO analyst_role"
224-
```
225-
226-
```sql
227-
MODEL (
228-
description "Example model",
229-
post_hooks [inline_sql('@py.grant_target(@@CTX:destination.qualified)')],
230-
);
231-
232-
SELECT 1 AS id
233-
```
234-
235-
See [SQL Hooks](models/hooks/sql.md) for hook lifecycle and context syntax.
236-
237-
## Macros must be deterministic
238-
239-
A macro must return the same result whenever it is called with the same arguments and sees the same
240-
context. Avoid module-level state such as counters, random values, the current time, and reads of
241-
files or environment variables that the call does not receive.
242-
243-
SQLBuild relies on this contract to reuse macro results:
244-
245-
- [Compile reuse](../cli/compile.md) replays an unchanged compile without running macros again.
246-
- A compile may run each distinct macro call once and reuse its result for identical calls
247-
elsewhere in the project.
248-
- A compile that fails stops at its first error. It does not run the project's macros a second
249-
time to report that error.
250-
- Macro call results are kept between compiles under `target/cache/` by default, so editing a
251-
model does not run its unchanged macro calls again. They are recomputed when
252-
macro or other Python files, configuration, declarations, seeds, sources, variables, the target,
253-
the adapter, the installed packages, or the SQLBuild version change. Editing only model SQL keeps
254-
them, and a changed call is always run again. Only the first compile in a process uses and saves
255-
them: later compiles in the same process, such as in a notebook or a long-running server, run their
256-
macros again. A compile that has loaded Python code it cannot read from a plain file, such as a
257-
module imported from a zip archive, neither uses nor saves them.
258-
259-
Pass configuration through what SQLBuild compares: macro arguments (including `@@ENV:NAME` and
260-
`@@var` values written in the calling SQL), `ctx.vars`, `ctx.target_name`, `ctx.adapter_name`,
261-
and visible constants and enums. Environment variables a macro reads directly, for example with
262-
`os.environ`, are compared only when their names start with `SQLBUILD_` or `SQB_`, the same set
263-
compile reuse tracks. A macro whose result changes for any other reason may return a stale result.
74+
The SQL macro expands first, and the Python macro receives its SQL as a string.
26475

26576
## Where macros work
26677

@@ -275,8 +86,21 @@ Macros are supported in:
27586
Macros are not accepted in ordinary `MODEL()` configuration fields. SQL hook entries are the
27687
exception because their contents are SQL.
27788

278-
## Next steps
89+
### Macros used only by tests
90+
91+
A macro that only tests use follows the usual [placement](declaration-scopes/placement.md)
92+
rule: it must be private to the narrowest folder that contains its uses, so the project-wide
93+
`macros/` directory rejects it with `S024`. Put it in `_sqlbuild/_macros/python/` or
94+
`_sqlbuild/_macros/sql/` in the folder of the tests that call it, such as
95+
`tests/unit/orders/_sqlbuild/_macros/python/`, or, when those tests define `__expected__` output
96+
for a model, in that model's `_sqlbuild/_macros/`. The `S024` error names the folder to use.
97+
`tests/unit/_sqlbuild/` itself is rejected with `D013`.
98+
99+
## Choose the right tool
279100

280-
Write typed, logic-free SQL fragments the compiler checks and sees inside.
281-
Compose macros through Python imports and use adapter or target context.
282-
Limit a macro to one folder, or to that folder and its child folders.
101+
| Use | When |
102+
|---|---|
103+
| [SQL macro](macros/sql-macros.md) | The fragment is plain SQL with typed inputs: a calculation, a filter, a set of CTEs or columns you repeat. The compiler sees inside it. |
104+
| [Python macro](macros/python-macros.md) | Generating the SQL needs logic: loops, conditions on arguments, the adapter or target, or reading project context. |
105+
| [SQL function](functions.md) | The logic belongs in the warehouse, to be called by other tools or evaluated per row there, or it is too large to inline everywhere. |
106+
| [Generic audit](audits.md) | The SQL checks data quality on a model rather than producing part of its query. |

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/macros/composition-and-context.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<!-- generated-by: sqlbuild skills -->
22

3-
# Composition and Context
3+
# Python Macro Composition and Context
44

5-
> Compose macros through Python, use compile context, and understand scoped imports.
5+
> Compose Python macros through imports, and use adapter, target, constants and enums.
66
77
Online: https://sqlbuild.com/docs/concepts/macros/composition-and-context/
88

@@ -14,8 +14,10 @@ Online: https://sqlbuild.com/docs/concepts/macros/composition-and-context/
1414
- Nested calls written in SQL
1515
- Macro context
1616

17-
Macros are Python functions, so reusable macros should compose through ordinary Python calls. A
18-
macro returns final SQL; SQLBuild does not treat its output as another layer of macro source.
17+
[Python macros](python-macros.md) are Python functions, so they compose through
18+
ordinary Python calls. A macro returns final SQL; SQLBuild does not treat its output as another
19+
layer of macro source. [SQL macros](sql-macros.md#compose-macros) compose by
20+
calling each other in their bodies.
1921

2022
## Compose helpers in one file
2123

0 commit comments

Comments
 (0)