From 443dc84ec84286777e4a815251ee74bbd9b0ae7a Mon Sep 17 00:00:00 2001 From: kokotatan Date: Sat, 3 Oct 2026 22:57:56 +0900 Subject: [PATCH] fix(output): escape line breaks in generated Python literals `OutputPythonFilter` currently puts raw CR/LF from SQL strings, quoted identifiers and comments inside single-quoted Python literals, producing `SyntaxError` when the output is parsed as Python. Escape these characters after existing backslash and quote escaping so literal backslash-n stays distinct from an actual newline. Whitespace splitting between tokens and PHP output keep their existing behavior. Add 20 public `format()` tests that parse the generated assignment with `ast.parse` and compare `ast.literal_eval` with the original SQL. The unchanged source fails 15 cases and passes 5 backslash-n controls. All 526 tests pass on Python 3.10 through 3.14, with two existing xfails and one existing xpass. Ruff, strict Sphinx HTML and wheel/sdist builds pass; `output.py` has 95% statement/branch coverage. Assisted-by: OpenAI GPT-6 --- CHANGELOG | 3 ++- sqlparse/filters/output.py | 4 +++- tests/test_format.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) 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')