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
3 changes: 2 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Features
--------
* Preserve the query as metadata when saving to Parquet with `.>`.
* Make completion candidate match order configurable.
* Make approximate-matching thresholds configurable.
* Make approximate-matching completion thresholds configurable.
* Make regex matching completion thresholds configurable.


2.23.0 (2026/09/09)
Expand Down
1 change: 1 addition & 0 deletions mycli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def __init__(
config_property_names=get_config_property_names(self.config),
completion_match_order=c['main'].as_list('completion_match_order') if 'completion_match_order' in c['main'] else (),
rapidfuzz_min_length=c['main'].as_int('rapidfuzz_min_length') if c['main'].get('rapidfuzz_min_length') else 4,
regex_match_distance=c['main'].as_int('regex_match_distance') if c['main'].get('regex_match_distance') else 3,
rapidfuzz_score_cutoff=c['main'].as_float('rapidfuzz_score_cutoff') if c['main'].get('rapidfuzz_score_cutoff') else 75.0,
rapidfuzz_length_coverage=c['main'].as_float('rapidfuzz_length_coverage')
if c['main'].get('rapidfuzz_length_coverage')
Expand Down
1 change: 1 addition & 0 deletions mycli/client_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def refresh_completions(self, reset: bool = False) -> list[SQLResult]:
'rapidfuzz_min_length': self.completer.rapidfuzz_min_length,
'rapidfuzz_length_coverage': self.completer.rapidfuzz_length_coverage,
'rapidfuzz_score_cutoff': self.completer.rapidfuzz_score_cutoff,
'regex_match_distance': self.completer.regex_match_distance,
},
)

Expand Down
5 changes: 5 additions & 0 deletions mycli/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ smart_completion = True
# * rapidfuzz - true approximate matching, _ie_ autcorrect
completion_match_order = perfect, regex, under_words, slash_words, camel_case, rapidfuzz

# Maximum intervening span length between input characters for regex completion
# candidate matches. Empty uses the default of 3. Higher is more liberal, but
# carries a perforance penalty.
regex_match_distance = 3

# Minimum input characters before using rapidfuzz approximate matching.
# Empty uses the default of 4. Set to 0 or less to remove the minimum.
rapidfuzz_min_length = 4
Expand Down
4 changes: 3 additions & 1 deletion mycli/sqlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ def __init__(
rapidfuzz_min_length: int = 4,
rapidfuzz_length_coverage: float = 0.67,
rapidfuzz_score_cutoff: float = 75.0,
regex_match_distance: int = 3,
) -> None:
super(self.__class__, self).__init__()
self.smart_completion = smart_completion
Expand All @@ -968,6 +969,7 @@ def __init__(
self.rapidfuzz_min_length = max(0, rapidfuzz_min_length)
self.rapidfuzz_length_coverage = max(0.0, rapidfuzz_length_coverage)
self.rapidfuzz_score_cutoff = max(0.0, min(100.0, rapidfuzz_score_cutoff))
self.regex_match_distance = max(0, regex_match_distance)
self.completion_config_errors: list[str] = []
default_order = tuple(category.name.lower() for category in Fuzziness)
order = tuple(name.strip().lower() for name in completion_match_order if name.strip())
Expand Down Expand Up @@ -1351,7 +1353,7 @@ def find_fuzzy_matches(
collection: Collection[Any],
) -> list[tuple[str, int]]:
completions: list[tuple[str, int]] = []
regex = '.{0,3}?'.join(map(re.escape, text))
regex = f'.{{0,{self.regex_match_distance}}}?'.join(map(re.escape, text))
pattern = re.compile(f'({regex})')
under_words_text = [x for x in text.split('_') if x]
case_words_text = re.split(_CASE_CHANGE_PAT, last)
Expand Down
5 changes: 5 additions & 0 deletions test/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ smart_completion = True
# * rapidfuzz - true approximate matching, _ie_ autcorrect
completion_match_order = perfect, regex, under_words, slash_words, camel_case, rapidfuzz

# Maximum intervening span length between input characters for regex completion
# candidate matches. Empty uses the default of 3. Higher is more liberal, but
# carries a perforance penalty.
regex_match_distance = 3

# Minimum input characters before using rapidfuzz approximate matching.
# Empty uses the default of 4. Set to 0 or less to remove the minimum.
rapidfuzz_min_length = 4
Expand Down
11 changes: 11 additions & 0 deletions test/pytests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ def test_init_configures_completion_ranking(monkeypatch: pytest.MonkeyPatch, tmp
assert cli.completer.completion_match_order == ('camel_case', 'under_words', 'perfect', 'regex', 'slash_words', 'rapidfuzz')


@pytest.mark.parametrize(('value', 'expected'), [(None, 3), ('', 3), ('5', 5), ('0', 0), ('-1', 0)])
def test_init_configures_regex_match_distance(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str | None, expected: int) -> None:
patch_constructor_side_effects(monkeypatch)
setting = f'regex_match_distance = {value}\n' if value is not None else ''
myclirc = write_myclirc(tmp_path, f'[main]\n{setting}')

cli = MyCli(myclirc=myclirc)

assert cli.completer.regex_match_distance == expected


@pytest.mark.parametrize(('value', 'expected'), [(None, 4), ('', 4), ('2', 2), ('0', 0), ('-1', 0)])
def test_init_configures_rapidfuzz_min_length(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str | None, expected: int) -> None:
patch_constructor_side_effects(monkeypatch)
Expand Down
4 changes: 4 additions & 0 deletions test/pytests/test_client_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def make_refresh_cli() -> tuple[Any, dict[str, Any]]:
rapidfuzz_min_length=2,
rapidfuzz_length_coverage=0.5,
rapidfuzz_score_cutoff=82.5,
regex_match_distance=5,
keyword_casing='upper',
indexed_column_suffix=' [indexed]',
set_dbname=lambda dbname: state['set_dbname_calls'].append(dbname),
Expand Down Expand Up @@ -82,6 +83,7 @@ def test_refresh_completions_passes_options_to_refresher() -> None:
'rapidfuzz_min_length': 2,
'rapidfuzz_length_coverage': 0.5,
'rapidfuzz_score_cutoff': 82.5,
'regex_match_distance': 5,
},
)
]
Expand Down Expand Up @@ -116,6 +118,7 @@ def test_refresh_completions_updates_dbname_when_reset() -> None:
rapidfuzz_min_length=4,
rapidfuzz_length_coverage=0.67,
rapidfuzz_score_cutoff=75.0,
regex_match_distance=3,
keyword_casing='lower',
indexed_column_suffix='*',
set_dbname=lambda dbname: set_dbname_calls.append(dbname),
Expand All @@ -141,6 +144,7 @@ def test_refresh_completions_uses_lock_when_reset() -> None:
rapidfuzz_min_length=4,
rapidfuzz_length_coverage=0.67,
rapidfuzz_score_cutoff=75.0,
regex_match_distance=3,
keyword_casing='lower',
indexed_column_suffix='*',
set_dbname=lambda dbname: None,
Expand Down
12 changes: 12 additions & 0 deletions test/pytests/test_sqlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ def test_find_fuzzy_matches_collects_item_level_matches(monkeypatch) -> None:
]


@pytest.mark.parametrize(
('distance', 'candidate', 'accepted'),
[(0, 'zab', True), (0, 'axb', False), (1, 'axb', True), (1, 'axxb', False), (5, 'axxxxxb', True), (5, 'axxxxxxb', False)],
)
def test_find_fuzzy_matches_uses_regex_match_distance(distance: int, candidate: str, accepted: bool) -> None:
completer = SQLCompleter(regex_match_distance=distance)

matches = completer.find_fuzzy_matches('ab', 'ab', [candidate])

assert matches == ([(candidate, Fuzziness.REGEX)] if accepted else [])


def test_find_fuzzy_matches_skips_rapidfuzz_for_short_text(monkeypatch) -> None:
monkeypatch.setattr(SQLCompleter, 'find_fuzzy_match', lambda *args, **kwargs: None)

Expand Down
Loading