The Tera extension lets a query render text from a row. That is useful for a report or a generated configuration file, but it also makes the template engine part of the database’s execution environment. Reading a template file should follow the same access rules as reading any other file from SQL.

Previously, Tera loaded files through its own Rust code. It also exposed a built-in get_env() function that could read the host process’s environment. The DuckDB 2.0 update moves file access through DuckDB and disables environment reads by default. It also fixes escaping for loaded templates and catches several kinds of recursion before they can exhaust the process stack.

Render a row directly

The context can be a JSON object or a DuckDB struct. A whole row works too:

SELECT tera_render('{{ id }}:{{ name }}', r) AS rendered
FROM (SELECT 7 AS id, 'duck' AS name) r;
result
rendered
7:duck

Nested objects and lists keep their structure:

SELECT tera_render(
'{{ a.b }} {{ c | join(sep="-") }}',
{'a': {'b': 1}, 'c': [1, 2]}
) AS rendered;
result
rendered
1 1-2

Contexts must now be JSON objects. The old implementation could silently ignore an array or scalar context. The new one says what is wrong, such as Invalid context: expected a JSON object, got an array. SQL NULL returns SQL NULL; a missing variable used by a template remains a rendering error.

Options are native DuckDB named parameters in this build. They are checked by the binder and appear alongside the positional arguments in duckdb_functions().

Make escaping consistent

Inline templates already escaped values by default:

SELECT tera_render('{{ company }}', '{"company": "B&O"}') AS rendered;
result
rendered
B&O

Earlier file-backed rendering did not consistently honor that same default. The 2.0 branch fixes it: with autoescape := true and the default empty autoescape_extensions list, every template is escaped. Supply a suffix list if only some named templates should be escaped.

For plain text where escaping is unwanted, make that choice explicit:

SELECT tera_render(
'{{ company }}', '{"company": "B&O"}',
autoescape := false
) AS rendered;
result
rendered
B&O

Existing file-based reports can therefore change output after upgrading. Check rendered ampersands, angle brackets, and quotes rather than assuming a successful query means identical text.

Give the loader the files it needs

With template_path, the first argument names a template from the loaded set. For a directory containing page.html, base.html, and a partials directory, a call can look like this:

SELECT tera_render(
'page.html', {'title': 'Weekly report'},
template_path := './report-templates/**/*.html'
);

This example needs those template files on disk. Their names are relative to the directory before the glob, so an include can refer to partials/header.html.

The extension reads matching files through DuckDB’s filesystem when binding the query. enable_external_access, allowed_directories, and allowed_paths now govern those reads. A file larger than 64 MiB is rejected. Includes and inheritance operate on the loaded templates; they do not provide a second path around the database’s file checks.

Inline rendering still works when external access is disabled. It does not need to open a template file.

Environment access is a separate choice

get_env() can reveal more than a convenient configuration value. The database process may have credentials in its environment, and a SQL caller should not gain access just by writing a template.

The new tera_allow_get_env setting defaults to false. Enabling it permits Tera’s environment function only while DuckDB external access is also enabled. Setting enable_external_access = false keeps it disabled even if tera_allow_get_env is true.

For ordinary rendering, pass required values in the context instead. The template then declares its inputs through the row it receives, and the query does not depend on the environment of the process that happens to run it.

Reject cycles before rendering them

Tera can include templates and import macro files. A cycle in either graph used to recurse without a useful stopping point. The extension now rejects cycles and include chains deeper than 32. Recursive macros are rejected too, including indirect recursion through other macros.

Before parsing, it also checks for more than 64 nested blocks or brackets, or more than 256 operators in one tag. String literals and raw blocks do not count as template structure. These are checks on pathological template shapes; they are not a general wall-clock or output-size limit for every possible render.

The implementation keeps parsed templates in query-local execution state, with a bounded cache for varying inline template text. It can reuse the template while supplying a different context for each row. NUL bytes in the rendered output are dropped instead of causing a panic at the Rust boundary.

The hardening update, escaping fix, and security tests document the changes. The Tera page has the full function reference. The main upgrade checks are file permissions, environment-dependent templates, recursive macros, and the escaping of file-backed output.