From e0aaec0dc82cbea793df9d56e166e76c63a2ae20 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Wed, 21 Jan 2026 08:37:03 -0500 Subject: [PATCH] add true fuzzy-match completions with rapidfuzz * don't attempt true fuzzy matches until the given text is 4+ characters * require a rapidfuzz WRatio score of 75+ * limit rapidfuzz candidates to 20 or fewer * don't report rapidfuzz candidates which are much shorter than the given text --- changelog.md | 1 + mycli/sqlcompleter.py | 20 +++++++++++++++++++ pyproject.toml | 1 + ...est_smart_completion_public_schema_only.py | 12 +++++++++++ 4 files changed, 34 insertions(+) diff --git a/changelog.md b/changelog.md index 08cbd2ad..d208325c 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ Features * Remove suggested quoting on completions for identifiers with uppercase. * Allow table names to be completed with leading schema names. * Soft deprecate the built-in SSH features. +* Add true fuzzy-match completions with rapidfuzz. Bug Fixes diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index e765a815..9cab2918 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -8,6 +8,7 @@ from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.completion.base import Document from pygments.lexers._mysql_builtins import MYSQL_DATATYPES, MYSQL_FUNCTIONS, MYSQL_KEYWORDS +import rapidfuzz from mycli.packages.completion_engine import suggest_type from mycli.packages.filepaths import complete_path, parse_path, suggest_path @@ -996,6 +997,25 @@ def find_matches( completions.append(item) continue + if len(text) >= 4: + rapidfuzz_matches = rapidfuzz.process.extract( + text, + collection, + scorer=rapidfuzz.fuzz.WRatio, + # todo: maybe make our own processor which only does case-folding + # because underscores are valuable info + processor=rapidfuzz.utils.default_process, + limit=20, + score_cutoff=75, + ) + for elt in rapidfuzz_matches: + item, _score, _type = elt + if len(item) < len(text) / 1.5: + continue + if item in completions: + continue + completions.append(item) + else: match_end_limit = len(text) if start_only else None for item in collection: diff --git a/pyproject.toml b/pyproject.toml index 1fff124b..1f4b6e55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "pyperclip >= 1.8.1", "pycryptodomex", "pyfzf >= 0.3.1", + "rapidfuzz ~= 3.14.3", ] [build-system] diff --git a/test/test_smart_completion_public_schema_only.py b/test/test_smart_completion_public_schema_only.py index 0d1ed11a..4567f815 100644 --- a/test/test_smart_completion_public_schema_only.py +++ b/test/test_smart_completion_public_schema_only.py @@ -422,6 +422,18 @@ def test_table_names_inter_partial(completer, complete_event): result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event)) assert result == [ Completion(text="time_zone_leap_second", start_position=-9), + Completion(text='time_zone_name', start_position=-9), + Completion(text='time_zone_transition', start_position=-9), + Completion(text='time_zone_transition_type', start_position=-9), + ] + + +def test_table_names_fuzzy(completer, complete_event): + text = "SELECT * FROM tim_leap" + position = len("SELECT * FROM tim_leap") + result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event)) + assert result == [ + Completion(text="time_zone_leap_second", start_position=-8), ]