📝

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_object and render against a template literal.
  • Config-file generation: Materialize one nginx / systemd / Kubernetes manifest per row of a configuration table. Disable autoescape with autoescape := false for 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 VARCHAR literal or column). Second argument, when present, is a DuckDB JSON value — 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, for with loop.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 := false when 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: tera ships a wider built-in filter and tester library, template inheritance via a template_path glob, 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.

Trusted templates only

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&amp;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 a template_path glob, 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

Scalar Function Render
Signature
minijinja_render(template: VARCHAR, varargs: ANY) → VARCHAR
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
1

Basic substitution

SELECT minijinja_render('{{ foo }}', '{"foo":"bar"}');
2

Nested fields

SELECT minijinja_render('Hello {{ user.name }}, {{ user.messages }} new', '{"user":{"name":"Alice","messages":5}}');
3

For loop with comma separator

SELECT minijinja_render('{% for x in xs %}{{ x }}{% if not loop.last %}, {% endif %}{% endfor %}', '{"xs":["A","B","C"]}');
4

Disable autoescape for raw text

SELECT minijinja_render('{{ v }}', '{"v":"B&O"}', autoescape := false);

minijinja_render_with_context

Scalar Function
Signature
minijinja_render_with_context(template: VARCHAR, context: JSON, varargs: ANY) → VARCHAR
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
1
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&amp;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

Software License MIT
Pricing Free
Written In Rust
Source Available Yes
View on GitHub
Usage
32,703+
loads in last 30 days

Platforms

Linux
Linux (musl)
macOS
Windows
WASM
Supported platform architectures
Linux: aarch64, x86_64
Linux (musl): x86_64
macOS: Intel, Apple Silicon
Windows: x86_64
WASM: threads, eh, mvp
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.

DuckDB Versions

Release calendar
Supported
v1.4.4 v1.5.2

Jinja Templates From SQL

Install MiniJinja to render templates with JSON context — for HTML, configs, notifications, and dynamic SQL generation.