From bf74d8bc2c572b978997f7785747ad262025566c Mon Sep 17 00:00:00 2001 From: Andi Albrecht Date: Fri, 12 Jul 2024 13:43:02 +0200 Subject: [PATCH] Add compact option to force a more compact formatting (fixes #783). --- CHANGELOG | 7 ++++++- docs/source/api.rst | 3 +++ sqlparse/cli.py | 9 ++++++++- sqlparse/filters/reindent.py | 14 ++++++++++---- sqlparse/formatter.py | 8 +++++++- tests/test_format.py | 11 +++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a5a1ba9e..6d9d71f8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ Development Version ------------------- +Enhancements + +* New "compact" option for formatter. If set, the formatter tries to produce + a more compact output by avoiding some line breaks (issue783). + Bug Fixes * The strip comments filter was a bit greedy and removed too much @@ -23,7 +28,7 @@ Notable Changes https://github.com/andialbrecht/sqlparse/security/advisories/GHSA-2m57-hf25-phgg The vulnerability was discovered by @uriyay-jfrog. Thanks for reporting! -Enhancements: +Enhancements * Splitting statements now allows to remove the semicolon at the end. Some database backends love statements without semicolon (issue742). diff --git a/docs/source/api.rst b/docs/source/api.rst index 40193d0b..6d49a57a 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -62,6 +62,9 @@ The :meth:`~sqlparse.format` function accepts the following keyword arguments. The column limit (in characters) for wrapping comma-separated lists. If unspecified, it puts every item in the list on its own line. +``compact``` + If ``True`` the formatter tries to produce more compact output. + ``output_format`` If given the output is additionally formatted to be used as a variable in a programming language. Allowed values are "python" and "php". diff --git a/sqlparse/cli.py b/sqlparse/cli.py index 7a8aacbf..a2ee4937 100755 --- a/sqlparse/cli.py +++ b/sqlparse/cli.py @@ -138,7 +138,14 @@ def create_parser(): default=False, type=bool, help='Insert linebreak before comma (default False)') - + + group.add_argument( + '--compact', + dest='compact', + default=False, + type=bool, + help='Try to produce more compact output (default False)') + group.add_argument( '--encoding', dest='encoding', diff --git a/sqlparse/filters/reindent.py b/sqlparse/filters/reindent.py index 9fb232f0..ee4c72c2 100644 --- a/sqlparse/filters/reindent.py +++ b/sqlparse/filters/reindent.py @@ -12,7 +12,7 @@ class ReindentFilter: def __init__(self, width=2, char=' ', wrap_after=0, n='\n', comma_first=False, indent_after_first=False, - indent_columns=False): + indent_columns=False, compact=False): self.n = n self.width = width self.char = char @@ -21,6 +21,7 @@ def __init__(self, width=2, char=' ', wrap_after=0, n='\n', self.wrap_after = wrap_after self.comma_first = comma_first self.indent_columns = indent_columns + self.compact = compact self._curr_stmt = None self._last_stmt = None self._last_func = None @@ -196,15 +197,20 @@ def _process_case(self, tlist): with offset(self, self._get_offset(tlist[0])): with offset(self, self._get_offset(first)): for cond, value in iterable: - token = value[0] if cond is None else cond[0] - tlist.insert_before(token, self.nl()) + str_cond = ''.join(str(x) for x in cond or []) + str_value = ''.join(str(x) for x in value) + end_pos = self.offset + 1 + len(str_cond) + len(str_value) + if (not self.compact + and end_pos > self.wrap_after): + token = value[0] if cond is None else cond[0] + tlist.insert_before(token, self.nl()) # Line breaks on group level are done. let's add an offset of # len "when ", "then ", "else " with offset(self, len("WHEN ")): self._process_default(tlist) end_idx, end = tlist.token_next_by(m=sql.Case.M_CLOSE) - if end_idx is not None: + if end_idx is not None and not self.compact: tlist.insert_before(end_idx, self.nl()) def _process_values(self, tlist): diff --git a/sqlparse/formatter.py b/sqlparse/formatter.py index 1d1871cf..761a8852 100644 --- a/sqlparse/formatter.py +++ b/sqlparse/formatter.py @@ -115,6 +115,11 @@ def validate_options(options): if comma_first not in [True, False]: raise SQLParseError('comma_first requires a boolean value') options['comma_first'] = comma_first + + compact = options.get('compact', False) + if compact not in [True, False]: + raise SQLParseError('compact requires a boolean value') + options['compact'] = compact right_margin = options.get('right_margin') if right_margin is not None: @@ -171,7 +176,8 @@ def build_filter_stack(stack, options): indent_after_first=options['indent_after_first'], indent_columns=options['indent_columns'], wrap_after=options['wrap_after'], - comma_first=options['comma_first'])) + comma_first=options['comma_first'], + compact=options['compact'],)) if options.get('reindent_aligned', False): stack.enable_grouping() diff --git a/tests/test_format.py b/tests/test_format.py index 6a4b6f16..4cbfcbe0 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -734,3 +734,14 @@ def test_format_json_ops(): # issue542 "select foo->'bar', foo->'bar';", reindent=True) expected = "select foo->'bar',\n foo->'bar';" assert formatted == expected + + +@pytest.mark.parametrize('sql, expected_normal, expected_compact', [ + ('case when foo then 1 else bar end', + 'case\n when foo then 1\n else bar\nend', + 'case when foo then 1 else bar end')]) +def test_compact(sql, expected_normal, expected_compact): # issue783 + formatted_normal = sqlparse.format(sql, reindent=True) + formatted_compact = sqlparse.format(sql, reindent=True, compact=True) + assert formatted_normal == expected_normal + assert formatted_compact == expected_compact