MiniJinja
Render Jinja2-compatible templates from SQL using the embedded MiniJinja engine. Generate HTML, config files, notifications, and dynamic SQL — full Jinja syntax (loops, filters, conditionals, autoescaping) with JSON context.
Install
-- Install the extension
INSTALL minijinja FROM community;
-- Load it into your session
LOAD minijinja;
-- Variable substitution
SELECT minijinja_render('Hello {{ name }}!', '{"name":"World"}');
-- Loops
SELECT minijinja_render(
'Items: {% for x in xs %}{{ x }}{% if not loop.last %}, {% endif %}{% endfor %}',
'{"xs":["A","B","C"]}'
);
-- Disable HTML autoescaping for raw text output
SELECT minijinja_render('{{ v }}', '{"v":"B&O"}', autoescape := false); Technical Overview
Why Use MiniJinja from DuckDB?
Render MiniJinja templates inside SQL with a JSON context. MiniJinja is Armin Ronacher's Rust port of Jinja2 — strict Python-Jinja2 semantics in a small, fast core. This extension exposes it as a scalar function, so each query row can produce its own rendered string — emails, config blocks, dynamic SQL, webhook payloads — without leaving DuckDB.
📝 What this extension is for
Anywhere you'd otherwise pull rows out of DuckDB to feed into a Jinja loop in Python, minijinja_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 MiniJinja as a meta-SQL preprocessor — render
SELECT ... FROM {{ table }} WHERE {{ pred }}against context columns, then execute the result. Treat templates as trusted; Jinja interpolation is not parameter-safe. - • Notifications and webhook payloads: Render Slack / PagerDuty / generic webhook JSON bodies straight from query results so a single SQL pipeline takes you from event to delivered message.
⚙️ How it works
A scalar function backed by the minijinja Rust crate. Each call compiles the template, binds the JSON context, and returns the rendered text. There is no ATTACH, no secrets, no template registry — one function name and a couple of optional named arguments.
- • Template + context: First argument is the template (a
VARCHARliteral or column). Second argument, when present, is a DuckDBJSONvalue — a literal string,json_object(...), or a column. Templates without variables work without any context at all. - • Strict Jinja2 semantics: MiniJinja deliberately tracks Python-Jinja2 behavior — undefined-variable handling, filter naming, scoping, autoescape rules. If a template renders correctly under Python's Jinja2 for these basic features, it should render the same here.
- • Standard vocabulary: Filters (
upper,lower,length,default,join,replace, etc.), control flow (if/elif/else,forwithloop.index/loop.first/loop.last), tests (is defined,is none), and string concatenation. See the MiniJinja crate docs for the full surface. - • 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).
🤝 MiniJinja vs Tera
This catalog ships two Jinja-style template engines as DuckDB extensions: MiniJinja and tera. They overlap heavily — both render {{ var }} / {% for %} / {% if %} against JSON context. The differences are real but narrow.
- • Pick MiniJinja for Jinja2 fidelity: MiniJinja deliberately tracks Python Jinja2 semantics — naming, scoping, filter behavior. Choose it when you have an existing Python-Jinja codebase and want your DuckDB templates to behave the same way, or when you're already fluent in Jinja2.
- • Pick Tera for the larger feature surface:
teraships a wider built-in filter and tester library, template inheritance via atemplate_pathglob, and Tera-specific extensions to the Jinja syntax. Choose it when your templates lean on those features or when you already maintain a Tera template library. - • 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 minijinja_render call producing the HTML body — fed straight into a downstream sender.
Generate per-host configuration
A virtual_hosts table plus a single MiniJinja 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 — MiniJinja 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 shortest path from “rows in a table” to “rendered HTML, one body per row”:
SELECT email,
minijinja_render(
'<h1>Hi {{ name }}!</h1><p>You have {{ unread }} new messages.</p>',
json_object('name', name, 'unread', unread_count)
) AS html_body
FROM users
WHERE unread_count > 0;
minijinja_render compiles the template, binds the json_object context, and returns the rendered string. The template body is plain Jinja2 syntax — anything that renders correctly under Python’s Jinja2 for these basic features should render the same way here.
MiniJinja interpolation is not parameter-safe against SQL injection. If you template SQL strings (the dynamic-SQL pattern), the template body and the values flowing through {{ ... }} must come from a source you trust. For untrusted user input, render the SQL shape with MiniJinja and bind values through prepared-statement parameters.
Autoescape is on by default for HTML output (B&O becomes B&O). Pass autoescape := false when rendering non-HTML targets like configs, JSON, or SQL — see the recipes in the Cookbook.
Architecture
A single scalar function backed by the minijinja Rust crate — Armin Ronacher’s MiniJinja, a small Jinja2-compatible engine with strict Python-Jinja2 semantics. Each call compiles the template, binds the JSON context, and returns the rendered text. There is no ATTACH, no secrets, no template registry — the function surface is one name plus a couple of optional named arguments.
The template argument is a VARCHAR literal or column. The context argument is any DuckDB JSON value — a literal string, json_object(...), or a JSON column. Inside the template you address fields with normal Jinja syntax: {{ user.name }}, {% for x in items %}, {{ items | length }}, {% if user.admin %}…{% endif %}.
Filters (upper, lower, length, default, join, replace, trim, …), control flow (if / elif / else, for with the loop object), and tests (is defined, is none) all work as documented in the Jinja2 template reference and the MiniJinja crate docs.
Compared to alternatives
- String concatenation in SQL — fine for two or three substitutions; falls apart once you need loops, conditionals, or autoescape. MiniJinja gives you the structured-template surface without leaving the query.
- Python / Node.js post-processing — pull rows, render in app code, push results back. Works, but introduces an extra hop and serializer per template. With this extension the rendering happens inline in DuckDB’s vectorized execution.
tera— the sibling Jinja-style engine in this catalog. Tera ships a wider built-in filter and tester library, template-file inclusion via atemplate_pathglob, and a handful of Tera-specific syntax extensions. Pick Tera when you want those features; pick MiniJinja when you want strict Jinja2 semantics that match an existing Python codebase.
API Reference
Function Documentation
Detailed documentation for each function including signatures, parameters, and examples.
minijinja_render
Signature
Parameters (Positional)
| Parameter | Type | Mode | Description |
|---|---|---|---|
template | VARCHAR | Positional | |
varargs | ANY | Positional | Varargs |
Returns
Description
Render a MiniJinja (Jinja2-compatible) template with a JSON context. Supports the full Jinja vocabulary: variable expressions, filters, conditionals, for/while loops, and template-level autoescaping. Optional named arguments tune behavior (e.g. autoescape := false).
Examples
Basic substitution
SELECT minijinja_render('{{ foo }}', '{"foo":"bar"}'); Nested fields
SELECT minijinja_render('Hello {{ user.name }}, {{ user.messages }} new', '{"user":{"name":"Alice","messages":5}}'); For loop with comma separator
SELECT minijinja_render('{% for x in xs %}{{ x }}{% if not loop.last %}, {% endif %}{% endfor %}', '{"xs":["A","B","C"]}'); Disable autoescape for raw text
SELECT minijinja_render('{{ v }}', '{"v":"B&O"}', autoescape := false); minijinja_render_with_context
Signature
Parameters (Positional)
| Parameter | Type | Mode | Description |
|---|---|---|---|
template | VARCHAR | Positional | |
context | JSON | Positional | |
varargs | ANY | Positional | Varargs |
Returns
Description
Render a Jinja2-style template with a JSON context
Examples
SELECT minijinja_render_with_context('Hello {{ name }}!', '{"name": "World"}'); Output
| minijinja_render_with_context('Hello {{ name }}!', '{"name": "World"}') |
|---|
| Hello World! |
Practical Examples
Cookbook
Real-world recipes and patterns for common use cases.
What MiniJinja can do
MiniJinja is a Rust port of Jinja2 — same syntax, same semantics, fast embedded engine. Templates are strings; context is JSON. Anywhere SQL gives you a string column, this extension can render it.
Variable substitution
-- Flat
SELECT minijinja_render('{{ name }}', '{"name":"Alice"}');
-- 'Alice'
-- Nested
SELECT minijinja_render(
'Hello {{ user.name }}, {{ user.messages }} new messages',
'{"user":{"name":"Alice","messages":5}}'
);
Loops
SELECT minijinja_render(
'Items: {% for x in items %}{{ x.name }}{% if not loop.last %}, {% endif %}{% endfor %}',
'{"items":[{"name":"Apple"},{"name":"Banana"},{"name":"Cherry"}]}'
);
-- 'Items: Apple, Banana, Cherry'
loop.last, loop.first, loop.index, loop.index0 are available inside for.
Conditionals
SELECT minijinja_render(
'{% if user.admin %}Admin{% else %}User{% endif %}: {{ user.name }}',
'{"user":{"name":"Alice","admin":true}}'
);
-- 'Admin: Alice'
Filters
-- upper, lower, title, length, default, etc.
SELECT minijinja_render('{{ name | upper }}', '{"name":"alice"}'); -- 'ALICE'
SELECT minijinja_render(
'Total: {{ items | length }}',
'{"items":[1,2,3,4,5]}'
);
-- 'Total: 5'
HTML rendering with autoescape
By default, special HTML characters in context values are escaped:
SELECT minijinja_render('{{ v }}', '{"v":"B&O"}');
-- 'B&O'
-- Disable when generating non-HTML content (configs, scripts, etc.)
SELECT minijinja_render('{{ v }}', '{"v":"B&O"}', autoescape := false);
-- 'B&O'
Generate HTML email per row
A common pattern — render one template per database row using the row’s columns as context:
SELECT email,
minijinja_render(
'<h1>Hi {{ name }}!</h1><p>You have {{ unread }} new messages.</p>',
json_object('name', name, 'unread', unread_count)
) AS html_body
FROM users
WHERE unread_count > 0;
The json_object(...) builds the context inline from columns.
Generate dynamic SQL
WITH plan AS (
SELECT minijinja_render(
'SELECT {{ cols | join(", ") }} FROM {{ table }} WHERE created_at > {{ since }}',
'{"cols":["id","name"],"table":"users","since":"2026-01-01"}'
) AS sql
)
SELECT sql FROM plan;
(Templated SQL is for trusted templates only — Jinja interpolation is not parameter-safe against injection. For untrusted input, use prepared-statement parameters.)
Generate config files / IaC
SELECT minijinja_render(
'server {
listen 80;
server_name {{ host }};
location / {
proxy_pass http://{{ upstream }};
}
}',
json_object('host', hostname, 'upstream', upstream)
) AS nginx_block
FROM virtual_hosts;
When to pick MiniJinja vs Tera
tera is the other Jinja-style engine in this catalog. They’re nearly equivalent — both support the standard Jinja syntax. Pick MiniJinja if you specifically want MiniJinja’s Rust crate compatibility (its filter set matches Python’s Jinja most closely), Tera if you already have a Tera template library elsewhere.
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.24 MB |
| Linux | x86_64 | 4.66 MB |
| Linux (musl) | x86_64 | 4.49 MB |
| macOS | Apple Silicon | 2.52 MB |
| macOS | Intel | 2.87 MB |
| Windows | x86_64 | 8.10 MB |
| WASM | eh | 330.6 KB |
| WASM | mvp | 327.7 KB |
| WASM | threads | 327.9 KB |
Gzipped download size from the DuckDB community-extensions registry.
Jinja Templates From SQL
Install MiniJinja to render templates with JSON context — for HTML, configs, notifications, and dynamic SQL generation.