fix: render Jinja blocks before splitting on semicolons (#2650)#2782
fix: render Jinja blocks before splitting on semicolons (#2650)#2782sfc-gh-moczko wants to merge 2 commits intomainfrom
Conversation
de27892 to
f6d865b
Compare
225511d to
1f5d863
Compare
`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>
1f5d863 to
4f42dd4
Compare
sfc-gh-olorek
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
| enable_standard_syntax=template_syntax_config.enable_standard_syntax, | ||
| enable_jinja_syntax=False, | ||
| ) | ||
| stmt_operators.append( | ||
| partial( | ||
| snowflake_sql_jinja_render, |
There was a problem hiding this comment.
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"
Summary
{% if %},{% for %}, etc.) containing semicolons were broken becausesplit_statements()splits on;before Jinja rendering, producing invalid template fragments.split_statements(), while legacy (&{ }) and standard (<% %>) syntax continues to render per-statement (they are variable-only, no blocks).Root Cause
The SQL execution pipeline called
split_statements()then per-statement template rendering. Sincesplit_statementsis unaware of Jinja block syntax, a query like:was split into two broken fragments:
{% if var == 'Jinja' %}select 1;-- unclosedifblock --> TemplateSyntaxError{% endif %}-- orphanedendif--> TemplateSyntaxErrorFix
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
statement_reader.pypre_renderparameter toquery_reader()andfiles_reader()manager.pytests/sql/test_sql.pyTest plan