DuckDB Extension
MiniJinja
Render Jinja-style templates from DuckDB SQL with MiniJinja. Pass JSON context to generate text, HTML, configuration, or SQL strings.
On this page
Turn query results into formatted text
MiniJinja is a template engine using Jinja-style expressions, conditions, loops, and filters. This extension combines a template with JSON context inside DuckDB and returns the rendered text as a string.
Use it to generate a message, configuration fragment, or HTML snippet from each query row. Your SQL supplies the values and the template controls their presentation. Choose escaping appropriate to the output: HTML needs different treatment from plain text.
Example
Render a message from JSON context
The template inserts a name and loops through a short list. This example produces plain text.
Tested with DuckDB 1.5.6. Browser examples use Haybarn-Wasm 1.5.6.
INSTALL minijinja FROM community;
LOAD minijinja;
SELECT minijinja_render_with_context(
'Hello {{ name }}! Items: {{ items | join(", ") }}',
'{"name":"Mina","items":["tea","coffee"]}',
autoescape := false
) AS message;
SET allow_extension_repositories = 'allowed';
CREATE EXTENSION REPOSITORY IF NOT EXISTS query_farm
WITH PREFIX 'https://duckdb-extensions.query.farm';
INSTALL minijinja FROM query_farm;
LOAD minijinja FROM query_farm;
SELECT minijinja_render_with_context(
'Hello {{ name }}! Items: {{ items | join(", ") }}',
'{"name":"Mina","items":["tea","coffee"]}',
autoescape := false
) AS message;
Expected result
| message |
|---|
| Hello Mina! Items: tea, coffee |
Use HTML autoescaping when rendering HTML. Disabling it here is appropriate for plain text; rendered SQL is still text until you explicitly execute it.
Common tasks
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_with_context( '<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_with_context 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
Two scalar entry points use the minijinja Rust crate: minijinja_render(template, *, options) for rendering without context, and minijinja_render_with_context(template, context, *, options) for JSON context. Use the latter for both literals and per-row context columns — minijinja_render takes exactly one positional argument, so passing a context to it is rejected when the query is bound.
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.
Options
The render options are native DuckDB named parameters, declared keyword-only with typed defaults — DuckDB matches the names, casts the values and fills in the defaults, so the signature you see in duckdb_functions() is the one the binder enforces:
minijinja_render(template VARCHAR, *, autoescape BOOLEAN := true, template_path VARCHAR := '', undefined_behavior VARCHAR := 'lenient', autoescape_extensions VARCHAR[] := [])
minijinja_render_with_context(template VARCHAR, context JSON, *, ...same options...)| Option | Default | Effect |
|---|---|---|
autoescape |
true |
HTML-escape interpolated values. Pass false for configs, SQL, JSON or plain text. |
template_path |
'' |
Directory to load templates from; template then names a file there, enabling {% include %} / {% extends %}. |
undefined_behavior |
'lenient' |
'lenient', 'chainable', 'semi_strict' or 'strict' — how undefined variables render. Other values are a binder error. |
autoescape_extensions |
[] |
When non-empty, autoescape only templates whose name ends with one of these suffixes. |
- Keyword-only. Options are always passed as
name := value;minijinja_render('Hello', false)does not match any signature. The positional arguments can be named too, in any order:minijinja_render_with_context(context := '{"a": 1}', template := '{{ a }}'). - Typed. A value is accepted if DuckDB can cast it implicitly — string literals included, so
autoescape := 'false'works — whileautoescape := 1or an unknown name such asautoescap := falsefails with DuckDB’s standard No function matches the given name and argument types error. - Constant. Options are read once when the query is bound, so they must be constant expressions (
autoescape := random() > 0.5is an error). Per-row values belong in the context. - NULL in, NULL out. A NULL template, a NULL context, or an explicit NULL option (
autoescape := NULL) returns NULL. A NULL element insideautoescape_extensionsis still an error.
SELECT minijinja_render('A & B', autoescape := 'false');-- 'A & B'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.
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Type | Description |
|---|---|---|
|
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() | Object type: Scalar function | Render a MiniJinja template without a JSON context. |
| minijinja_render_with_context() | Object type: Scalar function | Render a MiniJinja template with a JSON context, including contexts built from columns with json_object. |
No extension contents match that search.
API Reference
Function Reference
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_with_context('{{ name }}', '{"name":"Alice"}');-- 'Alice'-- NestedSELECT minijinja_render_with_context( 'Hello {{ user.name }}, {{ user.messages }} new messages', '{"user":{"name":"Alice","messages":5}}');Loops
SELECT minijinja_render_with_context( '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_with_context( '{% 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_with_context('{{ name | upper }}', '{"name":"alice"}'); -- 'ALICE'SELECT minijinja_render_with_context( '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_with_context('{{ v }}', '{"v":"B&O"}');-- 'B&O'-- Disable when generating non-HTML content (configs, scripts, etc.)SELECT minijinja_render_with_context('{{ 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_with_context( '<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. Use minijinja_render_with_context for this: minijinja_render takes only the template positionally, and its named options must be constant expressions.
Generate dynamic SQL
WITH plan AS ( SELECT minijinja_render_with_context( 'SELECT {{ cols | join(", ") }} FROM {{ table }} WHERE created_at > TIMESTAMP ''{{ since }}''', '{"cols":["id","name"],"table":"users","since":"2026-01-01"}', autoescape := false ) 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_with_context( '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
DuckDB 2.0.0
- Linux x86_64 aarch64
- Linux (musl) Not available
- macOS Apple Silicon
- Windows x86_64 x86_64 (MinGW)
- WASM Not available
Compiled binary sizes
| Platform | Architecture | Size |
|---|---|---|
| Linux | x86_64 | 5.64 MB |
| Linux | aarch64 | 4.94 MB |
| macOS | Apple Silicon | 3.50 MB |
| Windows | x86_64 | 10.80 MB |
| Windows | x86_64 (MinGW) | 19.75 MB |
Compressed download sizes for the release shown above.
DuckDB & Haybarn
Release calendar- DuckDB v2.0.0 Query.Farm repository Supported
- Haybarn v1.5.6 Built-in community repository Supported
Browser examples use Haybarn-Wasm 1.5.6.
Checked .