Skip to content

fix: render Jinja blocks before splitting on semicolons (#2650)#2782

Open
sfc-gh-moczko wants to merge 2 commits intomainfrom
fix/2650-jinja-block-semicolons
Open

fix: render Jinja blocks before splitting on semicolons (#2650)#2782
sfc-gh-moczko wants to merge 2 commits intomainfrom
fix/2650-jinja-block-semicolons

Conversation

@sfc-gh-moczko
Copy link
Collaborator

@sfc-gh-moczko sfc-gh-moczko commented Feb 24, 2026

Summary

  • Fixes SNOW-2397013: Jinja IF-statements are not interpreted correctly #2650: Jinja block statements ({% if %}, {% for %}, etc.) containing semicolons were broken because split_statements() splits on ; before Jinja rendering, producing invalid template fragments.
  • Moves Jinja rendering to a pre-render step on the whole content before split_statements(), while legacy (&{ }) and standard (<% %>) syntax continues to render per-statement (they are variable-only, no blocks).
  • Adds 7 new tests covering: if-blocks, for-loops, if/else, mixed Jinja+standard syntax, false conditions, file-based input, and undefined variable errors.

Root Cause

The SQL execution pipeline called split_statements() then per-statement template rendering. Since split_statements is unaware of Jinja block syntax, a query like:

{% if var == 'Jinja' %}select 1;{% endif %}

was split into two broken fragments:

  1. {% if var == 'Jinja' %}select 1; -- unclosed if block --> TemplateSyntaxError
  2. {% endif %} -- orphaned endif --> TemplateSyntaxError

Fix

Jinja block rendering now happens on the whole content before splitting. The per-statement pipeline still handles legacy/standard variable substitution (which never had block syntax). This is safe because Jinja {{ }}/{% %} syntax is orthogonal to <% %>/&{ } -- rendering one leaves the other untouched.

Changes

File Change
statement_reader.py Add pre_render parameter to query_reader() and files_reader()
manager.py Split Jinja into pre-render step (whole content) + per-statement step (legacy/standard only)
tests/sql/test_sql.py Add 7 test cases for Jinja block scenarios

Test plan

  • Reproduction test passes
  • For-loop test produces 3 statements
  • If/else test selects correct branch
  • Mixed Jinja + standard syntax works
  • False condition raises CliArgumentError (no statements to execute)
  • File-based Jinja blocks via -f flag work
  • Undefined variable in block raises clear error
  • All 83 tests in test_sql.py pass (including 10 existing parametrized syntax config tests)
  • All 33 test_statement_reader.py tests pass
  • All pre-commit hooks pass (ruff, black, mypy, codespell)
  • Live Snowflake smoke test: 3 scenarios pass end-to-end

@sfc-gh-moczko sfc-gh-moczko requested a review from a team as a code owner February 24, 2026 22:19
@sfc-gh-moczko sfc-gh-moczko force-pushed the fix/2650-jinja-block-semicolons branch 2 times, most recently from de27892 to f6d865b Compare March 3, 2026 09:06
@sfc-gh-moczko sfc-gh-moczko self-assigned this Mar 3, 2026
@sfc-gh-moczko sfc-gh-moczko marked this pull request as draft March 3, 2026 09:55
@sfc-gh-moczko sfc-gh-moczko marked this pull request as ready for review March 3, 2026 10:34
@sfc-gh-moczko sfc-gh-moczko force-pushed the fix/2650-jinja-block-semicolons branch from 225511d to 1f5d863 Compare March 5, 2026 14:01
Maciej Oczko and others added 2 commits March 18, 2026 17:42
`split_statements()` splits SQL on `;` before template rendering,
breaking Jinja block statements (`{% if %}`, `{% for %}`, etc.) that
contain semicolons. This moves Jinja rendering to a pre-render step
on the whole content before splitting, while legacy/standard syntax
continues to render per-statement.

Closes #2650

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
The for-loop renders to 3 separate SQL statements, which correctly
produces nested result sets consistent with all other multi-statement
output in the CLI.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
@sfc-gh-moczko sfc-gh-moczko force-pushed the fix/2650-jinja-block-semicolons branch from 1f5d863 to 4f42dd4 Compare March 18, 2026 17:43
Copy link

@sfc-gh-olorek sfc-gh-olorek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments, I am a bit scarred on changing the order of the rendering pipeline, as it may produce some unwanted side effects.

# per-statement rendering is fine.
# See: https://github.com/snowflakedb/snowflake-cli/issues/2650
jinja_pre_render = None
if template_syntax_config.enable_jinja_syntax:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure whether it may break existing users.

This changes the rendering order when both Jinja and standard/legacy syntax are enabled together.

Previously, within a single snowflake_sql_jinja_render call, the order was standard/legacy first, then Jinja. Now with the pre-render approach, Jinja runs first on the whole content, then standard/legacy runs per-statement after splitting.

This means a template like: {% if x %}<% name %>{% endif %}

Now resolves Jinja first → <% name %> then standard then Alice. Before: within the same render call, standard would have been attempted first on the unsplit content.

Could this break existing users who rely on the old order : for exmple: Jinja
templates that produce output containing {{ }} that should NOT be re-rendered?

Could we check with test like that:

@mock.patch("snowflake.cli._plugins.sql.manager.SqlManager._execute_string")
    def test_standard_variable_containing_jinja_block(mock_execute_query):
        manager = SqlManager()
        query = "<% snippet %>"
        _, results = manager.execute(
            query=query,
            files=None,
            std_in=False,
            data={"snippet": "{% if True %}select 1;{% endif %}"},
            template_syntax_config=SQLTemplateSyntaxConfig(
                enable_legacy_syntax=False,
                enable_standard_syntax=True,
                enable_jinja_syntax=True,
            ),
        )
        list(results)
        # Old behavior: resolves to "select 1;"
        # New behavior: resolves to "{% if True %}select 1;{% endif %}"
        mock_execute_query.assert_called_once_with("select 1;", cursor_class=mock.ANY)

stmts = split_statements(io.StringIO(f.read()), remove_comments)
content = f.read()
if pre_render:
content = pre_render(content)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre_render only applies to top-level file content. recursive_statatment_reader needs to recieve and apply pre_render to sourced file content also. I think test case as:

@mock.patch("snowflake.cli._plugins.sql.manager.SqlManager._execute_string")
    def test_jinja_block_in_sourced_file_with_semicolons(
        mock_execute, runner, tmp_path
    ):
        """Jinja blocks with semicolons in !source-included files
        should also be pre-rendered before splitting."""
        sourced = tmp_path / "sourced.sql"
        sourced.write_text("{% if True %}select 42;{% endif %}")

        main_file = tmp_path / "main.sql"
        main_file.write_text(f"!source {sourced};")

        result = runner.invoke(
            ["sql", "-f", str(main_file), "--enable-templating", "jinja"]
        )

        assert result.exit_code == 0, result.output
        mock_execute.assert_called_once_with("select 42;", cursor_class=mock.ANY)

would fail

content = f.read()
if pre_render:
content = pre_render(content)
stmts = split_statements(io.StringIO(content), remove_comments)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs before split_statements removes comments, so Jinja-like syntax inside SQL comments (e.g. -- {% if debug %}) would be resolved and could cause template errors.

I think sth like this will cause errors:

    @mock.patch("snowflake.cli._plugins.sql.manager.SqlManager._execute_string")
    def test_jinja_syntax_in_sql_comment_causes_error(mock_execute_query):
        """Jinja-like syntax in SQL comments is resolved by pre-render
        because pre-render runs before comment removal."""
        manager = SqlManager()
        query = "-- {% if debug %}enable tracing{% endif %}\nselect 1;"
        with pytest.raises(Exception, match="undefined"):
            _, results = manager.execute(
                query=query,
                files=None,
                std_in=False,
                data={},
                template_syntax_config=SQLTemplateSyntaxConfig(
                    enable_legacy_syntax=False,
                    enable_standard_syntax=False,
                    enable_jinja_syntax=True,
                ),
            )
            list(results)

Comment on lines +97 to 102
enable_standard_syntax=template_syntax_config.enable_standard_syntax,
enable_jinja_syntax=False,
)
stmt_operators.append(
partial(
snowflake_sql_jinja_render,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Jinja pre-render produces empty output this falls through to the generic "Use either query, filename or input option." — which is misleading since the user did
provide input, it just rendered to nothing. Worth a more specific error message after pre-render - "template rendered to empty output"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SNOW-2397013: Jinja IF-statements are not interpreted correctly

2 participants