DuckDB Extension
Tera
Render Tera templates from DuckDB SQL using JSON context. Generate text or HTML with variables, loops, and filters, or load templates from files.
On this page
Render a template for each query row
Tera is a template language inspired by Jinja2. It uses placeholders, conditions, loops, and filters to combine a template with structured input. The DuckDB extension takes JSON context and returns the rendered text.
Use it to generate configuration files, messages, or HTML from query results, particularly when you already maintain Tera templates. SQL selects and prepares the data; the template specifies the output format. Set HTML escaping according to where the rendered string will be used.
Example
Render a greeting
Pass a JSON object as the template context. Its name field fills the template variable.
Tested with DuckDB 1.5.6. Browser examples use Haybarn-Wasm 1.5.6.
INSTALL tera FROM community;
LOAD tera;
SELECT tera_render('Hello {{ name }}!', '{"name":"Mina"}') AS greeting;
SET allow_extension_repositories = 'allowed';
CREATE EXTENSION REPOSITORY IF NOT EXISTS query_farm
WITH PREFIX 'https://duckdb-extensions.query.farm';
INSTALL tera FROM query_farm;
LOAD tera FROM query_farm;
SELECT tera_render('Hello {{ name }}!', '{"name":"Mina"}') AS greeting;
Expected result
| greeting |
|---|
| Hello Mina! |
Tera and MiniJinja have similar syntax but different engines and filter support. Test existing templates before switching between them.
Common tasks
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 htmlFROM usersWHERE 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;The files are read and parsed once per query (per worker thread), not once per row, and inline templates are likewise parsed once and reused for every row that uses the same template text.
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'The same default applies to templates loaded with template_path, whatever their file extension. To escape only some files — say, .html templates but not .txt ones in the same glob — list the suffixes in autoescape_extensions:
SELECT tera_render('welcome.txt', json_object('name', name), template_path := './templates/*', autoescape_extensions := ['.html'])FROM users;-- welcome.txt renders unescaped; any .html template in the glob is escapedOptions
On DuckDB 2.0 the options are native named parameters, declared keyword-only with typed defaults. Both overloads share them:
tera_render(template VARCHAR, *, autoescape BOOLEAN := true, template_path VARCHAR := '', autoescape_extensions VARCHAR[] := [])
tera_render(template VARCHAR, context JSON, *, ...same options...)| Option | Default | Effect |
|---|---|---|
autoescape |
true |
HTML-escape interpolated values, in inline templates and template_path files alike. Pass false for configs, SQL, JSON or plain text. |
template_path |
'' |
Glob of template files; template is then a template name. |
autoescape_extensions |
[] |
With template_path, restrict escaping to file templates whose names end with one of these suffixes (for example ['.html']); the others render unescaped. With the default [], autoescape escapes every template whatever its extension. Inline templates ignore this list. |
- Keyword-only. Options are always passed as
name := value;tera_render('Hello', '{}', false)does not match any signature. The positional arguments can be named too, in any order:tera_render(context := '{"a": 1}', template := '{{ a }}'). - Typed. DuckDB casts the value to the declared type when it can do so implicitly — string literals included, so
autoescape := 'false'works — whileautoescape := 1or a misspelled name such asautoescap := falsefails with DuckDB’s standard No function matches the given name and argument types error. - Constant. Options are read when the query is bound, so they must be constant expressions; per-row values belong in the context.
- NULL in, NULL out. A NULL template, context, or option (
autoescape := NULL) returns NULL. A NULL element insideautoescape_extensionsis an error.
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, three 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.
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Type | Description |
|---|---|---|
|
Render
Render Tera templates with JSON context. Single-function surface; the optional |
||
| tera_render() | Object type: Scalar function | Render a Tera template, optionally with a JSON context. |
No extension contents match that search.
API Reference
Function Reference
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 escapedSELECT tera_render('{{ v }}', '{"v":"B&O"}');-- 'B&O'-- Off when generating non-HTMLSELECT 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');-- 'B&O' (file templates are HTML-escaped by default, like inline ones)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 htmlFROM usersWHERE 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_blockFROM 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
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 | 6.30 MB |
| Linux | aarch64 | 5.54 MB |
| macOS | Apple Silicon | 4.01 MB |
| Windows | x86_64 | 11.38 MB |
| Windows | x86_64 (MinGW) | 20.72 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 .