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.
Files changed (196) hide show
  1. {mycli-2.22.0 → mycli-2.24.0}/.github/workflows/publish.yml +1 -0
  2. {mycli-2.22.0/mycli.egg-info → mycli-2.24.0}/PKG-INFO +2 -2
  3. {mycli-2.22.0 → mycli-2.24.0}/README.md +1 -1
  4. {mycli-2.22.0 → mycli-2.24.0}/changelog.md +31 -0
  5. {mycli-2.22.0 → mycli-2.24.0/docker}/Dockerfile +10 -1
  6. mycli-2.24.0/docker/resources/test_db-1.0.7.tar.gz +0 -0
  7. {mycli-2.22.0 → mycli-2.24.0}/mycli/client.py +10 -0
  8. {mycli-2.22.0 → mycli-2.24.0}/mycli/client_query.py +6 -0
  9. {mycli-2.22.0 → mycli-2.24.0}/mycli/lexer.py +2 -1
  10. {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/repl.py +9 -0
  11. {mycli-2.22.0 → mycli-2.24.0}/mycli/myclirc +36 -0
  12. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/polars_transform.py +9 -3
  13. {mycli-2.22.0 → mycli-2.24.0}/mycli/sqlcompleter.py +67 -27
  14. {mycli-2.22.0 → mycli-2.24.0/mycli.egg-info}/PKG-INFO +2 -2
  15. {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/SOURCES.txt +2 -1
  16. {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/scm_file_list.json +2 -1
  17. mycli-2.24.0/mycli.egg-info/scm_version.json +8 -0
  18. {mycli-2.22.0 → mycli-2.24.0}/test/myclirc +36 -0
  19. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client.py +94 -0
  20. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client_query.py +24 -0
  21. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_repl.py +106 -8
  22. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_polars_transform.py +37 -3
  23. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sqlcompleter.py +162 -4
  24. mycli-2.22.0/mycli.egg-info/scm_version.json +0 -8
  25. {mycli-2.22.0 → mycli-2.24.0}/.git-blame-ignore-revs +0 -0
  26. {mycli-2.22.0 → mycli-2.24.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  27. {mycli-2.22.0 → mycli-2.24.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  28. {mycli-2.22.0 → mycli-2.24.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {mycli-2.22.0 → mycli-2.24.0}/.github/dependabot.yml +0 -0
  30. {mycli-2.22.0 → mycli-2.24.0}/.github/workflows/ci.yml +0 -0
  31. {mycli-2.22.0 → mycli-2.24.0}/.github/workflows/lint.yml +0 -0
  32. {mycli-2.22.0 → mycli-2.24.0}/.github/workflows/typecheck.yml +0 -0
  33. {mycli-2.22.0 → mycli-2.24.0}/.gitignore +0 -0
  34. {mycli-2.22.0 → mycli-2.24.0}/AGENTS.md +0 -0
  35. {mycli-2.22.0 → mycli-2.24.0}/AUTHORS.rst +0 -0
  36. {mycli-2.22.0 → mycli-2.24.0}/CONTRIBUTING.md +0 -0
  37. {mycli-2.22.0 → mycli-2.24.0}/LICENSE.txt +0 -0
  38. {mycli-2.22.0 → mycli-2.24.0}/MANIFEST.in +0 -0
  39. {mycli-2.22.0 → mycli-2.24.0}/SPONSORS.rst +0 -0
  40. {mycli-2.22.0 → mycli-2.24.0}/doc/key_bindings.rst +0 -0
  41. {mycli-2.22.0 → mycli-2.24.0}/doc/llm.md +0 -0
  42. {mycli-2.22.0 → mycli-2.24.0}/doc/screenshots/main.gif +0 -0
  43. {mycli-2.22.0 → mycli-2.24.0}/doc/screenshots/tables.png +0 -0
  44. {mycli-2.22.0 → mycli-2.24.0}/doc/screenshots/total_histogram.png +0 -0
  45. {mycli-2.22.0 → mycli-2.24.0}/doc/transforms.md +0 -0
  46. {mycli-2.22.0 → mycli-2.24.0}/mycli/AUTHORS +0 -0
  47. {mycli-2.22.0 → mycli-2.24.0}/mycli/SPONSORS +0 -0
  48. {mycli-2.22.0 → mycli-2.24.0}/mycli/TIPS +0 -0
  49. {mycli-2.22.0 → mycli-2.24.0}/mycli/__init__.py +0 -0
  50. {mycli-2.22.0 → mycli-2.24.0}/mycli/app_state.py +0 -0
  51. {mycli-2.22.0 → mycli-2.24.0}/mycli/boundary_tunnel.py +0 -0
  52. {mycli-2.22.0 → mycli-2.24.0}/mycli/cli_runner.py +0 -0
  53. {mycli-2.22.0 → mycli-2.24.0}/mycli/clibuffer.py +0 -0
  54. {mycli-2.22.0 → mycli-2.24.0}/mycli/client_commands.py +0 -0
  55. {mycli-2.22.0 → mycli-2.24.0}/mycli/client_connection.py +0 -0
  56. {mycli-2.22.0 → mycli-2.24.0}/mycli/clistyle.py +0 -0
  57. {mycli-2.22.0 → mycli-2.24.0}/mycli/clitoolbar.py +0 -0
  58. {mycli-2.22.0 → mycli-2.24.0}/mycli/compat.py +0 -0
  59. {mycli-2.22.0 → mycli-2.24.0}/mycli/completion_refresher.py +0 -0
  60. {mycli-2.22.0 → mycli-2.24.0}/mycli/config.py +0 -0
  61. {mycli-2.22.0 → mycli-2.24.0}/mycli/constants.py +0 -0
  62. {mycli-2.22.0 → mycli-2.24.0}/mycli/key_bindings.py +0 -0
  63. {mycli-2.22.0 → mycli-2.24.0}/mycli/kubectl_tunnel.py +0 -0
  64. {mycli-2.22.0 → mycli-2.24.0}/mycli/main.py +0 -0
  65. {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/batch.py +0 -0
  66. {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/checkup.py +0 -0
  67. {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/completions.py +0 -0
  68. {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/execute.py +0 -0
  69. {mycli-2.22.0 → mycli-2.24.0}/mycli/main_modes/list_dsn.py +0 -0
  70. {mycli-2.22.0 → mycli-2.24.0}/mycli/output.py +0 -0
  71. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/__init__.py +0 -0
  72. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/batch_utils.py +0 -0
  73. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/cli_utils.py +0 -0
  74. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/completion_engine.py +0 -0
  75. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/filepaths.py +0 -0
  76. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/hybrid_redirection.py +0 -0
  77. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/interactive_utils.py +0 -0
  78. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/key_binding_utils.py +0 -0
  79. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/polars_completion.py +0 -0
  80. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/__init__.py +0 -0
  81. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/fzf.py +0 -0
  82. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/history.py +0 -0
  83. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/ptoolkit/utils.py +0 -0
  84. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/__init__.py +0 -0
  85. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/dbcommands.py +0 -0
  86. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/delimitercommand.py +0 -0
  87. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/dsn_aliases.py +0 -0
  88. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/favoritequeries.py +0 -0
  89. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/iocommands.py +0 -0
  90. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/llm.py +0 -0
  91. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/main.py +0 -0
  92. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/source.py +0 -0
  93. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/special/utils.py +0 -0
  94. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/sql_utils.py +0 -0
  95. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/sqlresult.py +0 -0
  96. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/string_utils.py +0 -0
  97. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/tabular_output/__init__.py +0 -0
  98. {mycli-2.22.0 → mycli-2.24.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  99. {mycli-2.22.0 → mycli-2.24.0}/mycli/password_sources.py +0 -0
  100. {mycli-2.22.0 → mycli-2.24.0}/mycli/resources/completions/bash/mycli +0 -0
  101. {mycli-2.22.0 → mycli-2.24.0}/mycli/resources/completions/fish/mycli.fish +0 -0
  102. {mycli-2.22.0 → mycli-2.24.0}/mycli/resources/completions/zsh/_mycli +0 -0
  103. {mycli-2.22.0 → mycli-2.24.0}/mycli/schema_prefetcher.py +0 -0
  104. {mycli-2.22.0 → mycli-2.24.0}/mycli/sqlexecute.py +0 -0
  105. {mycli-2.22.0 → mycli-2.24.0}/mycli/ssh_tunnel.py +0 -0
  106. {mycli-2.22.0 → mycli-2.24.0}/mycli/types.py +0 -0
  107. {mycli-2.22.0 → mycli-2.24.0}/mycli/vault.py +0 -0
  108. {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/dependency_links.txt +0 -0
  109. {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/entry_points.txt +0 -0
  110. {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/requires.txt +0 -0
  111. {mycli-2.22.0 → mycli-2.24.0}/mycli.egg-info/top_level.txt +0 -0
  112. {mycli-2.22.0 → mycli-2.24.0}/pyproject.toml +0 -0
  113. {mycli-2.22.0 → mycli-2.24.0}/setup.cfg +0 -0
  114. {mycli-2.22.0 → mycli-2.24.0}/test/__init__.py +0 -0
  115. {mycli-2.22.0 → mycli-2.24.0}/test/features/__init__.py +0 -0
  116. {mycli-2.22.0 → mycli-2.24.0}/test/features/auto_vertical.feature +0 -0
  117. {mycli-2.22.0 → mycli-2.24.0}/test/features/basic_commands.feature +0 -0
  118. {mycli-2.22.0 → mycli-2.24.0}/test/features/connection.feature +0 -0
  119. {mycli-2.22.0 → mycli-2.24.0}/test/features/crud_database.feature +0 -0
  120. {mycli-2.22.0 → mycli-2.24.0}/test/features/crud_table.feature +0 -0
  121. {mycli-2.22.0 → mycli-2.24.0}/test/features/db_utils.py +0 -0
  122. {mycli-2.22.0 → mycli-2.24.0}/test/features/environment.py +0 -0
  123. {mycli-2.22.0 → mycli-2.24.0}/test/features/fixture_data/help_commands.txt +0 -0
  124. {mycli-2.22.0 → mycli-2.24.0}/test/features/fixture_utils.py +0 -0
  125. {mycli-2.22.0 → mycli-2.24.0}/test/features/iocommands.feature +0 -0
  126. {mycli-2.22.0 → mycli-2.24.0}/test/features/named_queries.feature +0 -0
  127. {mycli-2.22.0 → mycli-2.24.0}/test/features/specials.feature +0 -0
  128. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/__init__.py +0 -0
  129. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/auto_vertical.py +0 -0
  130. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/basic_commands.py +0 -0
  131. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/connection.py +0 -0
  132. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/crud_database.py +0 -0
  133. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/crud_table.py +0 -0
  134. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/iocommands.py +0 -0
  135. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/named_queries.py +0 -0
  136. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/specials.py +0 -0
  137. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/utils.py +0 -0
  138. {mycli-2.22.0 → mycli-2.24.0}/test/features/steps/wrappers.py +0 -0
  139. {mycli-2.22.0 → mycli-2.24.0}/test/features/wrappager.py +0 -0
  140. {mycli-2.22.0 → mycli-2.24.0}/test/mylogin.cnf +0 -0
  141. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/conftest.py +0 -0
  142. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_app_state.py +0 -0
  143. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_bash_completion.py +0 -0
  144. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_batch_utils.py +0 -0
  145. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_boundary_tunnel.py +0 -0
  146. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_checkup.py +0 -0
  147. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_cli_runner.py +0 -0
  148. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_cli_utils.py +0 -0
  149. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_clibuffer.py +0 -0
  150. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client_commands.py +0 -0
  151. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_client_connection.py +0 -0
  152. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_clistyle.py +0 -0
  153. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_clitoolbar.py +0 -0
  154. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_completer_use_switch.py +0 -0
  155. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_completion_engine.py +0 -0
  156. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_completion_refresher.py +0 -0
  157. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_config.py +0 -0
  158. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_delimitercommand.py +0 -0
  159. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_dsn_aliases.py +0 -0
  160. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_favoritequeries.py +0 -0
  161. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_filepaths.py +0 -0
  162. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_fish_completion.py +0 -0
  163. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_hybrid_redirection.py +0 -0
  164. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_interactive_utils.py +0 -0
  165. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_key_binding_utils.py +0 -0
  166. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_key_bindings.py +0 -0
  167. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_kubectl_tunnel.py +0 -0
  168. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main.py +0 -0
  169. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_batch.py +0 -0
  170. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_execute.py +0 -0
  171. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
  172. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_naive_completion.py +0 -0
  173. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_output.py +0 -0
  174. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_password_sources.py +0 -0
  175. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_polars_completion.py +0 -0
  176. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
  177. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ptoolkit_history.py +0 -0
  178. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ptoolkit_utils.py +0 -0
  179. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_schema_prefetcher.py +0 -0
  180. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
  181. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_dbcommands.py +0 -0
  182. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_init.py +0 -0
  183. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_iocommands.py +0 -0
  184. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_llm.py +0 -0
  185. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_main.py +0 -0
  186. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_source.py +0 -0
  187. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_special_utils.py +0 -0
  188. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sql_utils.py +0 -0
  189. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sqlexecute.py +0 -0
  190. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_sqlresult.py +0 -0
  191. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_ssh_tunnel.py +0 -0
  192. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_string_utils.py +0 -0
  193. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_tabular_output.py +0 -0
  194. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_vault.py +0 -0
  195. {mycli-2.22.0 → mycli-2.24.0}/test/pytests/test_zsh_completion.py +0 -0
  196. {mycli-2.22.0 → mycli-2.24.0}/test/utils.py +0 -0
@@ -304,6 +304,7 @@ jobs:
304
304
  uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
305
305
  with:
306
306
  context: .
307
+ file: docker/Dockerfile
307
308
  push: true
308
309
  tags: ${{ steps.meta.outputs.tags }}
309
310
  labels: ${{ steps.meta.outputs.labels }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.22.0
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 mycli --help
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 mycli --help
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 ["mycli", "--help"]
26
+ CMD ["sh", "-c", "service mariadb start && mycli --database employees"]
@@ -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.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 + 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
- 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.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,3}?'.join(map(re.escape, text))
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) >= 4:
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=75,
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) / 1.5 or item in existing:
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: -frecency_score(item[0], frecency))
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
- candidate_frecency = frecency_score(candidate, frecency) if frecency else 0.0
1882
+ tiebreaker = tiebreaker_key(candidate)
1841
1883
  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)
1884
+ return (0, rank, tiebreaker, 0)
1844
1885
  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)
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.22.0
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 mycli --help
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",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.24.0",
3
+ "distance": 0,
4
+ "node": "gf969367e7367f166722f1dc45f2dc8480156fb25",
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, 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]]] = []