From 027b9378ea78f07bbfb8a579bab04ef403de8d62 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Mon, 5 Oct 2026 19:49:40 -0700 Subject: [PATCH 1/2] Fix PostgreSQL JSON operators containing question marks Signed-off-by: Jaden Fix --- sqlparse/keywords.py | 4 +++- tests/test_format.py | 5 +++++ tests/test_parse.py | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/sqlparse/keywords.py b/sqlparse/keywords.py index dd6e5d15..8f5260a8 100644 --- a/sqlparse/keywords.py +++ b/sqlparse/keywords.py @@ -132,6 +132,8 @@ def find_delimited_spans(text): (r"`(``|[^`])*`", tokens.Name), (r"´(´´|[^´])*´", tokens.Name), + # Match unambiguous JSON operators before the single ? placeholder. + (r'(\?[|&]|@\?)', tokens.Operator), (r'\?', tokens.Name.Placeholder), (r'%(\(\w+\))?s', tokens.Name.Placeholder), (r'(?>?|#>>?|@>|<@|\?\|?|\?&|\-|#\-)', tokens.Operator), + (r'(\->>?|#>>?|@>|<@|\-|#\-)', tokens.Operator), (r'[<>=~!]+', tokens.Operator.Comparison), (r'[+/@#%^&|^-]+', tokens.Operator), ] diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..4445bac5 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -363,6 +363,11 @@ def test_bools(self): assert self.formatter( sql) == 'select * from table where a && b or c || d' + @pytest.mark.parametrize('operator', ['?|', '?&', '@?']) + def test_json_question_operators(self, operator): + sql = f'select data{operator}? from table' + assert self.formatter(sql) == f'select data {operator} ? from table' + def test_nested(self): sql = 'select *, case when a-b then c end from table' assert self.formatter( diff --git a/tests/test_parse.py b/tests/test_parse.py index 67168410..3af5d54f 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -632,8 +632,8 @@ def test_configurable_regex(): @pytest.mark.parametrize('sql', [ '->', '->>', '#>', '#>>', '@>', '<@', - # leaving ? out for now, they're somehow ambiguous as placeholders - # '?', '?|', '?&', + # A standalone ? remains a placeholder. + '?|', '?&', '@?', '@@', '||', '-', '#-' ]) def test_json_operators(sql): From f0687aa96fe19353c72db46e30cd5f137ea4acf9 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Mon, 5 Oct 2026 21:27:18 -0700 Subject: [PATCH 2/2] Limit JSON operator fix to unambiguous path operator Signed-off-by: Jaden Fix --- sqlparse/keywords.py | 5 ++--- tests/test_format.py | 12 ++++++++---- tests/test_parse.py | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/sqlparse/keywords.py b/sqlparse/keywords.py index 8f5260a8..c5c65688 100644 --- a/sqlparse/keywords.py +++ b/sqlparse/keywords.py @@ -132,8 +132,7 @@ def find_delimited_spans(text): (r"`(``|[^`])*`", tokens.Name), (r"´(´´|[^´])*´", tokens.Name), - # Match unambiguous JSON operators before the single ? placeholder. - (r'(\?[|&]|@\?)', tokens.Operator), + (r'@\?', tokens.Operator), (r'\?', tokens.Name.Placeholder), (r'%(\(\w+\))?s', tokens.Name.Placeholder), (r'(?>?|#>>?|@>|<@|\-|#\-)', tokens.Operator), + (r'(\->>?|#>>?|@>|<@|\?\|?|\?&|\-|#\-)', tokens.Operator), (r'[<>=~!]+', tokens.Operator.Comparison), (r'[+/@#%^&|^-]+', tokens.Operator), ] diff --git a/tests/test_format.py b/tests/test_format.py index 4445bac5..5253911c 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -363,10 +363,14 @@ def test_bools(self): assert self.formatter( sql) == 'select * from table where a && b or c || d' - @pytest.mark.parametrize('operator', ['?|', '?&', '@?']) - def test_json_question_operators(self, operator): - sql = f'select data{operator}? from table' - assert self.formatter(sql) == f'select data {operator} ? from table' + def test_json_path_operator(self): + sql = 'select data@?? from table' + assert self.formatter(sql) == 'select data @? ? from table' + + @pytest.mark.parametrize('operator', ['||', '|', '&']) + def test_operator_after_placeholder(self, operator): + sql = f'select ?{operator}? from table' + assert self.formatter(sql) == f'select ? {operator} ? from table' def test_nested(self): sql = 'select *, case when a-b then c end from table' diff --git a/tests/test_parse.py b/tests/test_parse.py index 3af5d54f..d1c41624 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -632,8 +632,8 @@ def test_configurable_regex(): @pytest.mark.parametrize('sql', [ '->', '->>', '#>', '#>>', '@>', '<@', - # A standalone ? remains a placeholder. - '?|', '?&', '@?', '@@', + # Question-mark prefixes remain ambiguous with placeholders. + '@?', '@@', '||', '-', '#-' ]) def test_json_operators(sql):