Tera
Render Tera templates from SQL β Rust's Jinja2-style templating engine. Variable substitution, filters, loops, conditionals, autoescape, and template file inclusion driven by JSON context.
Install
-- Install the extension
INSTALL tera FROM community;
-- Load it into your session
LOAD tera;
-- Variable substitution
SELECT tera_render('Hello {{ name }}!', '{"name":"World"}');
-- For loop
SELECT tera_render(
'{% for x in xs %}{{ x }}{% if not loop.last %}, {% endif %}{% endfor %}',
'{"xs":["A","B","C"]}'
);
-- Render a template file from disk
SELECT tera_render('email.html', '{"name":"Alice"}', template_path := './templates/*.html'); Technical Overview
Why Use Tera from DuckDB?
Render Tera templates inside SQL with JSON context. Tera is a Jinja2-inspired template engine written in Rust; this extension makes it a scalar function so each query row can produce its own rendered string β emails, config blocks, dynamic SQL, Slack payloads, IaC fragments β without leaving DuckDB.
π What this extension is for
Anywhere you'd otherwise pull rows out of DuckDB just to feed them into a template loop in Python or Node, tera_render collapses that loop into a single SQL projection. The template stays declarative; the data stays in the query.
- β’ Per-row HTML / email: Build the body of a transactional email per row: pass the row's columns as a JSON object via
json_objectand render against a template literal. - β’ Config-file generation: Materialize one nginx / systemd / Kubernetes manifest per row of a configuration table. Disable autoescape with
autoescape := falsefor non-HTML output. - β’ Dynamic SQL: Use Tera as a meta-SQL preprocessor β render
SELECT ... FROM {{ table }} WHERE {{ pred }}against context columns, then execute the result. Treat templates as trusted; Tera interpolation is not parameter-safe. - β’ Notifications and webhook payloads: Render Slack / PagerDuty / webhook JSON bodies from query results so a single SQL pipeline takes you from event to delivered message.
βοΈ How it works
A scalar function backed by the Tera Rust crate. Each call compiles (or reuses a cached) template, binds the JSON context, and returns the rendered text. There is no ATTACH, no secrets β the function surface is one name and a few optional named arguments.
- β’ Inline and file-backed templates: The first argument is either an inline template string or a template name. Pass
template_path := './templates/*.html'to load templates from disk by name; this enables{% extends %}/{% include %}across the matched files. See Tera's inheritance docs. - β’ JSON context: The second argument is a
JSONvalue β a literal string,json_object(...), or a column. Nested objects, arrays, and primitives are all addressable as{{ user.name }},{% for x in items %},{{ items | length }}. - β’ Autoescape on by default: HTML special characters in context values are escaped β safe default for HTML / email output. Pass
autoescape := falsewhen generating raw text (configs, SQL, JSON payloads). - β’ Full Tera vocabulary: Filters (
upper,lower,length,default,join,date,truncate, etc.), control flow (if/elif/else,forwithloop.index/loop.last), inheritance, macros, and includes β see the Tera manual for the full surface.
π€ Tera vs MiniJinja
This catalog ships two Jinja-style template engines as DuckDB extensions: Tera and minijinja. They overlap heavily β both render {{ var }} / {% for %} / {% if %} against JSON context. The differences are real but small.
- β’ Pick Tera for the bigger feature surface: Tera ships a wider built-in filter and tester library, template inheritance via
template_path, and macros β choose Tera when your templates lean on Jinja2's richer authoring features. - β’ Pick MiniJinja for speed and Jinja2 fidelity: MiniJinja targets strict Python-Jinja2 semantics with a tiny, fast core. Choose
minijinjawhen you want behavior that matches an existing Python Jinja codebase, or when render-rate per row matters most. - β’ Either is fine for everyday substitution: For
{{ name }}/{{ x | upper }}/{% for x in xs %}, both produce identical output. Lock in whichever syntax matches your team's existing templates.
π― Common Use Cases
Render transactional emails per row
One row per recipient, one tera_render call producing the HTML body β fed straight into a downstream sender.
Generate per-host configuration
A virtual_hosts table plus a single Tera template equals one rendered nginx / Caddy / Envoy block per host, ready to write to disk or push to a config repo.
Build dynamic SQL from metadata
Drive SELECT / CREATE TABLE shape from a metadata table β Tera renders the SQL, your driver executes it. Useful for codegen, schema migrations, and templated reporting.
Format Slack / webhook payloads
Render alert JSON bodies directly from the query that detected the alert. Pair with http_client to deliver them in the same pipeline.
Deep Dive
Technical Details
What you can do with one query
The single most useful pattern: render one transactional email body per row, directly inside the query that selected the recipients.
SELECT email,
tera_render(
'<h1>Hi {{ name }}!</h1>
<p>You have {{ unread }} new messages.</p>',
json_object('name', name, 'unread', unread_count)
) AS html
FROM users
WHERE unread_count > 0;
tera_render compiles the template once, binds json_object(...) as context for each row, and returns the rendered string as a regular VARCHAR column. Anywhere SQL gives you a string column, this extension can fill it with templated text β emails, config blocks, dynamic SQL, Slack JSON, IaC fragments.
This catalog ships two Jinja-style template engines. They overlap heavily on everyday syntax ({{ var }} / {% for %} / {% if %} / common filters); the differences only matter at the edges.
- Pick Tera for the larger feature surface β more built-in filters and testers, template inheritance via
template_path, and macros. Best when your templates lean on Jinja2βs richer authoring features. - Pick
minijinjafor strict Python-Jinja2 fidelity and faster per-row rendering. Best when youβre matching an existing Python Jinja codebase or render rate per row matters most.
For brand-new projects with simple substitution-and-loops templates, either is a defensible choice β lock in whichever your team already knows.
Inline templates vs files on disk
By default, the first argument to tera_render is the template body itself β handy for short templates that live inside the SQL.
For non-trivial templates, pass template_path := '<glob>' and use a template name:
SELECT tera_render(
'email.html',
json_object('name', name, 'unread', unread_count),
template_path := './templates/*.html'
)
FROM users;
Loading from disk is what unlocks Teraβs {% extends %} / {% include %} / {% import %} constructs β you canβt extends "base.html" against a template that has no name. Authoring large templates in a .html file alongside the rest of your project is also nicer than embedding them as SQL string literals.
Autoescape β on by default
Tera escapes HTML special characters in interpolated context values by default, which is the right default for HTML output:
SELECT tera_render('{{ v }}', '{"v":"B&O"}');
-- 'B&O'
Turn it off when youβre rendering anything that isnβt HTML β config files, SQL, Slack JSON, plain text β otherwise apostrophes and ampersands come out as ' and &:
SELECT tera_render('{{ v }}', '{"v":"B&O"}', autoescape := false);
-- 'B&O'
Treat templates as trusted code
Tera interpolation is not an injection-safe parameterization mechanism. A context value that contains {{ ... }} or {% ... %} syntax wonβt be re-evaluated, but a template body assembled from untrusted input absolutely can render arbitrary SQL or shell strings. Keep the template surface fixed and authored by trusted code; only the context (the second argument) should come from query data or user input.
For SQL specifically: use Tera to template the shape of a query (column lists, table names from a metadata table, predicates from a config), and use ordinary parameter binding for any value that originated outside your control.
Whatβs in the box
| Function | Purpose |
|---|---|
tera_render | Render an inline template, or a named template from a template_path glob, with JSON context. The full extension surface. |
This is a deliberately small extension β one function, two optional named arguments, the full Tera language available inside the templates. See the Tera manual for the template language itself; recipes for common SQL-side patterns live in the Cookbook.
API Reference
Function Documentation
Detailed documentation for each function including signatures, parameters, and examples.
tera_render
Signature
Parameters (Positional)
| Parameter | Type | Mode | Description |
|---|---|---|---|
template | VARCHAR | Positional | |
varargs | ANY | Positional | Varargs |
Returns
Description
Render a Tera template with JSON context. Supports the full Tera vocabulary: variables, filters, conditionals, for loops, autoescape, and inclusion. Optional named arguments tune behavior β autoescape := false for non-HTML output, template_path := './templates/*.html' to load templates from disk by name.
Examples
Inline variable
SELECT tera_render('{{ foo }}', '{"foo":"bar"}'); Nested context
SELECT tera_render('Hello {{ user.name }}', '{"user":{"name":"Alice"}}'); Disable autoescape
SELECT tera_render('{{ v }}', '{"v":"B&O"}', autoescape := false); Render a named template file
SELECT tera_render('email.html', '{"name":"Alice"}', template_path := './templates/*.html'); Practical Examples
Cookbook
Real-world recipes and patterns for common use cases.
Variable substitution
SELECT tera_render('{{ foo }}', '{"foo":"bar"}'); -- 'bar'
SELECT tera_render('Hello, World!'); -- 'Hello, World!'
Nested context
SELECT tera_render(
'Hello {{ user.name }}, you have {{ user.messages }} new messages',
'{"user":{"name":"Alice","messages":5}}'
);
Loops
SELECT tera_render(
'{% for x in items %}{{ x.name }}{% if not loop.last %}, {% endif %}{% endfor %}',
'{"items":[{"name":"Apple"},{"name":"Banana"},{"name":"Cherry"}]}'
);
-- 'Apple, Banana, Cherry'
Autoescape
-- Default: autoescape on, HTML special chars are escaped
SELECT tera_render('{{ v }}', '{"v":"B&O"}');
-- 'B&O'
-- Off when generating non-HTML
SELECT tera_render('{{ v }}', '{"v":"B&O"}', autoescape := false);
-- 'B&O'
Render template files from disk
Pass a glob with template_path and use template names instead of inline strings:
SELECT tera_render(
'index.html',
'{"v":"B&O"}',
autoescape := false,
template_path := './templates/*.html'
);
This is the killer feature over inline-only engines β separate authoring of templates from queries that render them.
Per-row HTML email
SELECT email,
tera_render(
'<h1>Hi {{ name }}!</h1><p>{{ unread }} new messages.</p>',
json_object('name', name, 'unread', unread_count)
) AS html
FROM users
WHERE unread_count > 0;
Generate per-host config
SELECT tera_render(
'server { listen 80; server_name {{ host }}; proxy_pass http://{{ upstream }}; }',
json_object('host', hostname, 'upstream', upstream)
) AS nginx_block
FROM virtual_hosts;
Tera vs MiniJinja
minijinja is the other Jinja-style engine in this catalog. Both speak the same dialect for everyday use. Pick Tera when you want template-file inclusion (template_path); pick MiniJinja when you want maximum Python-Jinja compatibility on the filter set. For brand-new projects, either is a defensible choice.
Platform Support
Compatibility
Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.
Quick Facts
Platforms
Supported platform architectures
Compiled binary sizes
| Platform | Architecture | Size |
|---|---|---|
| Linux | aarch64 | 4.86 MB |
| Linux | x86_64 | 5.34 MB |
| Linux (musl) | x86_64 | 5.18 MB |
| macOS | Apple Silicon | 3.05 MB |
| macOS | Intel | 3.46 MB |
| Windows | x86_64 | 8.69 MB |
| WASM | eh | 606.3 KB |
| WASM | mvp | 604.1 KB |
| WASM | threads | 603.9 KB |
Gzipped download size from the DuckDB community-extensions registry.
Tera Templates From SQL
Install Tera to render templated text from JSON context β dynamic config, HTML, notifications, IaC. Sister of MiniJinja with template-file support.