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.
Files changed (196) hide show
  1. {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/publish.yml +3 -0
  2. {mycli-2.23.0/mycli.egg-info → mycli-2.24.1}/PKG-INFO +1 -1
  3. {mycli-2.23.0 → mycli-2.24.1}/changelog.md +22 -0
  4. {mycli-2.23.0 → mycli-2.24.1}/mycli/client.py +10 -0
  5. {mycli-2.23.0 → mycli-2.24.1}/mycli/client_query.py +6 -0
  6. {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/repl.py +9 -0
  7. {mycli-2.23.0 → mycli-2.24.1}/mycli/myclirc +36 -0
  8. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/polars_transform.py +9 -3
  9. {mycli-2.23.0 → mycli-2.24.1}/mycli/sqlcompleter.py +90 -37
  10. {mycli-2.23.0 → mycli-2.24.1/mycli.egg-info}/PKG-INFO +1 -1
  11. mycli-2.24.1/mycli.egg-info/scm_version.json +8 -0
  12. {mycli-2.23.0 → mycli-2.24.1}/test/myclirc +36 -0
  13. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client.py +94 -0
  14. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client_query.py +24 -0
  15. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_repl.py +106 -8
  16. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_polars_transform.py +37 -3
  17. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sqlcompleter.py +280 -14
  18. mycli-2.23.0/mycli.egg-info/scm_version.json +0 -8
  19. {mycli-2.23.0 → mycli-2.24.1}/.git-blame-ignore-revs +0 -0
  20. {mycli-2.23.0 → mycli-2.24.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {mycli-2.23.0 → mycli-2.24.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  22. {mycli-2.23.0 → mycli-2.24.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {mycli-2.23.0 → mycli-2.24.1}/.github/dependabot.yml +0 -0
  24. {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/ci.yml +0 -0
  25. {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/lint.yml +0 -0
  26. {mycli-2.23.0 → mycli-2.24.1}/.github/workflows/typecheck.yml +0 -0
  27. {mycli-2.23.0 → mycli-2.24.1}/.gitignore +0 -0
  28. {mycli-2.23.0 → mycli-2.24.1}/AGENTS.md +0 -0
  29. {mycli-2.23.0 → mycli-2.24.1}/AUTHORS.rst +0 -0
  30. {mycli-2.23.0 → mycli-2.24.1}/CONTRIBUTING.md +0 -0
  31. {mycli-2.23.0 → mycli-2.24.1}/LICENSE.txt +0 -0
  32. {mycli-2.23.0 → mycli-2.24.1}/MANIFEST.in +0 -0
  33. {mycli-2.23.0 → mycli-2.24.1}/README.md +0 -0
  34. {mycli-2.23.0 → mycli-2.24.1}/SPONSORS.rst +0 -0
  35. {mycli-2.23.0 → mycli-2.24.1}/doc/key_bindings.rst +0 -0
  36. {mycli-2.23.0 → mycli-2.24.1}/doc/llm.md +0 -0
  37. {mycli-2.23.0 → mycli-2.24.1}/doc/screenshots/main.gif +0 -0
  38. {mycli-2.23.0 → mycli-2.24.1}/doc/screenshots/tables.png +0 -0
  39. {mycli-2.23.0 → mycli-2.24.1}/doc/screenshots/total_histogram.png +0 -0
  40. {mycli-2.23.0 → mycli-2.24.1}/doc/transforms.md +0 -0
  41. {mycli-2.23.0 → mycli-2.24.1}/docker/Dockerfile +0 -0
  42. {mycli-2.23.0 → mycli-2.24.1}/docker/resources/test_db-1.0.7.tar.gz +0 -0
  43. {mycli-2.23.0 → mycli-2.24.1}/mycli/AUTHORS +0 -0
  44. {mycli-2.23.0 → mycli-2.24.1}/mycli/SPONSORS +0 -0
  45. {mycli-2.23.0 → mycli-2.24.1}/mycli/TIPS +0 -0
  46. {mycli-2.23.0 → mycli-2.24.1}/mycli/__init__.py +0 -0
  47. {mycli-2.23.0 → mycli-2.24.1}/mycli/app_state.py +0 -0
  48. {mycli-2.23.0 → mycli-2.24.1}/mycli/boundary_tunnel.py +0 -0
  49. {mycli-2.23.0 → mycli-2.24.1}/mycli/cli_runner.py +0 -0
  50. {mycli-2.23.0 → mycli-2.24.1}/mycli/clibuffer.py +0 -0
  51. {mycli-2.23.0 → mycli-2.24.1}/mycli/client_commands.py +0 -0
  52. {mycli-2.23.0 → mycli-2.24.1}/mycli/client_connection.py +0 -0
  53. {mycli-2.23.0 → mycli-2.24.1}/mycli/clistyle.py +0 -0
  54. {mycli-2.23.0 → mycli-2.24.1}/mycli/clitoolbar.py +0 -0
  55. {mycli-2.23.0 → mycli-2.24.1}/mycli/compat.py +0 -0
  56. {mycli-2.23.0 → mycli-2.24.1}/mycli/completion_refresher.py +0 -0
  57. {mycli-2.23.0 → mycli-2.24.1}/mycli/config.py +0 -0
  58. {mycli-2.23.0 → mycli-2.24.1}/mycli/constants.py +0 -0
  59. {mycli-2.23.0 → mycli-2.24.1}/mycli/key_bindings.py +0 -0
  60. {mycli-2.23.0 → mycli-2.24.1}/mycli/kubectl_tunnel.py +0 -0
  61. {mycli-2.23.0 → mycli-2.24.1}/mycli/lexer.py +0 -0
  62. {mycli-2.23.0 → mycli-2.24.1}/mycli/main.py +0 -0
  63. {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/batch.py +0 -0
  64. {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/checkup.py +0 -0
  65. {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/completions.py +0 -0
  66. {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/execute.py +0 -0
  67. {mycli-2.23.0 → mycli-2.24.1}/mycli/main_modes/list_dsn.py +0 -0
  68. {mycli-2.23.0 → mycli-2.24.1}/mycli/output.py +0 -0
  69. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/__init__.py +0 -0
  70. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/batch_utils.py +0 -0
  71. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/cli_utils.py +0 -0
  72. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/completion_engine.py +0 -0
  73. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/filepaths.py +0 -0
  74. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/hybrid_redirection.py +0 -0
  75. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/interactive_utils.py +0 -0
  76. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/key_binding_utils.py +0 -0
  77. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/polars_completion.py +0 -0
  78. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/__init__.py +0 -0
  79. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/fzf.py +0 -0
  80. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/history.py +0 -0
  81. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/ptoolkit/utils.py +0 -0
  82. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/__init__.py +0 -0
  83. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/dbcommands.py +0 -0
  84. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/delimitercommand.py +0 -0
  85. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/dsn_aliases.py +0 -0
  86. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/favoritequeries.py +0 -0
  87. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/iocommands.py +0 -0
  88. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/llm.py +0 -0
  89. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/main.py +0 -0
  90. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/source.py +0 -0
  91. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/special/utils.py +0 -0
  92. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/sql_utils.py +0 -0
  93. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/sqlresult.py +0 -0
  94. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/string_utils.py +0 -0
  95. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/tabular_output/__init__.py +0 -0
  96. {mycli-2.23.0 → mycli-2.24.1}/mycli/packages/tabular_output/sql_format.py +0 -0
  97. {mycli-2.23.0 → mycli-2.24.1}/mycli/password_sources.py +0 -0
  98. {mycli-2.23.0 → mycli-2.24.1}/mycli/resources/completions/bash/mycli +0 -0
  99. {mycli-2.23.0 → mycli-2.24.1}/mycli/resources/completions/fish/mycli.fish +0 -0
  100. {mycli-2.23.0 → mycli-2.24.1}/mycli/resources/completions/zsh/_mycli +0 -0
  101. {mycli-2.23.0 → mycli-2.24.1}/mycli/schema_prefetcher.py +0 -0
  102. {mycli-2.23.0 → mycli-2.24.1}/mycli/sqlexecute.py +0 -0
  103. {mycli-2.23.0 → mycli-2.24.1}/mycli/ssh_tunnel.py +0 -0
  104. {mycli-2.23.0 → mycli-2.24.1}/mycli/types.py +0 -0
  105. {mycli-2.23.0 → mycli-2.24.1}/mycli/vault.py +0 -0
  106. {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/SOURCES.txt +0 -0
  107. {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/dependency_links.txt +0 -0
  108. {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/entry_points.txt +0 -0
  109. {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/requires.txt +0 -0
  110. {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/scm_file_list.json +0 -0
  111. {mycli-2.23.0 → mycli-2.24.1}/mycli.egg-info/top_level.txt +0 -0
  112. {mycli-2.23.0 → mycli-2.24.1}/pyproject.toml +0 -0
  113. {mycli-2.23.0 → mycli-2.24.1}/setup.cfg +0 -0
  114. {mycli-2.23.0 → mycli-2.24.1}/test/__init__.py +0 -0
  115. {mycli-2.23.0 → mycli-2.24.1}/test/features/__init__.py +0 -0
  116. {mycli-2.23.0 → mycli-2.24.1}/test/features/auto_vertical.feature +0 -0
  117. {mycli-2.23.0 → mycli-2.24.1}/test/features/basic_commands.feature +0 -0
  118. {mycli-2.23.0 → mycli-2.24.1}/test/features/connection.feature +0 -0
  119. {mycli-2.23.0 → mycli-2.24.1}/test/features/crud_database.feature +0 -0
  120. {mycli-2.23.0 → mycli-2.24.1}/test/features/crud_table.feature +0 -0
  121. {mycli-2.23.0 → mycli-2.24.1}/test/features/db_utils.py +0 -0
  122. {mycli-2.23.0 → mycli-2.24.1}/test/features/environment.py +0 -0
  123. {mycli-2.23.0 → mycli-2.24.1}/test/features/fixture_data/help_commands.txt +0 -0
  124. {mycli-2.23.0 → mycli-2.24.1}/test/features/fixture_utils.py +0 -0
  125. {mycli-2.23.0 → mycli-2.24.1}/test/features/iocommands.feature +0 -0
  126. {mycli-2.23.0 → mycli-2.24.1}/test/features/named_queries.feature +0 -0
  127. {mycli-2.23.0 → mycli-2.24.1}/test/features/specials.feature +0 -0
  128. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/__init__.py +0 -0
  129. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/auto_vertical.py +0 -0
  130. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/basic_commands.py +0 -0
  131. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/connection.py +0 -0
  132. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/crud_database.py +0 -0
  133. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/crud_table.py +0 -0
  134. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/iocommands.py +0 -0
  135. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/named_queries.py +0 -0
  136. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/specials.py +0 -0
  137. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/utils.py +0 -0
  138. {mycli-2.23.0 → mycli-2.24.1}/test/features/steps/wrappers.py +0 -0
  139. {mycli-2.23.0 → mycli-2.24.1}/test/features/wrappager.py +0 -0
  140. {mycli-2.23.0 → mycli-2.24.1}/test/mylogin.cnf +0 -0
  141. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/conftest.py +0 -0
  142. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_app_state.py +0 -0
  143. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_bash_completion.py +0 -0
  144. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_batch_utils.py +0 -0
  145. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_boundary_tunnel.py +0 -0
  146. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_checkup.py +0 -0
  147. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_cli_runner.py +0 -0
  148. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_cli_utils.py +0 -0
  149. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_clibuffer.py +0 -0
  150. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client_commands.py +0 -0
  151. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_client_connection.py +0 -0
  152. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_clistyle.py +0 -0
  153. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_clitoolbar.py +0 -0
  154. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_completer_use_switch.py +0 -0
  155. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_completion_engine.py +0 -0
  156. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_completion_refresher.py +0 -0
  157. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_config.py +0 -0
  158. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_delimitercommand.py +0 -0
  159. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_dsn_aliases.py +0 -0
  160. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_favoritequeries.py +0 -0
  161. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_filepaths.py +0 -0
  162. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_fish_completion.py +0 -0
  163. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_hybrid_redirection.py +0 -0
  164. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_interactive_utils.py +0 -0
  165. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_key_binding_utils.py +0 -0
  166. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_key_bindings.py +0 -0
  167. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_kubectl_tunnel.py +0 -0
  168. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main.py +0 -0
  169. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_batch.py +0 -0
  170. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_execute.py +0 -0
  171. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_main_modes_list_dsn.py +0 -0
  172. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_naive_completion.py +0 -0
  173. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_output.py +0 -0
  174. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_password_sources.py +0 -0
  175. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_polars_completion.py +0 -0
  176. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ptoolkit_fzf.py +0 -0
  177. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ptoolkit_history.py +0 -0
  178. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ptoolkit_utils.py +0 -0
  179. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_schema_prefetcher.py +0 -0
  180. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
  181. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_dbcommands.py +0 -0
  182. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_init.py +0 -0
  183. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_iocommands.py +0 -0
  184. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_llm.py +0 -0
  185. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_main.py +0 -0
  186. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_source.py +0 -0
  187. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_special_utils.py +0 -0
  188. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sql_utils.py +0 -0
  189. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sqlexecute.py +0 -0
  190. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_sqlresult.py +0 -0
  191. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_ssh_tunnel.py +0 -0
  192. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_string_utils.py +0 -0
  193. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_tabular_output.py +0 -0
  194. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_vault.py +0 -0
  195. {mycli-2.23.0 → mycli-2.24.1}/test/pytests/test_zsh_completion.py +0 -0
  196. {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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.23.0
3
+ Version: 2.24.1
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
@@ -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.write_parquet(output_path)
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.write_parquet(output_path)
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
- if pattern.search(item.lower()):
1317
- return Fuzziness.REGEX
1318
-
1319
- under_words_item = [x for x in item.lower().split('_') if x]
1320
- if self.word_parts_match(under_words_text, under_words_item):
1321
- return Fuzziness.UNDER_WORDS
1322
-
1323
- case_words_item = re.split(_CASE_CHANGE_PAT, item)
1324
- if self.word_parts_match(case_words_text, case_words_item):
1325
- return Fuzziness.CAMEL_CASE
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
- regex = '.{0,3}?'.join(map(re.escape, text))
1337
- pattern = re.compile(f'({regex})')
1338
- under_words_text = [x for x in text.split('_') if x]
1339
- case_words_text = re.split(_CASE_CHANGE_PAT, last)
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) >= 4:
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=75,
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) / 1.5 or item in existing:
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
- match_point = item.lower().find(text, 0, match_end_limit)
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: -frecency_score(item[0], frecency))
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
- candidate_frecency = frecency_score(candidate, frecency) if frecency else 0.0
1892
+ tiebreaker = tiebreaker_key(candidate)
1841
1893
  if not text_for_len:
1842
- # Sort by the rank (the order of the completion type), then frecency.
1843
- return (0, rank, -candidate_frecency, 0)
1844
- elif candidate.lower().startswith(text_for_len):
1845
- # Direct prefix matches are equally relevant; prefer frecency before length.
1846
- return (0, 0, -candidate_frecency, -1000 + len(candidate))
1847
- # Sort by fuzziness, rank, and frecency.
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
- for path in suggest_path_by_prefix(word, sql_only=sql_only):
1893
- yield (path, Fuzziness.SLASH_WORDS)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.23.0
3
+ Version: 2.24.1
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
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.24.1",
3
+ "distance": 0,
4
+ "node": "g7019d5e173c6fa6b93cd772382177c8a47d2cf39",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-12"
8
+ }
@@ -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,