mycli 2.23.0__tar.gz → 2.24.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/publish.yml +3 -0
- {mycli-2.23.0/mycli.egg-info → mycli-2.24.1}/PKG-INFO +1 -1
- {mycli-2.23.0 → mycli-2.24.1}/changelog.md +22 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/client.py +10 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/client_query.py +6 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/repl.py +9 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/myclirc +36 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/polars_transform.py +9 -3
- {mycli-2.23.0 → mycli-2.24.1}/mycli/sqlcompleter.py +90 -37
- {mycli-2.23.0 → mycli-2.24.1/mycli.egg-info}/PKG-INFO +1 -1
- mycli-2.24.1/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/myclirc +36 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client.py +94 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client_query.py +24 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_repl.py +106 -8
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_polars_transform.py +37 -3
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sqlcompleter.py +280 -14
- mycli-2.23.0/mycli.egg-info/scm_version.json +0 -8
- {mycli-2.23.0 → mycli-2.24.1}/.git-blame-ignore-revs +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.github/dependabot.yml +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/ci.yml +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/lint.yml +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/.gitignore +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/AGENTS.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/AUTHORS.rst +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/CONTRIBUTING.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/LICENSE.txt +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/MANIFEST.in +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/README.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/SPONSORS.rst +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/doc/key_bindings.rst +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/doc/llm.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/doc/screenshots/main.gif +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/doc/screenshots/tables.png +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/doc/screenshots/total_histogram.png +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/doc/transforms.md +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/docker/Dockerfile +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/docker/resources/test_db-1.0.7.tar.gz +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/AUTHORS +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/SPONSORS +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/TIPS +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/app_state.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/boundary_tunnel.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/cli_runner.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/clibuffer.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/client_commands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/client_connection.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/clistyle.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/clitoolbar.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/compat.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/completion_refresher.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/config.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/constants.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/key_bindings.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/kubectl_tunnel.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/lexer.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/main.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/batch.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/completions.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/execute.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/output.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/filepaths.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/polars_completion.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/iocommands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/llm.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/main.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/source.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/string_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/password_sources.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/resources/completions/bash/mycli +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/resources/completions/fish/mycli.fish +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/resources/completions/zsh/_mycli +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/sqlexecute.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/types.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli/vault.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/SOURCES.txt +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/requires.txt +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/scm_file_list.json +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/pyproject.toml +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/setup.cfg +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/auto_vertical.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/basic_commands.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/connection.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/crud_database.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/crud_table.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/db_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/environment.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/fixture_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/iocommands.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/named_queries.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/specials.feature +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/__init__.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/connection.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/crud_database.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/crud_table.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/iocommands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/named_queries.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/specials.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/wrappers.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/features/wrappager.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/mylogin.cnf +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/conftest.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_app_state.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_checkup.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_config.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_kubectl_tunnel.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_output.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_polars_completion.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_init.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_main.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_source.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_vault.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.23.0 → mycli-2.24.1}/test/utils.py +0 -0
|
@@ -305,6 +305,9 @@ jobs:
|
|
|
305
305
|
with:
|
|
306
306
|
context: .
|
|
307
307
|
file: docker/Dockerfile
|
|
308
|
+
# avoids pull error: unsupported media type application/vnd.oci.empty.v1+json
|
|
309
|
+
# by publishing a v2 mediaType
|
|
310
|
+
load: true
|
|
308
311
|
push: true
|
|
309
312
|
tags: ${{ steps.meta.outputs.tags }}
|
|
310
313
|
labels: ${{ steps.meta.outputs.labels }}
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
2.24.1 (2026/09/12)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Bug Fixes
|
|
5
|
+
--------
|
|
6
|
+
* Omitted completion-candidate methods are no longer applied at all.
|
|
7
|
+
* Set `docker` output-type on ghcr.io images.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
2.24.0 (2026/09/12)
|
|
11
|
+
==============
|
|
12
|
+
|
|
13
|
+
Features
|
|
14
|
+
--------
|
|
15
|
+
* Preserve the query as metadata when saving to Parquet with `.>`.
|
|
16
|
+
* Make completion candidate match order configurable.
|
|
17
|
+
* Make approximate-matching completion thresholds configurable.
|
|
18
|
+
* Make regex matching completion thresholds configurable.
|
|
19
|
+
* Make completion-candidate sorting tiebreaker configurable.
|
|
20
|
+
* Add a `\b` prompt format string to show transaction status.
|
|
21
|
+
|
|
22
|
+
|
|
1
23
|
2.23.0 (2026/09/09)
|
|
2
24
|
==============
|
|
3
25
|
|
|
@@ -212,7 +212,17 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
|
|
|
212
212
|
keyword_casing=keyword_casing,
|
|
213
213
|
indexed_column_suffix=indexed_column_suffix,
|
|
214
214
|
config_property_names=get_config_property_names(self.config),
|
|
215
|
+
completion_match_order=c['main'].as_list('completion_match_order') if 'completion_match_order' in c['main'] else (),
|
|
216
|
+
completion_tiebreaker=c['main'].get('completion_tiebreaker', 'frecency') or 'frecency',
|
|
217
|
+
rapidfuzz_min_length=c['main'].as_int('rapidfuzz_min_length') if c['main'].get('rapidfuzz_min_length') else 4,
|
|
218
|
+
regex_match_distance=c['main'].as_int('regex_match_distance') if c['main'].get('regex_match_distance') else 3,
|
|
219
|
+
rapidfuzz_score_cutoff=c['main'].as_float('rapidfuzz_score_cutoff') if c['main'].get('rapidfuzz_score_cutoff') else 75.0,
|
|
220
|
+
rapidfuzz_length_coverage=c['main'].as_float('rapidfuzz_length_coverage')
|
|
221
|
+
if c['main'].get('rapidfuzz_length_coverage')
|
|
222
|
+
else 0.67,
|
|
215
223
|
)
|
|
224
|
+
for error in self.completer.completion_config_errors:
|
|
225
|
+
self.echo(error, err=True, fg='red')
|
|
216
226
|
self._completer_lock = threading.Lock()
|
|
217
227
|
|
|
218
228
|
self.min_completion_trigger = c["main"].as_int("min_completion_trigger")
|
|
@@ -60,6 +60,12 @@ class ClientQueryMixin:
|
|
|
60
60
|
"indexed_column_suffix": self.completer.indexed_column_suffix,
|
|
61
61
|
"config_property_names": self.completer.config_property_names,
|
|
62
62
|
'frecency_provider': self.completer.frecency_provider,
|
|
63
|
+
'completion_match_order': self.completer.completion_match_order,
|
|
64
|
+
'completion_tiebreaker': self.completer.completion_tiebreaker,
|
|
65
|
+
'rapidfuzz_min_length': self.completer.rapidfuzz_min_length,
|
|
66
|
+
'rapidfuzz_length_coverage': self.completer.rapidfuzz_length_coverage,
|
|
67
|
+
'rapidfuzz_score_cutoff': self.completer.rapidfuzz_score_cutoff,
|
|
68
|
+
'regex_match_distance': self.completer.regex_match_distance,
|
|
63
69
|
},
|
|
64
70
|
)
|
|
65
71
|
|
|
@@ -43,6 +43,7 @@ from prompt_toolkit.lexers import PygmentsLexer
|
|
|
43
43
|
from prompt_toolkit.output import ColorDepth
|
|
44
44
|
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
|
|
45
45
|
import pymysql
|
|
46
|
+
from pymysql.constants.SERVER_STATUS import SERVER_STATUS_IN_TRANS
|
|
46
47
|
from pymysql.cursors import Cursor
|
|
47
48
|
|
|
48
49
|
import mycli as mycli_package
|
|
@@ -343,6 +344,13 @@ def render_prompt_string(
|
|
|
343
344
|
strings = [x.replace('\\_', ' ') for x in strings]
|
|
344
345
|
|
|
345
346
|
checker_string = ' '.join(strings)
|
|
347
|
+
if r'\b' in checker_string:
|
|
348
|
+
connection = getattr(sqlexecute, 'conn', None)
|
|
349
|
+
if connection:
|
|
350
|
+
connection.ping(reconnect=False)
|
|
351
|
+
server_status = getattr(connection, 'server_status', 0) or 0
|
|
352
|
+
transaction_indicator = '[TX]' if server_status & SERVER_STATUS_IN_TRANS else ''
|
|
353
|
+
strings = [x.replace(r'\b', transaction_indicator) for x in strings]
|
|
346
354
|
if r'\e' in checker_string:
|
|
347
355
|
if mycli.prompt_session:
|
|
348
356
|
edit_mode = mycli.prompt_session.editing_mode.value.lower()
|
|
@@ -940,6 +948,7 @@ def _one_iteration(
|
|
|
940
948
|
polars_transform,
|
|
941
949
|
results,
|
|
942
950
|
polars_pipeline.output_path,
|
|
951
|
+
original_query=original_text,
|
|
943
952
|
image_protocol=mycli.image_protocol,
|
|
944
953
|
plot_scale_factor=mycli.plot_scale_factor,
|
|
945
954
|
plot_ppi=mycli.plot_ppi,
|
|
@@ -9,6 +9,41 @@ show_warnings = False
|
|
|
9
9
|
# possible completions will be listed.
|
|
10
10
|
smart_completion = True
|
|
11
11
|
|
|
12
|
+
# Methods to find completion candidates, in priority order. Empty means the
|
|
13
|
+
# default list. Filename matching requires perfect and/or slash_words to be
|
|
14
|
+
# listed. Polars completions uses its own methods.
|
|
15
|
+
# * perfect - exact leading match
|
|
16
|
+
# * regex - ordered charater match with limited intervening spans ("rgx" matches "regex")
|
|
17
|
+
# * under_words - like regex but follows underscores ("uw" matches "under_words")
|
|
18
|
+
# * slash_words - like regex but follows slahes ("sw" matches "slash/words")
|
|
19
|
+
# * camel_case - like regex but follows capitalization ("cc" matches "CamelCase")
|
|
20
|
+
# * rapidfuzz - true approximate matching, _ie_ autcorrect
|
|
21
|
+
completion_match_order = perfect, regex, under_words, slash_words, camel_case, rapidfuzz
|
|
22
|
+
|
|
23
|
+
# Break completion candiate sorting ties by
|
|
24
|
+
# * frecency - history frequency combined with recency, the default
|
|
25
|
+
# * length - shortest first
|
|
26
|
+
# * lexicographic - alphabetical, ignoring case
|
|
27
|
+
completion_tiebreaker = frecency
|
|
28
|
+
|
|
29
|
+
# Maximum intervening span length between input characters for regex completion
|
|
30
|
+
# candidate matches. Empty uses the default of 3. Higher is more liberal, but
|
|
31
|
+
# carries a perforance penalty.
|
|
32
|
+
regex_match_distance = 3
|
|
33
|
+
|
|
34
|
+
# Minimum input characters before using rapidfuzz approximate matching.
|
|
35
|
+
# Empty uses the default of 4. Set to 0 or less to remove the minimum.
|
|
36
|
+
rapidfuzz_min_length = 4
|
|
37
|
+
|
|
38
|
+
# Minimum rapidfuzz candidate length as a fraction of the input length.
|
|
39
|
+
# Empty uses 0.67. Lower is more liberal. Set to 0 or less to suppress
|
|
40
|
+
# the length filter.
|
|
41
|
+
rapidfuzz_length_coverage = 0.67
|
|
42
|
+
|
|
43
|
+
# Minimum rapidfuzz similarity score (0-100). Lower is more liberal.
|
|
44
|
+
# Empty uses the default of 75.
|
|
45
|
+
rapidfuzz_score_cutoff = 75
|
|
46
|
+
|
|
12
47
|
# Text appended to indexed column names in the completion menu. This text is
|
|
13
48
|
# not inserted into the query. Leave empty to disable the marker. Quote values
|
|
14
49
|
# containing spaces, commas, or comment characters.
|
|
@@ -165,6 +200,7 @@ wider_completion_menu = False
|
|
|
165
200
|
# * \W - number of warnings, or the empty string (requires frequent trips to the server)
|
|
166
201
|
# * \y - uptime in seconds (requires frequent trips to the server)
|
|
167
202
|
# * \Y - uptime in words (requires frequent trips to the server)
|
|
203
|
+
# * \b - "[TX]" when in a transaction, otherwise empty (requires frequent trips to the server)
|
|
168
204
|
# * \A - DSN alias
|
|
169
205
|
# * \n - a newline
|
|
170
206
|
# * \_ - a space
|
|
@@ -17,6 +17,7 @@ from mycli.types import ImageProtocol, OutputMode
|
|
|
17
17
|
|
|
18
18
|
delimiter_command = DelimiterCommand()
|
|
19
19
|
PLOT_FORMATS = ('png', 'pdf', 'svg', 'html')
|
|
20
|
+
PARQUET_QUERY_METADATA_KEY = 'mycli_query'
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class PolarsTransformError(RuntimeError):
|
|
@@ -151,7 +152,7 @@ def _pipeline_operator_indexes(
|
|
|
151
152
|
|
|
152
153
|
|
|
153
154
|
def _parse_output_path(path: str) -> str:
|
|
154
|
-
if path[0] in ('
|
|
155
|
+
if path[0] in ("'", '"'):
|
|
155
156
|
if len(path) < 2 or path[-1] != path[0]:
|
|
156
157
|
raise PolarsTransformError('File save paths must use matching quotes.')
|
|
157
158
|
path = path[1:-1]
|
|
@@ -223,6 +224,7 @@ def run_polars_transform(
|
|
|
223
224
|
results: Iterable[SQLResult],
|
|
224
225
|
output_path: str | None = None,
|
|
225
226
|
*,
|
|
227
|
+
original_query: str | None = None,
|
|
226
228
|
image_protocol: ImageProtocol = 'none',
|
|
227
229
|
plot_scale_factor: float = 1.0,
|
|
228
230
|
plot_ppi: int = 200,
|
|
@@ -278,7 +280,7 @@ def run_polars_transform(
|
|
|
278
280
|
if not output_path.lower().endswith('.parquet'):
|
|
279
281
|
raise PolarsTransformError('Polars DataFrame results can only be written to ".parquet" files.')
|
|
280
282
|
try:
|
|
281
|
-
value.
|
|
283
|
+
_write_parquet(value, output_path, original_query or transform.sql)
|
|
282
284
|
except Exception as exc:
|
|
283
285
|
raise PolarsTransformError(f'Unable to write Parquet file "{output_path}": {type(exc).__name__}: {exc}') from exc
|
|
284
286
|
return SQLResult(status=f'Wrote {len(value)} rows to {output_path}.')
|
|
@@ -290,7 +292,7 @@ def run_polars_transform(
|
|
|
290
292
|
raise PolarsTransformError('Polars Series results can only be written to ".parquet" files.')
|
|
291
293
|
try:
|
|
292
294
|
series_dataframe = value.rename(column_name).to_frame()
|
|
293
|
-
series_dataframe.
|
|
295
|
+
_write_parquet(series_dataframe, output_path, original_query or transform.sql)
|
|
294
296
|
except Exception as exc:
|
|
295
297
|
raise PolarsTransformError(f'Unable to write Parquet file "{output_path}": {type(exc).__name__}: {exc}') from exc
|
|
296
298
|
return SQLResult(status=f'Wrote {len(series_dataframe)} rows to {output_path}.')
|
|
@@ -339,3 +341,7 @@ def run_polars_transform(
|
|
|
339
341
|
return SQLResult()
|
|
340
342
|
|
|
341
343
|
return SQLResult(status=f'Nothing could be displayed for return type: {type(value)}')
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _write_parquet(dataframe: Any, parquet_path: str, original_query: str) -> None:
|
|
347
|
+
dataframe.write_parquet(parquet_path, metadata={PARQUET_QUERY_METADATA_KEY: original_query})
|
|
@@ -955,12 +955,40 @@ class SQLCompleter(Completer):
|
|
|
955
955
|
indexed_column_suffix: str = '*',
|
|
956
956
|
config_property_names: Collection[str] = (),
|
|
957
957
|
frecency_provider: Callable[[], Mapping[str, float]] | None = None,
|
|
958
|
+
completion_match_order: Collection[str] = (),
|
|
959
|
+
rapidfuzz_min_length: int = 4,
|
|
960
|
+
rapidfuzz_length_coverage: float = 0.67,
|
|
961
|
+
rapidfuzz_score_cutoff: float = 75.0,
|
|
962
|
+
regex_match_distance: int = 3,
|
|
963
|
+
completion_tiebreaker: str = 'frecency',
|
|
958
964
|
) -> None:
|
|
959
965
|
super(self.__class__, self).__init__()
|
|
960
966
|
self.smart_completion = smart_completion
|
|
961
967
|
self.indexed_column_suffix = indexed_column_suffix
|
|
962
968
|
self.config_property_names = tuple(sorted(config_property_names))
|
|
963
969
|
self.frecency_provider = frecency_provider
|
|
970
|
+
self.rapidfuzz_min_length = max(0, rapidfuzz_min_length)
|
|
971
|
+
self.rapidfuzz_length_coverage = max(0.0, rapidfuzz_length_coverage)
|
|
972
|
+
self.rapidfuzz_score_cutoff = max(0.0, min(100.0, rapidfuzz_score_cutoff))
|
|
973
|
+
self.regex_match_distance = max(0, regex_match_distance)
|
|
974
|
+
self.completion_config_errors: list[str] = []
|
|
975
|
+
self.completion_tiebreaker: Literal['frecency', 'length', 'lexicographic'] = 'frecency'
|
|
976
|
+
tiebreaker = completion_tiebreaker.strip().lower()
|
|
977
|
+
if tiebreaker == 'length':
|
|
978
|
+
self.completion_tiebreaker = 'length'
|
|
979
|
+
elif tiebreaker == 'lexicographic':
|
|
980
|
+
self.completion_tiebreaker = 'lexicographic'
|
|
981
|
+
elif tiebreaker not in ('', 'frecency'):
|
|
982
|
+
self.completion_config_errors.append('Invalid completion_tiebreaker; using frecency.')
|
|
983
|
+
default_order = tuple(category.name.lower() for category in Fuzziness)
|
|
984
|
+
order = tuple(name.strip().lower() for name in completion_match_order if name.strip())
|
|
985
|
+
if len(set(order)) != len(order) or any(name not in default_order for name in order):
|
|
986
|
+
self.completion_config_errors.append('Invalid completion_match_order; using the default order.')
|
|
987
|
+
order = ()
|
|
988
|
+
self.completion_match_order = order or default_order
|
|
989
|
+
self._match_priorities: dict[int, int] = {
|
|
990
|
+
Fuzziness[name.upper()]: priority for priority, name in enumerate(self.completion_match_order)
|
|
991
|
+
}
|
|
964
992
|
self.reserved_words = set()
|
|
965
993
|
for x in self.keywords:
|
|
966
994
|
self.reserved_words.update(x.split())
|
|
@@ -1309,20 +1337,21 @@ class SQLCompleter(Completer):
|
|
|
1309
1337
|
def find_fuzzy_match(
|
|
1310
1338
|
self,
|
|
1311
1339
|
item: str,
|
|
1312
|
-
pattern: re.Pattern[str],
|
|
1340
|
+
pattern: re.Pattern[str] | None,
|
|
1313
1341
|
under_words_text: list[str],
|
|
1314
1342
|
case_words_text: list[str],
|
|
1315
1343
|
) -> int | None:
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1344
|
+
for name in self.completion_match_order:
|
|
1345
|
+
if name == 'regex' and pattern is not None and pattern.search(item.lower()):
|
|
1346
|
+
return Fuzziness.REGEX
|
|
1347
|
+
if name == 'under_words':
|
|
1348
|
+
under_words_item = [x for x in item.lower().split('_') if x]
|
|
1349
|
+
if self.word_parts_match(under_words_text, under_words_item):
|
|
1350
|
+
return Fuzziness.UNDER_WORDS
|
|
1351
|
+
if name == 'camel_case':
|
|
1352
|
+
case_words_item = re.split(_CASE_CHANGE_PAT, item)
|
|
1353
|
+
if self.word_parts_match(case_words_text, case_words_item):
|
|
1354
|
+
return Fuzziness.CAMEL_CASE
|
|
1326
1355
|
|
|
1327
1356
|
return None
|
|
1328
1357
|
|
|
@@ -1333,17 +1362,22 @@ class SQLCompleter(Completer):
|
|
|
1333
1362
|
collection: Collection[Any],
|
|
1334
1363
|
) -> list[tuple[str, int]]:
|
|
1335
1364
|
completions: list[tuple[str, int]] = []
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1365
|
+
pattern = None
|
|
1366
|
+
if 'regex' in self.completion_match_order:
|
|
1367
|
+
regex = f'.{{0,{self.regex_match_distance}}}?'.join(map(re.escape, text))
|
|
1368
|
+
pattern = re.compile(f'({regex})')
|
|
1369
|
+
under_words_text = [x for x in text.split('_') if x] if 'under_words' in self.completion_match_order else []
|
|
1370
|
+
case_words_text = re.split(_CASE_CHANGE_PAT, last) if 'camel_case' in self.completion_match_order else []
|
|
1340
1371
|
|
|
1341
1372
|
for item in collection:
|
|
1342
1373
|
fuzziness = self.find_fuzzy_match(item, pattern, under_words_text, case_words_text)
|
|
1374
|
+
if 'perfect' in self.completion_match_order and self._matches_prefix(item, text):
|
|
1375
|
+
if fuzziness is None or self._match_priorities[Fuzziness.PERFECT] < self._match_priorities[fuzziness]:
|
|
1376
|
+
fuzziness = Fuzziness.PERFECT
|
|
1343
1377
|
if fuzziness is not None:
|
|
1344
1378
|
completions.append((item, fuzziness))
|
|
1345
1379
|
|
|
1346
|
-
if len(text) >=
|
|
1380
|
+
if 'rapidfuzz' in self.completion_match_order and len(text) >= self.rapidfuzz_min_length:
|
|
1347
1381
|
rapidfuzz_matches = rapidfuzz.process.extract(
|
|
1348
1382
|
text,
|
|
1349
1383
|
collection,
|
|
@@ -1352,27 +1386,38 @@ class SQLCompleter(Completer):
|
|
|
1352
1386
|
# because underscores are valuable info
|
|
1353
1387
|
processor=rapidfuzz.utils.default_process,
|
|
1354
1388
|
limit=20,
|
|
1355
|
-
score_cutoff=
|
|
1389
|
+
score_cutoff=self.rapidfuzz_score_cutoff,
|
|
1356
1390
|
)
|
|
1357
|
-
existing = {c[0] for c in completions}
|
|
1391
|
+
existing = {c[0]: index for index, c in enumerate(completions)}
|
|
1358
1392
|
for item, _score, _type in rapidfuzz_matches:
|
|
1359
|
-
if len(item) < len(text)
|
|
1393
|
+
if len(item) < len(text) * self.rapidfuzz_length_coverage:
|
|
1394
|
+
continue
|
|
1395
|
+
if item in existing:
|
|
1396
|
+
index = existing[item]
|
|
1397
|
+
if self._match_priorities[Fuzziness.RAPIDFUZZ] < self._match_priorities[completions[index][1]]:
|
|
1398
|
+
completions[index] = (item, Fuzziness.RAPIDFUZZ)
|
|
1360
1399
|
continue
|
|
1400
|
+
existing[item] = len(completions)
|
|
1361
1401
|
completions.append((item, Fuzziness.RAPIDFUZZ))
|
|
1362
1402
|
|
|
1363
1403
|
return completions
|
|
1364
1404
|
|
|
1405
|
+
def _matches_prefix(self, candidate: str, text: str) -> bool:
|
|
1406
|
+
if not text.startswith('`'):
|
|
1407
|
+
candidate = self._strip_backticks(candidate)
|
|
1408
|
+
return candidate.lower().startswith(text)
|
|
1409
|
+
|
|
1365
1410
|
def find_perfect_matches(
|
|
1366
1411
|
self,
|
|
1367
1412
|
text: str,
|
|
1368
1413
|
collection: Collection[Any],
|
|
1369
1414
|
start_only: bool,
|
|
1370
1415
|
) -> list[tuple[str, int]]:
|
|
1416
|
+
if 'perfect' not in self.completion_match_order:
|
|
1417
|
+
return []
|
|
1371
1418
|
completions: list[tuple[str, int]] = []
|
|
1372
|
-
match_end_limit = len(text) if start_only else None
|
|
1373
1419
|
for item in collection:
|
|
1374
|
-
|
|
1375
|
-
if match_point >= 0:
|
|
1420
|
+
if self._matches_prefix(item, text) if start_only else text in item.lower():
|
|
1376
1421
|
completions.append((item, Fuzziness.PERFECT))
|
|
1377
1422
|
return completions
|
|
1378
1423
|
|
|
@@ -1457,7 +1502,14 @@ class SQLCompleter(Completer):
|
|
|
1457
1502
|
last_for_len = last_word(word_before_cursor, include="most_punctuations")
|
|
1458
1503
|
text_for_len = last_for_len.lower()
|
|
1459
1504
|
path_for_len = word_before_cursor
|
|
1460
|
-
frecency = self.frecency_provider() if self.frecency_provider is not None else {}
|
|
1505
|
+
frecency = self.frecency_provider() if self.completion_tiebreaker == 'frecency' and self.frecency_provider is not None else {}
|
|
1506
|
+
|
|
1507
|
+
def tiebreaker_key(candidate: str) -> tuple[float, str]:
|
|
1508
|
+
if self.completion_tiebreaker == 'length':
|
|
1509
|
+
return (len(candidate), '')
|
|
1510
|
+
if self.completion_tiebreaker == 'lexicographic':
|
|
1511
|
+
return (0, candidate.casefold())
|
|
1512
|
+
return (-frecency_score(candidate, frecency) if frecency else 0.0, '')
|
|
1461
1513
|
|
|
1462
1514
|
if smart_completion is None:
|
|
1463
1515
|
smart_completion = self.smart_completion
|
|
@@ -1472,8 +1524,8 @@ class SQLCompleter(Completer):
|
|
|
1472
1524
|
fuzzy=False,
|
|
1473
1525
|
text_before_cursor=document.text_before_cursor,
|
|
1474
1526
|
)
|
|
1475
|
-
if frecency:
|
|
1476
|
-
matches = sorted(matches, key=lambda item:
|
|
1527
|
+
if frecency or self.completion_tiebreaker != 'frecency':
|
|
1528
|
+
matches = sorted(matches, key=lambda item: tiebreaker_key(item[0]))
|
|
1477
1529
|
return (Completion(x[0], -len(text_for_len)) for x in matches)
|
|
1478
1530
|
|
|
1479
1531
|
completions: list[tuple[str, int, int]] = []
|
|
@@ -1835,18 +1887,16 @@ class SQLCompleter(Completer):
|
|
|
1835
1887
|
]
|
|
1836
1888
|
break
|
|
1837
1889
|
|
|
1838
|
-
def completion_sort_key(item: tuple[str, int, int], text_for_len: str):
|
|
1890
|
+
def completion_sort_key(item: tuple[str, int, int], text_for_len: str) -> tuple[int, int, tuple[float, str], int]:
|
|
1839
1891
|
candidate, fuzziness, rank = item
|
|
1840
|
-
|
|
1892
|
+
tiebreaker = tiebreaker_key(candidate)
|
|
1841
1893
|
if not text_for_len:
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
return (0, 0,
|
|
1847
|
-
|
|
1848
|
-
# todo add alpha here, or original order?
|
|
1849
|
-
return (fuzziness, rank, -candidate_frecency, 0)
|
|
1894
|
+
return (0, rank, tiebreaker, 0)
|
|
1895
|
+
elif self._matches_prefix(candidate, text_for_len):
|
|
1896
|
+
# Preserve the shorter-prefix fallback for equal frecency scores.
|
|
1897
|
+
length = -1000 + len(candidate) if self.completion_tiebreaker == 'frecency' else 0
|
|
1898
|
+
return (0, 0, tiebreaker, length)
|
|
1899
|
+
return (self._match_priorities[fuzziness], rank, tiebreaker, 0)
|
|
1850
1900
|
|
|
1851
1901
|
if rigid_sort:
|
|
1852
1902
|
uniq_completions_str = dict.fromkeys(x[0] for x in completions)
|
|
@@ -1889,10 +1939,13 @@ class SQLCompleter(Completer):
|
|
|
1889
1939
|
|
|
1890
1940
|
"""
|
|
1891
1941
|
if '/' in word:
|
|
1892
|
-
|
|
1893
|
-
|
|
1942
|
+
if 'slash_words' in self.completion_match_order:
|
|
1943
|
+
for path in suggest_path_by_prefix(word, sql_only=sql_only):
|
|
1944
|
+
yield (path, Fuzziness.SLASH_WORDS)
|
|
1894
1945
|
return
|
|
1895
1946
|
|
|
1947
|
+
if 'perfect' not in self.completion_match_order:
|
|
1948
|
+
return
|
|
1896
1949
|
# todo position is ignored, but may need to be used
|
|
1897
1950
|
base_path, last_path, position = parse_path(word)
|
|
1898
1951
|
paths = suggest_path(word, sql_only=sql_only)
|
|
@@ -9,6 +9,41 @@ show_warnings = False
|
|
|
9
9
|
# possible completions will be listed.
|
|
10
10
|
smart_completion = True
|
|
11
11
|
|
|
12
|
+
# Methods to find completion candidates, in priority order. Empty means the
|
|
13
|
+
# default list. Filename matching requires perfect and/or slash_words to be
|
|
14
|
+
# listed. Polars completions uses its own methods.
|
|
15
|
+
# * perfect - exact leading match
|
|
16
|
+
# * regex - ordered charater match with limited intervening spans ("rgx" matches "regex")
|
|
17
|
+
# * under_words - like regex but follows underscores ("uw" matches "under_words")
|
|
18
|
+
# * slash_words - like regex but follows slahes ("sw" matches "slash/words")
|
|
19
|
+
# * camel_case - like regex but follows capitalization ("cc" matches "CamelCase")
|
|
20
|
+
# * rapidfuzz - true approximate matching, _ie_ autcorrect
|
|
21
|
+
completion_match_order = perfect, regex, under_words, slash_words, camel_case, rapidfuzz
|
|
22
|
+
|
|
23
|
+
# Break completion candiate sorting ties by
|
|
24
|
+
# * frecency - history frequency combined with recency, the default
|
|
25
|
+
# * length - shortest first
|
|
26
|
+
# * lexicographic - alphabetical, ignoring case
|
|
27
|
+
completion_tiebreaker = frecency
|
|
28
|
+
|
|
29
|
+
# Maximum intervening span length between input characters for regex completion
|
|
30
|
+
# candidate matches. Empty uses the default of 3. Higher is more liberal, but
|
|
31
|
+
# carries a perforance penalty.
|
|
32
|
+
regex_match_distance = 3
|
|
33
|
+
|
|
34
|
+
# Minimum input characters before using rapidfuzz approximate matching.
|
|
35
|
+
# Empty uses the default of 4. Set to 0 or less to remove the minimum.
|
|
36
|
+
rapidfuzz_min_length = 4
|
|
37
|
+
|
|
38
|
+
# Minimum rapidfuzz candidate length as a fraction of the input length.
|
|
39
|
+
# Empty uses 0.67. Lower is more liberal. Set to 0 or less to suppress
|
|
40
|
+
# the length filter.
|
|
41
|
+
rapidfuzz_length_coverage = 0.67
|
|
42
|
+
|
|
43
|
+
# Minimum rapidfuzz similarity score (0-100). Lower is more liberal.
|
|
44
|
+
# Empty uses the default of 75.
|
|
45
|
+
rapidfuzz_score_cutoff = 75
|
|
46
|
+
|
|
12
47
|
# Text appended to indexed column names in the completion menu. This text is
|
|
13
48
|
# not inserted into the query. Leave empty to disable the marker. Quote values
|
|
14
49
|
# containing spaces, commas, or comment characters.
|
|
@@ -165,6 +200,7 @@ wider_completion_menu = False
|
|
|
165
200
|
# * \W - number of warnings, or the empty string (requires frequent trips to the server)
|
|
166
201
|
# * \y - uptime in seconds (requires frequent trips to the server)
|
|
167
202
|
# * \Y - uptime in words (requires frequent trips to the server)
|
|
203
|
+
# * \b - "[TX]" when in a transaction, otherwise empty (requires frequent trips to the server)
|
|
168
204
|
# * \A - DSN alias
|
|
169
205
|
# * \n - a newline
|
|
170
206
|
# * \_ - a space
|
|
@@ -27,6 +27,100 @@ def patch_constructor_side_effects(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
27
27
|
monkeypatch.setattr(client_module, 'get_mylogin_cnf_path', lambda: None)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def test_init_configures_completion_ranking(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
31
|
+
patch_constructor_side_effects(monkeypatch)
|
|
32
|
+
myclirc = write_myclirc(tmp_path, '[main]\ncompletion_match_order = CAMEL_CASE, under_words\n')
|
|
33
|
+
|
|
34
|
+
cli = MyCli(myclirc=myclirc)
|
|
35
|
+
|
|
36
|
+
assert cli.completer.completion_match_order == ('camel_case', 'under_words')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize(
|
|
40
|
+
('value', 'expected'),
|
|
41
|
+
[(None, 'frecency'), ('', 'frecency'), ('length', 'length'), ('LEXICOGRAPHIC', 'lexicographic'), ('invalid', 'frecency')],
|
|
42
|
+
)
|
|
43
|
+
def test_init_configures_completion_tiebreaker(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str | None, expected: str) -> None:
|
|
44
|
+
patch_constructor_side_effects(monkeypatch)
|
|
45
|
+
messages: list[str] = []
|
|
46
|
+
monkeypatch.setattr(MyCli, 'echo', lambda self, message, **kwargs: messages.append(message))
|
|
47
|
+
setting = f'completion_tiebreaker = {value}\n' if value is not None else ''
|
|
48
|
+
cli = MyCli(myclirc=write_myclirc(tmp_path, f'[main]\n{setting}'))
|
|
49
|
+
|
|
50
|
+
assert cli.completer.completion_tiebreaker == expected
|
|
51
|
+
assert messages == (['Invalid completion_tiebreaker; using frecency.'] if value == 'invalid' else [])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize(('value', 'expected'), [(None, 3), ('', 3), ('5', 5), ('0', 0), ('-1', 0)])
|
|
55
|
+
def test_init_configures_regex_match_distance(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str | None, expected: int) -> None:
|
|
56
|
+
patch_constructor_side_effects(monkeypatch)
|
|
57
|
+
setting = f'regex_match_distance = {value}\n' if value is not None else ''
|
|
58
|
+
myclirc = write_myclirc(tmp_path, f'[main]\n{setting}')
|
|
59
|
+
|
|
60
|
+
cli = MyCli(myclirc=myclirc)
|
|
61
|
+
|
|
62
|
+
assert cli.completer.regex_match_distance == expected
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize(('value', 'expected'), [(None, 4), ('', 4), ('2', 2), ('0', 0), ('-1', 0)])
|
|
66
|
+
def test_init_configures_rapidfuzz_min_length(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str | None, expected: int) -> None:
|
|
67
|
+
patch_constructor_side_effects(monkeypatch)
|
|
68
|
+
setting = f'rapidfuzz_min_length = {value}\n' if value is not None else ''
|
|
69
|
+
myclirc = write_myclirc(tmp_path, f'[main]\n{setting}')
|
|
70
|
+
|
|
71
|
+
cli = MyCli(myclirc=myclirc)
|
|
72
|
+
|
|
73
|
+
assert cli.completer.rapidfuzz_min_length == expected
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.parametrize(('value', 'expected'), [(None, 0.67), ('', 0.67), ('0.5', 0.5), ('0', 0.0), ('-1', 0.0)])
|
|
77
|
+
def test_init_configures_rapidfuzz_length_coverage(
|
|
78
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str | None, expected: float
|
|
79
|
+
) -> None:
|
|
80
|
+
patch_constructor_side_effects(monkeypatch)
|
|
81
|
+
setting = f'rapidfuzz_length_coverage = {value}\n' if value is not None else ''
|
|
82
|
+
myclirc = write_myclirc(tmp_path, f'[main]\n{setting}')
|
|
83
|
+
|
|
84
|
+
cli = MyCli(myclirc=myclirc)
|
|
85
|
+
|
|
86
|
+
assert cli.completer.rapidfuzz_length_coverage == expected
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize(('value', 'expected'), [(None, 75.0), ('', 75.0), ('82.5', 82.5), ('0', 0.0), ('-1', 0.0), ('101', 100.0)])
|
|
90
|
+
def test_init_configures_rapidfuzz_score_cutoff(
|
|
91
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str | None, expected: float
|
|
92
|
+
) -> None:
|
|
93
|
+
patch_constructor_side_effects(monkeypatch)
|
|
94
|
+
setting = f'rapidfuzz_score_cutoff = {value}\n' if value is not None else ''
|
|
95
|
+
myclirc = write_myclirc(tmp_path, f'[main]\n{setting}')
|
|
96
|
+
|
|
97
|
+
cli = MyCli(myclirc=myclirc)
|
|
98
|
+
|
|
99
|
+
assert cli.completer.rapidfuzz_score_cutoff == expected
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.parametrize('value', ['', 'rapidfuzz'])
|
|
103
|
+
def test_init_reads_empty_or_single_match_order(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str) -> None:
|
|
104
|
+
patch_constructor_side_effects(monkeypatch)
|
|
105
|
+
myclirc = write_myclirc(tmp_path, f'[main]\ncompletion_match_order = {value}\n')
|
|
106
|
+
|
|
107
|
+
cli = MyCli(myclirc=myclirc)
|
|
108
|
+
|
|
109
|
+
assert cli.completer.completion_match_order[0] == (value or 'perfect')
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_init_reports_invalid_completion_match_order(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
113
|
+
patch_constructor_side_effects(monkeypatch)
|
|
114
|
+
messages: list[str] = []
|
|
115
|
+
monkeypatch.setattr(MyCli, 'echo', lambda self, message, **kwargs: messages.append(message))
|
|
116
|
+
myclirc = write_myclirc(tmp_path, '[main]\ncompletion_match_order = invalid\n')
|
|
117
|
+
|
|
118
|
+
cli = MyCli(myclirc=myclirc)
|
|
119
|
+
|
|
120
|
+
assert cli.completer.completion_match_order == ('perfect', 'regex', 'under_words', 'slash_words', 'camel_case', 'rapidfuzz')
|
|
121
|
+
assert messages == ['Invalid completion_match_order; using the default order.']
|
|
122
|
+
|
|
123
|
+
|
|
30
124
|
def test_init_reports_invalid_ssl_mode(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
31
125
|
patch_constructor_side_effects(monkeypatch)
|
|
32
126
|
echo_calls: list[tuple[str, dict[str, Any]]] = []
|
|
@@ -28,6 +28,12 @@ def make_refresh_cli() -> tuple[Any, dict[str, Any]]:
|
|
|
28
28
|
cli.completer = SimpleNamespace(
|
|
29
29
|
config_property_names=('main.show_warnings',),
|
|
30
30
|
frecency_provider=lambda: {'select': 1.0},
|
|
31
|
+
completion_match_order=('under_words', 'regex'),
|
|
32
|
+
completion_tiebreaker='length',
|
|
33
|
+
rapidfuzz_min_length=2,
|
|
34
|
+
rapidfuzz_length_coverage=0.5,
|
|
35
|
+
rapidfuzz_score_cutoff=82.5,
|
|
36
|
+
regex_match_distance=5,
|
|
31
37
|
keyword_casing='upper',
|
|
32
38
|
indexed_column_suffix=' [indexed]',
|
|
33
39
|
set_dbname=lambda dbname: state['set_dbname_calls'].append(dbname),
|
|
@@ -74,6 +80,12 @@ def test_refresh_completions_passes_options_to_refresher() -> None:
|
|
|
74
80
|
'indexed_column_suffix': ' [indexed]',
|
|
75
81
|
'config_property_names': ('main.show_warnings',),
|
|
76
82
|
'frecency_provider': cli.completer.frecency_provider,
|
|
83
|
+
'completion_match_order': ('under_words', 'regex'),
|
|
84
|
+
'completion_tiebreaker': 'length',
|
|
85
|
+
'rapidfuzz_min_length': 2,
|
|
86
|
+
'rapidfuzz_length_coverage': 0.5,
|
|
87
|
+
'rapidfuzz_score_cutoff': 82.5,
|
|
88
|
+
'regex_match_distance': 5,
|
|
77
89
|
},
|
|
78
90
|
)
|
|
79
91
|
]
|
|
@@ -104,6 +116,12 @@ def test_refresh_completions_updates_dbname_when_reset() -> None:
|
|
|
104
116
|
cli.completer = SimpleNamespace(
|
|
105
117
|
config_property_names=(),
|
|
106
118
|
frecency_provider=None,
|
|
119
|
+
completion_match_order=(),
|
|
120
|
+
completion_tiebreaker='frecency',
|
|
121
|
+
rapidfuzz_min_length=4,
|
|
122
|
+
rapidfuzz_length_coverage=0.67,
|
|
123
|
+
rapidfuzz_score_cutoff=75.0,
|
|
124
|
+
regex_match_distance=3,
|
|
107
125
|
keyword_casing='lower',
|
|
108
126
|
indexed_column_suffix='*',
|
|
109
127
|
set_dbname=lambda dbname: set_dbname_calls.append(dbname),
|
|
@@ -125,6 +143,12 @@ def test_refresh_completions_uses_lock_when_reset() -> None:
|
|
|
125
143
|
cli.completer = SimpleNamespace(
|
|
126
144
|
config_property_names=(),
|
|
127
145
|
frecency_provider=None,
|
|
146
|
+
completion_match_order=(),
|
|
147
|
+
completion_tiebreaker='frecency',
|
|
148
|
+
rapidfuzz_min_length=4,
|
|
149
|
+
rapidfuzz_length_coverage=0.67,
|
|
150
|
+
rapidfuzz_score_cutoff=75.0,
|
|
151
|
+
regex_match_distance=3,
|
|
128
152
|
keyword_casing='lower',
|
|
129
153
|
indexed_column_suffix='*',
|
|
130
154
|
set_dbname=lambda dbname: None,
|