mycli 2.30.3__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.3 → mycli-2.31.0}/.github/workflows/ci.yml +2 -2
  2. {mycli-2.30.3 → mycli-2.31.0}/.github/workflows/publish.yml +3 -3
  3. {mycli-2.30.3/mycli.egg-info → mycli-2.31.0}/PKG-INFO +7 -7
  4. {mycli-2.30.3 → mycli-2.31.0}/changelog.md +40 -0
  5. {mycli-2.30.3 → mycli-2.31.0}/docker/resources/test_db-1.0.7.tar.gz +1 -1
  6. {mycli-2.30.3 → mycli-2.31.0}/mycli/AUTHORS.txt +4 -0
  7. {mycli-2.30.3 → mycli-2.31.0}/mycli/cli_runner.py +2 -2
  8. {mycli-2.30.3 → mycli-2.31.0}/mycli/client.py +1 -1
  9. {mycli-2.30.3 → mycli-2.31.0}/mycli/client_commands.py +1 -1
  10. {mycli-2.30.3 → mycli-2.31.0}/mycli/client_connection.py +2 -3
  11. {mycli-2.30.3 → mycli-2.31.0}/mycli/client_query.py +1 -1
  12. {mycli-2.30.3 → mycli-2.31.0}/mycli/constants.py +0 -4
  13. {mycli-2.30.3 → mycli-2.31.0}/mycli/main_modes/batch.py +12 -4
  14. {mycli-2.30.3 → mycli-2.31.0}/mycli/main_modes/repl.py +5 -3
  15. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/execution/sql_execute.py +4 -4
  16. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/io_commands.py +4 -4
  17. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/special_command_utils.py +3 -1
  18. mycli-2.31.0/mycli/packages/utils/batch_utils.py +71 -0
  19. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/string_utils.py +11 -0
  20. {mycli-2.30.3 → mycli-2.31.0/mycli.egg-info}/PKG-INFO +7 -7
  21. {mycli-2.30.3 → mycli-2.31.0}/mycli.egg-info/SOURCES.txt +1 -0
  22. {mycli-2.30.3 → mycli-2.31.0}/mycli.egg-info/requires.txt +6 -6
  23. {mycli-2.30.3 → mycli-2.31.0}/mycli.egg-info/scm_file_list.json +1 -0
  24. mycli-2.31.0/mycli.egg-info/scm_version.json +8 -0
  25. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_batch_utils.py +29 -0
  26. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_cli_runner.py +40 -0
  27. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_client.py +1 -0
  28. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_client_commands.py +33 -6
  29. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_client_connection.py +3 -2
  30. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_client_query.py +8 -2
  31. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_main.py +2 -2
  32. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_batch.py +47 -7
  33. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_repl.py +23 -1
  34. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_special_io_commands.py +128 -0
  35. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_special_main.py +39 -0
  36. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_special_utils.py +18 -0
  37. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_sql_execute.py +71 -1
  38. mycli-2.31.0/mycli_test/pytests/test_sql_format.py +97 -0
  39. mycli-2.31.0/mycli_test/pytests/test_string_utils.py +44 -0
  40. {mycli-2.30.3 → mycli-2.31.0}/pyproject.toml +7 -6
  41. mycli-2.30.3/mycli/packages/utils/batch_utils.py +0 -36
  42. mycli-2.30.3/mycli.egg-info/scm_version.json +0 -8
  43. mycli-2.30.3/mycli_test/pytests/test_string_utils.py +0 -27
  44. {mycli-2.30.3 → mycli-2.31.0}/.git-blame-ignore-revs +0 -0
  45. {mycli-2.30.3 → mycli-2.31.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  46. {mycli-2.30.3 → mycli-2.31.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  47. {mycli-2.30.3 → mycli-2.31.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  48. {mycli-2.30.3 → mycli-2.31.0}/.github/actions/lint/action.yml +0 -0
  49. {mycli-2.30.3 → mycli-2.31.0}/.github/actions/typecheck/action.yml +0 -0
  50. {mycli-2.30.3 → mycli-2.31.0}/.github/dependabot.yml +0 -0
  51. {mycli-2.30.3 → mycli-2.31.0}/.github/workflows/lint.yml +0 -0
  52. {mycli-2.30.3 → mycli-2.31.0}/.github/workflows/typecheck.yml +0 -0
  53. {mycli-2.30.3 → mycli-2.31.0}/.gitignore +0 -0
  54. {mycli-2.30.3 → mycli-2.31.0}/AGENTS.md +0 -0
  55. {mycli-2.30.3 → mycli-2.31.0}/AUTHORS.md +0 -0
  56. {mycli-2.30.3 → mycli-2.31.0}/CONTRIBUTING.md +0 -0
  57. {mycli-2.30.3 → mycli-2.31.0}/LICENSE.txt +0 -0
  58. {mycli-2.30.3 → mycli-2.31.0}/MANIFEST.in +0 -0
  59. {mycli-2.30.3 → mycli-2.31.0}/README.md +0 -0
  60. {mycli-2.30.3 → mycli-2.31.0}/SPONSORS.md +0 -0
  61. {mycli-2.30.3 → mycli-2.31.0}/doc/cookbook.md +0 -0
  62. {mycli-2.30.3 → mycli-2.31.0}/doc/key_bindings.md +0 -0
  63. {mycli-2.30.3 → mycli-2.31.0}/doc/known_bugs.md +0 -0
  64. {mycli-2.30.3 → mycli-2.31.0}/doc/llm.md +0 -0
  65. {mycli-2.30.3 → mycli-2.31.0}/doc/screenshots/main.gif +0 -0
  66. {mycli-2.30.3 → mycli-2.31.0}/doc/screenshots/polars_describe.png +0 -0
  67. {mycli-2.30.3 → mycli-2.31.0}/doc/screenshots/polars_group_by.png +0 -0
  68. {mycli-2.30.3 → mycli-2.31.0}/doc/screenshots/polars_histogram.png +0 -0
  69. {mycli-2.30.3 → mycli-2.31.0}/doc/screenshots/polars_pivot.png +0 -0
  70. {mycli-2.30.3 → mycli-2.31.0}/doc/screenshots/tables.png +0 -0
  71. {mycli-2.30.3 → mycli-2.31.0}/doc/transforms.md +0 -0
  72. {mycli-2.30.3 → mycli-2.31.0}/docker/Dockerfile +0 -0
  73. {mycli-2.30.3 → mycli-2.31.0}/docker/resources/dot_mycli_history.txt +0 -0
  74. {mycli-2.30.3 → mycli-2.31.0}/docker/resources/test_db_license.md +0 -0
  75. {mycli-2.30.3 → mycli-2.31.0}/mycli/SPONSORS.txt +0 -0
  76. {mycli-2.30.3 → mycli-2.31.0}/mycli/TIPS.txt +0 -0
  77. {mycli-2.30.3 → mycli-2.31.0}/mycli/__init__.py +0 -0
  78. {mycli-2.30.3 → mycli-2.31.0}/mycli/app_state.py +0 -0
  79. {mycli-2.30.3 → mycli-2.31.0}/mycli/compat.py +0 -0
  80. {mycli-2.30.3 → mycli-2.31.0}/mycli/config.py +0 -0
  81. {mycli-2.30.3 → mycli-2.31.0}/mycli/main.py +0 -0
  82. {mycli-2.30.3 → mycli-2.31.0}/mycli/main_modes/checkup.py +0 -0
  83. {mycli-2.30.3 → mycli-2.31.0}/mycli/main_modes/completions.py +0 -0
  84. {mycli-2.30.3 → mycli-2.31.0}/mycli/main_modes/execute.py +0 -0
  85. {mycli-2.30.3 → mycli-2.31.0}/mycli/main_modes/list_dsn.py +0 -0
  86. {mycli-2.30.3 → mycli-2.31.0}/mycli/myclirc +0 -0
  87. {mycli-2.30.3 → mycli-2.31.0}/mycli/output.py +0 -0
  88. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/__init__.py +0 -0
  89. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/completion/__init__.py +0 -0
  90. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/completion/completion_engine.py +0 -0
  91. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/completion/completion_refresher.py +0 -0
  92. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/completion/schema_prefetcher.py +0 -0
  93. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/completion/sql_completer.py +0 -0
  94. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/credentials/__init__.py +0 -0
  95. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/credentials/password_sources.py +0 -0
  96. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/dataframes/__init__.py +0 -0
  97. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/dataframes/completion.py +0 -0
  98. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/dataframes/transform.py +0 -0
  99. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/execution/__init__.py +0 -0
  100. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/execution/background_runner.py +0 -0
  101. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/__init__.py +0 -0
  102. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/boundary/__init__.py +0 -0
  103. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/boundary/boundary_tunnel.py +0 -0
  104. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/fzf/__init__.py +0 -0
  105. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/fzf/fzf.py +0 -0
  106. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/kubernetes/__init__.py +0 -0
  107. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/kubernetes/kubectl_tunnel.py +0 -0
  108. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/macos/__init__.py +0 -0
  109. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/macos/macos_keychain.py +0 -0
  110. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/openssh/__init__.py +0 -0
  111. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/openssh/ssh_tunnel.py +0 -0
  112. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/vault/__init__.py +0 -0
  113. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/integrations/vault/vault_credentials.py +0 -0
  114. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/__init__.py +0 -0
  115. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/fzf_history.py +0 -0
  116. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/history.py +0 -0
  117. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/key_bindings.py +0 -0
  118. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/multiline.py +0 -0
  119. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/style.py +0 -0
  120. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/toolbar.py +0 -0
  121. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/prompt_toolkit/utils.py +0 -0
  122. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/pygments/__init__.py +0 -0
  123. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/pygments/mycli_lexer.py +0 -0
  124. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/redirection/__init__.py +0 -0
  125. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/redirection/hybrid_redirection.py +0 -0
  126. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/__init__.py +0 -0
  127. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/db_commands.py +0 -0
  128. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/delimiter_command.py +0 -0
  129. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/dsn_aliases.py +0 -0
  130. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/favorite_queries.py +0 -0
  131. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/llm.py +0 -0
  132. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/main.py +0 -0
  133. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/special_commands/source.py +0 -0
  134. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/sql_result/__init__.py +0 -0
  135. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/sql_result/sql_result.py +0 -0
  136. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/tabular_output/__init__.py +0 -0
  137. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  138. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/__init__.py +0 -0
  139. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/cli_utils.py +0 -0
  140. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/dsn_utils.py +0 -0
  141. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/interactive_utils.py +0 -0
  142. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/key_binding_utils.py +0 -0
  143. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/keyring_utils.py +0 -0
  144. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/path_utils.py +0 -0
  145. {mycli-2.30.3 → mycli-2.31.0}/mycli/packages/utils/sql_utils.py +0 -0
  146. {mycli-2.30.3 → mycli-2.31.0}/mycli/resources/completions/bash/mycli +0 -0
  147. {mycli-2.30.3 → mycli-2.31.0}/mycli/resources/completions/fish/mycli.fish +0 -0
  148. {mycli-2.30.3 → mycli-2.31.0}/mycli/resources/completions/zsh/_mycli +0 -0
  149. {mycli-2.30.3 → mycli-2.31.0}/mycli/types.py +0 -0
  150. {mycli-2.30.3 → mycli-2.31.0}/mycli.egg-info/dependency_links.txt +0 -0
  151. {mycli-2.30.3 → mycli-2.31.0}/mycli.egg-info/entry_points.txt +0 -0
  152. {mycli-2.30.3 → mycli-2.31.0}/mycli.egg-info/top_level.txt +0 -0
  153. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/__init__.py +0 -0
  154. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/__init__.py +0 -0
  155. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/auto_vertical.feature +0 -0
  156. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/basic_commands.feature +0 -0
  157. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/connection.feature +0 -0
  158. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/crud_database.feature +0 -0
  159. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/crud_table.feature +0 -0
  160. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/db_utils.py +0 -0
  161. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/environment.py +0 -0
  162. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/fixture_data/help_commands.txt +0 -0
  163. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/fixture_utils.py +0 -0
  164. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/iocommands.feature +0 -0
  165. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/named_queries.feature +0 -0
  166. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/specials.feature +0 -0
  167. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/__init__.py +0 -0
  168. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/auto_vertical.py +0 -0
  169. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/basic_commands.py +0 -0
  170. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/connection.py +0 -0
  171. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/crud_database.py +0 -0
  172. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/crud_table.py +0 -0
  173. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/iocommands.py +0 -0
  174. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/named_queries.py +0 -0
  175. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/specials.py +0 -0
  176. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/utils.py +0 -0
  177. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/steps/wrappers.py +0 -0
  178. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/features/wrappager.py +0 -0
  179. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/myclirc +0 -0
  180. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/mylogin.cnf +0 -0
  181. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/conftest.py +0 -0
  182. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_app_state.py +0 -0
  183. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_background_runner.py +0 -0
  184. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_bash_completion.py +0 -0
  185. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_boundary_tunnel.py +0 -0
  186. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_checkup.py +0 -0
  187. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_cli_utils.py +0 -0
  188. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_clibuffer.py +0 -0
  189. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_completer_use_switch.py +0 -0
  190. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_completion_engine.py +0 -0
  191. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_completion_refresher.py +0 -0
  192. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_config.py +0 -0
  193. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_delimitercommand.py +0 -0
  194. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_dsn_aliases.py +0 -0
  195. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_favoritequeries.py +0 -0
  196. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_fish_completion.py +0 -0
  197. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_fzf.py +0 -0
  198. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_hybrid_redirection.py +0 -0
  199. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_interactive_utils.py +0 -0
  200. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_key_binding_utils.py +0 -0
  201. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_key_bindings.py +0 -0
  202. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_keyring_utils.py +0 -0
  203. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_kubectl_tunnel.py +0 -0
  204. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_macos_keychain.py +0 -0
  205. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_execute.py +0 -0
  206. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_list_dsn.py +0 -0
  207. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_naive_completion.py +0 -0
  208. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_output.py +0 -0
  209. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_password_sources.py +0 -0
  210. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_path_utils.py +0 -0
  211. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_polars_completion.py +0 -0
  212. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_polars_transform.py +0 -0
  213. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_fzf_history.py +0 -0
  214. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_history.py +0 -0
  215. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_style.py +0 -0
  216. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_toolbar.py +0 -0
  217. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_utils.py +0 -0
  218. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_schema_prefetcher.py +0 -0
  219. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_smart_completion_public_schema_only.py +0 -0
  220. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_special_commands_init.py +0 -0
  221. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_special_db_commands.py +0 -0
  222. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_special_llm.py +0 -0
  223. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_special_source.py +0 -0
  224. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_sql_utils.py +0 -0
  225. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_sqlcompleter.py +0 -0
  226. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_sqlresult.py +0 -0
  227. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_ssh_tunnel.py +0 -0
  228. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_tabular_output.py +0 -0
  229. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_vault.py +0 -0
  230. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/pytests/test_zsh_completion.py +0 -0
  231. {mycli-2.30.3 → mycli-2.31.0}/mycli_test/utils.py +0 -0
  232. {mycli-2.30.3 → mycli-2.31.0}/setup.cfg +0 -0
@@ -130,7 +130,7 @@ jobs:
130
130
  run: uv sync --extra dev,llm,dataframe -p ${{ matrix.python-version }}
131
131
 
132
132
  - name: Set up MySQL
133
- uses: shogo82148/actions-setup-mysql@fb8ae72ad9f2f8244e594d6d48a981e975aee78e # v1.56.1
133
+ uses: shogo82148/actions-setup-mysql@13a38f2af408e58a0c80168fc620a63a15357339 # v1.56.2
134
134
  with:
135
135
  mysql-version: '9.7'
136
136
  root-password: root
@@ -166,7 +166,7 @@ jobs:
166
166
  run: |
167
167
  mkdir -p /root/mycli
168
168
 
169
- - uses: ubuntu/wsl/.github/actions/wsl-checkout@e7fb23c6481945b25b974b6cf7a5c7f4550b6cf8
169
+ - uses: ubuntu/wsl/.github/actions/wsl-checkout@c1984d32aea99733825250f86df5f9f6d7e284c3
170
170
  with:
171
171
  distro: Ubuntu-26.04
172
172
  working-dir: /root/mycli
@@ -158,7 +158,7 @@ jobs:
158
158
  run: uv sync --extra dev,llm,dataframe --python ${{ matrix.python-version }}
159
159
 
160
160
  - name: Set up MySQL
161
- uses: shogo82148/actions-setup-mysql@fb8ae72ad9f2f8244e594d6d48a981e975aee78e # v1.56.1
161
+ uses: shogo82148/actions-setup-mysql@13a38f2af408e58a0c80168fc620a63a15357339 # v1.56.2
162
162
  with:
163
163
  mysql-version: '9.7'
164
164
  root-password: root
@@ -195,7 +195,7 @@ jobs:
195
195
  run: |
196
196
  mkdir -p /root/mycli
197
197
 
198
- - uses: ubuntu/wsl/.github/actions/wsl-checkout@e7fb23c6481945b25b974b6cf7a5c7f4550b6cf8
198
+ - uses: ubuntu/wsl/.github/actions/wsl-checkout@c1984d32aea99733825250f86df5f9f6d7e284c3
199
199
  with:
200
200
  distro: Ubuntu-26.04
201
201
  working-dir: /root/mycli
@@ -309,7 +309,7 @@ jobs:
309
309
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
310
310
 
311
311
  - name: Log in to the Container registry
312
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
312
+ uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4.8.0
313
313
  with:
314
314
  registry: ${{ env.REGISTRY }}
315
315
  username: ${{ github.actor }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.30.3
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
@@ -33,8 +33,8 @@ Requires-Dist: clickdc~=0.1.1
33
33
  Requires-Dist: cryptography~=50.0.2
34
34
  Requires-Dist: Pygments~=2.21.0
35
35
  Requires-Dist: prompt_toolkit~=3.0.53
36
- Requires-Dist: PyMySQL~=1.2.3
37
- Requires-Dist: sqlparse~=0.6.0
36
+ Requires-Dist: PyMySQL~=1.2.4
37
+ Requires-Dist: sqlparse~=0.6.1
38
38
  Requires-Dist: sqlglot~=30.25.0
39
39
  Requires-Dist: sqlglotc~=30.25.0
40
40
  Requires-Dist: configobj~=5.0.9
@@ -48,9 +48,9 @@ Requires-Dist: jinja2~=3.1.6
48
48
  Requires-Dist: markupsafe~=3.0.4
49
49
  Requires-Dist: yaspin~=3.5.1
50
50
  Provides-Extra: llm
51
- Requires-Dist: llm~=0.36; extra == "llm"
51
+ Requires-Dist: llm~=0.37.1; extra == "llm"
52
52
  Requires-Dist: setuptools==84.*; extra == "llm"
53
- Requires-Dist: pip~=26.2.2; extra == "llm"
53
+ Requires-Dist: pip~=26.3; extra == "llm"
54
54
  Provides-Extra: dataframe
55
55
  Requires-Dist: polars~=2.1.0; extra == "dataframe"
56
56
  Requires-Dist: altair~=6.3.0; extra == "dataframe"
@@ -59,8 +59,8 @@ Provides-Extra: all
59
59
  Requires-Dist: mycli[dataframe,llm]; extra == "all"
60
60
  Provides-Extra: dev
61
61
  Requires-Dist: behave~=1.3.3; extra == "dev"
62
- Requires-Dist: coverage~=7.17.0; extra == "dev"
63
- Requires-Dist: mypy~=2.4.0; extra == "dev"
62
+ Requires-Dist: coverage~=7.17.2; extra == "dev"
63
+ Requires-Dist: mypy~=2.4.1; extra == "dev"
64
64
  Requires-Dist: pexpect~=4.9.0; extra == "dev"
65
65
  Requires-Dist: pytest~=9.2.0; extra == "dev"
66
66
  Requires-Dist: pytest-cov~=7.1.0; extra == "dev"
@@ -1,3 +1,43 @@
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
+
19
+ v2.30.4 (2026/11/12)
20
+ ==============
21
+
22
+ Bug Fixes
23
+ --------
24
+ * Decode percent-encoded database names in DSN URIs, including aliases with environment variable expansion enabled.
25
+
26
+
27
+ Internal
28
+ --------
29
+ * Improve test coverage for `packages/special_commands/main.py`.
30
+ * Improve test coverage for `packages/special_commands/io_commands.py`.
31
+ * Improve test coverage for `packages/execution/sql_execute.py`.
32
+ * Improve test coverage for `packages/tabular_output/sql_format.py`.
33
+ * Update `pymysql` dependency to v1.2.4.
34
+ * Update `sqlparse` dependency to v0.6.1.
35
+ * Update `llm` dependency to v0.37.1.
36
+ * Update `pip` dependency to v26.3.
37
+ * Update `coverage` dev dependency to v7.17.2.
38
+ * Update `mypy` dev dependency to v2.4.1.
39
+
40
+
1
41
  v2.30.3 (2026/11/05)
2
42
  ==============
3
43
 
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: mycli-2.30.3/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
@@ -122,6 +122,10 @@ Contributors:
122
122
  * Chris Burr
123
123
  * Wei-Lun Li
124
124
  * Shaurya Kesarwani
125
+ * Alex Castillo
126
+ * Karim Masmoudi
127
+ * Jay Hemnani
128
+ * Alpenj
125
129
 
126
130
 
127
131
  Created by:
@@ -117,7 +117,7 @@ def expand_dsn_alias_env_vars(
117
117
  expand_dsn_alias_env_var(unquote(password) if password is not None else None, alias_name),
118
118
  expand_dsn_alias_env_var(host, alias_name),
119
119
  port_number,
120
- expand_dsn_alias_env_var(uri.path[1:], alias_name) or '',
120
+ expand_dsn_alias_env_var(unquote(uri.path[1:]), alias_name) or '',
121
121
  params,
122
122
  )
123
123
 
@@ -310,7 +310,7 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory | None
310
310
  dsn_password = unquote(uri.password) if uri.password is not None else None
311
311
  dsn_host = uri.hostname
312
312
  dsn_port = uri.port
313
- dsn_database = uri.path[1:]
313
+ dsn_database = unquote(uri.path[1:])
314
314
  dsn_params = parse_qs(uri.query, keep_blank_values=True) if uri.query else {}
315
315
 
316
316
  if not database:
@@ -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
@@ -10,7 +10,7 @@ import click
10
10
  import keyring
11
11
  import pymysql
12
12
  from pymysql.constants.CR import CR_SERVER_LOST
13
- from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR
13
+ from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR, MUST_CHANGE_PASSWORD_LOGIN
14
14
 
15
15
  from mycli.compat import WIN
16
16
  from mycli.config import str_to_bool
@@ -19,7 +19,6 @@ from mycli.constants import (
19
19
  DEFAULT_HOST,
20
20
  DEFAULT_PORT,
21
21
  EMPTY_PASSWORD_FLAG_SENTINEL,
22
- ER_MUST_CHANGE_PASSWORD_LOGIN,
23
22
  )
24
23
  from mycli.packages.credentials.password_sources import PasswordCandidates
25
24
  from mycli.packages.execution.sql_execute import SQLExecute
@@ -433,7 +432,7 @@ class ClientConnectionMixin:
433
432
  keyring_retrieved_cleanly=keyring_retrieved_cleanly,
434
433
  keyring_save_eligible=keyring_save_eligible,
435
434
  )
436
- elif e1.args[0] == ER_MUST_CHANGE_PASSWORD_LOGIN:
435
+ elif e1.args[0] == MUST_CHANGE_PASSWORD_LOGIN:
437
436
  self.echo(
438
437
  "Your password has expired and the server rejected the connection.",
439
438
  err=True,
@@ -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
@@ -38,10 +38,6 @@ TEST_DATABASE = 'mycli_test_db'
38
38
  DEFAULT_WIDTH = 80
39
39
  DEFAULT_HEIGHT = 25
40
40
 
41
- # MySQL error codes not available in pymysql.constants.ER
42
- ER_MUST_CHANGE_PASSWORD_LOGIN = 1862
43
- ER_MUST_CHANGE_PASSWORD = 1820
44
-
45
41
  EMPTY_PASSWORD_FLAG_SENTINEL = -1
46
42
  DEFAULT_PROMPT = "\\t \\u@\\h:\\d> "
47
43
 
@@ -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
@@ -49,6 +49,7 @@ from pymysql.constants.CR import (
49
49
  CR_SERVER_GONE_ERROR,
50
50
  CR_SERVER_LOST,
51
51
  )
52
+ from pymysql.constants.ER import MUST_CHANGE_PASSWORD
52
53
  from pymysql.constants.SERVER_STATUS import SERVER_STATUS_IN_TRANS
53
54
  from pymysql.cursors import Cursor
54
55
 
@@ -57,7 +58,6 @@ from mycli.compat import WIN
57
58
  from mycli.constants import (
58
59
  DEFAULT_HOST,
59
60
  DEFAULT_WIDTH,
60
- ER_MUST_CHANGE_PASSWORD,
61
61
  HOME_URL,
62
62
  ISSUES_URL,
63
63
  QueryState,
@@ -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('\\\\')
@@ -1110,7 +1112,7 @@ def _one_iteration(
1110
1112
  mycli.echo(str(exc), err=True, fg='red')
1111
1113
  except pymysql.OperationalError as e1:
1112
1114
  mycli.logger.debug('Exception: %r', e1)
1113
- if e1.args[0] == ER_MUST_CHANGE_PASSWORD:
1115
+ if e1.args[0] == MUST_CHANGE_PASSWORD:
1114
1116
  mycli.sandbox_mode = True
1115
1117
  mycli.echo(
1116
1118
  "ERROR 1820: You must reset your password using ALTER USER or SET PASSWORD before executing this statement.",
@@ -12,10 +12,10 @@ import pymysql
12
12
  from pymysql.connections import Connection
13
13
  from pymysql.constants import FIELD_TYPE
14
14
  from pymysql.constants.CR import CR_SSL_CONNECTION_ERROR
15
+ from pymysql.constants.ER import MUST_CHANGE_PASSWORD
15
16
  from pymysql.converters import conversions, convert_date, convert_datetime, convert_time, decoders
16
17
  from pymysql.cursors import Cursor, SSCursor
17
18
 
18
- from mycli.constants import ER_MUST_CHANGE_PASSWORD
19
19
  from mycli.packages.execution.background_runner import BackgroundRunner
20
20
  from mycli.packages.special_commands import io_commands
21
21
  from mycli.packages.special_commands.main import CommandNotFound, execute
@@ -296,9 +296,9 @@ class SQLExecute:
296
296
  connect_kwargs['ssl_disabled'] = True
297
297
  conn = pymysql.connect(**connect_kwargs) # type: ignore[misc]
298
298
  except pymysql.OperationalError as e:
299
- if e.args[0] == ER_MUST_CHANGE_PASSWORD:
299
+ if e.args[0] == MUST_CHANGE_PASSWORD:
300
300
  # Post-handshake queries (SET NAMES, SET AUTOCOMMIT, init_command)
301
- # fail with ER_MUST_CHANGE_PASSWORD in sandbox mode.
301
+ # fail with MUST_CHANGE_PASSWORD in sandbox mode.
302
302
  # Reconnect with only the raw handshake.
303
303
  connect_kwargs['defer_connect'] = True
304
304
  connect_kwargs['autocommit'] = None
@@ -659,7 +659,7 @@ class SQLExecute:
659
659
  """Connect in sandbox mode, performing only the handshake.
660
660
 
661
661
  pymysql's normal connect() runs post-handshake queries (SET NAMES,
662
- SET AUTOCOMMIT, init_command) that all fail with ER_MUST_CHANGE_PASSWORD
662
+ SET AUTOCOMMIT, init_command) that all fail with MUST_CHANGE_PASSWORD
663
663
  in sandbox mode. This method performs the raw socket connection and
664
664
  authentication handshake only.
665
665
  """
@@ -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.3
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
@@ -33,8 +33,8 @@ Requires-Dist: clickdc~=0.1.1
33
33
  Requires-Dist: cryptography~=50.0.2
34
34
  Requires-Dist: Pygments~=2.21.0
35
35
  Requires-Dist: prompt_toolkit~=3.0.53
36
- Requires-Dist: PyMySQL~=1.2.3
37
- Requires-Dist: sqlparse~=0.6.0
36
+ Requires-Dist: PyMySQL~=1.2.4
37
+ Requires-Dist: sqlparse~=0.6.1
38
38
  Requires-Dist: sqlglot~=30.25.0
39
39
  Requires-Dist: sqlglotc~=30.25.0
40
40
  Requires-Dist: configobj~=5.0.9
@@ -48,9 +48,9 @@ Requires-Dist: jinja2~=3.1.6
48
48
  Requires-Dist: markupsafe~=3.0.4
49
49
  Requires-Dist: yaspin~=3.5.1
50
50
  Provides-Extra: llm
51
- Requires-Dist: llm~=0.36; extra == "llm"
51
+ Requires-Dist: llm~=0.37.1; extra == "llm"
52
52
  Requires-Dist: setuptools==84.*; extra == "llm"
53
- Requires-Dist: pip~=26.2.2; extra == "llm"
53
+ Requires-Dist: pip~=26.3; extra == "llm"
54
54
  Provides-Extra: dataframe
55
55
  Requires-Dist: polars~=2.1.0; extra == "dataframe"
56
56
  Requires-Dist: altair~=6.3.0; extra == "dataframe"
@@ -59,8 +59,8 @@ Provides-Extra: all
59
59
  Requires-Dist: mycli[dataframe,llm]; extra == "all"
60
60
  Provides-Extra: dev
61
61
  Requires-Dist: behave~=1.3.3; extra == "dev"
62
- Requires-Dist: coverage~=7.17.0; extra == "dev"
63
- Requires-Dist: mypy~=2.4.0; extra == "dev"
62
+ Requires-Dist: coverage~=7.17.2; extra == "dev"
63
+ Requires-Dist: mypy~=2.4.1; extra == "dev"
64
64
  Requires-Dist: pexpect~=4.9.0; extra == "dev"
65
65
  Requires-Dist: pytest~=9.2.0; extra == "dev"
66
66
  Requires-Dist: pytest-cov~=7.1.0; extra == "dev"
@@ -216,6 +216,7 @@ mycli_test/pytests/test_special_main.py
216
216
  mycli_test/pytests/test_special_source.py
217
217
  mycli_test/pytests/test_special_utils.py
218
218
  mycli_test/pytests/test_sql_execute.py
219
+ mycli_test/pytests/test_sql_format.py
219
220
  mycli_test/pytests/test_sql_utils.py
220
221
  mycli_test/pytests/test_sqlcompleter.py
221
222
  mycli_test/pytests/test_sqlresult.py
@@ -3,8 +3,8 @@ clickdc~=0.1.1
3
3
  cryptography~=50.0.2
4
4
  Pygments~=2.21.0
5
5
  prompt_toolkit~=3.0.53
6
- PyMySQL~=1.2.3
7
- sqlparse~=0.6.0
6
+ PyMySQL~=1.2.4
7
+ sqlparse~=0.6.1
8
8
  sqlglot~=30.25.0
9
9
  sqlglotc~=30.25.0
10
10
  configobj~=5.0.9
@@ -28,8 +28,8 @@ vl-convert-python~=1.9.0
28
28
 
29
29
  [dev]
30
30
  behave~=1.3.3
31
- coverage~=7.17.0
32
- mypy~=2.4.0
31
+ coverage~=7.17.2
32
+ mypy~=2.4.1
33
33
  pexpect~=4.9.0
34
34
  pytest~=9.2.0
35
35
  pytest-cov~=7.1.0
@@ -39,6 +39,6 @@ pdbpp~=0.12.1
39
39
  ruff~=0.17.3
40
40
 
41
41
  [llm]
42
- llm~=0.36
42
+ llm~=0.37.1
43
43
  setuptools==84.*
44
- pip~=26.2.2
44
+ pip~=26.3
@@ -208,6 +208,7 @@
208
208
  "mycli_test/pytests/test_special_source.py",
209
209
  "mycli_test/pytests/test_special_utils.py",
210
210
  "mycli_test/pytests/test_sql_execute.py",
211
+ "mycli_test/pytests/test_sql_format.py",
211
212
  "mycli_test/pytests/test_sql_utils.py",
212
213
  "mycli_test/pytests/test_sqlcompleter.py",
213
214
  "mycli_test/pytests/test_sqlresult.py",
@@ -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
+ ]