MiniJinja
Render Jinja2-compatible templates from SQL using the embedded MiniJinja engine.
On this page
Technical Overview
Jinja2 semantics, exposed as a scalar function
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 taking a JSON context, so each query row can produce its own rendered string — emails, config blocks, dynamic SQL, webhook payloads — without leaving DuckDB.
A template loop, collapsed into a SQL projection
The usual shape is: pull rows out of DuckDB, loop over them in Python or Node feeding each into a Jinja render(), then push the results back. That's an extra hop and a serializer round-trip per template. This extension turns the whole loop into a single SQL projection — the template stays declarative, the data stays in the query, and rendering happens inline in DuckDB's vectorized executor. Anywhere SQL hands you a string column, MiniJinja can render it: per-recipient email bodies, one config file per row of a configuration table, dynamic SQL driven from a metadata table, or webhook/Slack JSON payloads built straight from the query that produced the event.
How it works
A single scalar function backed by the minijinja Rust crate. The first argument is the template (a VARCHAR literal or column); the second, when present, is any DuckDB JSON value — a literal, json_object(...), or a JSON column. There is no ATTACH, no secrets, and no template registry: just one function name and a couple of optional named arguments like autoescape.
- • Compiled per call, against the row's context: Each invocation compiles the template, binds the JSON context, and returns the rendered text. Because compilation happens per call, a template column is rendered row by row with each row's own context — there's no precompiled-template handle to manage and nothing persists between rows.
- • Strict Jinja2 semantics: MiniJinja deliberately tracks Python-Jinja2 behavior — undefined-variable handling, filter naming, scoping, autoescape rules. A template that renders correctly under Python's Jinja2 for these features should render the same here.
-
•
Trusted templates, not parameter-safe: Jinja interpolation is string substitution, not SQL parameter binding — it is not injection-safe. When you template SQL (the dynamic-SQL pattern), the template body and the values flowing through
{{ ... }}must come from a source you trust. For untrusted input, render the SQL shape with MiniJinja and bind the values through prepared-statement parameters. -
•
Autoescape on by default: HTML special characters in context values are escaped (
B&ObecomesB&O) — the safe default for HTML and email. Passautoescape := falsewhen generating raw text such as configs, SQL, or 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, and for everyday substitution both produce identical output. 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 you want your DuckDB templates to match, 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.
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_bodyFROM usersWHERE 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.
Install
INSTALL minijinja FROM community;
LOAD minijinja;
Quick Start
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);
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Type | Description |
|---|---|---|
|
Functions
|
||
| minijinja_render_with_context() | Render a Jinja2-style template with a JSON context | |
|
Render
Render Jinja2-compatible templates with JSON context. Supports variable substitution, filters, conditionals, for/while loops, and HTML autoescaping. The single-function surface — pass template + context, get rendered text. |
||
| minijinja_render() | Render a MiniJinja (Jinja2-compatible) template with a JSON context. | |
No extension contents match that search.
API Reference
Function Documentation
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
-- FlatSELECT minijinja_render('{{ name }}', '{"name":"Alice"}');-- 'Alice'-- NestedSELECT 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_bodyFROM usersWHERE 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_blockFROM 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
- Linux x86_64 aarch64
- Linux (musl) Not available
- macOS Intel Apple Silicon
- Windows x86_64
- WASM eh mvp threads
Compiled binary sizes
| Platform | Architecture | Size |
|---|---|---|
| Linux | x86_64 | 4.77 MB |
| Linux | aarch64 | 4.36 MB |
| macOS | Intel | 2.75 MB |
| macOS | Apple Silicon | 2.42 MB |
| Windows | x86_64 | 7.96 MB |
| WASM | eh | 413.3 KB |
| WASM | mvp | 507.6 KB |
| WASM | threads | 410.2 KB |
Compressed download size from the Haybarn extension repository.
DuckDB & Haybarn
Release calendar- DuckDB v1.5.5 Haybarn 1.5.5-rc1 Supported