mycli 2.22.0__tar.gz → 2.24.0__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.22.0 → mycli-2.24.0}/.github/workflows/publish.yml +1 -0
- {mycli-2.22.0/mycli.egg-info → mycli-2.24.0}/PKG-INFO +2 -2
- {mycli-2.22.0 → mycli-2.24.0}/README.md +1 -1
- {mycli-2.22.0 → mycli-2.24.0}/changelog.md +31 -0
- {mycli-2.22.0 → mycli-2.24.0/docker}/Dockerfile +10 -1
- mycli-2.24.0/docker/resources/test_db-1.0.7.tar.gz +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/client.py +10 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/client_query.py +6 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/lexer.py +2 -1
- {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/repl.py +9 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/myclirc +36 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/polars_transform.py +9 -3
- {mycli-2.22.0 → mycli-2.24.0}/mycli/sqlcompleter.py +67 -27
- {mycli-2.22.0 → mycli-2.24.0/mycli.egg-info}/PKG-INFO +2 -2
- {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/SOURCES.txt +2 -1
- {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/scm_file_list.json +2 -1
- mycli-2.24.0/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/myclirc +36 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client.py +94 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client_query.py +24 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_repl.py +106 -8
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_polars_transform.py +37 -3
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sqlcompleter.py +162 -4
- mycli-2.22.0/mycli.egg-info/scm_version.json +0 -8
- {mycli-2.22.0 → mycli-2.24.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.github/dependabot.yml +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.github/workflows/ci.yml +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.github/workflows/lint.yml +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/.gitignore +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/AGENTS.md +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/AUTHORS.rst +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/CONTRIBUTING.md +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/LICENSE.txt +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/MANIFEST.in +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/SPONSORS.rst +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/doc/key_bindings.rst +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/doc/llm.md +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/doc/screenshots/total_histogram.png +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/doc/transforms.md +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/AUTHORS +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/SPONSORS +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/TIPS +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/app_state.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/boundary_tunnel.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/cli_runner.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/clibuffer.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/client_commands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/client_connection.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/clistyle.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/clitoolbar.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/compat.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/completion_refresher.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/config.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/constants.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/key_bindings.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/kubectl_tunnel.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/main.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/batch.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/completions.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/execute.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/output.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/filepaths.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/polars_completion.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/iocommands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/llm.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/main.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/source.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/string_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/password_sources.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/resources/completions/bash/mycli +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/resources/completions/fish/mycli.fish +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/resources/completions/zsh/_mycli +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/sqlexecute.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/types.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli/vault.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/requires.txt +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/pyproject.toml +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/setup.cfg +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/auto_vertical.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/basic_commands.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/connection.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/crud_database.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/crud_table.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/db_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/environment.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/fixture_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/iocommands.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/named_queries.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/specials.feature +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/__init__.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/connection.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/crud_database.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/crud_table.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/iocommands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/named_queries.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/specials.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/wrappers.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/features/wrappager.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/mylogin.cnf +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/conftest.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_app_state.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_checkup.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_config.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_kubectl_tunnel.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_output.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_polars_completion.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_init.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_main.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_source.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_vault.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.22.0 → mycli-2.24.0}/test/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.24.0
|
|
4
4
|
Summary: Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes
|
|
5
5
|
Author: Mycli Core Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -238,7 +238,7 @@ sudo dnf install mycli fzf python-pygments
|
|
|
238
238
|
The images on Dockerhub are out of date. ghcr.io is preferred. Example:
|
|
239
239
|
|
|
240
240
|
```
|
|
241
|
-
docker run -it ghcr.io/dbcli/mycli:latest
|
|
241
|
+
docker run --pull=always -it ghcr.io/dbcli/mycli:latest
|
|
242
242
|
```
|
|
243
243
|
|
|
244
244
|
### Windows
|
|
@@ -166,7 +166,7 @@ sudo dnf install mycli fzf python-pygments
|
|
|
166
166
|
The images on Dockerhub are out of date. ghcr.io is preferred. Example:
|
|
167
167
|
|
|
168
168
|
```
|
|
169
|
-
docker run -it ghcr.io/dbcli/mycli:latest
|
|
169
|
+
docker run --pull=always -it ghcr.io/dbcli/mycli:latest
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
### Windows
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
2.24.0 (2026/09/12)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Features
|
|
5
|
+
--------
|
|
6
|
+
* Preserve the query as metadata when saving to Parquet with `.>`.
|
|
7
|
+
* Make completion candidate match order configurable.
|
|
8
|
+
* Make approximate-matching completion thresholds configurable.
|
|
9
|
+
* Make regex matching completion thresholds configurable.
|
|
10
|
+
* Make completion-candidate sorting tiebreaker configurable.
|
|
11
|
+
* Add a `\b` prompt format string to show transaction status.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
2.23.0 (2026/09/09)
|
|
15
|
+
==============
|
|
16
|
+
|
|
17
|
+
Features
|
|
18
|
+
--------
|
|
19
|
+
* Include a MariaDB server and example data in the Docker image.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Bug Fixes
|
|
23
|
+
--------
|
|
24
|
+
* Tokenize leading `/` and `\` as symbol names for better highlighting.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Documentation
|
|
28
|
+
--------
|
|
29
|
+
* Add `--pull=always` to documented `docker run` command.
|
|
30
|
+
|
|
31
|
+
|
|
1
32
|
2.22.0 (2026/09/09)
|
|
2
33
|
==============
|
|
3
34
|
|
|
@@ -5,6 +5,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|
|
5
5
|
RUN apt-get update -qq && \
|
|
6
6
|
apt-get install -y --no-install-recommends --no-install-suggests -qq \
|
|
7
7
|
fzf \
|
|
8
|
+
mariadb-server \
|
|
8
9
|
&& \
|
|
9
10
|
apt-get clean autoclean && \
|
|
10
11
|
apt-get autoremove --yes && \
|
|
@@ -12,6 +13,14 @@ RUN apt-get update -qq && \
|
|
|
12
13
|
|
|
13
14
|
COPY . /app
|
|
14
15
|
|
|
16
|
+
RUN service mariadb restart && \
|
|
17
|
+
cd /app/docker/resources && \
|
|
18
|
+
gunzip -c test_db-1.0.7.tar.gz | tar -xf - && \
|
|
19
|
+
cd test_db && \
|
|
20
|
+
mariadb < employees.sql && \
|
|
21
|
+
cd .. && \
|
|
22
|
+
rm -r -- ./test_db
|
|
23
|
+
|
|
15
24
|
RUN cd /app && pip install --root-user-action=ignore -e .[llm,dataframe]
|
|
16
25
|
|
|
17
|
-
CMD ["
|
|
26
|
+
CMD ["sh", "-c", "service mariadb start && mycli --database employees"]
|
|
Binary file
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pygments.lexer import inherit
|
|
2
2
|
from pygments.lexers.sql import MySqlLexer
|
|
3
|
-
from pygments.token import Keyword
|
|
3
|
+
from pygments.token import Keyword, Name
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class MyCliLexer(MySqlLexer):
|
|
@@ -8,6 +8,7 @@ class MyCliLexer(MySqlLexer):
|
|
|
8
8
|
|
|
9
9
|
tokens = {
|
|
10
10
|
"root": [
|
|
11
|
+
(r'^[/\\]', Name),
|
|
11
12
|
(r"\brepair\b", Keyword),
|
|
12
13
|
(r"\boffset\b", Keyword),
|
|
13
14
|
inherit,
|
|
@@ -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, highest priority first. Omitted
|
|
13
|
+
# methods follow in the default order; empty uses the default. Filename
|
|
14
|
+
# and Polars completions retain their own dedicated ordering.
|
|
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 + tuple(name for name in default_order if name not in 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())
|
|
@@ -1313,16 +1341,17 @@ class SQLCompleter(Completer):
|
|
|
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.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,7 +1362,7 @@ class SQLCompleter(Completer):
|
|
|
1333
1362
|
collection: Collection[Any],
|
|
1334
1363
|
) -> list[tuple[str, int]]:
|
|
1335
1364
|
completions: list[tuple[str, int]] = []
|
|
1336
|
-
regex = '.{0,
|
|
1365
|
+
regex = f'.{{0,{self.regex_match_distance}}}?'.join(map(re.escape, text))
|
|
1337
1366
|
pattern = re.compile(f'({regex})')
|
|
1338
1367
|
under_words_text = [x for x in text.split('_') if x]
|
|
1339
1368
|
case_words_text = re.split(_CASE_CHANGE_PAT, last)
|
|
@@ -1343,7 +1372,7 @@ class SQLCompleter(Completer):
|
|
|
1343
1372
|
if fuzziness is not None:
|
|
1344
1373
|
completions.append((item, fuzziness))
|
|
1345
1374
|
|
|
1346
|
-
if len(text) >=
|
|
1375
|
+
if len(text) >= self.rapidfuzz_min_length:
|
|
1347
1376
|
rapidfuzz_matches = rapidfuzz.process.extract(
|
|
1348
1377
|
text,
|
|
1349
1378
|
collection,
|
|
@@ -1352,12 +1381,18 @@ class SQLCompleter(Completer):
|
|
|
1352
1381
|
# because underscores are valuable info
|
|
1353
1382
|
processor=rapidfuzz.utils.default_process,
|
|
1354
1383
|
limit=20,
|
|
1355
|
-
score_cutoff=
|
|
1384
|
+
score_cutoff=self.rapidfuzz_score_cutoff,
|
|
1356
1385
|
)
|
|
1357
|
-
existing = {c[0] for c in completions}
|
|
1386
|
+
existing = {c[0]: index for index, c in enumerate(completions)}
|
|
1358
1387
|
for item, _score, _type in rapidfuzz_matches:
|
|
1359
|
-
if len(item) < len(text)
|
|
1388
|
+
if len(item) < len(text) * self.rapidfuzz_length_coverage:
|
|
1360
1389
|
continue
|
|
1390
|
+
if item in existing:
|
|
1391
|
+
index = existing[item]
|
|
1392
|
+
if self._match_priorities[Fuzziness.RAPIDFUZZ] < self._match_priorities[completions[index][1]]:
|
|
1393
|
+
completions[index] = (item, Fuzziness.RAPIDFUZZ)
|
|
1394
|
+
continue
|
|
1395
|
+
existing[item] = len(completions)
|
|
1361
1396
|
completions.append((item, Fuzziness.RAPIDFUZZ))
|
|
1362
1397
|
|
|
1363
1398
|
return completions
|
|
@@ -1457,7 +1492,14 @@ class SQLCompleter(Completer):
|
|
|
1457
1492
|
last_for_len = last_word(word_before_cursor, include="most_punctuations")
|
|
1458
1493
|
text_for_len = last_for_len.lower()
|
|
1459
1494
|
path_for_len = word_before_cursor
|
|
1460
|
-
frecency = self.frecency_provider() if self.frecency_provider is not None else {}
|
|
1495
|
+
frecency = self.frecency_provider() if self.completion_tiebreaker == 'frecency' and self.frecency_provider is not None else {}
|
|
1496
|
+
|
|
1497
|
+
def tiebreaker_key(candidate: str) -> tuple[float, str]:
|
|
1498
|
+
if self.completion_tiebreaker == 'length':
|
|
1499
|
+
return (len(candidate), '')
|
|
1500
|
+
if self.completion_tiebreaker == 'lexicographic':
|
|
1501
|
+
return (0, candidate.casefold())
|
|
1502
|
+
return (-frecency_score(candidate, frecency) if frecency else 0.0, '')
|
|
1461
1503
|
|
|
1462
1504
|
if smart_completion is None:
|
|
1463
1505
|
smart_completion = self.smart_completion
|
|
@@ -1472,8 +1514,8 @@ class SQLCompleter(Completer):
|
|
|
1472
1514
|
fuzzy=False,
|
|
1473
1515
|
text_before_cursor=document.text_before_cursor,
|
|
1474
1516
|
)
|
|
1475
|
-
if frecency:
|
|
1476
|
-
matches = sorted(matches, key=lambda item:
|
|
1517
|
+
if frecency or self.completion_tiebreaker != 'frecency':
|
|
1518
|
+
matches = sorted(matches, key=lambda item: tiebreaker_key(item[0]))
|
|
1477
1519
|
return (Completion(x[0], -len(text_for_len)) for x in matches)
|
|
1478
1520
|
|
|
1479
1521
|
completions: list[tuple[str, int, int]] = []
|
|
@@ -1835,18 +1877,16 @@ class SQLCompleter(Completer):
|
|
|
1835
1877
|
]
|
|
1836
1878
|
break
|
|
1837
1879
|
|
|
1838
|
-
def completion_sort_key(item: tuple[str, int, int], text_for_len: str):
|
|
1880
|
+
def completion_sort_key(item: tuple[str, int, int], text_for_len: str) -> tuple[int, int, tuple[float, str], int]:
|
|
1839
1881
|
candidate, fuzziness, rank = item
|
|
1840
|
-
|
|
1882
|
+
tiebreaker = tiebreaker_key(candidate)
|
|
1841
1883
|
if not text_for_len:
|
|
1842
|
-
|
|
1843
|
-
return (0, rank, -candidate_frecency, 0)
|
|
1884
|
+
return (0, rank, tiebreaker, 0)
|
|
1844
1885
|
elif candidate.lower().startswith(text_for_len):
|
|
1845
|
-
#
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
return (fuzziness, rank, -candidate_frecency, 0)
|
|
1886
|
+
# Preserve the shorter-prefix fallback for equal frecency scores.
|
|
1887
|
+
length = -1000 + len(candidate) if self.completion_tiebreaker == 'frecency' else 0
|
|
1888
|
+
return (0, 0, tiebreaker, length)
|
|
1889
|
+
return (self._match_priorities[fuzziness], rank, tiebreaker, 0)
|
|
1850
1890
|
|
|
1851
1891
|
if rigid_sort:
|
|
1852
1892
|
uniq_completions_str = dict.fromkeys(x[0] for x in completions)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.24.0
|
|
4
4
|
Summary: Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes
|
|
5
5
|
Author: Mycli Core Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -238,7 +238,7 @@ sudo dnf install mycli fzf python-pygments
|
|
|
238
238
|
The images on Dockerhub are out of date. ghcr.io is preferred. Example:
|
|
239
239
|
|
|
240
240
|
```
|
|
241
|
-
docker run -it ghcr.io/dbcli/mycli:latest
|
|
241
|
+
docker run --pull=always -it ghcr.io/dbcli/mycli:latest
|
|
242
242
|
```
|
|
243
243
|
|
|
244
244
|
### Windows
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
AGENTS.md
|
|
4
4
|
AUTHORS.rst
|
|
5
5
|
CONTRIBUTING.md
|
|
6
|
-
Dockerfile
|
|
7
6
|
LICENSE.txt
|
|
8
7
|
MANIFEST.in
|
|
9
8
|
README.md
|
|
@@ -24,6 +23,8 @@ doc/transforms.md
|
|
|
24
23
|
doc/screenshots/main.gif
|
|
25
24
|
doc/screenshots/tables.png
|
|
26
25
|
doc/screenshots/total_histogram.png
|
|
26
|
+
docker/Dockerfile
|
|
27
|
+
docker/resources/test_db-1.0.7.tar.gz
|
|
27
28
|
mycli/AUTHORS
|
|
28
29
|
mycli/SPONSORS
|
|
29
30
|
mycli/TIPS
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"AGENTS.md",
|
|
14
14
|
"AUTHORS.rst",
|
|
15
15
|
"CONTRIBUTING.md",
|
|
16
|
-
"Dockerfile",
|
|
17
16
|
"LICENSE.txt",
|
|
18
17
|
"MANIFEST.in",
|
|
19
18
|
"README.md",
|
|
@@ -25,6 +24,8 @@
|
|
|
25
24
|
"doc/screenshots/tables.png",
|
|
26
25
|
"doc/screenshots/total_histogram.png",
|
|
27
26
|
"doc/transforms.md",
|
|
27
|
+
"docker/Dockerfile",
|
|
28
|
+
"docker/resources/test_db-1.0.7.tar.gz",
|
|
28
29
|
"mycli/AUTHORS",
|
|
29
30
|
"mycli/SPONSORS",
|
|
30
31
|
"mycli/TIPS",
|
|
@@ -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, highest priority first. Omitted
|
|
13
|
+
# methods follow in the default order; empty uses the default. Filename
|
|
14
|
+
# and Polars completions retain their own dedicated ordering.
|
|
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', 'perfect', 'regex', 'slash_words', 'rapidfuzz')
|
|
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]]] = []
|