A template can do more work than its output suggests. Two nested loops can run for a long time without printing a character. A report template can include itself. A missing field can quietly disappear from a rendered message.

The new work on MiniJinja’s DuckDB 2.0 branch makes those cases explicit. It adds an instruction budget, fixes undefined-value modes, moves file loading under DuckDB’s access controls, and reuses compiled templates across rows. The normal use remains the same: pass a template and the row you want it to render.

Pass the context explicitly

Use minijinja_render_with_context when the template needs values:

SELECT minijinja_render_with_context(
'{{ name }}: {{ tags | join(",") }}', t
) AS rendered
FROM (VALUES ('Ada', ['x', 'y'])) t(name, tags);
result
rendered
Ada: x,y

A DuckDB struct, including a whole row, is cast to JSON for the context. The context must be an object. Arrays, numbers, and JSON null now raise clear errors rather than being accepted as something a template cannot use as named variables. SQL NULL still produces SQL NULL.

The 2.0 API declares options such as autoescape, template_path, and undefined_behavior as native named parameters. DuckDB checks the names and types, supplies the defaults, and exposes them in duckdb_functions().

Spend a fixed number of instructions

The new fuel option limits the work of one render:

SELECT minijinja_render(
'{% for i in range(10) %}{{ i }}{% endfor %}',
fuel := 10000
) AS rendered;
expected result · November 7 build
rendered
0123456789

When a template spends that allowance, rendering fails with OutOfFuel. The allowance starts again for the next render; it is not a shared budget for the whole column.

The default is fuel := 0, which disables the instruction limit. Set a nonzero budget when you want a limit. Fuel is an evaluator instruction count, not milliseconds or output bytes, so choose it against the templates you expect to run.

Other checks apply independently. range() is capped at 100,000 elements. Deeply nested templates hit parser or runtime recursion limits, and tags containing more than 10,000 operators are rejected before the resulting expression tree can exhaust the stack. These checks still matter when fuel is unlimited.

Choose what a missing value means

semi_strict previously behaved like lenient. That is a significant difference for a report: a missing optional field should be testable, but printing it by accident should fail.

The modes now differ as follows:

Mode Missing-value behavior
lenient Can render a missing value as empty text; accessing its attributes can still fail
strict Rejects use of an undefined value, including a conditional test
semi_strict Allows testing a missing value, but rejects printing it
chainable Allows chains such as missing.a.b to remain undefined and render empty

For example:

SELECT minijinja_render(
'[{% if missing %}x{% endif %}]',
undefined_behavior := 'semi_strict'
) AS rendered;
expected result · November 7 build
rendered
[]

Changing the template to '[{{ missing }}]' with the same mode raises UndefinedError. A misspelled mode is rejected instead of silently selecting another behavior. If an old template relied on semi_strict dropping missing printed values, this update will expose it.

Load files through the database

template_path now reads templates through DuckDB’s filesystem. That puts file access under enable_external_access, allowed_directories, and allowed_paths. Inline templates still render with external access disabled; they cannot use an include to read an arbitrary host file.

A path can identify a directory, file, or glob. The matching templates are read when the query is bound and named relative to the selected directory. Includes and inheritance then resolve against that loaded set. There are also limits on the number and total size of loaded files.

This changes the old arrangement in which the Rust library opened files itself. Applications that restrict DuckDB’s filesystem access should expect those restrictions to apply to template loading too.

Reuse the template, keep each row’s context

The extension now retains rendering environments in thread-local query state. An inline template used for a whole column can be compiled once; a varying template column uses a bounded cache of 64 distinct texts. The context is supplied separately for every render.

The update also enables tojson and urlencode, supports the configured MiniJinja convenience filters and string methods, and fixes embedded NUL handling. NUL characters in rendered text are dropped rather than reaching a Rust string conversion that aborts the process. That is deliberate output behavior, so do not use this renderer to preserve binary text byte-for-byte.

Build covered here

This article covers the November 7 source update and its hardening tests. While preparing it on November 11, the extension repository served build 5241d1f9d5, which predates this work. The new fuel and hardening behavior therefore needs a refreshed build before the examples can be followed from a normal install. The extension page remains the installation and API reference.