Skip to content

Build1 publisher3 min readPublished

Jinja Is Four Jobs In One Costume, And Only One Of Them Needs A Template

Interlace drops templating from its dbt-style models. Its argument: parse the SQL, and the dependency graph you were asked to declare twice is already sitting in the FROM clause.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Jinja Is Four Jobs In One Costume, And Only One Of Them Needs A Template
Generated illustration

What happened

  • Interlace has no templating language; a model is a .sql file containing SQL or a .py file containing a function, with no {{ }} and no {% %}.
  • The article's argument is that Jinja is doing four unrelated jobs, which is why one mechanism doing all four feels both indispensable and awkward; separated, each has a better answer than a templating language.
  • The article calls ref() the most common use of Jinja and the least necessary.
  • ref() exists because dbt does not parse your SQL; it needs you to declare the edge separately in the text, and then it substitutes the physical name.
  • Interlace parses the SQL with sqlglot and reads dependency edges out of the AST.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

Interlace, a transformation tool with a dbt-shaped project layout, ships without a templating language: a model is a .sql file of SQL or a .py file containing a function, with no `{{ }}` and no `{% %}` anywhere [1]. That is only interesting because of the argument attached to it, which is that Jinja in a dbt project is doing four unrelated jobs at once, and that is why the mechanism feels simultaneously indispensable and awkward [2].

Job one is `ref()`, which the project's own write-up calls the most common use and the least necessary [3]. The reasoning is mechanical rather than aesthetic: `ref()` exists because dbt does not parse your SQL, so it needs the edge declared separately in the text and then substitutes the physical name [4]. But the edge is already in the query. It is the FROM clause. Interlace parses the SQL with sqlglot and reads the edges out of the AST [5]. The worked example is a three-table query where one reference sits inside a correlated subquery in the WHERE clause, and `table_references()` returns customers, orders and watermark with nothing declared [6][7]. The side effect matters more than the keystrokes saved: a model file that is bare SQL is valid SQL you can paste into any client, which a Jinja template is not [8].

Job two, loops, is the one described as Jinja's legitimate job, the thing people reach for with fifty tenants or a pivot over payment methods [9]. Here the answer is not a feature but a consequence. Because .py model files are imported and executed when the project loads, registering a model is a function call, so a loop over a list is a loop over a list [10][15]. The published example loops over three tenants and produces three real models, each with its own snapshot table, environment view, fingerprint, plan entry and checks [11]. The plan output shows orders_acme, orders_globex and orders_initech all depending on raw, at 0.12s, 0.08s and 0.05s [12]. The consequential detail is what the tenant list can be: in Jinja it has to be something the templating context can reach, whereas a Python function can query a database, read a file or call an API, and can be unit-tested [13][14].

Job three, macros, is where the project revised itself. An August 2026 update says Interlace has since gained macros, as SQL expressions expanded into the AST rather than as templates [16]. A macro is declared as `CREATE MACRO cents_to_dollars(amount) AS (amount / 100)::numeric(16, 2);` and expanded into the calling model's AST while it compiles, before the fingerprint, before lineage, before transpilation [17][18]. Two things follow from that ordering. One definition covers every engine, because sqlglot renders Postgres's integer-division cast and BigQuery's NUMERIC from the same line, where dbt needs `default__`, `postgres__` and `bigquery__` variants plus a dispatcher [19]. And because expansion precedes fingerprinting, editing a macro re-plans every model that calls it, which a macro registered in the warehouse could not do because the callers' SQL would not have changed [20].

When a macro is really a program, the escape hatch is Python: a function returning a SQL fragment interpolated into a ModelDef, imported from a helper file beside the model, where names starting with `_` are excluded from being models [21]. Job four, configuration, moves to a leading block comment namespaced under `interlace` carrying strategy, key and checks [22].

What to watch is whether the f-string interpolation in the loop example holds up. `f"... WHERE tenant_id = '{tenant}'"` is string concatenation into SQL [11], which is the thing AST-first tooling is supposed to have retired. Also worth watching: how the AST edge reader handles CTEs, table functions and dynamic identifiers, none of which the published example covers [6].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories