diff --git a/CHANGELOG b/CHANGELOG index 44d5938e..35d8bddc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ Development Version ------------------- -Nothing yet. +* Preserve whitespace and line endings inside quoted values containing a + backslash followed by a newline when formatting SQL. Release 0.6.0 (Aug 13, 2026) diff --git a/sqlparse/utils.py b/sqlparse/utils.py index 58c0245a..d0126380 100644 --- a/sqlparse/utils.py +++ b/sqlparse/utils.py @@ -28,7 +28,7 @@ '(?:[^'\\]|\\.)*' # Match single quoted strings ) ) -""", re.VERBOSE) +""", re.VERBOSE | re.DOTALL) LINE_MATCH = re.compile(r'(\r\n|\r|\n)') diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..0dfb8985 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -175,6 +175,30 @@ def test_notransform_of_quoted_crlf(self): == "SELECT some_column LIKE 'value\\\\\\'\r' WHERE id = 1\n") +@pytest.mark.parametrize('quote', ["'", '"']) +@pytest.mark.parametrize('newline', ['\n', '\r', '\r\n']) +@pytest.mark.parametrize('options', [{}, {'strip_whitespace': True}, + {'reindent': True}]) +def test_preserve_quoted_newlines_after_backslash(quote, newline, options): + text = 'SELECT ' + quote + 'a\\' + newline + 'b \r\nc' + quote + ';' + + assert sqlparse.format(text, **options) == text + + +@pytest.mark.parametrize('mode', ['strip_whitespace', 'reindent']) +def test_keyword_transform_preserves_multiple_literals(mode): + left = "'select a\\\nb \r\nc'" + right = "'from d\\\ne \r\nf'" + text = f'select {left} as x,\n{right} as y;' + + actual = sqlparse.format(text, keyword_case='upper', **{mode: True}) + + assert actual.startswith('SELECT ') + assert actual.count(' AS ') == 2 + assert left in actual + assert right in actual + + class TestFormatReindentAligned: @staticmethod def formatter(sql): diff --git a/tests/test_utils.py b/tests/test_utils.py index d020f3fa..9405d1f2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,3 +10,14 @@ ['`foo`', 'foo'])) def test_remove_quotes(value, expected): assert utils.remove_quotes(value) == expected + + +@pytest.mark.parametrize('quote', ["'", '"']) +@pytest.mark.parametrize('newline', ['\n', '\r', '\r\n']) +@pytest.mark.parametrize('backslashes', [1, 2]) +def test_split_unquoted_newlines_after_backslash(quote, newline, backslashes): + quoted = (quote + 'a' + '\\' * backslashes + newline + + 'b \r\nc' + quote) + text = 'SELECT\r\n' + quoted + '\rFROM t' + + assert utils.split_unquoted_newlines(text) == ['SELECT', quoted, 'FROM t']