Skip to content

Commit d042e46

Browse files
authored
Merge pull request #1712 from dbcli/RW/extend-list-of-operators
Extend the parser's list of binary operators
2 parents 724182f + 8e9d823 commit d042e46

File tree

3 files changed

+58
-4
lines changed

3 files changed

+58
-4
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Features
1010
Bug Fixes
1111
---------
1212
* Suppress warnings when `sqlglotrs` is installed.
13+
* Improve completions after operators, by recognizing more operators.
1314

1415

1516
1.64.0 (2026/03/13)

mycli/packages/completion_engine.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@
1717
re.IGNORECASE,
1818
)
1919

20+
# missing because not binary
21+
# BETWEEN
22+
# CASE
23+
# missing because parens are used
24+
# IN(), and others
25+
# unary operands might need to have another set
26+
# not, !, ~
27+
# arrow operators only take a literal on the right
28+
# and so might need different treatment
29+
# := might also need a different context
30+
# sqlparse would call these identifiers, so they are excluded
31+
# xor
32+
# these are hitting the recursion guard, and so not completing after
33+
# so we might as well leave them out:
34+
# is, 'is not', mod
35+
# sqlparse might also parse "not null" together
36+
# should also verify how sqlparse parses every space-containing case
37+
BINARY_OPERANDS = {
38+
'&', '>', '>>', '>=', '<', '<>', '!=', '<<', '<=', '<=>', '%',
39+
'*', '+', '-', '->', '->>', '/', ':=', '=', '^', 'and', '&&', 'div',
40+
'like', 'not like', 'not regexp', 'or', '||', 'regexp', 'rlike',
41+
'sounds like', '|',
42+
} # fmt: skip
43+
2044

2145
def _enum_value_suggestion(text_before_cursor: str, full_text: str) -> dict[str, Any] | None:
2246
match = _ENUM_VALUE_RE.search(text_before_cursor)
@@ -333,8 +357,6 @@ def suggest_based_on_last_token(
333357
else:
334358
token_v = token.value.lower()
335359

336-
is_operand = lambda x: x and any(x.endswith(op) for op in ["+", "-", "*", "/"]) # noqa: E731
337-
338360
if not token:
339361
return [{"type": "keyword"}, {"type": "special"}]
340362

@@ -512,11 +534,19 @@ def suggest_based_on_last_token(
512534
elif is_inside_quotes(text_before_cursor, -1) in ['single', 'double']:
513535
return []
514536

515-
elif token_v.endswith(",") or is_operand(token_v) or token_v in ["=", "and", "or"]:
537+
elif token_v.endswith(",") or token_v in BINARY_OPERANDS:
516538
original_text = text_before_cursor
517539
prev_keyword, text_before_cursor = find_prev_keyword(text_before_cursor)
518540
enum_suggestion = _enum_value_suggestion(original_text, full_text)
519-
fallback = suggest_based_on_last_token(prev_keyword, text_before_cursor, None, full_text, identifier) if prev_keyword else []
541+
542+
# guard against non-progressing parser rewinds, which can otherwise
543+
# recurse forever on some operator shapes.
544+
if prev_keyword and text_before_cursor.rstrip() != original_text.rstrip():
545+
fallback = suggest_based_on_last_token(prev_keyword, text_before_cursor, None, full_text, identifier)
546+
else:
547+
# perhaps this fallback should include columns
548+
fallback = [{"type": "keyword"}]
549+
520550
if enum_suggestion and _is_where_or_having(prev_keyword):
521551
return [enum_suggestion] + fallback
522552
return fallback

test/test_completion_engine.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,27 @@ def test_operand_inside_function_suggests_cols2():
126126
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]
127127

128128

129+
def test_operand_inside_function_suggests_cols3():
130+
suggestion = suggest_type("SELECT MAX(col1 || FROM tbl", "SELECT MAX(col1 || ")
131+
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]
132+
133+
134+
def test_operand_inside_function_suggests_cols4():
135+
suggestion = suggest_type("SELECT MAX(col1 LIKE FROM tbl", "SELECT MAX(col1 LIKE ")
136+
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]
137+
138+
139+
def test_operand_inside_function_suggests_cols5():
140+
suggestion = suggest_type("SELECT MAX(col1 DIV FROM tbl", "SELECT MAX(col1 DIV ")
141+
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]
142+
143+
144+
@pytest.mark.xfail
145+
def test_arrow_op_inside_function_suggests_nothing():
146+
suggestion = suggest_type("SELECT MAX(col1-> FROM tbl", "SELECT MAX(col1->")
147+
assert suggestion == []
148+
149+
129150
def test_select_suggests_cols_and_funcs():
130151
suggestions = suggest_type("SELECT ", "SELECT ")
131152
assert sorted_dicts(suggestions) == sorted_dicts([
@@ -418,6 +439,8 @@ def test_join_alias_dot_suggests_cols2(sql):
418439
[
419440
"select a.x, b.y from abc a join bcd b on ",
420441
"select a.x, b.y from abc a join bcd b on a.id = b.id OR ",
442+
"select a.x, b.y from abc a join bcd b on a.id = b.id + ",
443+
"select a.x, b.y from abc a join bcd b on a.id = b.id < ",
421444
],
422445
)
423446
def test_on_suggests_aliases(sql):

0 commit comments

Comments
 (0)