Skip to content

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.

Use model(), source(), and seed() from sqlbuild.refs in the depends_on of a task, asset, or check:

from sqlbuild.refs import model, seed, source
from 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:

  1. It tells SQLBuild the node reads that SQL resource, so the node is scheduled after the resource is built (read-side).
  2. 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.

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.

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.toml

An 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(...), or ctx.loader(...), a hook’s own model (ctx.destination), and a loader’s own source
  • Relations that are not project relations, such as information_schema tables, 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.

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.

  • Tasks, assets, and checks may declare model(), source(), and seed() references and read them with ctx.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 with ctx.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.