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.
- {mycli-2.30.4/mycli.egg-info → mycli-2.31.0}/PKG-INFO +1 -1
- {mycli-2.30.4 → mycli-2.31.0}/changelog.md +18 -0
- {mycli-2.30.4 → mycli-2.31.0}/docker/resources/test_db-1.0.7.tar.gz +1 -1
- {mycli-2.30.4 → mycli-2.31.0}/mycli/AUTHORS.txt +3 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/client.py +1 -1
- {mycli-2.30.4 → mycli-2.31.0}/mycli/client_commands.py +1 -1
- {mycli-2.30.4 → mycli-2.31.0}/mycli/client_query.py +1 -1
- {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/batch.py +12 -4
- {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/repl.py +3 -1
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/io_commands.py +4 -4
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/special_command_utils.py +3 -1
- mycli-2.31.0/mycli/packages/utils/batch_utils.py +71 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/string_utils.py +11 -0
- {mycli-2.30.4 → mycli-2.31.0/mycli.egg-info}/PKG-INFO +1 -1
- mycli-2.31.0/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_batch_utils.py +29 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client.py +1 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client_commands.py +33 -6
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client_query.py +8 -2
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_batch.py +47 -7
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_repl.py +22 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_io_commands.py +40 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_utils.py +18 -0
- mycli-2.31.0/mycli_test/pytests/test_string_utils.py +44 -0
- mycli-2.30.4/mycli/packages/utils/batch_utils.py +0 -36
- mycli-2.30.4/mycli.egg-info/scm_version.json +0 -8
- mycli-2.30.4/mycli_test/pytests/test_string_utils.py +0 -27
- {mycli-2.30.4 → mycli-2.31.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/actions/lint/action.yml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/actions/typecheck/action.yml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/dependabot.yml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/ci.yml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/lint.yml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/publish.yml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/.gitignore +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/AGENTS.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/AUTHORS.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/CONTRIBUTING.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/LICENSE.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/MANIFEST.in +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/README.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/SPONSORS.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/cookbook.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/key_bindings.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/known_bugs.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/llm.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_describe.png +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_group_by.png +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_histogram.png +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/polars_pivot.png +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/doc/transforms.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/docker/Dockerfile +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/docker/resources/dot_mycli_history.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/docker/resources/test_db_license.md +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/SPONSORS.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/TIPS.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/app_state.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/cli_runner.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/client_connection.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/compat.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/config.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/constants.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/main.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/completions.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/execute.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/myclirc +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/output.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/completion_engine.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/completion_refresher.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/schema_prefetcher.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/completion/sql_completer.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/credentials/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/credentials/password_sources.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/dataframes/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/dataframes/completion.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/dataframes/transform.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/execution/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/execution/background_runner.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/execution/sql_execute.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/boundary/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/boundary/boundary_tunnel.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/fzf/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/fzf/fzf.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/kubernetes/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/kubernetes/kubectl_tunnel.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/macos/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/macos/macos_keychain.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/openssh/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/openssh/ssh_tunnel.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/vault/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/integrations/vault/vault_credentials.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/fzf_history.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/history.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/key_bindings.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/multiline.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/style.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/toolbar.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/prompt_toolkit/utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/pygments/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/pygments/mycli_lexer.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/redirection/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/redirection/hybrid_redirection.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/db_commands.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/delimiter_command.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/dsn_aliases.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/favorite_queries.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/llm.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/main.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/special_commands/source.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/sql_result/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/sql_result/sql_result.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/cli_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/dsn_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/interactive_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/key_binding_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/keyring_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/path_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/packages/utils/sql_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/resources/completions/bash/mycli +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/resources/completions/fish/mycli.fish +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/resources/completions/zsh/_mycli +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli/types.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/SOURCES.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/requires.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/scm_file_list.json +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/auto_vertical.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/basic_commands.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/connection.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/crud_database.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/crud_table.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/db_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/environment.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/fixture_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/iocommands.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/named_queries.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/specials.feature +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/__init__.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/auto_vertical.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/basic_commands.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/connection.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/crud_database.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/crud_table.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/iocommands.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/named_queries.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/specials.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/steps/wrappers.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/features/wrappager.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/myclirc +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/mylogin.cnf +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/conftest.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_app_state.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_background_runner.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_bash_completion.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_checkup.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_cli_runner.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_cli_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_clibuffer.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_client_connection.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_completion_engine.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_config.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_fish_completion.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_fzf.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_key_bindings.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_keyring_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_kubectl_tunnel.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_macos_keychain.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_naive_completion.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_output.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_password_sources.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_path_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_polars_completion.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_polars_transform.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_fzf_history.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_history.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_style.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_toolbar.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_prompt_toolkit_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_smart_completion_public_schema_only.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_commands_init.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_db_commands.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_llm.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_main.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_special_source.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sql_execute.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sql_format.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sql_utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sqlcompleter.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_sqlresult.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_tabular_output.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_vault.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/mycli_test/utils.py +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/pyproject.toml +0 -0
- {mycli-2.30.4 → mycli-2.31.0}/setup.cfg +0 -0
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
|
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
|
|
@@ -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,
|
|
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(
|