Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,15 @@ E2E_DUCKDB_BUILD_CORE_PATHS := \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_dag_json_behavior.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_enum_contract.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_execution_limits.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_explicit_references.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_expression_sources.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_lifecycle_commands.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_manifest_artifact_gating.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_no_tests_no_audits_flags.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_plan_command_surface.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_python_hooks.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_python_source_deferral.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_sql_hook_reads.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_python_node_identity.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_sql_hook_identity.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_query_change_tracking.py \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ Bundled copies of every page on the SQLBuild documentation site, matching the in
- [Loaders](concepts/python-nodes/loaders.md) (`concepts/python-nodes/loaders`) - Load external data into source tables with Python functions.
- [Tasks](concepts/python-nodes/tasks.md) (`concepts/python-nodes/tasks`) - Run Python computation and side effects as nodes in the SQLBuild graph.
- [Assets](concepts/python-nodes/assets.md) (`concepts/python-nodes/assets`) - Produce or observe external artifacts as nodes in the SQLBuild graph.
- [Checks](concepts/python-nodes/checks.md) (`concepts/python-nodes/checks`) - Validate tasks, assets, and loaders with Python checks.
- [Checks](concepts/python-nodes/checks.md) (`concepts/python-nodes/checks`) - Validate tasks, assets, loaders, and SQL relations with Python checks.
- [Factories](concepts/python-nodes/factories.md) (`concepts/python-nodes/factories`) - Generate Python nodes programmatically with @factory.
- [Providers](concepts/python-nodes/providers.md) (`concepts/python-nodes/providers`) - Shared runtime services for Python nodes and hooks.
- [SQL References](concepts/python-nodes/sql-references.md) (`concepts/python-nodes/sql-references`) - Read SQL models and sources from Python nodes without creating SQL dependencies.
- [SQL References](concepts/python-nodes/sql-references.md) (`concepts/python-nodes/sql-references`) - Read SQL models, sources, and seeds from Python nodes without creating SQL dependencies.

## Integrations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ Results are written to `target/run/checks/python_checks.json`.

## Relationship to build

`sqb build` runs relevant Python checks by default after the tasks, assets, and loaders they depend on have run. `sqb build --no-audits` skips checks alongside audits. `sqb check` runs only checks, on demand.
`sqb build` runs relevant Python checks by default after the tasks, assets, loaders, models, sources, and seeds they depend on have run. `sqb build --no-audits` skips checks alongside audits. `sqb check` runs only checks, on demand.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Online: https://sqlbuild.com/docs/concepts/macros/
- Create a macro
- Call a macro from SQL
- Arguments
- Typed reference arguments
- Keep implementation details private
- Use macros in tests
- Use macros in hooks
Expand Down Expand Up @@ -77,6 +78,8 @@ Macro calls accept Python literal values:
- Dictionaries: `{"key": "value"}`
- `None`
- The result of another macro call
- `__ref("model")`, `__source("source")`, and `__seed("seed")` references, alone or inside lists
and dictionaries

Positional and keyword arguments are supported:

Expand All @@ -87,6 +90,48 @@ Positional and keyword arguments are supported:
Use quoted strings when passing SQL expressions such as column names. The macro decides how to
place that text into its returned SQL.

## Typed reference arguments

Pass the models, sources, and seeds a macro reads as reference arguments:

```sql
SELECT * FROM @union_all([__ref("orders_eu"), __ref("orders_us")])
```

The macro receives each reference as a typed `SqlResourceRef` from `sqlbuild.refs`, the same type
Python nodes use. It has `kind` and `name` attributes, and formatting it into SQL renders the
relation with the adapter's quoting:

```python
# models/marts/_sqlbuild/_macros/unions.py
from sqlbuild.refs import SqlResourceRef

def union_all(relations: list[SqlResourceRef]) -> str:
return " UNION ALL ".join(f"SELECT order_id FROM {relation}" for relation in relations)
```

The dependency belongs to the model that makes the call. The graph, column lineage, and
declaration scopes treat `@union_all([__ref("orders_eu"), __ref("orders_us")])` exactly like a
model that wrote `__ref("orders_eu")` and `__ref("orders_us")` itself.

### References in macro output

A macro must not produce references on its own. Any `__ref()`, `__source()`, or `__seed()` in a
macro's expanded output fails to compile with `P006`, whether the macro's Python code wrote it or
it arrived as a quoted string argument such as `@wrap('__ref("stg_orders")')`:

```
error[P006]: model:order_summary depends on model:stg_orders through macro orders_base()
--> models/marts/_sqlbuild/_macros/orders.py
= help: write the reference in the model, or pass it in: @orders_base(__ref("stg_orders"))
= help: while migrating a project, allow macro-generated references with [references] enforce_explicit = false in sqlbuild_project.toml
```

A dependency hidden inside a macro is invisible to reviewers of the model file and bypasses
declaration scopes. For projects migrating from other tools,
[`[references] enforce_explicit = false`](project-configuration.md#references) allows
macro-generated references project-wide.

## Keep implementation details private

Only public functions owned by the file are exported as macros. Prefix helpers, constants, classes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@

Online: https://sqlbuild.com/docs/concepts/models/hooks/python/

## Contents

- Define a Python hook
- Declared reads
- Invoke a Python hook
- Signature validation
- Hook context
- Providers
- Skips and failures
- Identity and diagnostics

Python hooks run model lifecycle logic that needs Python control flow, providers, warehouse queries, logging, or explicit skip decisions.

For shared lifecycle ordering, failure timing, naming, and identity rules, see the [Hooks overview](../hooks.md).
Expand All @@ -29,7 +40,47 @@ def notify_complete(ctx, channel="#data-builds"):
)
```

By default, the hook name is the function name. The decorator accepts optional `name` and `description` arguments. A Python file may define multiple decorated hooks.
By default, the hook name is the function name. The decorator accepts optional `name`, `description`, and `reads` arguments. A Python file may define multiple decorated hooks.

## Declared reads

A hook that queries other models, sources, or seeds declares them with `reads` and resolves each
one with `ctx.relation(...)`:

```python
from sqlbuild.hooks import hook
from sqlbuild.refs import model

@hook(reads=model("dim_customers"))
def refresh_lookup(ctx):
customers = ctx.relation(model("dim_customers"))
ctx.execute_sql(f"CREATE OR REPLACE TABLE customer_lookup AS SELECT * FROM {customers}")
```

`reads` accepts one reference or a list of `model(...)`, `source(...)`, and `seed(...)`
references from `sqlbuild.refs`. `ctx.relation(...)` returns the adapter-qualified relation for
the current run, following deferral like SQL references do. Resolving a reference that is not
declared raises, as it does for [Python nodes](../../python-nodes/sql-references.md).

Declared reads order the build: a model that runs the hook waits for the resources the hook reads
when both are in the build. In a partial build, unselected reads are not built, and the hook reads
their existing relations. A hook cannot read a resource built from the model that runs it, because
that model would have to wait for its own dependants; that fails to compile with `P007`. Reading
an unknown model, source, or seed fails to compile. Use `ctx.destination` for the hook's own model.

The Dagster integration does not add declared reads to its asset graph, the same as reads of
attached audits. Within one `sqb build` the ordering holds; when Dagster runs the hooked model in a
separate build from the resources it reads, the hook reads their existing relations.

Declaring reads does not change who may call the hook: the model must still see the hook under the
usual [scope rules](../hooks.md#project-layout).

SQL hooks declare reads by referencing resources in their SQL; see
[Read models, sources, and seeds](sql.md#read-models-sources-and-seeds).

Literal project relation names in `ctx.query()` and `ctx.execute_sql()` SQL fail to compile, and
dynamically built names produce run-time warnings; see
[Hard-coded relation names](../../python-nodes/sql-references.md#hard-coded-relation-names).

Files named `__init__.py` or beginning with `_` are skipped. Imported decorated functions are not registered again from the importing module.

Expand Down Expand Up @@ -73,6 +124,7 @@ Python hooks declare a `HookContext` parameter named `ctx`, `context`, `_ctx`, o
| `ctx.target` | Active target |
| `ctx.vars` | Effective project variables |
| `ctx.destination` | Destination relation metadata |
| `ctx.relation(ref)` | Adapter-qualified relation for a [declared read](#declared-reads) |
| `ctx.adapter_name` | Active adapter name |
| `ctx.adapter` | Adapter instance |
| `ctx.connection` | Live connection |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Online: https://sqlbuild.com/docs/concepts/models/hooks/sql/
- SQL hook arguments
- Inline SQL hooks
- Compile-time context
- Read models, sources, and seeds
- Validation and diagnostics

SQL hooks submit one rendered SQL payload to the adapter before or after model materialization. Use a named SQL hook for reusable behavior and `inline_sql(...)` for short model-specific payloads.
Expand Down Expand Up @@ -163,6 +164,46 @@ LANGUAGE SQL
AS 'SELECT 1'
```

## Read models, sources, and seeds

SQL hooks reference other resources the same way model SQL does, with `__ref("...")`,
`__source("...")`, and `__seed("...")`, written directly or passed as typed macro arguments:

```sql
MODEL (
materialized table,
pre_hooks [
inline_sql('DELETE FROM @@CTX:destination.qualified WHERE order_id IN (SELECT order_id FROM __ref("cancelled_orders"))'),
],
post_hooks [sql("record_counts")],
);

SELECT order_id FROM __ref("stg_orders")
```

**`models/marts/_sqlbuild/_hooks/sql/record_counts.sql`**

```sql
HOOK ();

@count_rows(__ref("stg_orders"))
```

At plan time each reference resolves exactly like one in model SQL: adapter quoting, source
deferral, `--defer`, and target overrides all apply. Every reference is also a hook read, with the
same rules as [declared Python hook reads](python.md#declared-reads):

- The model that runs the hook waits for the resources it reads when both are in the build. Reads
do not change selection; in a partial build the hook reads the existing relations.
- A hook cannot read a resource built from its own model; that fails to compile with `P007`. Use
`@@CTX:destination.qualified` for the hook's own model.
- Referencing an unknown model, source, or seed fails to compile.
- A reference passed to a macro is a read even when the macro does not render it. A reference a
macro writes into its output instead fails with `P006`, as in model SQL.

The model must still see a named hook under the usual
[scope rules](../hooks.md#project-layout).

## Validation and diagnostics

SQLBuild strictly validates hook resource syntax, arguments, interpolation, macros, metadata, and non-empty payloads. It does not classify executable statement kinds or implement vendor SQL grammar. Each rendered hook payload is passed to the adapter in one `execute` call; whether a driver accepts multiple statements or client-side batch separators is adapter and warehouse behavior. Use separate hook entries when portable ordering between statements matters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Online: https://sqlbuild.com/docs/concepts/project-configuration/
- Settings
- Rules
- Scopes
- References
- Project variables
- Janitor
- Scenario
Expand Down Expand Up @@ -430,6 +431,31 @@ enforce_placement = false
Visibility errors are unaffected. See
[Where to Put Declarations](declaration-scopes/placement.md).

## References

Every reference to a model, source, or seed must be explicit and typed. By default:

- A macro whose expanded output contains `__ref()`, `__source()`, or `__seed()` fails to compile
with `P006`. Pass the reference to the macro as an argument instead; see
[Typed reference arguments](macros.md#typed-reference-arguments).
- Literal SQL passed to `ctx.query()` or `ctx.execute_sql()` in a task, asset, check, loader, or
Python hook that names a project relation fails to compile with `P008`.
- The same check runs on SQL sent at run time and reports a `P008` warning; the query still runs.
See [Hard-coded relation names](python-nodes/sql-references.md#hard-coded-relation-names).

One compile reports every `P006`, `P007`, and `P008` violation in the project, in the terminal and in
`sqb compile --json`.

While migrating a project from another tool, turn these checks off for the whole project:

```toml
[references]
enforce_explicit = false # default true
```

The switch has no per-model or per-folder form. Undeclared `ctx.relation(...)` calls still fail,
and declared hook reads still order the build.

## Project variables

Variables are simple string substitutions available in model SQL via the `@@name` syntax:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ See [Tasks](tasks.md#retries) for the full retry policy fields.
| Parameter | Description |
|-----------|-------------|
| `name` | Override the node name (defaults to the function name) |
| `depends_on` | Upstream nodes (function, tuple, or list); `model()`/`source()` for read-only SQL refs |
| `depends_on` | Upstream nodes (function, tuple, or list); `model()`/`source()`/`seed()` for read-only SQL refs |
| `columns` | Column declarations for the produced artifact |
| `column_lineage` | Map of asset column to upstream `{node, column}` references |
| `tags` | Labels for selection and grouping |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Checks

> Validate tasks, assets, and loaders with Python checks.
> Validate tasks, assets, loaders, and SQL relations with Python checks.

Online: https://sqlbuild.com/docs/concepts/python-nodes/checks/

Expand All @@ -16,7 +16,7 @@ Online: https://sqlbuild.com/docs/concepts/python-nodes/checks/
- Running checks
- Checks vs audits

Checks are Python nodes that validate other Python nodes. They are the Python analog of SQL [audits](../audits.md): audits validate SQL relations, checks validate the output of tasks, assets, and loaders. See [Python Nodes](overview.md) for the shared model.
Checks are Python nodes that validate the output of tasks, assets, and loaders, or the data in SQL models, sources, and seeds. They are the Python analog of SQL [audits](../audits.md). See [Python Nodes](overview.md) for the shared model.

Checks are separate graph nodes, not callbacks embedded in a task or asset. A check declares what it validates through `depends_on`.

Expand All @@ -39,6 +39,21 @@ def check_orders_exported(ctx: CheckContext):

The check receives a `CheckContext` and reads its dependencies' persisted results with `ctx.result_of(...)`.

A check can also validate SQL relations. Declare them with `model()`, `source()`, or `seed()` from `sqlbuild.refs`, and resolve them with `ctx.relation(...)`:

```python
from sqlbuild.checks import check
from sqlbuild.refs import model

@check(depends_on=model("fact_orders"))
def orders_present(ctx):
orders = ctx.relation(model("fact_orders"))
count = ctx.query(f"SELECT count(*) FROM {orders}").fetchone()[0]
return ctx.pass_() if count else ctx.fail("no orders")
```

Do not hard-code relation names in check SQL; see [Hard-coded relation names](sql-references.md#hard-coded-relation-names).

## Results

Return a result through the context helpers, or a bool shorthand:
Expand Down Expand Up @@ -79,7 +94,7 @@ def orders_freshness(ctx):
## What checks can depend on

- Checks may depend on **tasks, assets, and loaders**.
- Checks may **not** depend on SQL models, sources, seeds, or functions. Use SQL [audits](../audits.md) to validate SQL relations.
- Checks may depend on SQL **models, sources, and seeds** through `model()`, `source()`, and `seed()` references. They may **not** depend on functions.
- Checks may **not** depend on other checks.
- Checks may **not** depend on a terminal source loader directly. Validate loaded source data with a source audit instead.

Expand All @@ -89,7 +104,7 @@ A check that depends on a single node is displayed grouped under that node. Mult

| Parameter | Description |
|-----------|-------------|
| `depends_on` | Required. Tasks/assets/loaders to validate (function, tuple, or list) |
| `depends_on` | Required. Tasks/assets/loaders to validate, and `model()`/`source()`/`seed()` references (function, reference, tuple, or list) |
| `name` | Override the node name (defaults to the function name) |
| `severity` | `error` (default) or `warn` |
| `tags` | Labels for selection and grouping |
Expand All @@ -101,7 +116,7 @@ Checks do not support `columns`, `column_lineage`, or `retry`.

## Running checks

Checks run automatically during `sqb build` when their Python dependencies run. They are skipped when `--no-audits` is passed. To run checks on their own, use [`sqb check`](../../cli/check.md):
Checks run automatically at the end of `sqb build` when all their Python and SQL dependencies ran in that build. They are skipped when `--no-audits` is passed. To run checks on their own, use [`sqb check`](../../cli/check.md):

```bash
# Run all checks
Expand All @@ -114,13 +129,13 @@ sqb check --select +check_orders_exported
sqb check --select tag:exports
```

`sqb check` rejects selecting non-check nodes; use `sqb build` to run tasks and assets. Check results are written to `target/run/checks/python_checks.json`, and `sqb check --json` prints them to stdout.
`sqb check` rejects selecting non-check nodes; use `sqb build` to run tasks and assets. The models, sources, and seeds a check depends on must already exist; `sqb check` does not build them. Check results are written to `target/run/checks/python_checks.json`, and `sqb check --json` prints them to stdout.

## Checks vs audits

| | Checks | Audits |
|---|--------|--------|
| Validates | Python tasks, assets, loaders | SQL relations |
| Validates | Python tasks, assets, loaders, and SQL relations | SQL relations |
| Authored in | `python/` (Python) | `MODEL()` headers / `audits/` (SQL) |
| Run by | `sqb build`, `sqb check` | `sqb build`, `sqb audit` |
| Severity | `error`, `warn` | `error`, `warn` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ def raw_account_metrics(ctx: LoaderContext) -> list[dict[str, object]]:
]
```

Read upstream relations through `ctx.loader(...)` and `ctx.source(...)`, not by writing their table names in SQL. SQLBuild rejects literal SQL that names a project source, model, or seed with `P008`, and warns at run time when dynamic SQL does; loaders run before models and seeds and cannot read them. See [Hard-coded relation names](sql-references.md#hard-coded-relation-names).

Dependencies form a DAG. SQLBuild schedules loaders in topological order and executes independent loaders concurrently when `--concurrency` is set.

Intermediate loaders (those referenced only via `depends_on`, with no managed source of the same name) are given synthetic source entries and write to `__loader__<name>` tables by default. Only the terminal loader - the one whose name matches a managed source - populates that source; intermediate loaders feed it. Use the `destination` parameter on the decorator to override the intermediate relation:
Expand Down
Loading
Loading