diff --git a/CHANGELOG b/CHANGELOG index 44d5938e..03465894 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ Development Version ------------------- -Nothing yet. +* Escape embedded line breaks in Python output so multiline SQL strings, + quoted identifiers and comments produce valid Python string literals. Release 0.6.0 (Aug 13, 2026) diff --git a/sqlparse/filters/output.py b/sqlparse/filters/output.py index d4e20f29..9c038396 100644 --- a/sqlparse/filters/output.py +++ b/sqlparse/filters/output.py @@ -66,7 +66,9 @@ def _process(self, stream, varname, has_nl): # quote cannot break out of the generated string literal # (GHSA-3496-9g83-7v6x). else: - token.value = token.value.replace('\\', '\\\\').replace("'", "\\'") + token.value = (token.value.replace('\\', '\\\\') + .replace("'", "\\'") + .replace('\r', '\\r').replace('\n', '\\n')) # Put the token yield sql.Token(T.Text, token.value) diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..fc2ec23f 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,3 +1,5 @@ +import ast + import pytest import sqlparse @@ -651,6 +653,20 @@ def test_insert_values(self): class TestOutputFormat: + @pytest.mark.parametrize('line_break', ['\n', '\r', '\r\n', r'\n']) + @pytest.mark.parametrize('template', [ + "SELECT 'first{}second''quote\\path'", + 'SELECT "first{}second"', + 'SELECT $body$first{}second$body$', + 'SELECT 1 /* first{}second */', + 'SELECT 1 -- first{}FROM t', + ]) + def test_python_multiline_tokens(self, line_break, template): + sql = template.format(line_break) + formatted = sqlparse.format(sql, output_format='python') + assignment = ast.parse(formatted).body[0] + assert ast.literal_eval(assignment.value) == sql + def test_python(self): sql = 'select * from foo;' f = lambda sql: sqlparse.format(sql, output_format='python')