mycli 2.30.4__tar.gz → 2.31.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 (232) hide show
  1. {mycli-2.30.4/mycli.egg-info → mycli-2.31.0}/PKG-INFO +1 -1
  2. {mycli-2.30.4 → mycli-2.31.0}/changelog.md +18 -0
  3. {mycli-2.30.4 → mycli-2.31.0}/docker/resources/test_db-1.0.7.tar.gz +1 -1
  4. {mycli-2.30.4 → mycli-2.31.0}/mycli/AUTHORS.txt +3 -0
  5. {mycli-2.30.4 → mycli-2.31.0}/mycli/client.py +1 -1
  6. {mycli-2.30.4 → mycli-2.31.0}/mycli/client_commands.py +1 -1
  7. {mycli-2.30.4 → mycli-2.31.0}/mycli/client_query.py +1 -1
  8. {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/batch.py +12 -4
  9. {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/repl.py +3 -1
  10. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/io_commands.py +4 -4
  11. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/special_command_utils.py +3 -1
  12. mycli-2.31.0/mycli/packages/utils/batch_utils.py +71 -0
  13. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/string_utils.py +11 -0
  14. {mycli-2.30.4 → mycli-2.31.0/mycli.egg-info}/PKG-INFO +1 -1
  15. mycli-2.31.0/mycli.egg-info/scm_version.json +8 -0
  16. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_batch_utils.py +29 -0
  17. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client.py +1 -0
  18. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client_commands.py +33 -6
  19. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client_query.py +8 -2
  20. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_batch.py +47 -7
  21. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_repl.py +22 -0
  22. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_io_commands.py +40 -0
  23. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_utils.py +18 -0
  24. mycli-2.31.0/mycli_test/pytests/test_string_utils.py +44 -0
  25. mycli-2.30.4/mycli/packages/utils/batch_utils.py +0 -36
  26. mycli-2.30.4/mycli.egg-info/scm_version.json +0 -8
  27. mycli-2.30.4/mycli_test/pytests/test_string_utils.py +0 -27
  28. {mycli-2.30.4 → mycli-2.31.0}/.git-blame-ignore-revs +0 -0
  29. {mycli-2.30.4 → mycli-2.31.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {mycli-2.30.4 → mycli-2.31.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  31. {mycli-2.30.4 → mycli-2.31.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {mycli-2.30.4 → mycli-2.31.0}/.github/actions/lint/action.yml +0 -0
  33. {mycli-2.30.4 → mycli-2.31.0}/.github/actions/typecheck/action.yml +0 -0
  34. {mycli-2.30.4 → mycli-2.31.0}/.github/dependabot.yml +0 -0
  35. {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/ci.yml +0 -0
  36. {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/lint.yml +0 -0
  37. {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/publish.yml +0 -0
  38. {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/typecheck.yml +0 -0
  39. {mycli-2.30.4 → mycli-2.31.0}/.gitignore +0 -0
  40. {mycli-2.30.4 → mycli-2.31.0}/AGENTS.md +0 -0
  41. {mycli-2.30.4 → mycli-2.31.0}/AUTHORS.md +0 -0
  42. {mycli-2.30.4 → mycli-2.31.0}/CONTRIBUTING.md +0 -0
  43. {mycli-2.30.4 → mycli-2.31.0}/LICENSE.txt +0 -0
  44. {mycli-2.30.4 → mycli-2.31.0}/MANIFEST.in +0 -0
  45. {mycli-2.30.4 → mycli-2.31.0}/README.md +0 -0
  46. {mycli-2.30.4 → mycli-2.31.0}/SPONSORS.md +0 -0
  47. {mycli-2.30.4 → mycli-2.31.0}/doc/cookbook.md +0 -0
  48. {mycli-2.30.4 → mycli-2.31.0}/doc/key_bindings.md +0 -0
  49. {mycli-2.30.4 → mycli-2.31.0}/doc/known_bugs.md +0 -0
  50. {mycli-2.30.4 → mycli-2.31.0}/doc/llm.md +0 -0
  51. {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/main.gif +0 -0
  52. {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_describe.png +0 -0
  53. {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_group_by.png +0 -0
  54. {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_histogram.png +0 -0
  55. {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_pivot.png +0 -0
  56. {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/tables.png +0 -0
  57. {mycli-2.30.4 → mycli-2.31.0}/doc/transforms.md +0 -0
  58. {mycli-2.30.4 → mycli-2.31.0}/docker/Dockerfile +0 -0
  59. {mycli-2.30.4 → mycli-2.31.0}/docker/resources/dot_mycli_history.txt +0 -0
  60. {mycli-2.30.4 → mycli-2.31.0}/docker/resources/test_db_license.md +0 -0
  61. {mycli-2.30.4 → mycli-2.31.0}/mycli/SPONSORS.txt +0 -0
  62. {mycli-2.30.4 → mycli-2.31.0}/mycli/TIPS.txt +0 -0
  63. {mycli-2.30.4 → mycli-2.31.0}/mycli/__init__.py +0 -0
  64. {mycli-2.30.4 → mycli-2.31.0}/mycli/app_state.py +0 -0
  65. {mycli-2.30.4 → mycli-2.31.0}/mycli/cli_runner.py +0 -0
  66. {mycli-2.30.4 → mycli-2.31.0}/mycli/client_connection.py +0 -0
  67. {mycli-2.30.4 → mycli-2.31.0}/mycli/compat.py +0 -0
  68. {mycli-2.30.4 → mycli-2.31.0}/mycli/config.py +0 -0
  69. {mycli-2.30.4 → mycli-2.31.0}/mycli/constants.py +0 -0
  70. {mycli-2.30.4 → mycli-2.31.0}/mycli/main.py +0 -0
  71. {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/checkup.py +0 -0
  72. {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/completions.py +0 -0
  73. {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/execute.py +0 -0
  74. {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/list_dsn.py +0 -0
  75. {mycli-2.30.4 → mycli-2.31.0}/mycli/myclirc +0 -0
  76. {mycli-2.30.4 → mycli-2.31.0}/mycli/output.py +0 -0
  77. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/__init__.py +0 -0
  78. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/__init__.py +0 -0
  79. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/completion_engine.py +0 -0
  80. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/completion_refresher.py +0 -0
  81. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/schema_prefetcher.py +0 -0
  82. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/sql_completer.py +0 -0
  83. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/credentials/__init__.py +0 -0
  84. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/credentials/password_sources.py +0 -0
  85. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/dataframes/__init__.py +0 -0
  86. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/dataframes/completion.py +0 -0
  87. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/dataframes/transform.py +0 -0
  88. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/execution/__init__.py +0 -0
  89. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/execution/background_runner.py +0 -0
  90. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/execution/sql_execute.py +0 -0
  91. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/__init__.py +0 -0
  92. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/boundary/__init__.py +0 -0
  93. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/boundary/boundary_tunnel.py +0 -0
  94. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/fzf/__init__.py +0 -0
  95. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/fzf/fzf.py +0 -0
  96. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/kubernetes/__init__.py +0 -0
  97. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/kubernetes/kubectl_tunnel.py +0 -0
  98. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/macos/__init__.py +0 -0
  99. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/macos/macos_keychain.py +0 -0
  100. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/openssh/__init__.py +0 -0
  101. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/openssh/ssh_tunnel.py +0 -0
  102. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/vault/__init__.py +0 -0
  103. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/vault/vault_credentials.py +0 -0
  104. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/__init__.py +0 -0
  105. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/fzf_history.py +0 -0
  106. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/history.py +0 -0
  107. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/key_bindings.py +0 -0
  108. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/multiline.py +0 -0
  109. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/style.py +0 -0
  110. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/toolbar.py +0 -0
  111. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/utils.py +0 -0
  112. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/pygments/__init__.py +0 -0
  113. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/pygments/mycli_lexer.py +0 -0
  114. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/redirection/__init__.py +0 -0
  115. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/redirection/hybrid_redirection.py +0 -0
  116. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/__init__.py +0 -0
  117. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/db_commands.py +0 -0
  118. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/delimiter_command.py +0 -0
  119. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/dsn_aliases.py +0 -0
  120. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/favorite_queries.py +0 -0
  121. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/llm.py +0 -0
  122. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/main.py +0 -0
  123. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/source.py +0 -0
  124. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/sql_result/__init__.py +0 -0
  125. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/sql_result/sql_result.py +0 -0
  126. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/tabular_output/__init__.py +0 -0
  127. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  128. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/__init__.py +0 -0
  129. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/cli_utils.py +0 -0
  130. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/dsn_utils.py +0 -0
  131. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/interactive_utils.py +0 -0
  132. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/key_binding_utils.py +0 -0
  133. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/keyring_utils.py +0 -0
  134. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/path_utils.py +0 -0
  135. {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/sql_utils.py +0 -0
  136. {mycli-2.30.4 → mycli-2.31.0}/mycli/resources/completions/bash/mycli +0 -0
  137. {mycli-2.30.4 → mycli-2.31.0}/mycli/resources/completions/fish/mycli.fish +0 -0
  138. {mycli-2.30.4 → mycli-2.31.0}/mycli/resources/completions/zsh/_mycli +0 -0
  139. {mycli-2.30.4 → mycli-2.31.0}/mycli/types.py +0 -0
  140. {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/SOURCES.txt +0 -0
  141. {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/dependency_links.txt +0 -0
  142. {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/entry_points.txt +0 -0
  143. {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/requires.txt +0 -0
  144. {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/scm_file_list.json +0 -0
  145. {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/top_level.txt +0 -0
  146. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/__init__.py +0 -0
  147. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/__init__.py +0 -0
  148. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/auto_vertical.feature +0 -0
  149. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/basic_commands.feature +0 -0
  150. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/connection.feature +0 -0
  151. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/crud_database.feature +0 -0
  152. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/crud_table.feature +0 -0
  153. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/db_utils.py +0 -0
  154. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/environment.py +0 -0
  155. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/fixture_data/help_commands.txt +0 -0
  156. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/fixture_utils.py +0 -0
  157. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/iocommands.feature +0 -0
  158. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/named_queries.feature +0 -0
  159. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/specials.feature +0 -0
  160. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/__init__.py +0 -0
  161. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/auto_vertical.py +0 -0
  162. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/basic_commands.py +0 -0
  163. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/connection.py +0 -0
  164. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/crud_database.py +0 -0
  165. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/crud_table.py +0 -0
  166. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/iocommands.py +0 -0
  167. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/named_queries.py +0 -0
  168. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/specials.py +0 -0
  169. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/utils.py +0 -0
  170. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/wrappers.py +0 -0
  171. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/wrappager.py +0 -0
  172. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/myclirc +0 -0
  173. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/mylogin.cnf +0 -0
  174. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/conftest.py +0 -0
  175. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_app_state.py +0 -0
  176. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_background_runner.py +0 -0
  177. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_bash_completion.py +0 -0
  178. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_boundary_tunnel.py +0 -0
  179. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_checkup.py +0 -0
  180. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_cli_runner.py +0 -0
  181. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_cli_utils.py +0 -0
  182. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_clibuffer.py +0 -0
  183. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client_connection.py +0 -0
  184. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_completer_use_switch.py +0 -0
  185. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_completion_engine.py +0 -0
  186. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_completion_refresher.py +0 -0
  187. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_config.py +0 -0
  188. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_delimitercommand.py +0 -0
  189. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_dsn_aliases.py +0 -0
  190. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_favoritequeries.py +0 -0
  191. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_fish_completion.py +0 -0
  192. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_fzf.py +0 -0
  193. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_hybrid_redirection.py +0 -0
  194. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_interactive_utils.py +0 -0
  195. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_key_binding_utils.py +0 -0
  196. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_key_bindings.py +0 -0
  197. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_keyring_utils.py +0 -0
  198. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_kubectl_tunnel.py +0 -0
  199. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_macos_keychain.py +0 -0
  200. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main.py +0 -0
  201. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_execute.py +0 -0
  202. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_list_dsn.py +0 -0
  203. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_naive_completion.py +0 -0
  204. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_output.py +0 -0
  205. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_password_sources.py +0 -0
  206. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_path_utils.py +0 -0
  207. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_polars_completion.py +0 -0
  208. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_polars_transform.py +0 -0
  209. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_fzf_history.py +0 -0
  210. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_history.py +0 -0
  211. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_style.py +0 -0
  212. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_toolbar.py +0 -0
  213. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_utils.py +0 -0
  214. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_schema_prefetcher.py +0 -0
  215. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_smart_completion_public_schema_only.py +0 -0
  216. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_commands_init.py +0 -0
  217. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_db_commands.py +0 -0
  218. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_llm.py +0 -0
  219. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_main.py +0 -0
  220. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_source.py +0 -0
  221. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sql_execute.py +0 -0
  222. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sql_format.py +0 -0
  223. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sql_utils.py +0 -0
  224. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sqlcompleter.py +0 -0
  225. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sqlresult.py +0 -0
  226. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_ssh_tunnel.py +0 -0
  227. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_tabular_output.py +0 -0
  228. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_vault.py +0 -0
  229. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_zsh_completion.py +0 -0
  230. {mycli-2.30.4 → mycli-2.31.0}/mycli_test/utils.py +0 -0
  231. {mycli-2.30.4 → mycli-2.31.0}/pyproject.toml +0 -0
  232. {mycli-2.30.4 → mycli-2.31.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.30.4
3
+ Version: 2.31.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
@@ -1,3 +1,21 @@
1
+ v2.31.0 (2026/11/14)
2
+ ==============
3
+
4
+ Features
5
+ --------
6
+ * Bracket IPv6 literals in prompt strings.
7
+
8
+
9
+ Bug Fixes
10
+ --------
11
+ * Read `/source` files and the `\e` editor file as UTF-8 instead of the locale encoding, so non-ASCII SQL no longer fails or is mis-decoded under non-UTF-8 locales.
12
+ * Open the audit log and the `\T`/`\o` output files as UTF-8, so non-ASCII output no longer fails under non-UTF-8 locales.
13
+ * Bracket IPv6 hosts in the DSN shown and saved by `\dsn`, so a saved alias for `::1` can be used to connect again.
14
+ * Support `DELIMITER` in batch input (`mycli < file.sql`, `--batch`), so dumps with triggers or routines restore.
15
+ * Read `--batch` files and read and write `--checkpoint` files as UTF-8, fixing `--resume` and non-ASCII batch files on Windows.
16
+ * Terminate `use`, `source`, `\u` and `\.` at newline in batch input, as the vendor client does.
17
+
18
+
1
19
  v2.30.4 (2026/11/12)
2
20
  ==============
3
21
 
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: mycli-2.30.4/docker/resources/test_db-1.0.7.tar.gz
2
+ name: mycli-2.31.0/docker/resources/test_db-1.0.7.tar.gz
3
3
  size: 35607473 bytes
4
4
  sha256: c44c140f352f35d47fdb65df60f52b779ef552822fad6c4efcfa7b134c3faf84
@@ -123,6 +123,9 @@ Contributors:
123
123
  * Wei-Lun Li
124
124
  * Shaurya Kesarwani
125
125
  * Alex Castillo
126
+ * Karim Masmoudi
127
+ * Jay Hemnani
128
+ * Alpenj
126
129
 
127
130
 
128
131
  Created by:
@@ -180,7 +180,7 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
180
180
  # audit log
181
181
  if self.logfile is None and "audit_log" in c["main"]:
182
182
  try:
183
- self.logfile = open(os.path.expanduser(c["main"]["audit_log"]), "a")
183
+ self.logfile = open(os.path.expanduser(c["main"]["audit_log"]), "a", encoding="utf-8")
184
184
  except (IOError, OSError):
185
185
  self.echo("Error: Unable to open the audit log file. Your queries will not be logged.", err=True, fg="red")
186
186
  self.logfile = False
@@ -305,7 +305,7 @@ class ClientCommandsMixin:
305
305
  return
306
306
 
307
307
  try:
308
- file_h = open(os.path.expanduser(filename))
308
+ file_h = open(os.path.expanduser(filename), encoding='utf-8')
309
309
  except OSError as error:
310
310
  yield SQLResult(status=str(error))
311
311
  return
@@ -97,7 +97,7 @@ class ClientQueryMixin:
97
97
  assert self.sql_execute is not None
98
98
  self.log_query(query)
99
99
  if checkpoint and not self.checkpoint:
100
- self.checkpoint = click.open_file(checkpoint, mode='a')
100
+ self.checkpoint = click.open_file(checkpoint, mode='a', encoding='utf-8')
101
101
  results = self.sql_execute.run(query)
102
102
  for result in results:
103
103
  self.main_formatter.query = query
@@ -25,6 +25,11 @@ class CheckpointReplayError(Exception):
25
25
  pass
26
26
 
27
27
 
28
+ def batch_file_encoding(batch_path: str) -> str | None:
29
+ # Leave standard input to click, which picks the console encoding.
30
+ return None if batch_path == '-' else 'utf-8'
31
+
32
+
28
33
  def replay_checkpoint_file(
29
34
  batch_path: str,
30
35
  checkpoint_path: str | None,
@@ -48,7 +53,10 @@ def replay_checkpoint_file(
48
53
  spinner = yaspin(text='replaying checkpoint', side='right', stream=sys.stderr)
49
54
  spinner.start()
50
55
  try:
51
- with click.open_file(batch_path) as batch_h, click.open_file(checkpoint_path, mode='r', encoding='utf-8') as checkpoint_h:
56
+ with (
57
+ click.open_file(batch_path, encoding=batch_file_encoding(batch_path)) as batch_h,
58
+ click.open_file(checkpoint_path, mode='r', encoding='utf-8') as checkpoint_h,
59
+ ):
52
60
  try:
53
61
  batch_gen = statements_from_filehandle(batch_h)
54
62
  except ValueError as e:
@@ -140,13 +148,13 @@ def main_batch_with_progress_bar(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
140
148
  return 1
141
149
  try:
142
150
  completed_statement_count = replay_checkpoint_file(cli_args.batch, cli_args.checkpoint, cli_args.resume, progress=True)
143
- batch_count_h = click.open_file(cli_args.batch)
151
+ batch_count_h = click.open_file(cli_args.batch, encoding=batch_file_encoding(cli_args.batch))
144
152
  with yaspin(text='validating batch ', side='right', stream=sys.stderr) as spinner:
145
153
  for _statement, _counter in statements_from_filehandle(batch_count_h):
146
154
  goal_statements += 1
147
155
  spinner.ok('✔')
148
156
  batch_count_h.close()
149
- batch_h = click.open_file(cli_args.batch)
157
+ batch_h = click.open_file(cli_args.batch, encoding=batch_file_encoding(cli_args.batch))
150
158
  batch_gen = statements_from_filehandle(batch_h)
151
159
  except (OSError, FileNotFoundError):
152
160
  click.secho(f'Failed to open --batch file: {cli_args.batch}', err=True, fg='red')
@@ -192,7 +200,7 @@ def main_batch_without_progress_bar(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
192
200
  click.secho('Ignoring STDIN since --batch was also given.', err=True, fg='red')
193
201
  try:
194
202
  completed_statement_count = replay_checkpoint_file(cli_args.batch, cli_args.checkpoint, cli_args.resume)
195
- batch_h = click.open_file(cli_args.batch)
203
+ batch_h = click.open_file(cli_args.batch, encoding=batch_file_encoding(cli_args.batch))
196
204
  except (OSError, FileNotFoundError):
197
205
  click.secho(f'Failed to open --batch file: {cli_args.batch}', err=True, fg='red')
198
206
  return 1
@@ -98,7 +98,7 @@ from mycli.packages.utils.sql_utils import (
98
98
  need_completion_refresh,
99
99
  need_completion_reset,
100
100
  )
101
- from mycli.packages.utils.string_utils import sanitize_terminal_title
101
+ from mycli.packages.utils.string_utils import bracket_ipv6_literal, sanitize_terminal_title
102
102
  from mycli.types import Query
103
103
 
104
104
  if TYPE_CHECKING:
@@ -321,6 +321,8 @@ def render_prompt_string(
321
321
  short_prompt_host, _, _ = prompt_host.partition('.')
322
322
  if re.match(r'^[\d\.]+$', short_prompt_host):
323
323
  short_prompt_host = prompt_host
324
+ prompt_host = bracket_ipv6_literal(prompt_host)
325
+ short_prompt_host = bracket_ipv6_literal(short_prompt_host)
324
326
  now = datetime.now()
325
327
  species_name = sql_execute.server_info.species.name if sql_execute.server_info and sql_execute.server_info.species else 'MySQL'
326
328
  strings = string.split('\\\\')
@@ -289,7 +289,7 @@ def open_external_editor(filename: str | None = None, sql: str | None = None) ->
289
289
  message = None
290
290
  click.edit(filename=filename)
291
291
  try:
292
- with open(filename, 'r') as f:
292
+ with open(filename, 'r', encoding='utf-8') as f:
293
293
  query = f.read()
294
294
  except IOError:
295
295
  message = f'Error reading file: {filename}'
@@ -899,7 +899,7 @@ def set_tee(arg: str, **_) -> list[SQLResult]:
899
899
  global tee_file
900
900
 
901
901
  try:
902
- tee_file = open(*parseargfile(arg))
902
+ tee_file = open(*parseargfile(arg), encoding='utf-8')
903
903
  except (IOError, OSError) as e:
904
904
  raise OSError(f"Cannot write to file '{e.filename}': {e.strerror}") from e
905
905
 
@@ -949,7 +949,7 @@ def _set_once_file(filename: str, mode: str) -> list[SQLResult]:
949
949
  global once_file, written_to_once_file
950
950
 
951
951
  try:
952
- once_file = open(os.path.expanduser(filename), mode)
952
+ once_file = open(os.path.expanduser(filename), mode, encoding='utf-8')
953
953
  except (IOError, OSError) as e:
954
954
  raise OSError(f"Cannot write to file '{e.filename}': {e.strerror}") from e
955
955
  written_to_once_file = False
@@ -1096,7 +1096,7 @@ def flush_pipe_once_if_written(
1096
1096
  returncode = PIPE_ONCE['process'].returncode
1097
1097
  if PIPE_ONCE['stdout_file']:
1098
1098
  if not timed_out and not returncode and (stdout_data or force):
1099
- with open(PIPE_ONCE['stdout_file'], PIPE_ONCE['stdout_mode']) as f:
1099
+ with open(PIPE_ONCE['stdout_file'], PIPE_ONCE['stdout_mode'], encoding='utf-8') as f:
1100
1100
  if stdout_data:
1101
1101
  print(stdout_data, file=f)
1102
1102
  _run_post_redirect_hook(post_redirect_command, PIPE_ONCE['stdout_file'])
@@ -9,6 +9,8 @@ import click
9
9
  import pymysql
10
10
  from pymysql.cursors import Cursor
11
11
 
12
+ from mycli.packages.utils.string_utils import bracket_ipv6_literal
13
+
12
14
  logger = logging.getLogger(__name__)
13
15
 
14
16
  CACHED_SSL_VERSION: dict[tuple, str | None] = {}
@@ -166,7 +168,7 @@ def format_connection_dsn(
166
168
  ) -> str:
167
169
  if user_part := urlquote(user or ''):
168
170
  user_part = f'{user_part}@'
169
- host_part = host or 'localhost'
171
+ host_part = bracket_ipv6_literal(host or 'localhost')
170
172
  port_part = f':{port}' if port is not None else ''
171
173
  if db_part := urlquote(database or ''):
172
174
  db_part = f'/{db_part}'
@@ -0,0 +1,71 @@
1
+ import re
2
+ from typing import IO, Generator
3
+
4
+ import sqlglot
5
+ import sqlparse
6
+
7
+ MAX_MULTILINE_BATCH_STATEMENT = 5000
8
+
9
+ DELIMITER_RE = re.compile(r'^\s*delimiter\s+(\S+)\s*$', re.IGNORECASE)
10
+ # Client commands the vendor client ends at the newline, without a delimiter
11
+ LINE_COMMAND_RE = re.compile(r'^\s*(/?use|/?source|[/\\]u|[/\\]\.)\s+\S', re.IGNORECASE)
12
+
13
+
14
+ def _has_tokens(text: str) -> bool:
15
+ try:
16
+ return bool(sqlglot.tokenize(text, read='mysql'))
17
+ except sqlglot.errors.TokenError:
18
+ return True
19
+
20
+
21
+ def statements_from_filehandle(file_h: IO) -> Generator[tuple[str, int], None, None]:
22
+ statements = ''
23
+ line_counter = 0
24
+ batch_counter = 0
25
+ delimiter = ';'
26
+ for batch_text in file_h:
27
+ line_counter += 1
28
+ if line_counter > MAX_MULTILINE_BATCH_STATEMENT:
29
+ raise ValueError(f'Saw single input statement greater than {MAX_MULTILINE_BATCH_STATEMENT} lines; assuming a parsing error.')
30
+ # DELIMITER is a client command, as in the mysql client: switch delimiters, don't send it
31
+ delimiter_match = DELIMITER_RE.match(batch_text)
32
+ if delimiter_match and not _has_tokens(statements):
33
+ delimiter = delimiter_match.group(1)
34
+ statements = ''
35
+ line_counter = 0
36
+ continue
37
+ if LINE_COMMAND_RE.match(batch_text) and not _has_tokens(statements) and not batch_text.rstrip().endswith(delimiter):
38
+ yield (batch_text.strip(), batch_counter)
39
+ batch_counter += 1
40
+ statements = ''
41
+ line_counter = 0
42
+ continue
43
+ statements += batch_text
44
+ if delimiter != ';':
45
+ statement = statements.rstrip()
46
+ # A custom delimiter is only recognized at the end of a line, so
47
+ # two statements on one line are sent together as one
48
+ if statement.endswith(delimiter):
49
+ yield (statement[: -len(delimiter)].strip(), batch_counter)
50
+ batch_counter += 1
51
+ statements = ''
52
+ line_counter = 0
53
+ continue
54
+ try:
55
+ tokens = sqlglot.tokenize(statements, read='mysql')
56
+ if not tokens:
57
+ continue
58
+ if tokens[-1].text == ';':
59
+ # The advantage of sqlparse for splitting is that it preserves the input.
60
+ # https://github.com/tobymao/sqlglot/issues/2587#issuecomment-1823109501
61
+ for statement in sqlparse.split(statements):
62
+ yield (statement, batch_counter)
63
+ batch_counter += 1
64
+ statements = ''
65
+ line_counter = 0
66
+ except sqlglot.errors.TokenError:
67
+ continue
68
+ if statements:
69
+ for statement in sqlparse.split(statements):
70
+ yield (statement, batch_counter)
71
+ batch_counter += 1
@@ -1,3 +1,4 @@
1
+ import ipaddress
1
2
  import re
2
3
 
3
4
  from cli_helpers.utils import strip_ansi
@@ -13,3 +14,13 @@ def sanitize_terminal_title(title: FormattedText) -> str:
13
14
  sanitized = sanitized.replace('\n', ' ')
14
15
  sanitized = re.sub('[\x00-\x1f\x7f]', '', sanitized)
15
16
  return sanitized
17
+
18
+
19
+ def bracket_ipv6_literal(host: str) -> str:
20
+ try:
21
+ ip_obj = ipaddress.ip_address(host)
22
+ except ValueError:
23
+ return host
24
+ if isinstance(ip_obj, ipaddress.IPv6Address) and not host.startswith('['):
25
+ return f'[{host}]'
26
+ return host
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.30.4
3
+ Version: 2.31.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
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.31.0",
3
+ "distance": 0,
4
+ "node": "g9a216b6c68294b4222e1780bfe66d00d3c1d1a91",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-11-14"
8
+ }
@@ -99,3 +99,32 @@ def test_statements_from_filehandle_continues_when_tokenizer_returns_no_tokens(m
99
99
  ('select 1;', 0),
100
100
  ('select 2;', 1),
101
101
  ]
102
+
103
+
104
+ def test_statements_from_filehandle_handles_delimiter_blocks() -> None:
105
+ statements = collect_statements('DELIMITER //\nCREATE PROCEDURE p1()\nBEGIN\n SELECT 1;\nEND //\nDELIMITER ;\nCALL p1();\n')
106
+
107
+ assert statements == [
108
+ ('CREATE PROCEDURE p1()\nBEGIN\n SELECT 1;\nEND', 0),
109
+ ('CALL p1();', 1),
110
+ ]
111
+
112
+
113
+ def test_statements_from_filehandle_ends_use_at_newline() -> None:
114
+ statements = collect_statements('select 1;\nuse mysql\nselect database();\n')
115
+
116
+ assert statements == [
117
+ ('select 1;', 0),
118
+ ('use mysql', 1),
119
+ ('select database();', 2),
120
+ ]
121
+
122
+
123
+ def test_statements_from_filehandle_ends_slash_use_at_newline() -> None:
124
+ statements = collect_statements('select 1;\n/use mysql\nselect database();\n')
125
+
126
+ assert statements == [
127
+ ('select 1;', 0),
128
+ ('/use mysql', 1),
129
+ ('select database();', 2),
130
+ ]
@@ -423,6 +423,7 @@ def test_init_opens_audit_log_from_config(monkeypatch: pytest.MonkeyPatch, tmp_p
423
423
  try:
424
424
  assert isinstance(cli.logfile, TextIOWrapper)
425
425
  assert Path(cli.logfile.name) == audit_log
426
+ assert cli.logfile.encoding.lower().replace('_', '-') == 'utf-8'
426
427
  finally:
427
428
  if isinstance(cli.logfile, TextIOWrapper):
428
429
  cli.logfile.close()
@@ -4,6 +4,8 @@ from collections.abc import Generator
4
4
  from io import StringIO
5
5
  import logging
6
6
  from pathlib import Path
7
+ import subprocess
8
+ import sys
7
9
  from types import SimpleNamespace
8
10
  from typing import Any
9
11
 
@@ -574,7 +576,7 @@ def test_execute_from_file_iterates_statements_without_reading_entire_file(monke
574
576
  file_h = IteratedFile('select 1;\nselect\n 2;\nselect 3; select 4;\nselect 5')
575
577
  opened_paths: list[str] = []
576
578
 
577
- def open_file(path: str) -> IteratedFile:
579
+ def open_file(path: str, **_: Any) -> IteratedFile:
578
580
  opened_paths.append(path)
579
581
  return file_h
580
582
 
@@ -596,7 +598,7 @@ def test_execute_from_file_reports_read_errors(monkeypatch: pytest.MonkeyPatch)
596
598
  client = DummyClient()
597
599
  client.sql_execute = FakeSQLExecute()
598
600
  file_h = FailingFile()
599
- monkeypatch.setattr(client_commands, 'open', lambda path: file_h, raising=False)
601
+ monkeypatch.setattr(client_commands, 'open', lambda path, **_: file_h, raising=False)
600
602
 
601
603
  assert list(client.execute_from_file('query.sql')) == [SQLResult(status='read failed')]
602
604
  assert file_h.closed is True
@@ -655,7 +657,7 @@ def test_execute_from_file_help_is_terminal(monkeypatch: pytest.MonkeyPatch) ->
655
657
  client.sql_execute = FakeSQLExecute()
656
658
  opened_paths: list[str] = []
657
659
  sleep_calls: list[float] = []
658
- monkeypatch.setattr(client_commands, 'open', lambda path: opened_paths.append(path), raising=False)
660
+ monkeypatch.setattr(client_commands, 'open', lambda path, **_: opened_paths.append(path), raising=False)
659
661
  monkeypatch.setattr(client_commands.time, 'sleep', lambda seconds: sleep_calls.append(seconds))
660
662
 
661
663
  assert list(client.execute_from_file('--page --help ignored.sql')) == [
@@ -716,7 +718,7 @@ def test_execute_from_file_does_not_throttle_after_declined_statement(monkeypatc
716
718
  def test_execute_from_file_reports_invalid_throttle_without_opening_file(monkeypatch: pytest.MonkeyPatch) -> None:
717
719
  client = DummyClient()
718
720
  opened_paths: list[str] = []
719
- monkeypatch.setattr(client_commands, 'open', lambda path: opened_paths.append(path), raising=False)
721
+ monkeypatch.setattr(client_commands, 'open', lambda path, **_: opened_paths.append(path), raising=False)
720
722
 
721
723
  assert list(client.execute_from_file('--throttle nope query.sql')) == [
722
724
  SQLResult(
@@ -782,6 +784,31 @@ def test_execute_from_file_shows_each_query(capsys: pytest.CaptureFixture[str],
782
784
  assert capsys.readouterr().out == '> select 1;\n> select 2;\n'
783
785
 
784
786
 
787
+ def test_execute_from_file_does_not_use_locale_encoding(tmp_path: Path) -> None:
788
+ # SQL files are commonly UTF-8; the locale encoding (cp1252, cp932, ...) would
789
+ # mis-decode or reject them. EncodingWarning is raised for any open() that
790
+ # falls back to the locale encoding.
791
+ sql_file = tmp_path / 'query.sql'
792
+ sql_file.write_text('-- caf\u00e9\nselect 1;', encoding='utf-8')
793
+ script = '\n'.join((
794
+ 'import sys',
795
+ 'from mycli.client_commands import ClientCommandsMixin',
796
+ 'class C(ClientCommandsMixin):',
797
+ ' pass',
798
+ 'try:',
799
+ ' list(C().execute_from_file(sys.argv[1]))',
800
+ 'except AttributeError:',
801
+ ' pass',
802
+ ))
803
+ result = subprocess.run(
804
+ [sys.executable, '-X', 'warn_default_encoding', '-W', 'error::EncodingWarning', '-c', script, str(sql_file)],
805
+ capture_output=True,
806
+ encoding='utf8',
807
+ errors='replace',
808
+ )
809
+ assert 'EncodingWarning' not in result.stderr, result.stderr
810
+
811
+
785
812
  def test_execute_from_file_parses_special_option_and_preserves_filename(
786
813
  monkeypatch: pytest.MonkeyPatch,
787
814
  ) -> None:
@@ -791,7 +818,7 @@ def test_execute_from_file_parses_special_option_and_preserves_filename(
791
818
  file_h = IteratedFile('select 1;')
792
819
  opened_paths: list[str] = []
793
820
 
794
- def open_file(path: str) -> IteratedFile:
821
+ def open_file(path: str, **_: Any) -> IteratedFile:
795
822
  opened_paths.append(path)
796
823
  return file_h
797
824
 
@@ -815,7 +842,7 @@ def test_execute_from_file_reports_missing_filename_after_options(options: str)
815
842
  def test_execute_from_file_rejects_unquoted_filename_with_spaces(monkeypatch: pytest.MonkeyPatch) -> None:
816
843
  client = DummyClient()
817
844
  opened_paths: list[str] = []
818
- monkeypatch.setattr(client_commands, 'open', lambda path: opened_paths.append(path), raising=False)
845
+ monkeypatch.setattr(client_commands, 'open', lambda path, **_: opened_paths.append(path), raising=False)
819
846
 
820
847
  assert list(client.execute_from_file('query file.sql')) == [SQLResult(status=source_command.INVALID_SOURCE_FILENAME, is_error=True)]
821
848
  assert opened_paths == []
@@ -225,7 +225,7 @@ def test_on_completions_refreshed_starts_schema_prefetch() -> None:
225
225
  assert state['prefetch_started'] == [True]
226
226
 
227
227
 
228
- def run_query_with_state(monkeypatch, tmp_path, *, warnings_enabled: bool = True) -> dict[str, Any]:
228
+ def run_query_with_state(monkeypatch, tmp_path, *, warnings_enabled: bool = True, query: str = 'select 1;\n') -> dict[str, Any]:
229
229
  cli = make_bare_mycli()
230
230
  normal_rows = FakeCursorBase(rows=[('one',)], warning_count=1)
231
231
  warning_rows = FakeCursorBase(rows=[('warning',)], warning_count=0)
@@ -258,7 +258,7 @@ def run_query_with_state(monkeypatch, tmp_path, *, warnings_enabled: bool = True
258
258
  cli.log_query = lambda query: state['logged_queries'].append(query)
259
259
  cli.log_output = lambda line: state['logged_output'].append(line)
260
260
  cli.format_sqlresult = format_sqlresult
261
- mycli_client.MyCli.run_query(cli, 'select 1;\n', checkpoint=str(state['checkpoint_path']), new_line=False)
261
+ mycli_client.MyCli.run_query(cli, query, checkpoint=str(state['checkpoint_path']), new_line=False)
262
262
  state['cli'] = cli
263
263
  return state
264
264
 
@@ -336,6 +336,12 @@ def test_run_query_writes_checkpoint(monkeypatch, tmp_path) -> None:
336
336
  assert state['checkpoint_path'].read_text(encoding='utf-8') == 'select 1;\n'
337
337
 
338
338
 
339
+ def test_run_query_writes_checkpoint_as_utf8(monkeypatch, tmp_path) -> None:
340
+ state = run_query_with_state(monkeypatch, tmp_path, warnings_enabled=False, query="select 'Łukasz ✨';\n")
341
+
342
+ assert state['checkpoint_path'].read_text(encoding='utf-8') == "select 'Łukasz ✨';\n"
343
+
344
+
339
345
  def test_run_query_raises_for_error_result_when_requested(tmp_path) -> None:
340
346
  cli = make_bare_mycli()
341
347
  logged_output: list[str] = []
@@ -513,7 +513,7 @@ def test_main_batch_with_progress_bar_handles_open_errors(monkeypatch) -> None:
513
513
  cli_args = DummyCliArgs(batch='missing.sql')
514
514
 
515
515
  monkeypatch.setattr(batch_mode.os.path, 'exists', lambda _path: False)
516
- monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path: (_ for _ in ()).throw(FileNotFoundError()))
516
+ monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path, **_kwargs: (_ for _ in ()).throw(FileNotFoundError()))
517
517
  monkeypatch.setattr(batch_mode.click, 'secho', lambda message, err, fg: messages.append((message, err, fg)))
518
518
  monkeypatch.setattr(batch_mode, 'sys', make_fake_sys(stdin_tty=True))
519
519
 
@@ -529,7 +529,7 @@ def test_main_batch_with_progress_bar_handles_counting_value_errors(monkeypatch)
529
529
  cli_args = DummyCliArgs(batch='statements.sql')
530
530
 
531
531
  monkeypatch.setattr(batch_mode.os.path, 'exists', lambda _path: False)
532
- monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path: count_handle)
532
+ monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path, **_kwargs: count_handle)
533
533
  monkeypatch.setattr(batch_mode, 'statements_from_filehandle', lambda _handle: (_ for _ in ()).throw(ValueError('bad sql')))
534
534
  monkeypatch.setattr(batch_mode.click, 'secho', lambda message, err, fg: messages.append((message, err, fg)))
535
535
  monkeypatch.setattr(batch_mode, 'sys', make_fake_sys(stdin_tty=True))
@@ -548,7 +548,7 @@ def test_main_batch_with_progress_bar_processes_all_statements(monkeypatch) -> N
548
548
  dispatch_calls: list[tuple[str, int]] = []
549
549
  cli_args = DummyCliArgs(batch='statements.sql')
550
550
 
551
- def fake_open_file(path: str) -> DummyFile:
551
+ def fake_open_file(path: str, **_kwargs: Any) -> DummyFile:
552
552
  open_calls.append(path)
553
553
  return count_handle if len(open_calls) == 1 else run_handle
554
554
 
@@ -588,7 +588,7 @@ def test_main_batch_with_progress_bar_returns_error_when_dispatch_fails(monkeypa
588
588
  open_calls = 0
589
589
  cli_args = DummyCliArgs(batch='statements.sql')
590
590
 
591
- def fake_open_file(_path: str) -> DummyFile:
591
+ def fake_open_file(_path: str, **_kwargs: Any) -> DummyFile:
592
592
  nonlocal open_calls
593
593
  open_calls += 1
594
594
  return count_handle if open_calls == 1 else run_handle
@@ -626,7 +626,7 @@ def test_main_batch_without_progress_bar_handles_open_errors(monkeypatch) -> Non
626
626
  messages: list[tuple[str, bool, str]] = []
627
627
  cli_args = DummyCliArgs(batch='missing.sql')
628
628
 
629
- monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path: (_ for _ in ()).throw(FileNotFoundError()))
629
+ monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path, **_kwargs: (_ for _ in ()).throw(FileNotFoundError()))
630
630
  monkeypatch.setattr(batch_mode.click, 'secho', lambda message, err, fg: messages.append((message, err, fg)))
631
631
  monkeypatch.setattr(batch_mode, 'sys', make_fake_sys(stdin_tty=True))
632
632
 
@@ -642,7 +642,7 @@ def test_main_batch_without_progress_bar_processes_statements(monkeypatch) -> No
642
642
  dispatch_calls: list[tuple[str, int]] = []
643
643
  cli_args = DummyCliArgs(batch='statements.sql')
644
644
 
645
- monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path: batch_handle)
645
+ monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path, **_kwargs: batch_handle)
646
646
  monkeypatch.setattr(batch_mode, 'statements_from_filehandle', lambda _handle: iter([('select 1;', 0), ('select 2;', 1)]))
647
647
  monkeypatch.setattr(
648
648
  batch_mode,
@@ -680,6 +680,26 @@ def test_main_batch_without_progress_bar_skips_checkpoint_prefix(monkeypatch, tm
680
680
  assert dispatch_calls == [('select 3;', 2)]
681
681
 
682
682
 
683
+ def test_main_batch_without_progress_bar_reads_utf8_batch_and_checkpoint(monkeypatch, tmp_path: Path) -> None:
684
+ batch_path = write_batch_file(tmp_path, "select 'Łukasz ✨';\nselect 2;\n")
685
+ dispatch_calls: list[tuple[str, int]] = []
686
+
687
+ monkeypatch.setattr(
688
+ batch_mode,
689
+ 'dispatch_batch_statements',
690
+ lambda _mycli, _cli_args, statement, counter: dispatch_calls.append((statement, counter)),
691
+ )
692
+ monkeypatch.setattr(batch_mode, 'sys', make_fake_sys(stdin_tty=True))
693
+
694
+ checkpoint = write_checkpoint_file(tmp_path, "select 'Łukasz ✨';\n")
695
+
696
+ cli_args = DummyCliArgs(batch=batch_path, checkpoint=checkpoint, resume=True)
697
+ result = main_batch_without_progress_bar(DummyMyCli(), cli_args)
698
+
699
+ assert result == 0
700
+ assert dispatch_calls == [('select 2;', 1)]
701
+
702
+
683
703
  def test_main_batch_without_progress_bar_skips_only_matching_duplicate_prefix(monkeypatch, tmp_path: Path) -> None:
684
704
  batch_path = write_batch_file(tmp_path, 'select 1;\nselect 1;\nselect 2;\n')
685
705
  dispatch_calls: list[tuple[str, int]] = []
@@ -764,6 +784,26 @@ def test_main_batch_with_progress_bar_skips_checkpoint_prefix_and_counts_all_sta
764
784
  assert DummyProgressBar.calls == [[0, 1, 2]]
765
785
 
766
786
 
787
+ def test_main_batch_with_progress_bar_reads_utf8_batch(monkeypatch, tmp_path: Path) -> None:
788
+ batch_path = write_batch_file(tmp_path, "select 'Łukasz ✨';\nselect 2;\n")
789
+ dispatch_calls: list[tuple[str, int]] = []
790
+
791
+ DummyProgressBar.calls.clear()
792
+ monkeypatch.setattr(batch_mode, 'ProgressBar', DummyProgressBar)
793
+ monkeypatch.setattr(batch_mode.prompt_toolkit.output, 'create_output', lambda **_kwargs: object())
794
+ monkeypatch.setattr(
795
+ batch_mode,
796
+ 'dispatch_batch_statements',
797
+ lambda _mycli, _cli_args, statement, counter: dispatch_calls.append((statement, counter)),
798
+ )
799
+ monkeypatch.setattr(batch_mode, 'sys', make_fake_sys(stdin_tty=True))
800
+
801
+ result = main_batch_with_progress_bar(DummyMyCli(), DummyCliArgs(batch=batch_path))
802
+
803
+ assert result == 0
804
+ assert dispatch_calls == [("select 'Łukasz ✨';", 0), ('select 2;', 1)]
805
+
806
+
767
807
  def test_main_batch_with_progress_bar_returns_error_when_checkpoint_replay_fails(monkeypatch, tmp_path: Path) -> None:
768
808
  batch_path = write_batch_file(tmp_path, 'select 1;\n')
769
809
  messages: list[tuple[str, bool, str]] = []
@@ -785,7 +825,7 @@ def test_main_batch_without_progress_bar_returns_error_when_iteration_fails(monk
785
825
  batch_handle = DummyFile('run')
786
826
  cli_args = DummyCliArgs(batch='statements.sql')
787
827
 
788
- monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path: batch_handle)
828
+ monkeypatch.setattr(batch_mode.click, 'open_file', lambda _path, **_kwargs: batch_handle)
789
829
  monkeypatch.setattr(batch_mode, 'statements_from_filehandle', lambda _handle: (_ for _ in ()).throw(ValueError('bad sql')))
790
830
  monkeypatch.setattr(batch_mode.click, 'secho', lambda message, err, fg: messages.append((message, err, fg)))
791
831
  monkeypatch.setattr(batch_mode, 'sys', make_fake_sys(stdin_tty=True))
@@ -1178,6 +1178,28 @@ def test_transaction_prompt_reuses_cached_render_without_ping() -> None:
1178
1178
  ping.assert_called_once_with()
1179
1179
 
1180
1180
 
1181
+ @pytest.mark.parametrize('escape', [r'\h', r'\H'])
1182
+ @pytest.mark.parametrize(
1183
+ ('host', 'expected'),
1184
+ [('::1', '[::1]:3306'), ('2001:db8::1', '[2001:db8::1]:3306'), ('fe80::1%en0', '[fe80::1%en0]:3306'), ('[::1]', '[::1]:3306')],
1185
+ )
1186
+ def test_render_prompt_string_brackets_ipv6_host(escape: str, host: str, expected: str) -> None:
1187
+ cli = make_repl_cli(SimpleNamespace(user='alice', host=host, dbname='db', port=3306, socket=None, server_info=None, conn=None))
1188
+
1189
+ prompt = repl_mode.render_prompt_string(cli, escape + r':\p', 0)
1190
+
1191
+ assert to_plain_text(prompt) == expected
1192
+
1193
+
1194
+ @pytest.mark.parametrize('escape', [r'\h', r'\H'])
1195
+ def test_render_prompt_string_preserves_colon_in_login_path(escape: str) -> None:
1196
+ cli = make_repl_cli(SimpleNamespace(user='alice', host='::1', dbname='db', port=3306, socket=None, server_info=None, conn=None))
1197
+ cli.login_path = 'prod:readonly'
1198
+ cli.login_path_as_host = True
1199
+
1200
+ assert to_plain_text(repl_mode.render_prompt_string(cli, escape, 0)) == 'prod:readonly'
1201
+
1202
+
1181
1203
  def test_render_prompt_string_includes_current_edit_mode() -> None:
1182
1204
  cli = make_repl_cli(
1183
1205
  SimpleNamespace(