From 182ee0bd05d8dd58c1ba0589c86516ea666fdbd5 Mon Sep 17 00:00:00 2001 From: adenzhou1350 <209601943+adenzhou1350@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:28:50 +0800 Subject: [PATCH] Keep PostgreSQL case-insensitive pattern operators intact --- sqlparse/keywords.py | 2 ++ tests/test_pattern_operators.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/test_pattern_operators.py diff --git a/sqlparse/keywords.py b/sqlparse/keywords.py index dd6e5d15..09714eb1 100644 --- a/sqlparse/keywords.py +++ b/sqlparse/keywords.py @@ -199,6 +199,8 @@ def find_delimited_spans(text): (r'[;:()\[\],\.]', tokens.Punctuation), # JSON operators (r'(\->>?|#>>?|@>|<@|\?\|?|\?&|\-|#\-)', tokens.Operator), + # PostgreSQL case-insensitive regular-expression and LIKE operators. + (r'!?~~?\*', tokens.Operator.Comparison), (r'[<>=~!]+', tokens.Operator.Comparison), (r'[+/@#%^&|^-]+', tokens.Operator), ] diff --git a/tests/test_pattern_operators.py b/tests/test_pattern_operators.py new file mode 100644 index 00000000..9b0d5821 --- /dev/null +++ b/tests/test_pattern_operators.py @@ -0,0 +1,39 @@ +import pytest + +import sqlparse +from sqlparse import lexer, sql +from sqlparse import tokens as T + + +@pytest.mark.parametrize('operator', ['~*', '!~*', '~~*', '!~~*']) +def test_pattern_operator_token_and_group(operator): + assert list(lexer.tokenize(operator)) == [(T.Operator.Comparison, operator)] + parsed = sqlparse.parse(f"name {operator} 'pattern'")[0] + assert len(parsed.tokens) == 1 + assert isinstance(parsed.tokens[0], sql.Comparison) + assert parsed.tokens[0].tokens[2].value == operator + assert parsed.tokens[0].right.value == "'pattern'" + + +@pytest.mark.parametrize('operator', ['~*', '!~*', '~~*', '!~~*']) +@pytest.mark.parametrize('separator', ['', ' ']) +def test_pattern_operator_spacing(operator, separator): + text = f"SELECT name{separator}{operator}{separator}'pattern' FROM tab" + expected = f"SELECT name {operator} 'pattern' FROM tab" + assert sqlparse.format(text, use_space_around_operators=True) == expected + assert sqlparse.format(expected, use_space_around_operators=True) == expected + + +def test_issue613_operator_spacing(): + text = "SELECT ARRAY[1] <@ ARRAY[1, 2], 'a' !~* 'xyz'" + assert sqlparse.format(text, use_space_around_operators=True) == text + + +@pytest.mark.parametrize('text, expected', [ + ('SELECT a*b, * FROM tab', 'SELECT a * b, * FROM tab'), + ("SELECT '~*', '!~~*', * FROM tab", "SELECT '~*', '!~~*', * FROM tab"), + ("SELECT name ~ * 'pattern'", "SELECT name ~ * 'pattern'"), + ("SELECT name ~/* keep */*'pattern'", "SELECT name ~ /* keep */*'pattern'"), +]) +def test_pattern_operator_controls(text, expected): + assert sqlparse.format(text, use_space_around_operators=True) == expected