Python Nodes
SQL References
Read SQL models, sources, and seeds from Python nodes without creating SQL dependencies.
Python nodes can read SQL models, sources, and seeds at runtime, but SQL cannot depend on Python nodes (see the SQL boundary). Typed references make this read-only access explicit and safe.
Declaring a reference
Section titled “Declaring a reference”Use model(), source(), and seed() from sqlbuild.refs in the depends_on of a task, asset, or check:
from sqlbuild.refs import model, seed, sourcefrom sqlbuild.tasks import task
@task(depends_on=model("fact_orders"))def export_orders(ctx): """Export orders.""" ...
@task(depends_on=source("raw_orders"))def inspect_raw(ctx): """Inspect raw.""" ...
@task(depends_on=seed("country_codes"))def export_countries(ctx): """Export countries.""" ...Declaring the reference does two things:
- It tells SQLBuild the node reads that SQL resource, so the node is scheduled after the resource is built (read-side).
- It authorizes
ctx.relation(...)to resolve that reference at runtime.
It does not make the SQL resource depend on the Python node. The dependency is one-way: Python reads SQL, never the reverse.
The reference is a graph edge. It appears in sqb dag --json, and selector expansion follows it: +task:export_countries also selects the country_codes seed and its upstream, and country_codes+ also selects the tasks and assets that read it. Checks are never added by expansion; they run when their dependencies run.
Resolving with ctx.relation
Section titled “Resolving with ctx.relation”Always resolve a reference to its concrete relation with ctx.relation(...) instead of hardcoding the table name:
@task(depends_on=model("fact_orders"))def export_orders(ctx): """Export orders.""" relation = ctx.relation(model("fact_orders")) rows = ctx.query(f"SELECT * FROM {relation}").fetchall() return ctx.result(metadata={"rows": len(rows)})ctx.relation(...) returns the adapter-qualified relation name for the current run. Passing a reference that was not declared in depends_on raises an error.
Hard-coded relation names
Section titled “Hard-coded relation names”SQLBuild checks SQL passed to ctx.query() and ctx.execute_sql() in tasks, assets, checks,
loaders, and Python hooks for names of project relations:
models, sources, and seeds.
At compile time, SQL written as a string literal, or as an f-string whose table names are
literal text, is parsed with the project’s SQL dialect. Naming a project relation fails to compile
with P008:
error[P008]: task:export_orders names model:fact_orders as 'fact_orders' in SQL passed to ctx.query() --> python/tasks/export.py:9 = help: declare it with depends_on=model("fact_orders") and use ctx.relation(model("fact_orders")) instead of the relation name = help: if 'fact_orders' is an external table that shares the name, qualify it with its schema = help: while migrating a project, allow hard-coded relation names with [references] enforce_explicit = false in sqlbuild_project.tomlAn unqualified name that matches a project relation is treated as that relation. If you mean an external table that shares the name, qualify it with its schema.
Loaders run before models and seeds, so a loader naming a model or seed fails with a help saying it
cannot read them. A loader naming a source is told to use ctx.source("name"), or
ctx.loader(upstream_loader) when the source is populated by a loader it depends on.
The old name of a renamed model counts as a project
relation while a compatibility view is kept there, so literal SQL still reading it fails with
P008 and names the new model. A declared migrate_from is checked at compile time; an
automatically discovered rename is checked when planning, and stops the build. A hook’s own model
does not exempt its old name.
At run time, SQL built dynamically cannot be checked earlier. Before sending it,
ctx.query() and ctx.execute_sql() parse it in the adapter’s dialect, resolve unqualified names
against the current schema, and compare them with the project relations resolved for the run. A
project relation that the node did not obtain through ctx.relation(...) (or, in a loader,
ctx.source(...) or ctx.loader(...)) produces a P008 warning. The query still runs. The warning appears in the build summary, counts the node as
WARN, and is stored under the node’s warnings in the --json run results.
Both checks ignore:
- Relations resolved through
ctx.relation(...),ctx.source(...), orctx.loader(...), a hook’s own model (ctx.destination), and a loader’s own source - Relations that are not project relations, such as
information_schematables, external tables, and temporary tables the node creates - CTE names
- SQL that cannot be parsed; it is never blocked, only logged at debug level
This is a guard against accidental hard-coding, not a sandbox. SQL sent directly through
ctx.connection or ctx.adapter is not checked. Run-time checks apply to sqb build,
sqb check, and sqb load. sqb load does not compile models, so it checks loaders against
the project’s sources. To turn both checks off while migrating a project,
set [references] enforce_explicit = false.
model vs source
Section titled “model vs source”| Reference | Resolves to |
|---|---|
model("name") |
The built model relation |
source("name") |
The source read relation, following deferred-source semantics |
seed("name") |
The loaded seed relation |
source(...) respects defer_sources_to, so a node reading a source in dev can read the deferred target’s data just like SQL models do.
Where references are allowed
Section titled “Where references are allowed”- Tasks, assets, and checks may declare
model(),source(), andseed()references and read them withctx.relation(...). - Python hooks declare the models, sources, and seeds they read with
@hook(reads=...); see Python Hooks. - Loaders may not reference SQL resources; they read sources with
ctx.source(...)and upstream loaders withctx.loader(...). - A Python node referencing a SQL resource never turns into a SQL dependency; selector expansion will not pull Python outputs into SQL model dependencies.