Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Features
Bug Fixes
---------
* Suppress warnings when `sqlglotrs` is installed.
* Improve completions after operators, by recognizing more operators.


1.64.0 (2026/03/13)
Expand Down
38 changes: 34 additions & 4 deletions mycli/packages/completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@
re.IGNORECASE,
)

# missing because not binary
# BETWEEN
# CASE
# missing because parens are used
# IN(), and others
# unary operands might need to have another set
# not, !, ~
# arrow operators only take a literal on the right
# and so might need different treatment
# := might also need a different context
# sqlparse would call these identifiers, so they are excluded
# xor
# these are hitting the recursion guard, and so not completing after
# so we might as well leave them out:
# is, 'is not', mod
# sqlparse might also parse "not null" together
# should also verify how sqlparse parses every space-containing case
BINARY_OPERANDS = {
'&', '>', '>>', '>=', '<', '<>', '!=', '<<', '<=', '<=>', '%',
'*', '+', '-', '->', '->>', '/', ':=', '=', '^', 'and', '&&', 'div',
'like', 'not like', 'not regexp', 'or', '||', 'regexp', 'rlike',
'sounds like', '|',
} # fmt: skip


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

is_operand = lambda x: x and any(x.endswith(op) for op in ["+", "-", "*", "/"]) # noqa: E731

if not token:
return [{"type": "keyword"}, {"type": "special"}]

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

elif token_v.endswith(",") or is_operand(token_v) or token_v in ["=", "and", "or"]:
elif token_v.endswith(",") or token_v in BINARY_OPERANDS:
original_text = text_before_cursor
prev_keyword, text_before_cursor = find_prev_keyword(text_before_cursor)
enum_suggestion = _enum_value_suggestion(original_text, full_text)
fallback = suggest_based_on_last_token(prev_keyword, text_before_cursor, None, full_text, identifier) if prev_keyword else []

# guard against non-progressing parser rewinds, which can otherwise
# recurse forever on some operator shapes.
if prev_keyword and text_before_cursor.rstrip() != original_text.rstrip():
fallback = suggest_based_on_last_token(prev_keyword, text_before_cursor, None, full_text, identifier)
else:
# perhaps this fallback should include columns
fallback = [{"type": "keyword"}]

if enum_suggestion and _is_where_or_having(prev_keyword):
return [enum_suggestion] + fallback
return fallback
Expand Down
23 changes: 23 additions & 0 deletions test/test_completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,27 @@ def test_operand_inside_function_suggests_cols2():
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]


def test_operand_inside_function_suggests_cols3():
suggestion = suggest_type("SELECT MAX(col1 || FROM tbl", "SELECT MAX(col1 || ")
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]


def test_operand_inside_function_suggests_cols4():
suggestion = suggest_type("SELECT MAX(col1 LIKE FROM tbl", "SELECT MAX(col1 LIKE ")
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]


def test_operand_inside_function_suggests_cols5():
suggestion = suggest_type("SELECT MAX(col1 DIV FROM tbl", "SELECT MAX(col1 DIV ")
assert suggestion == [{"type": "column", "tables": [(None, "tbl", None)]}]


@pytest.mark.xfail
def test_arrow_op_inside_function_suggests_nothing():
suggestion = suggest_type("SELECT MAX(col1-> FROM tbl", "SELECT MAX(col1->")
assert suggestion == []


def test_select_suggests_cols_and_funcs():
suggestions = suggest_type("SELECT ", "SELECT ")
assert sorted_dicts(suggestions) == sorted_dicts([
Expand Down Expand Up @@ -418,6 +439,8 @@ def test_join_alias_dot_suggests_cols2(sql):
[
"select a.x, b.y from abc a join bcd b on ",
"select a.x, b.y from abc a join bcd b on a.id = b.id OR ",
"select a.x, b.y from abc a join bcd b on a.id = b.id + ",
"select a.x, b.y from abc a join bcd b on a.id = b.id < ",
],
)
def test_on_suggests_aliases(sql):
Expand Down
Loading