mycli 2.13.1__tar.gz → 2.13.2__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 (189) hide show
  1. {mycli-2.13.1/mycli.egg-info → mycli-2.13.2}/PKG-INFO +3 -2
  2. {mycli-2.13.1 → mycli-2.13.2}/changelog.md +19 -0
  3. {mycli-2.13.1 → mycli-2.13.2}/mycli/client.py +4 -0
  4. {mycli-2.13.1 → mycli-2.13.2}/mycli/client_query.py +1 -0
  5. {mycli-2.13.1 → mycli-2.13.2}/mycli/completion_refresher.py +54 -4
  6. {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/repl.py +8 -2
  7. {mycli-2.13.1 → mycli-2.13.2/mycli.egg-info}/PKG-INFO +3 -2
  8. {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/requires.txt +1 -0
  9. mycli-2.13.2/mycli.egg-info/scm_version.json +8 -0
  10. {mycli-2.13.1 → mycli-2.13.2}/pyproject.toml +3 -2
  11. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client.py +11 -13
  12. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client_query.py +14 -3
  13. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_completion_refresher.py +206 -0
  14. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_repl.py +24 -1
  15. mycli-2.13.1/mycli.egg-info/scm_version.json +0 -8
  16. {mycli-2.13.1 → mycli-2.13.2}/.git-blame-ignore-revs +0 -0
  17. {mycli-2.13.1 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {mycli-2.13.1 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  19. {mycli-2.13.1 → mycli-2.13.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {mycli-2.13.1 → mycli-2.13.2}/.github/dependabot.yml +0 -0
  21. {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/ci.yml +0 -0
  22. {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/lint.yml +0 -0
  23. {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/publish.yml +0 -0
  24. {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/typecheck.yml +0 -0
  25. {mycli-2.13.1 → mycli-2.13.2}/.gitignore +0 -0
  26. {mycli-2.13.1 → mycli-2.13.2}/AGENTS.md +0 -0
  27. {mycli-2.13.1 → mycli-2.13.2}/AUTHORS.rst +0 -0
  28. {mycli-2.13.1 → mycli-2.13.2}/CONTRIBUTING.md +0 -0
  29. {mycli-2.13.1 → mycli-2.13.2}/LICENSE.txt +0 -0
  30. {mycli-2.13.1 → mycli-2.13.2}/MANIFEST.in +0 -0
  31. {mycli-2.13.1 → mycli-2.13.2}/README.md +0 -0
  32. {mycli-2.13.1 → mycli-2.13.2}/SPONSORS.rst +0 -0
  33. {mycli-2.13.1 → mycli-2.13.2}/doc/key_bindings.rst +0 -0
  34. {mycli-2.13.1 → mycli-2.13.2}/doc/llm.md +0 -0
  35. {mycli-2.13.1 → mycli-2.13.2}/doc/screenshots/main.gif +0 -0
  36. {mycli-2.13.1 → mycli-2.13.2}/doc/screenshots/tables.png +0 -0
  37. {mycli-2.13.1 → mycli-2.13.2}/doc/screenshots/total_histogram.png +0 -0
  38. {mycli-2.13.1 → mycli-2.13.2}/doc/transforms.md +0 -0
  39. {mycli-2.13.1 → mycli-2.13.2}/mycli/AUTHORS +0 -0
  40. {mycli-2.13.1 → mycli-2.13.2}/mycli/SPONSORS +0 -0
  41. {mycli-2.13.1 → mycli-2.13.2}/mycli/TIPS +0 -0
  42. {mycli-2.13.1 → mycli-2.13.2}/mycli/__init__.py +0 -0
  43. {mycli-2.13.1 → mycli-2.13.2}/mycli/app_state.py +0 -0
  44. {mycli-2.13.1 → mycli-2.13.2}/mycli/boundary_tunnel.py +0 -0
  45. {mycli-2.13.1 → mycli-2.13.2}/mycli/cli_runner.py +0 -0
  46. {mycli-2.13.1 → mycli-2.13.2}/mycli/clibuffer.py +0 -0
  47. {mycli-2.13.1 → mycli-2.13.2}/mycli/client_commands.py +0 -0
  48. {mycli-2.13.1 → mycli-2.13.2}/mycli/client_connection.py +0 -0
  49. {mycli-2.13.1 → mycli-2.13.2}/mycli/clistyle.py +0 -0
  50. {mycli-2.13.1 → mycli-2.13.2}/mycli/clitoolbar.py +0 -0
  51. {mycli-2.13.1 → mycli-2.13.2}/mycli/compat.py +0 -0
  52. {mycli-2.13.1 → mycli-2.13.2}/mycli/config.py +0 -0
  53. {mycli-2.13.1 → mycli-2.13.2}/mycli/constants.py +0 -0
  54. {mycli-2.13.1 → mycli-2.13.2}/mycli/key_bindings.py +0 -0
  55. {mycli-2.13.1 → mycli-2.13.2}/mycli/lexer.py +0 -0
  56. {mycli-2.13.1 → mycli-2.13.2}/mycli/main.py +0 -0
  57. {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/batch.py +0 -0
  58. {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/checkup.py +0 -0
  59. {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/completions.py +0 -0
  60. {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/execute.py +0 -0
  61. {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/list_dsn.py +0 -0
  62. {mycli-2.13.1 → mycli-2.13.2}/mycli/myclirc +0 -0
  63. {mycli-2.13.1 → mycli-2.13.2}/mycli/output.py +0 -0
  64. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/__init__.py +0 -0
  65. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/batch_utils.py +0 -0
  66. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/cli_utils.py +0 -0
  67. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/completion_engine.py +0 -0
  68. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/filepaths.py +0 -0
  69. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/hybrid_redirection.py +0 -0
  70. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/interactive_utils.py +0 -0
  71. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/key_binding_utils.py +0 -0
  72. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/polars_transform.py +0 -0
  73. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/__init__.py +0 -0
  74. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/fzf.py +0 -0
  75. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/history.py +0 -0
  76. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/utils.py +0 -0
  77. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/__init__.py +0 -0
  78. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/dbcommands.py +0 -0
  79. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/delimitercommand.py +0 -0
  80. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/dsn_aliases.py +0 -0
  81. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/favoritequeries.py +0 -0
  82. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/iocommands.py +0 -0
  83. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/llm.py +0 -0
  84. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/main.py +0 -0
  85. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/utils.py +0 -0
  86. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/sql_utils.py +0 -0
  87. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/sqlresult.py +0 -0
  88. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/string_utils.py +0 -0
  89. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/tabular_output/__init__.py +0 -0
  90. {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/tabular_output/sql_format.py +0 -0
  91. {mycli-2.13.1 → mycli-2.13.2}/mycli/password_sources.py +0 -0
  92. {mycli-2.13.1 → mycli-2.13.2}/mycli/resources/completions/bash/mycli +0 -0
  93. {mycli-2.13.1 → mycli-2.13.2}/mycli/resources/completions/fish/mycli.fish +0 -0
  94. {mycli-2.13.1 → mycli-2.13.2}/mycli/resources/completions/zsh/_mycli +0 -0
  95. {mycli-2.13.1 → mycli-2.13.2}/mycli/schema_prefetcher.py +0 -0
  96. {mycli-2.13.1 → mycli-2.13.2}/mycli/sqlcompleter.py +0 -0
  97. {mycli-2.13.1 → mycli-2.13.2}/mycli/sqlexecute.py +0 -0
  98. {mycli-2.13.1 → mycli-2.13.2}/mycli/ssh_tunnel.py +0 -0
  99. {mycli-2.13.1 → mycli-2.13.2}/mycli/types.py +0 -0
  100. {mycli-2.13.1 → mycli-2.13.2}/mycli/vault.py +0 -0
  101. {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/SOURCES.txt +0 -0
  102. {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/dependency_links.txt +0 -0
  103. {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/entry_points.txt +0 -0
  104. {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/scm_file_list.json +0 -0
  105. {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/top_level.txt +0 -0
  106. {mycli-2.13.1 → mycli-2.13.2}/setup.cfg +0 -0
  107. {mycli-2.13.1 → mycli-2.13.2}/test/__init__.py +0 -0
  108. {mycli-2.13.1 → mycli-2.13.2}/test/features/__init__.py +0 -0
  109. {mycli-2.13.1 → mycli-2.13.2}/test/features/auto_vertical.feature +0 -0
  110. {mycli-2.13.1 → mycli-2.13.2}/test/features/basic_commands.feature +0 -0
  111. {mycli-2.13.1 → mycli-2.13.2}/test/features/connection.feature +0 -0
  112. {mycli-2.13.1 → mycli-2.13.2}/test/features/crud_database.feature +0 -0
  113. {mycli-2.13.1 → mycli-2.13.2}/test/features/crud_table.feature +0 -0
  114. {mycli-2.13.1 → mycli-2.13.2}/test/features/db_utils.py +0 -0
  115. {mycli-2.13.1 → mycli-2.13.2}/test/features/environment.py +0 -0
  116. {mycli-2.13.1 → mycli-2.13.2}/test/features/fixture_data/help_commands.txt +0 -0
  117. {mycli-2.13.1 → mycli-2.13.2}/test/features/fixture_utils.py +0 -0
  118. {mycli-2.13.1 → mycli-2.13.2}/test/features/iocommands.feature +0 -0
  119. {mycli-2.13.1 → mycli-2.13.2}/test/features/named_queries.feature +0 -0
  120. {mycli-2.13.1 → mycli-2.13.2}/test/features/specials.feature +0 -0
  121. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/__init__.py +0 -0
  122. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/auto_vertical.py +0 -0
  123. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/basic_commands.py +0 -0
  124. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/connection.py +0 -0
  125. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/crud_database.py +0 -0
  126. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/crud_table.py +0 -0
  127. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/iocommands.py +0 -0
  128. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/named_queries.py +0 -0
  129. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/specials.py +0 -0
  130. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/utils.py +0 -0
  131. {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/wrappers.py +0 -0
  132. {mycli-2.13.1 → mycli-2.13.2}/test/features/wrappager.py +0 -0
  133. {mycli-2.13.1 → mycli-2.13.2}/test/myclirc +0 -0
  134. {mycli-2.13.1 → mycli-2.13.2}/test/mylogin.cnf +0 -0
  135. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/conftest.py +0 -0
  136. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_app_state.py +0 -0
  137. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_bash_completion.py +0 -0
  138. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_batch_utils.py +0 -0
  139. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_boundary_tunnel.py +0 -0
  140. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_checkup.py +0 -0
  141. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_cli_runner.py +0 -0
  142. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_cli_utils.py +0 -0
  143. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_clibuffer.py +0 -0
  144. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client_commands.py +0 -0
  145. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client_connection.py +0 -0
  146. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_clistyle.py +0 -0
  147. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_clitoolbar.py +0 -0
  148. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_completer_use_switch.py +0 -0
  149. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_completion_engine.py +0 -0
  150. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_config.py +0 -0
  151. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_delimitercommand.py +0 -0
  152. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_dsn_aliases.py +0 -0
  153. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_favoritequeries.py +0 -0
  154. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_filepaths.py +0 -0
  155. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_fish_completion.py +0 -0
  156. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_hybrid_redirection.py +0 -0
  157. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_interactive_utils.py +0 -0
  158. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_key_binding_utils.py +0 -0
  159. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_key_bindings.py +0 -0
  160. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_lexer.py +0 -0
  161. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main.py +0 -0
  162. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_batch.py +0 -0
  163. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_execute.py +0 -0
  164. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_list_dsn.py +0 -0
  165. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_naive_completion.py +0 -0
  166. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_output.py +0 -0
  167. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_password_sources.py +0 -0
  168. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_polars_transform.py +0 -0
  169. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ptoolkit_fzf.py +0 -0
  170. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ptoolkit_history.py +0 -0
  171. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ptoolkit_utils.py +0 -0
  172. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_schema_prefetcher.py +0 -0
  173. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
  174. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_dbcommands.py +0 -0
  175. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_init.py +0 -0
  176. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_iocommands.py +0 -0
  177. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_llm.py +0 -0
  178. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_main.py +0 -0
  179. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_utils.py +0 -0
  180. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sql_utils.py +0 -0
  181. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sqlcompleter.py +0 -0
  182. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sqlexecute.py +0 -0
  183. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sqlresult.py +0 -0
  184. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ssh_tunnel.py +0 -0
  185. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_string_utils.py +0 -0
  186. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_tabular_output.py +0 -0
  187. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_vault.py +0 -0
  188. {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_zsh_completion.py +0 -0
  189. {mycli-2.13.1 → mycli-2.13.2}/test/utils.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.13.1
4
- Summary: Rich terminal client for MySQL with autocompletion, syntax highlighting, and dataframes
3
+ Version: 2.13.2
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
7
7
  Project-URL: Homepage, https://mycli.net
@@ -51,6 +51,7 @@ Provides-Extra: llm
51
51
  Requires-Dist: llm~=0.31.1; extra == "llm"
52
52
  Requires-Dist: setuptools==83.*; extra == "llm"
53
53
  Requires-Dist: pip~=26.1.2; extra == "llm"
54
+ Requires-Dist: openai~=2.48.0; extra == "llm"
54
55
  Provides-Extra: dataframe
55
56
  Requires-Dist: polars~=1.42.1; extra == "dataframe"
56
57
  Requires-Dist: altair~=6.2.2; extra == "dataframe"
@@ -1,3 +1,22 @@
1
+ 2.13.2 (2026/08/12)
2
+ ==============
3
+
4
+ Bugfixes
5
+ ---------
6
+ * Avoid an exception when exiting while completions are being refreshed.
7
+ * Avoid a completions exception when changing to a nonexistent database.
8
+
9
+
10
+ Documentation
11
+ ---------
12
+ * Shorten/clarify `pyproject.toml` project description.
13
+
14
+
15
+ Internal
16
+ ---------
17
+ * Fix CI by pinning the `openai` library to v2.48.0.
18
+
19
+
1
20
  2.13.1 (2026/08/11)
2
21
  ==============
3
22
 
@@ -241,6 +241,10 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
241
241
  self.prompt_session.app.invalidate()
242
242
 
243
243
  def close(self) -> None:
244
+ try:
245
+ self.completion_refresher.stop()
246
+ except Exception:
247
+ pass
244
248
  try:
245
249
  self.schema_prefetcher.stop()
246
250
  except Exception:
@@ -40,6 +40,7 @@ class ClientQueryMixin:
40
40
 
41
41
  assert self.sqlexecute is not None
42
42
  if reset:
43
+ self.completion_refresher.stop()
43
44
  # Update the active completer's current-schema pointer right
44
45
  # away so unqualified completions reflect a schema switch
45
46
  # even before the background refresh finishes.
@@ -3,6 +3,7 @@ from time import monotonic
3
3
  from typing import Callable
4
4
 
5
5
  import pymysql
6
+ from pymysql.constants.ER import BAD_DB_ERROR
6
7
 
7
8
  from mycli.packages.special.main import COMMANDS
8
9
  from mycli.packages.sqlresult import SQLResult
@@ -18,6 +19,9 @@ class CompletionRefresher:
18
19
  def __init__(self, invalidate_app: Callable[[], None] | None = None) -> None:
19
20
  self._completer_thread: threading.Thread | None = None
20
21
  self._restart_refresh = threading.Event()
22
+ self._stop_refresh = threading.Event()
23
+ self._executor_lock = threading.Lock()
24
+ self._active_executor: SQLExecute | None = None
21
25
  self._refresh_visible_until = 0.0
22
26
  self._visibility_timer: threading.Timer | None = None
23
27
  self._invalidate_app = invalidate_app
@@ -49,6 +53,7 @@ class CompletionRefresher:
49
53
  if self._visibility_timer is not None:
50
54
  self._visibility_timer.cancel()
51
55
  self._visibility_timer = None
56
+ self._stop_refresh.clear()
52
57
  self._refresh_visible_until = monotonic() + MIN_COMPLETION_REFRESH_MESSAGE_SECONDS
53
58
  self._completer_thread = threading.Thread(
54
59
  target=self._bg_refresh, args=(executor, callbacks, completer_options), name="completion_refresh"
@@ -63,6 +68,29 @@ class CompletionRefresher:
63
68
  def _thread_is_alive(self) -> bool:
64
69
  return bool(self._completer_thread and self._completer_thread.is_alive())
65
70
 
71
+ def stop(self) -> None:
72
+ """Stop and wait for an in-flight completion refresh."""
73
+ self._stop_refresh.set()
74
+ self._restart_refresh.clear()
75
+ self._refresh_visible_until = 0.0
76
+ if self._visibility_timer is not None:
77
+ self._visibility_timer.cancel()
78
+ self._visibility_timer = None
79
+
80
+ with self._executor_lock:
81
+ executor = self._active_executor
82
+ if executor is not None:
83
+ try:
84
+ executor.close()
85
+ except Exception:
86
+ pass
87
+
88
+ thread = self._completer_thread
89
+ if thread is not None and thread.is_alive() and thread is not threading.current_thread():
90
+ thread.join()
91
+ if thread is not None and not thread.is_alive():
92
+ self._completer_thread = None
93
+
66
94
  def _bg_refresh(
67
95
  self,
68
96
  sqlexecute: SQLExecute,
@@ -89,7 +117,12 @@ class CompletionRefresher:
89
117
  self._finish_refreshing()
90
118
  return
91
119
 
120
+ with self._executor_lock:
121
+ self._active_executor = executor
92
122
  try:
123
+ if self._stop_refresh.is_set():
124
+ return
125
+
93
126
  # If callbacks is a single function then push it into a list.
94
127
  if callable(callbacks):
95
128
  callbacks = [callbacks]
@@ -97,6 +130,8 @@ class CompletionRefresher:
97
130
  while 1:
98
131
  for refresher in self.refreshers.values():
99
132
  refresher(completer, executor)
133
+ if self._stop_refresh.is_set():
134
+ return
100
135
  if self._restart_refresh.is_set():
101
136
  self._restart_refresh.clear()
102
137
  break
@@ -109,11 +144,26 @@ class CompletionRefresher:
109
144
  # break statement.
110
145
  continue
111
146
 
112
- for callback in callbacks:
113
- callback(completer)
147
+ if not self._stop_refresh.is_set():
148
+ for callback in callbacks:
149
+ callback(completer)
150
+ except pymysql.err.OperationalError as error:
151
+ if not self._stop_refresh.is_set() and error.args[0] != BAD_DB_ERROR:
152
+ raise
153
+ except Exception:
154
+ if not self._stop_refresh.is_set():
155
+ raise
114
156
  finally:
115
- executor.close()
116
- self._finish_refreshing()
157
+ with self._executor_lock:
158
+ if self._active_executor is executor:
159
+ self._active_executor = None
160
+ try:
161
+ executor.close()
162
+ except Exception:
163
+ if not self._stop_refresh.is_set():
164
+ raise
165
+ finally:
166
+ self._finish_refreshing()
117
167
 
118
168
  def _finish_refreshing(self) -> None:
119
169
  self._invalidate()
@@ -742,6 +742,10 @@ def _one_iteration(
742
742
  mycli.echo('Wise choice!')
743
743
  return
744
744
 
745
+ dropping_active_database = is_dropping_database(text, sqlexecute.dbname)
746
+ if dropping_active_database:
747
+ mycli.completion_refresher.stop()
748
+
745
749
  successful = False
746
750
  try:
747
751
  mycli.logger.debug('sql: %r', text)
@@ -870,13 +874,15 @@ def _one_iteration(
870
874
  fg='yellow',
871
875
  )
872
876
 
873
- if is_dropping_database(text, sqlexecute.dbname):
877
+ if dropping_active_database:
874
878
  sqlexecute.dbname = None
875
879
  sqlexecute.connect()
876
880
 
877
881
  if need_completion_refresh(text):
878
- mycli.refresh_completions(reset=need_completion_reset(text))
882
+ mycli.refresh_completions(reset=dropping_active_database or need_completion_reset(text))
879
883
  finally:
884
+ if dropping_active_database and not successful:
885
+ mycli.refresh_completions()
880
886
  if mycli.logfile is False:
881
887
  mycli.echo('Warning: This query was not logged.', err=True, fg='red')
882
888
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.13.1
4
- Summary: Rich terminal client for MySQL with autocompletion, syntax highlighting, and dataframes
3
+ Version: 2.13.2
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
7
7
  Project-URL: Homepage, https://mycli.net
@@ -51,6 +51,7 @@ Provides-Extra: llm
51
51
  Requires-Dist: llm~=0.31.1; extra == "llm"
52
52
  Requires-Dist: setuptools==83.*; extra == "llm"
53
53
  Requires-Dist: pip~=26.1.2; extra == "llm"
54
+ Requires-Dist: openai~=2.48.0; extra == "llm"
54
55
  Provides-Extra: dataframe
55
56
  Requires-Dist: polars~=1.42.1; extra == "dataframe"
56
57
  Requires-Dist: altair~=6.2.2; extra == "dataframe"
@@ -42,3 +42,4 @@ ruff~=0.15.0
42
42
  llm~=0.31.1
43
43
  setuptools==83.*
44
44
  pip~=26.1.2
45
+ openai~=2.48.0
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.13.2",
3
+ "distance": 0,
4
+ "node": "g8da77508588513a62fa7ec1811a17f0c6ae14e86",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-12"
8
+ }
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "mycli"
3
3
  dynamic = ["version"]
4
- description = "Rich terminal client for MySQL with autocompletion, syntax highlighting, and dataframes"
4
+ description = "Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  license = "BSD-3-Clause"
@@ -67,6 +67,7 @@ llm = [
67
67
  "llm ~= 0.31.1",
68
68
  "setuptools == 83.*", # Required by llm commands to install models
69
69
  "pip ~= 26.1.2",
70
+ "openai ~= 2.48.0", # openai v3.0.0 broke CI and would need work to support
70
71
  ]
71
72
  dataframe = [
72
73
  "polars ~= 1.42.1",
@@ -100,7 +101,7 @@ include = ["mycli*"]
100
101
 
101
102
  [tool.uv]
102
103
  exclude-newer = '1 week'
103
- exclude-newer-package = { cli_helpers = false, openai = false, cryptography = false }
104
+ exclude-newer-package = { cli_helpers = false, cryptography = false }
104
105
 
105
106
  [tool.ruff]
106
107
  target-version = 'py310'
@@ -391,23 +391,18 @@ def test_init_reads_mylogin_cnf(monkeypatch: pytest.MonkeyPatch, tmp_path: Path)
391
391
  assert cli.mylogin_cnf == {'client': 'config'}
392
392
 
393
393
 
394
- def test_close_stops_schema_prefetcher_and_closes_sqlexecute() -> None:
394
+ def test_close_stops_refreshers_before_closing_connection_and_tunnels() -> None:
395
395
  cli = MyCli.__new__(MyCli)
396
- stopped: list[bool] = []
397
- closed: list[bool] = []
398
- ssh_tunnel_closed: list[bool] = []
399
- boundary_tunnel_closed: list[bool] = []
400
- cli.schema_prefetcher = SimpleNamespace(stop=lambda: stopped.append(True))
401
- cli.sqlexecute = SimpleNamespace(close=lambda: closed.append(True)) # type: ignore[assignment]
402
- cast(Any, cli).ssh_tunnel = SimpleNamespace(close=lambda: ssh_tunnel_closed.append(True))
403
- cli.boundary_tunnel = SimpleNamespace(close=lambda: boundary_tunnel_closed.append(True)) # type: ignore[assignment]
396
+ calls: list[str] = []
397
+ cli.completion_refresher = SimpleNamespace(stop=lambda: calls.append('completion'))
398
+ cli.schema_prefetcher = SimpleNamespace(stop=lambda: calls.append('prefetch'))
399
+ cli.sqlexecute = SimpleNamespace(close=lambda: calls.append('connection')) # type: ignore[assignment]
400
+ cast(Any, cli).ssh_tunnel = SimpleNamespace(close=lambda: calls.append('ssh'))
401
+ cli.boundary_tunnel = SimpleNamespace(close=lambda: calls.append('boundary')) # type: ignore[assignment]
404
402
 
405
403
  MyCli.close(cli)
406
404
 
407
- assert stopped == [True]
408
- assert closed == [True]
409
- assert ssh_tunnel_closed == [True]
410
- assert boundary_tunnel_closed == [True]
405
+ assert calls == ['completion', 'prefetch', 'connection', 'ssh', 'boundary']
411
406
 
412
407
 
413
408
  def test_close_swallows_cleanup_errors() -> None:
@@ -416,6 +411,7 @@ def test_close_swallows_cleanup_errors() -> None:
416
411
  def fail() -> None:
417
412
  raise RuntimeError('cleanup failed')
418
413
 
414
+ cli.completion_refresher = SimpleNamespace(stop=fail)
419
415
  cli.schema_prefetcher = SimpleNamespace(stop=fail)
420
416
  cli.sqlexecute = SimpleNamespace(close=fail) # type: ignore[assignment]
421
417
  cast(Any, cli).ssh_tunnel = SimpleNamespace(close=fail)
@@ -425,6 +421,8 @@ def test_close_swallows_cleanup_errors() -> None:
425
421
 
426
422
  def test_close_swallows_boundary_tunnel_close_error() -> None:
427
423
  cli = MyCli.__new__(MyCli)
424
+ cli.completion_refresher = SimpleNamespace(stop=lambda: None)
425
+ cli.schema_prefetcher = SimpleNamespace(stop=lambda: None)
428
426
  cli.sqlexecute = None
429
427
  tunnel_closed: list[bool] = []
430
428
  cast(Any, cli).ssh_tunnel = SimpleNamespace(close=lambda: tunnel_closed.append(True))
@@ -15,6 +15,7 @@ def make_refresh_cli() -> tuple[Any, dict[str, Any]]:
15
15
  cli = make_bare_mycli()
16
16
  state: dict[str, Any] = {
17
17
  'stopped': [],
18
+ 'completion_stopped': [],
18
19
  'refresh_calls': [],
19
20
  'set_dbname_calls': [],
20
21
  }
@@ -30,7 +31,8 @@ def make_refresh_cli() -> tuple[Any, dict[str, Any]]:
30
31
  )
31
32
  cli.main_formatter = SimpleNamespace(supported_formats=['ascii', 'csv'])
32
33
  cli.completion_refresher = SimpleNamespace(
33
- refresh=lambda executor, callbacks, options: state['refresh_calls'].append((executor, callbacks, options))
34
+ stop=lambda: state['completion_stopped'].append(True),
35
+ refresh=lambda executor, callbacks, options: state['refresh_calls'].append((executor, callbacks, options)),
34
36
  )
35
37
  cli.smart_completion = True
36
38
  state['callback'] = callback
@@ -79,6 +81,15 @@ def test_refresh_completions_does_not_update_dbname_without_reset() -> None:
79
81
  main.MyCli.refresh_completions(cli)
80
82
 
81
83
  assert state['set_dbname_calls'] == []
84
+ assert state['completion_stopped'] == []
85
+
86
+
87
+ def test_refresh_completions_stops_completion_worker_when_reset() -> None:
88
+ cli, state = make_refresh_cli()
89
+
90
+ main.MyCli.refresh_completions(cli, reset=True)
91
+
92
+ assert state['completion_stopped'] == [True]
82
93
 
83
94
 
84
95
  def test_refresh_completions_updates_dbname_when_reset() -> None:
@@ -93,7 +104,7 @@ def test_refresh_completions_updates_dbname_when_reset() -> None:
93
104
  set_dbname=lambda dbname: set_dbname_calls.append(dbname),
94
105
  )
95
106
  cli.main_formatter = SimpleNamespace(supported_formats=['table'])
96
- cli.completion_refresher = SimpleNamespace(refresh=lambda executor, callbacks, options: None)
107
+ cli.completion_refresher = SimpleNamespace(stop=lambda: None, refresh=lambda executor, callbacks, options: None)
97
108
 
98
109
  main.MyCli.refresh_completions(cli, reset=True)
99
110
 
@@ -113,7 +124,7 @@ def test_refresh_completions_uses_lock_when_reset() -> None:
113
124
  set_dbname=lambda dbname: None,
114
125
  )
115
126
  cli.main_formatter = SimpleNamespace(supported_formats=['table'])
116
- cli.completion_refresher = SimpleNamespace(refresh=lambda executor, callbacks, options: None)
127
+ cli.completion_refresher = SimpleNamespace(stop=lambda: None, refresh=lambda executor, callbacks, options: None)
117
128
 
118
129
  main.MyCli.refresh_completions(cli, reset=True)
119
130
 
@@ -256,6 +256,108 @@ def test_refresh_cancels_pending_visibility_timer(monkeypatch, refresher) -> Non
256
256
  assert refresher._visibility_timer is None
257
257
 
258
258
 
259
+ def test_stop_interrupts_and_joins_active_refresh(monkeypatch, refresher) -> None:
260
+ refresh_started = completion_refresher.threading.Event()
261
+ executor_closed = completion_refresher.threading.Event()
262
+ callback = Mock()
263
+ timer = Mock()
264
+
265
+ class FakeCompleter:
266
+ def __init__(self, **options) -> None:
267
+ pass
268
+
269
+ class FakeExecutor:
270
+ def __init__(self, *args) -> None:
271
+ pass
272
+
273
+ def close(self) -> None:
274
+ executor_closed.set()
275
+
276
+ def blocking_refresh(completer, executor) -> None:
277
+ refresh_started.set()
278
+ assert executor_closed.wait(timeout=1)
279
+ raise completion_refresher.pymysql.err.OperationalError(2013, 'connection closed')
280
+
281
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', FakeCompleter)
282
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', FakeExecutor)
283
+ refresher.refreshers = {'blocking': blocking_refresh}
284
+
285
+ refresher.refresh(make_sqlexecute(), callback)
286
+ assert refresh_started.wait(timeout=1)
287
+ refresher._visibility_timer = timer
288
+
289
+ refresher.stop()
290
+
291
+ assert executor_closed.is_set()
292
+ assert refresher._completer_thread is None
293
+ assert refresher._active_executor is None
294
+ assert refresher._restart_refresh.is_set() is False
295
+ assert refresher.is_refreshing() is False
296
+ timer.cancel.assert_called_once_with()
297
+ callback.assert_not_called()
298
+
299
+
300
+ def test_stop_before_executor_is_ready_prevents_refresh_and_callback(monkeypatch, refresher) -> None:
301
+ constructor_started = completion_refresher.threading.Event()
302
+ release_constructor = completion_refresher.threading.Event()
303
+ stop_finished = completion_refresher.threading.Event()
304
+ refresh = Mock()
305
+ callback = Mock()
306
+
307
+ class FakeCompleter:
308
+ def __init__(self, **options) -> None:
309
+ pass
310
+
311
+ class FakeExecutor:
312
+ def __init__(self, *args) -> None:
313
+ constructor_started.set()
314
+ assert release_constructor.wait(timeout=1)
315
+
316
+ def close(self) -> None:
317
+ pass
318
+
319
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', FakeCompleter)
320
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', FakeExecutor)
321
+ refresher.refreshers = {'refresh': refresh}
322
+
323
+ refresher.refresh(make_sqlexecute(), callback)
324
+ assert constructor_started.wait(timeout=1)
325
+ stop_thread = completion_refresher.threading.Thread(target=lambda: (refresher.stop(), stop_finished.set()))
326
+ stop_thread.start()
327
+ assert refresher._stop_refresh.wait(timeout=1)
328
+ assert stop_finished.is_set() is False
329
+ release_constructor.set()
330
+ stop_thread.join(timeout=1)
331
+
332
+ assert stop_finished.is_set()
333
+ refresh.assert_not_called()
334
+ callback.assert_not_called()
335
+ assert refresher._completer_thread is None
336
+
337
+
338
+ def test_stop_tolerates_executor_close_error_without_worker(refresher) -> None:
339
+ executor = Mock()
340
+ executor.close.side_effect = RuntimeError('close failed')
341
+ refresher._active_executor = executor
342
+
343
+ refresher.stop()
344
+
345
+ executor.close.assert_called_once_with()
346
+ assert refresher._completer_thread is None
347
+
348
+
349
+ def test_stop_does_not_join_current_worker(monkeypatch, refresher) -> None:
350
+ thread = Mock()
351
+ thread.is_alive.return_value = True
352
+ refresher._completer_thread = thread
353
+ monkeypatch.setattr(completion_refresher.threading, 'current_thread', lambda: thread)
354
+
355
+ refresher.stop()
356
+
357
+ thread.join.assert_not_called()
358
+ assert refresher._completer_thread is thread
359
+
360
+
259
361
  def test_finish_refreshing_schedules_delayed_invalidation_before_deadline(monkeypatch, refresher) -> None:
260
362
  now = 10.0
261
363
  monkeypatch.setattr(completion_refresher, 'monotonic', lambda: now)
@@ -474,6 +576,110 @@ def test_bg_refresh_returns_when_executor_connection_fails(monkeypatch, refreshe
474
576
  callback.assert_not_called()
475
577
 
476
578
 
579
+ def test_bg_refresh_stops_after_current_refresher(monkeypatch, refresher) -> None:
580
+ callback = Mock()
581
+ executor = Mock()
582
+
583
+ def stop_refresh(completer, active_executor) -> None:
584
+ refresher._stop_refresh.set()
585
+
586
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
587
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
588
+ refresher.refreshers = {'stop': stop_refresh}
589
+
590
+ refresher._bg_refresh(make_sqlexecute(), callback, {})
591
+
592
+ callback.assert_not_called()
593
+ executor.close.assert_called_once_with()
594
+
595
+
596
+ def test_bg_refresh_suppresses_non_operational_error_during_stop(monkeypatch, refresher) -> None:
597
+ executor = Mock()
598
+
599
+ def stop_with_error(completer, active_executor) -> None:
600
+ refresher._stop_refresh.set()
601
+ raise RuntimeError('cancelled refresh')
602
+
603
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
604
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
605
+ refresher.refreshers = {'stop': stop_with_error}
606
+
607
+ refresher._bg_refresh(make_sqlexecute(), Mock(), {})
608
+
609
+ executor.close.assert_called_once_with()
610
+
611
+
612
+ def test_bg_refresh_skips_callbacks_when_stopped_after_refresh(monkeypatch, refresher) -> None:
613
+ callback = Mock()
614
+ executor = Mock()
615
+ is_stopped = Mock(side_effect=[False, True])
616
+ monkeypatch.setattr(refresher._stop_refresh, 'is_set', is_stopped)
617
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
618
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
619
+ refresher.refreshers = {}
620
+
621
+ refresher._bg_refresh(make_sqlexecute(), callback, {})
622
+
623
+ callback.assert_not_called()
624
+ assert is_stopped.call_count == 2
625
+
626
+
627
+ def test_bg_refresh_propagates_unexpected_refresher_error(monkeypatch, refresher) -> None:
628
+ executor = Mock()
629
+
630
+ def fail_refresh(completer, active_executor) -> None:
631
+ refresher._active_executor = None
632
+ raise RuntimeError('refresh failed')
633
+
634
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
635
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
636
+ refresher.refreshers = {'fail': fail_refresh}
637
+
638
+ with pytest.raises(RuntimeError, match='refresh failed'):
639
+ refresher._bg_refresh(make_sqlexecute(), Mock(), {})
640
+
641
+ executor.close.assert_called_once_with()
642
+
643
+
644
+ @pytest.mark.parametrize('error_code', [completion_refresher.BAD_DB_ERROR, 2003])
645
+ def test_bg_refresh_only_suppresses_stale_database_error(monkeypatch, refresher, error_code) -> None:
646
+ executor = Mock()
647
+ callback = Mock()
648
+
649
+ def fail_refresh(completer, active_executor) -> None:
650
+ raise completion_refresher.pymysql.err.OperationalError(error_code, 'metadata failed')
651
+
652
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
653
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
654
+ refresher.refreshers = {'fail': fail_refresh}
655
+
656
+ if error_code == completion_refresher.BAD_DB_ERROR:
657
+ refresher._bg_refresh(make_sqlexecute(), callback, {})
658
+ else:
659
+ with pytest.raises(completion_refresher.pymysql.err.OperationalError, match='metadata failed'):
660
+ refresher._bg_refresh(make_sqlexecute(), callback, {})
661
+
662
+ callback.assert_not_called()
663
+ executor.close.assert_called_once_with()
664
+
665
+
666
+ @pytest.mark.parametrize('stopping', [False, True])
667
+ def test_bg_refresh_only_suppresses_executor_close_error_when_stopping(monkeypatch, refresher, stopping) -> None:
668
+ executor = Mock()
669
+ executor.close.side_effect = RuntimeError('close failed')
670
+ monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
671
+ monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
672
+ refresher.refreshers = {}
673
+ if stopping:
674
+ refresher._stop_refresh.set()
675
+
676
+ if stopping:
677
+ refresher._bg_refresh(make_sqlexecute(), Mock(), {})
678
+ else:
679
+ with pytest.raises(RuntimeError, match='close failed'):
680
+ refresher._bg_refresh(make_sqlexecute(), Mock(), {})
681
+
682
+
477
683
  def test_refresher_decorator_registers_function() -> None:
478
684
  refreshers: dict[str, object] = {}
479
685
 
@@ -193,6 +193,7 @@ def make_repl_cli(sqlexecute: Any | None = None) -> Any:
193
193
  timing_calls: list[tuple[str, bool]] = []
194
194
  log_queries: list[str] = []
195
195
  cli.refresh_calls = refresh_calls
196
+ cli.completion_stop_calls = []
196
197
  cli.output_calls = output_calls
197
198
  cli.echo_calls = echo_calls
198
199
  cli.timing_calls = timing_calls
@@ -208,6 +209,7 @@ def make_repl_cli(sqlexecute: Any | None = None) -> Any:
208
209
  return [SQLResult(status='refresh')]
209
210
 
210
211
  cli.refresh_completions = refresh_completions
212
+ cli.completion_refresher = SimpleNamespace(stop=lambda: cli.completion_stop_calls.append(True))
211
213
 
212
214
  def output_timing(timing: str, is_warnings_style: bool = False) -> None:
213
215
  cli.timing_calls.append((timing, is_warnings_style))
@@ -1162,6 +1164,7 @@ def test_one_iteration_covers_redirect_destructive_success_refresh_and_logfile(m
1162
1164
 
1163
1165
  sqlexecute = FakeSQLExecute()
1164
1166
  cli = make_repl_cli(sqlexecute)
1167
+ cli.completion_refresher = SimpleNamespace(stop=lambda: sqlexecute.calls.append('stop'))
1165
1168
  cli.logfile = False
1166
1169
  cli.destructive_warning = True
1167
1170
  monkeypatch.setattr(repl_mode, 'is_redirect_command', lambda text: text == 'redirect')
@@ -1185,7 +1188,7 @@ def test_one_iteration_covers_redirect_destructive_success_refresh_and_logfile(m
1185
1188
  assert cli.query_history[-1].successful is True
1186
1189
  assert cli.query_history[-1].mutating is True
1187
1190
  assert sqlexecute.dbname is None
1188
- assert sqlexecute.calls == ['dropdb', 'connect']
1191
+ assert sqlexecute.calls == ['stop', 'dropdb', 'connect']
1189
1192
  assert 'Warning: This query was not logged.' in cli.echo_calls
1190
1193
 
1191
1194
  repl_mode._one_iteration(cli, repl_mode.ReplState(), 'approved')
@@ -1195,6 +1198,26 @@ def test_one_iteration_covers_redirect_destructive_success_refresh_and_logfile(m
1195
1198
  assert 'Wise choice!' in cli.echo_calls
1196
1199
 
1197
1200
 
1201
+ def test_one_iteration_restarts_completions_when_active_database_drop_fails(monkeypatch: pytest.MonkeyPatch) -> None:
1202
+ patch_repl_runtime_defaults(monkeypatch)
1203
+
1204
+ class FakeSQLExecute:
1205
+ dbname = 'db'
1206
+ connection_id = 0
1207
+
1208
+ def run(self, text: str) -> Iterator[SQLResult]:
1209
+ raise pymysql.OperationalError(1064, 'drop failed')
1210
+
1211
+ cli = make_repl_cli(FakeSQLExecute())
1212
+ monkeypatch.setattr(repl_mode, 'is_dropping_database', lambda text, dbname: True)
1213
+
1214
+ repl_mode._one_iteration(cli, repl_mode.ReplState(), 'drop database db')
1215
+
1216
+ assert cli.completion_stop_calls == [True]
1217
+ assert cli.refresh_calls == [False]
1218
+ assert cli.sqlexecute.dbname == 'db'
1219
+
1220
+
1198
1221
  @pytest.mark.parametrize(
1199
1222
  ('terminator', 'setter_name'),
1200
1223
  [(r'\x', 'set_explorer_output'), (r'\G', 'set_expanded_output')],
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.13.1",
3
- "distance": 0,
4
- "node": "gcd0aab2489d312967f74f213ed69582aeff79488",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-11"
8
- }
File without changes
File without changes
File without changes
File without changes