mycli 2.15.0__tar.gz → 2.16.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.15.0/mycli.egg-info → mycli-2.16.0}/PKG-INFO +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/changelog.md +17 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/TIPS +4 -4
- {mycli-2.15.0 → mycli-2.16.0}/mycli/client_commands.py +167 -7
- {mycli-2.15.0 → mycli-2.16.0}/mycli/client_query.py +9 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/main.py +0 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/execute.py +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/repl.py +146 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/completion_engine.py +38 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/filepaths.py +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/iocommands.py +2 -2
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/main.py +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/sqlresult.py +3 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/resources/completions/bash/mycli +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/resources/completions/fish/mycli.fish +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/resources/completions/zsh/_mycli +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/mycli/sqlcompleter.py +40 -1
- {mycli-2.15.0 → mycli-2.16.0/mycli.egg-info}/PKG-INFO +1 -1
- mycli-2.16.0/mycli.egg-info/scm_version.json +8 -0
- mycli-2.16.0/test/features/fixture_data/help_commands.txt +42 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client_commands.py +345 -3
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client_query.py +41 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_completion_engine.py +37 -4
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_filepaths.py +1 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main.py +2 -2
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_execute.py +6 -6
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_repl.py +342 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_smart_completion_public_schema_only.py +76 -9
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_main.py +1 -1
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sqlresult.py +2 -0
- mycli-2.15.0/mycli.egg-info/scm_version.json +0 -8
- mycli-2.15.0/test/features/fixture_data/help_commands.txt +0 -42
- {mycli-2.15.0 → mycli-2.16.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/dependabot.yml +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/ci.yml +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/lint.yml +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/publish.yml +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/.gitignore +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/AGENTS.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/AUTHORS.rst +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/CONTRIBUTING.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/LICENSE.txt +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/MANIFEST.in +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/README.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/SPONSORS.rst +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/doc/key_bindings.rst +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/doc/llm.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/doc/screenshots/total_histogram.png +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/doc/transforms.md +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/AUTHORS +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/SPONSORS +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/app_state.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/boundary_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/cli_runner.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/clibuffer.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/client.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/client_connection.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/clistyle.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/clitoolbar.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/compat.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/completion_refresher.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/config.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/constants.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/key_bindings.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/lexer.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/batch.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/completions.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/myclirc +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/output.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/polars_transform.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/llm.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/string_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/password_sources.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/sqlexecute.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/types.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli/vault.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/SOURCES.txt +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/requires.txt +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/scm_file_list.json +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/pyproject.toml +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/setup.cfg +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/auto_vertical.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/basic_commands.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/connection.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/crud_database.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/crud_table.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/db_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/environment.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/fixture_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/iocommands.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/named_queries.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/specials.feature +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/connection.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/crud_database.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/crud_table.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/iocommands.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/named_queries.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/specials.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/wrappers.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/features/wrappager.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/myclirc +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/mylogin.cnf +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/conftest.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_app_state.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_checkup.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_config.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_lexer.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_output.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_polars_transform.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_init.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sqlcompleter.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_vault.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.16.0}/test/utils.py +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
2.16.0 (2026/08/22)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Features
|
|
5
|
+
---------
|
|
6
|
+
* Advertise beta `--boundary-id` option in helpdoc and completions.
|
|
7
|
+
* Add `/source --special` to allow executing some special commands.
|
|
8
|
+
* Add `/source --show` to display each query before executing it.
|
|
9
|
+
* Add `/source --page` to display all output in one pager session.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Bug Fixes
|
|
13
|
+
---------
|
|
14
|
+
* Show CLI error on invalid `--execute` containing `/source`.
|
|
15
|
+
* Require `/source` filenames containing spaces to be quoted.
|
|
16
|
+
|
|
17
|
+
|
|
1
18
|
2.15.0 (2026/08/20)
|
|
2
19
|
==============
|
|
3
20
|
|
|
@@ -54,7 +54,7 @@ copy the previous query to the clipboard with /clip!
|
|
|
54
54
|
|
|
55
55
|
edit a query in an external editor using <query>\edit!
|
|
56
56
|
|
|
57
|
-
edit a query in an external editor using /edit <
|
|
57
|
+
edit a query in an external editor using /edit <file>!
|
|
58
58
|
|
|
59
59
|
/f lists favorite queries; /f <name> executes a favorite!
|
|
60
60
|
|
|
@@ -68,7 +68,7 @@ Manage favorite queries with /favorite!
|
|
|
68
68
|
|
|
69
69
|
/l lists databases!
|
|
70
70
|
|
|
71
|
-
/once <
|
|
71
|
+
/once <file> appends the next result to <file>!
|
|
72
72
|
|
|
73
73
|
/| <command> sends the next result to a subprocess!
|
|
74
74
|
|
|
@@ -306,9 +306,9 @@ customize password sources/precedence with "password_sources" in ~/.myclirc!
|
|
|
306
306
|
|
|
307
307
|
redirect query output to a shell command with "$| <command>"!
|
|
308
308
|
|
|
309
|
-
redirect query output to a CSV file with "$> <
|
|
309
|
+
redirect query output to a CSV file with "$> <file>"!
|
|
310
310
|
|
|
311
|
-
append query output to a CSV file with "$>> <
|
|
311
|
+
append query output to a CSV file with "$>> <file>"!
|
|
312
312
|
|
|
313
313
|
run a command after shell redirects with "post_redirect_command" in ~/.myclirc!
|
|
314
314
|
|
|
@@ -4,16 +4,21 @@ from collections.abc import Generator, Mapping
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import shlex
|
|
7
8
|
from typing import TYPE_CHECKING, Any, cast
|
|
8
9
|
|
|
9
10
|
import click
|
|
11
|
+
import sqlparse
|
|
10
12
|
|
|
13
|
+
from mycli.compat import WIN
|
|
11
14
|
from mycli.config import write_default_config
|
|
12
15
|
from mycli.main_modes.repl import set_all_external_titles
|
|
13
16
|
from mycli.packages import special
|
|
14
17
|
from mycli.packages.batch_utils import statements_from_filehandle
|
|
15
18
|
from mycli.packages.filepaths import dir_path_exists
|
|
16
19
|
from mycli.packages.interactive_utils import confirm_destructive_query
|
|
20
|
+
from mycli.packages.special import main as special_main
|
|
21
|
+
from mycli.packages.special.iocommands import expand_favorite_query
|
|
17
22
|
from mycli.packages.special.main import ArgType, SpecialCommandAlias
|
|
18
23
|
from mycli.packages.sqlresult import SQLResult
|
|
19
24
|
from mycli.sqlexecute import SQLExecute
|
|
@@ -30,6 +35,29 @@ MISSING_CONFIG_VALUE = object()
|
|
|
30
35
|
DSN_CONFIG_VALUE = object()
|
|
31
36
|
FAVORITES_CONFIG_VALUE = object()
|
|
32
37
|
HIDDEN_CONFIG_SECTIONS = frozenset({'alias_dsn', 'favorite_queries'})
|
|
38
|
+
INVALID_SOURCE_FILENAME = 'Source accepts exactly one filename; filenames containing spaces must be quoted.'
|
|
39
|
+
SOURCE_SAFE_SPECIAL_COMMANDS = frozenset({
|
|
40
|
+
'connect',
|
|
41
|
+
'fd',
|
|
42
|
+
'fs',
|
|
43
|
+
'help',
|
|
44
|
+
'l',
|
|
45
|
+
'nowarnings',
|
|
46
|
+
'prompt',
|
|
47
|
+
'redirectformat',
|
|
48
|
+
'rehash',
|
|
49
|
+
'status',
|
|
50
|
+
'tableformat',
|
|
51
|
+
'timing',
|
|
52
|
+
'use',
|
|
53
|
+
'warnings',
|
|
54
|
+
'dt',
|
|
55
|
+
})
|
|
56
|
+
SOURCE_SAFE_SUBCOMMANDS = {
|
|
57
|
+
'config': frozenset({'help', 'get', 'search'}),
|
|
58
|
+
'dsn': frozenset({'help', 'list', 'show', 'save', 'delete'}),
|
|
59
|
+
'favorite': frozenset({'help', 'list', 'reload', 'run', 'save', 'delete'}),
|
|
60
|
+
}
|
|
33
61
|
|
|
34
62
|
|
|
35
63
|
def _render_config_value(value: Any) -> str:
|
|
@@ -38,6 +66,106 @@ def _render_config_value(value: Any) -> str:
|
|
|
38
66
|
return str(value)
|
|
39
67
|
|
|
40
68
|
|
|
69
|
+
def _parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool]:
|
|
70
|
+
allow_special = False
|
|
71
|
+
show_queries = False
|
|
72
|
+
page_output = False
|
|
73
|
+
filename = arg
|
|
74
|
+
while arguments := filename.split(maxsplit=1):
|
|
75
|
+
if arguments[0] == '--special':
|
|
76
|
+
allow_special = True
|
|
77
|
+
elif arguments[0] == '--show':
|
|
78
|
+
show_queries = True
|
|
79
|
+
elif arguments[0] == '--page':
|
|
80
|
+
page_output = True
|
|
81
|
+
else:
|
|
82
|
+
break
|
|
83
|
+
filename = arguments[1] if len(arguments) == 2 else ''
|
|
84
|
+
return filename, allow_special, show_queries, page_output
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _has_unquoted_whitespace(value: str) -> bool:
|
|
88
|
+
quote: str | None = None
|
|
89
|
+
escaped = False
|
|
90
|
+
for character in value:
|
|
91
|
+
if escaped:
|
|
92
|
+
if quote is None and character.isspace():
|
|
93
|
+
return True
|
|
94
|
+
escaped = False
|
|
95
|
+
continue
|
|
96
|
+
if not WIN and character == '\\' and quote != "'":
|
|
97
|
+
escaped = True
|
|
98
|
+
continue
|
|
99
|
+
if character in ("'", '"'):
|
|
100
|
+
if quote is None:
|
|
101
|
+
quote = character
|
|
102
|
+
elif quote == character:
|
|
103
|
+
quote = None
|
|
104
|
+
elif quote is None and character.isspace():
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_source_filename(filename: str) -> str:
|
|
110
|
+
if not filename:
|
|
111
|
+
return ''
|
|
112
|
+
if _has_unquoted_whitespace(filename):
|
|
113
|
+
raise ValueError(INVALID_SOURCE_FILENAME)
|
|
114
|
+
try:
|
|
115
|
+
arguments = shlex.split(filename, posix=not WIN)
|
|
116
|
+
except ValueError as error:
|
|
117
|
+
raise ValueError(f'Invalid source filename: {error}.') from None
|
|
118
|
+
if len(arguments) != 1:
|
|
119
|
+
raise ValueError(INVALID_SOURCE_FILENAME)
|
|
120
|
+
parsed_filename = arguments[0]
|
|
121
|
+
if WIN and len(parsed_filename) >= 2 and parsed_filename[0] == parsed_filename[-1] and parsed_filename[0] in ("'", '"'):
|
|
122
|
+
parsed_filename = parsed_filename[1:-1]
|
|
123
|
+
return parsed_filename
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _registered_special_command(query: str) -> tuple[str, str] | None:
|
|
127
|
+
command, _verbosity, arg = special.parse_special_command(query)
|
|
128
|
+
registered = special_main.COMMANDS.get(command)
|
|
129
|
+
if registered is None:
|
|
130
|
+
registered = special_main.COMMANDS.get(command.lower())
|
|
131
|
+
if registered is None:
|
|
132
|
+
return None
|
|
133
|
+
return registered.command.removeprefix('\\').removeprefix('/').lower(), arg
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _favorite_source_command_is_safe(arg: str) -> bool:
|
|
137
|
+
query, _error = expand_favorite_query(arg)
|
|
138
|
+
if query is None:
|
|
139
|
+
return True
|
|
140
|
+
return not any(special.is_special_command(statement.rstrip(';')) for statement in sqlparse.split(query))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _source_special_command_is_safe(query: str) -> bool:
|
|
144
|
+
parsed = _registered_special_command(query)
|
|
145
|
+
if parsed is None:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
command, arg = parsed
|
|
149
|
+
if command == 'f':
|
|
150
|
+
return not arg or _favorite_source_command_is_safe(arg)
|
|
151
|
+
if command in ('fd', 'fs'):
|
|
152
|
+
return True
|
|
153
|
+
if command in SOURCE_SAFE_SPECIAL_COMMANDS:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
subcommands = SOURCE_SAFE_SUBCOMMANDS.get(command)
|
|
157
|
+
if subcommands is None:
|
|
158
|
+
return False
|
|
159
|
+
arguments = arg.split(maxsplit=1)
|
|
160
|
+
subcommand = arguments[0].lower() if arguments else 'help'
|
|
161
|
+
if subcommand not in subcommands:
|
|
162
|
+
return False
|
|
163
|
+
if command == 'favorite' and subcommand == 'run':
|
|
164
|
+
run_arg = arguments[1] if len(arguments) == 2 else ''
|
|
165
|
+
return not run_arg or _favorite_source_command_is_safe(run_arg)
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
41
169
|
def _iter_config_values(
|
|
42
170
|
config: Mapping[str, Any],
|
|
43
171
|
prefix: str = '',
|
|
@@ -143,7 +271,7 @@ class ClientCommandsMixin:
|
|
|
143
271
|
special.register_special_command(
|
|
144
272
|
self.execute_from_file,
|
|
145
273
|
"source",
|
|
146
|
-
"/source <
|
|
274
|
+
"/source [--special|--show|--page] <file>",
|
|
147
275
|
"Execute queries from a file.",
|
|
148
276
|
aliases=[SpecialCommandAlias("\\.", case_sensitive=False)],
|
|
149
277
|
)
|
|
@@ -266,12 +394,20 @@ class ClientCommandsMixin:
|
|
|
266
394
|
yield SQLResult(status=msg)
|
|
267
395
|
|
|
268
396
|
def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
|
|
269
|
-
|
|
270
|
-
|
|
397
|
+
filename, allow_special, show_queries, page_output = _parse_source_arguments(arg)
|
|
398
|
+
if page_output:
|
|
399
|
+
yield SQLResult(command={'name': 'source_page'})
|
|
400
|
+
try:
|
|
401
|
+
filename = _parse_source_filename(filename)
|
|
402
|
+
except ValueError as error:
|
|
403
|
+
yield SQLResult(status=str(error), is_error=True)
|
|
404
|
+
return
|
|
405
|
+
if not filename:
|
|
406
|
+
yield SQLResult(status="Missing required argument: filename.", is_error=True)
|
|
271
407
|
return
|
|
272
408
|
|
|
273
409
|
try:
|
|
274
|
-
file_h = open(os.path.expanduser(
|
|
410
|
+
file_h = open(os.path.expanduser(filename))
|
|
275
411
|
except OSError as error:
|
|
276
412
|
yield SQLResult(status=str(error))
|
|
277
413
|
return
|
|
@@ -288,12 +424,36 @@ class ClientCommandsMixin:
|
|
|
288
424
|
yield SQLResult(status=str(error))
|
|
289
425
|
return
|
|
290
426
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
427
|
+
special_query = query.rstrip(';')
|
|
428
|
+
if special.is_special_command(special_query):
|
|
429
|
+
if not allow_special:
|
|
430
|
+
yield SQLResult(
|
|
431
|
+
status='Special commands are not supported without /source --special.',
|
|
432
|
+
is_error=True,
|
|
433
|
+
)
|
|
434
|
+
return
|
|
435
|
+
if not _source_special_command_is_safe(special_query):
|
|
436
|
+
command, _verbosity, _arg = special.parse_special_command(special_query)
|
|
437
|
+
yield SQLResult(
|
|
438
|
+
status=f'Special command is never permitted in source files: {command}.',
|
|
439
|
+
is_error=True,
|
|
440
|
+
)
|
|
441
|
+
return
|
|
442
|
+
if show_queries:
|
|
443
|
+
if page_output:
|
|
444
|
+
yield SQLResult(command={'name': 'source_show', 'text': special_query})
|
|
445
|
+
else:
|
|
446
|
+
click.secho(f'> {special_query}')
|
|
447
|
+
yield from self.sqlexecute.run(special_query)
|
|
448
|
+
continue
|
|
294
449
|
|
|
295
450
|
if self.destructive_warning and confirm_destructive_query(self.destructive_keywords, query) is False:
|
|
296
451
|
continue
|
|
452
|
+
if show_queries:
|
|
453
|
+
if page_output:
|
|
454
|
+
yield SQLResult(command={'name': 'source_show', 'text': query})
|
|
455
|
+
else:
|
|
456
|
+
click.secho(f'> {query}')
|
|
297
457
|
yield from self.sqlexecute.run(query)
|
|
298
458
|
|
|
299
459
|
def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
|
|
@@ -10,6 +10,10 @@ from mycli.packages.sqlresult import SQLResult
|
|
|
10
10
|
from mycli.sqlcompleter import SQLCompleter
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class QueryError(Exception):
|
|
14
|
+
"""An error reported while consuming query results."""
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
class ClientQueryMixin:
|
|
14
18
|
if TYPE_CHECKING:
|
|
15
19
|
schema_prefetcher: Any
|
|
@@ -80,6 +84,7 @@ class ClientQueryMixin:
|
|
|
80
84
|
query: str,
|
|
81
85
|
checkpoint: str | None = None,
|
|
82
86
|
new_line: bool = True,
|
|
87
|
+
raise_on_error: bool = False,
|
|
83
88
|
) -> None:
|
|
84
89
|
"""Runs *query*."""
|
|
85
90
|
assert self.sqlexecute is not None
|
|
@@ -91,6 +96,10 @@ class ClientQueryMixin:
|
|
|
91
96
|
self.main_formatter.query = query
|
|
92
97
|
self.redirect_formatter.query = query
|
|
93
98
|
self.explorer_formatter.query = query
|
|
99
|
+
if result.is_error and raise_on_error:
|
|
100
|
+
message = result.status_plain or 'Query failed.'
|
|
101
|
+
self.log_output(message)
|
|
102
|
+
raise QueryError(message)
|
|
94
103
|
output = self.format_sqlresult(
|
|
95
104
|
result,
|
|
96
105
|
is_expanded=special.is_expanded_output(),
|
|
@@ -46,7 +46,7 @@ def main_execute_from_cli(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
|
|
|
46
46
|
mycli.logger.warning('Unable to open TTY as stdin.')
|
|
47
47
|
raise e
|
|
48
48
|
if execution_confirmed:
|
|
49
|
-
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint)
|
|
49
|
+
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint, raise_on_error=True)
|
|
50
50
|
return 0
|
|
51
51
|
else:
|
|
52
52
|
return 1
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Iterable
|
|
3
|
+
from collections.abc import Generator, Iterable
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
import functools
|
|
7
7
|
from functools import partial
|
|
8
8
|
import html
|
|
9
9
|
from importlib import resources
|
|
10
|
+
import itertools
|
|
10
11
|
import os
|
|
11
12
|
import random
|
|
12
13
|
import re
|
|
@@ -441,6 +442,27 @@ def _output_results(
|
|
|
441
442
|
sqlexecute = mycli.sqlexecute
|
|
442
443
|
assert sqlexecute is not None
|
|
443
444
|
|
|
445
|
+
result_iterator = iter(results)
|
|
446
|
+
try:
|
|
447
|
+
first_result = next(result_iterator)
|
|
448
|
+
except StopIteration:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
if first_result.command is not None and first_result.command['name'] == 'source_page':
|
|
452
|
+
if special.is_redirected():
|
|
453
|
+
_output_results(mycli, state, result_iterator, start)
|
|
454
|
+
return
|
|
455
|
+
paged_output = _single_paged_output_results(mycli, state, result_iterator, start)
|
|
456
|
+
try:
|
|
457
|
+
click.echo_via_pager(paged_output)
|
|
458
|
+
except KeyboardInterrupt:
|
|
459
|
+
pass
|
|
460
|
+
finally:
|
|
461
|
+
paged_output.close()
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
results = itertools.chain([first_result], result_iterator)
|
|
465
|
+
|
|
444
466
|
result_count = 0
|
|
445
467
|
watch_count = 0
|
|
446
468
|
for result in results:
|
|
@@ -450,6 +472,9 @@ def _output_results(
|
|
|
450
472
|
mycli.logger.debug('status: %r', result.status)
|
|
451
473
|
mycli.logger.debug('command: %r', result.command)
|
|
452
474
|
threshold = 1000
|
|
475
|
+
if result.command is not None and result.command['name'] == 'source_show':
|
|
476
|
+
click.secho(f"> {result.command['text']}")
|
|
477
|
+
continue
|
|
453
478
|
if result.command is not None and result.command['name'] == 'set_buffer':
|
|
454
479
|
state.buffer_text = str(result.command['text'])
|
|
455
480
|
continue
|
|
@@ -534,6 +559,126 @@ def _output_results(
|
|
|
534
559
|
mycli.output_timing(f'Time: {warnings_duration:0.03f}s', is_warnings_style=True)
|
|
535
560
|
|
|
536
561
|
|
|
562
|
+
def _single_paged_output_results(
|
|
563
|
+
mycli: 'MyCli',
|
|
564
|
+
state: ReplState,
|
|
565
|
+
results: Iterable[SQLResult],
|
|
566
|
+
start: float,
|
|
567
|
+
) -> Generator[str, None, None]:
|
|
568
|
+
"""Render results lazily through one pager session."""
|
|
569
|
+
sqlexecute = mycli.sqlexecute
|
|
570
|
+
assert sqlexecute is not None
|
|
571
|
+
result_iterator = iter(results)
|
|
572
|
+
result_count = 0
|
|
573
|
+
watch_count = 0
|
|
574
|
+
try:
|
|
575
|
+
for result in result_iterator:
|
|
576
|
+
mycli.logger.debug('preamble: %r', result.preamble)
|
|
577
|
+
mycli.logger.debug('header: %r', result.header)
|
|
578
|
+
mycli.logger.debug('rows: %r', result.rows)
|
|
579
|
+
mycli.logger.debug('status: %r', result.status)
|
|
580
|
+
mycli.logger.debug('command: %r', result.command)
|
|
581
|
+
|
|
582
|
+
if result.command is not None and result.command['name'] == 'source_show':
|
|
583
|
+
yield f"> {result.command['text']}\n"
|
|
584
|
+
continue
|
|
585
|
+
if result.command is not None and result.command['name'] == 'set_buffer':
|
|
586
|
+
state.buffer_text = str(result.command['text'])
|
|
587
|
+
continue
|
|
588
|
+
if result.command is not None and result.command['name'] == 'watch':
|
|
589
|
+
if watch_count > 0:
|
|
590
|
+
try:
|
|
591
|
+
start += float(result.command['seconds'])
|
|
592
|
+
except ValueError as error:
|
|
593
|
+
message = f'Invalid watch sleep time provided ({error}).'
|
|
594
|
+
mycli.log_output(message)
|
|
595
|
+
yield f'{message}\n'
|
|
596
|
+
return
|
|
597
|
+
else:
|
|
598
|
+
watch_count += 1
|
|
599
|
+
|
|
600
|
+
if mycli.auto_vertical_output:
|
|
601
|
+
if mycli.prompt_session is not None:
|
|
602
|
+
max_width = mycli.prompt_session.output.get_size().columns
|
|
603
|
+
else:
|
|
604
|
+
max_width = DEFAULT_WIDTH
|
|
605
|
+
else:
|
|
606
|
+
max_width = None
|
|
607
|
+
|
|
608
|
+
formatted = mycli.format_sqlresult(
|
|
609
|
+
result,
|
|
610
|
+
is_expanded=special.is_expanded_output(),
|
|
611
|
+
is_redirected=False,
|
|
612
|
+
null_string=mycli.null_string,
|
|
613
|
+
numeric_alignment=mycli.numeric_alignment,
|
|
614
|
+
binary_display=mycli.binary_display,
|
|
615
|
+
max_width=max_width,
|
|
616
|
+
)
|
|
617
|
+
duration = time.time() - start
|
|
618
|
+
|
|
619
|
+
if result_count > 0:
|
|
620
|
+
mycli.log_output('')
|
|
621
|
+
yield '\n'
|
|
622
|
+
for line in formatted:
|
|
623
|
+
mycli.log_output(line)
|
|
624
|
+
special.write_tee(line)
|
|
625
|
+
special.write_once(line)
|
|
626
|
+
special.write_pipe_once(line)
|
|
627
|
+
yield f'{line}\n'
|
|
628
|
+
if result.status:
|
|
629
|
+
mycli.log_output(result.status_plain)
|
|
630
|
+
yield f'{result.status_plain}\n'
|
|
631
|
+
|
|
632
|
+
if mycli.beep_after_seconds > 0 and duration >= mycli.beep_after_seconds:
|
|
633
|
+
assert mycli.prompt_session is not None
|
|
634
|
+
mycli.prompt_session.output.bell()
|
|
635
|
+
if special.is_timing_enabled():
|
|
636
|
+
timing = f'Time: {duration:0.03f}s'
|
|
637
|
+
mycli.log_output(timing)
|
|
638
|
+
yield f'{timing}\n'
|
|
639
|
+
|
|
640
|
+
start = time.time()
|
|
641
|
+
result_count += 1
|
|
642
|
+
state.mutating = state.mutating or is_mutating(result.status_plain)
|
|
643
|
+
|
|
644
|
+
if special.is_show_warnings_enabled() and isinstance(result.rows, Cursor) and result.rows.warning_count > 0:
|
|
645
|
+
warnings = sqlexecute.run('SHOW WARNINGS')
|
|
646
|
+
warnings_duration = time.time() - start
|
|
647
|
+
saw_warning = False
|
|
648
|
+
for warning in warnings:
|
|
649
|
+
saw_warning = True
|
|
650
|
+
warning_output = mycli.format_sqlresult(
|
|
651
|
+
warning,
|
|
652
|
+
is_expanded=special.is_expanded_output(),
|
|
653
|
+
is_redirected=False,
|
|
654
|
+
null_string=mycli.null_string,
|
|
655
|
+
numeric_alignment=mycli.numeric_alignment,
|
|
656
|
+
binary_display=mycli.binary_display,
|
|
657
|
+
max_width=max_width,
|
|
658
|
+
is_warnings_style=True,
|
|
659
|
+
)
|
|
660
|
+
mycli.log_output('')
|
|
661
|
+
yield '\n'
|
|
662
|
+
for line in warning_output:
|
|
663
|
+
mycli.log_output(line)
|
|
664
|
+
special.write_tee(line)
|
|
665
|
+
special.write_once(line)
|
|
666
|
+
special.write_pipe_once(line)
|
|
667
|
+
yield f'{line}\n'
|
|
668
|
+
if warning.status:
|
|
669
|
+
mycli.log_output(warning.status_plain)
|
|
670
|
+
yield f'{warning.status_plain}\n'
|
|
671
|
+
|
|
672
|
+
if saw_warning and special.is_timing_enabled():
|
|
673
|
+
timing = f'Time: {warnings_duration:0.03f}s'
|
|
674
|
+
mycli.log_output(timing)
|
|
675
|
+
yield f'{timing}\n'
|
|
676
|
+
finally:
|
|
677
|
+
close = getattr(result_iterator, 'close', None)
|
|
678
|
+
if close is not None:
|
|
679
|
+
close()
|
|
680
|
+
|
|
681
|
+
|
|
537
682
|
def _keepalive_hook(
|
|
538
683
|
mycli: 'MyCli',
|
|
539
684
|
_context: Any,
|
|
@@ -703,6 +703,10 @@ def suggest_type(full_text: str, text_before_cursor: str) -> list[dict[str, Any]
|
|
|
703
703
|
A scope for a column category will be a list of tables.
|
|
704
704
|
"""
|
|
705
705
|
|
|
706
|
+
stripped_text = text_before_cursor.lstrip()
|
|
707
|
+
if re.match(r'^(?:source|/source|\\\.|/\.)\s', stripped_text, re.IGNORECASE):
|
|
708
|
+
return suggest_special(text_before_cursor)
|
|
709
|
+
|
|
706
710
|
word_before_cursor = last_word(text_before_cursor, include="many_punctuations")
|
|
707
711
|
|
|
708
712
|
identifier: Identifier | None = None
|
|
@@ -811,6 +815,40 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
|
|
|
811
815
|
r'/.',
|
|
812
816
|
'source',
|
|
813
817
|
'/source',
|
|
818
|
+
]:
|
|
819
|
+
source_options = ['--special', '--show', '--page']
|
|
820
|
+
source_arguments = _arg.split()
|
|
821
|
+
if not source_arguments:
|
|
822
|
+
return [
|
|
823
|
+
{'type': 'special_subcommand', 'subcommands': source_options},
|
|
824
|
+
{'type': 'file_name', 'quote_spaces': True, 'source_filename': ''},
|
|
825
|
+
]
|
|
826
|
+
|
|
827
|
+
used_options: set[str] = set()
|
|
828
|
+
argument_index = 0
|
|
829
|
+
while argument_index < len(source_arguments) and source_arguments[argument_index] in source_options:
|
|
830
|
+
used_options.add(source_arguments[argument_index])
|
|
831
|
+
argument_index += 1
|
|
832
|
+
remaining_options = [option for option in source_options if option not in used_options]
|
|
833
|
+
source_filename = _arg
|
|
834
|
+
for _index in range(argument_index):
|
|
835
|
+
parsed_argument = source_filename.split(maxsplit=1)
|
|
836
|
+
source_filename = parsed_argument[1] if len(parsed_argument) == 2 else ''
|
|
837
|
+
file_suggestion = {'type': 'file_name', 'quote_spaces': True, 'source_filename': source_filename}
|
|
838
|
+
|
|
839
|
+
if argument_index < len(source_arguments):
|
|
840
|
+
if source_arguments[argument_index].startswith('-'):
|
|
841
|
+
return [{'type': 'special_subcommand', 'subcommands': remaining_options}]
|
|
842
|
+
return [file_suggestion]
|
|
843
|
+
if not text[-1].isspace():
|
|
844
|
+
return []
|
|
845
|
+
suggestions: list[dict[str, Any]] = []
|
|
846
|
+
if remaining_options:
|
|
847
|
+
suggestions.append({'type': 'special_subcommand', 'subcommands': remaining_options})
|
|
848
|
+
suggestions.append(file_suggestion)
|
|
849
|
+
return suggestions
|
|
850
|
+
|
|
851
|
+
if cmd.lower() in [
|
|
814
852
|
r'\o',
|
|
815
853
|
'/o',
|
|
816
854
|
r'\once',
|
|
@@ -85,7 +85,7 @@ def suggest_path(root_dir: str) -> list[str]:
|
|
|
85
85
|
*list_path(os.curdir),
|
|
86
86
|
]
|
|
87
87
|
|
|
88
|
-
if root_dir[0] not in ('/', '~') and root_dir[0:
|
|
88
|
+
if root_dir[0] not in ('/', '~') and root_dir[0:2] != './':
|
|
89
89
|
return list_path(os.curdir)
|
|
90
90
|
|
|
91
91
|
if "~" in root_dir:
|
|
@@ -813,7 +813,7 @@ def parseargfile(arg: str) -> tuple[str, str]:
|
|
|
813
813
|
|
|
814
814
|
@special_command(
|
|
815
815
|
"tee",
|
|
816
|
-
"/tee [-o] <
|
|
816
|
+
"/tee [-o] <file>",
|
|
817
817
|
"Append all results to an output file (overwrite using -o).",
|
|
818
818
|
)
|
|
819
819
|
def set_tee(arg: str, **_) -> list[SQLResult]:
|
|
@@ -856,7 +856,7 @@ def write_tee(output: str | ANSI | FormattedText, nl: bool = True) -> None:
|
|
|
856
856
|
|
|
857
857
|
@special_command(
|
|
858
858
|
"\\once",
|
|
859
|
-
"/once [-o] <
|
|
859
|
+
"/once [-o] <file>",
|
|
860
860
|
"Append next result to an output file (overwrite using -o).",
|
|
861
861
|
aliases=[SpecialCommandAlias("\\o", case_sensitive=False)],
|
|
862
862
|
)
|
|
@@ -17,11 +17,13 @@ class SQLResult:
|
|
|
17
17
|
command: dict[str, str | float] | None = None
|
|
18
18
|
image: bytes | None = None
|
|
19
19
|
image_protocol: ImageProtocol = 'none'
|
|
20
|
+
is_error: bool = False
|
|
20
21
|
|
|
21
22
|
def __str__(self):
|
|
22
23
|
image = f'<{len(self.image)} bytes>' if self.image is not None else None
|
|
23
24
|
return (
|
|
24
|
-
f"{self.preamble}, {self.header}, {self.rows}, {self.postamble}, {self.status}, {self.command},
|
|
25
|
+
f"{self.preamble}, {self.header}, {self.rows}, {self.postamble}, {self.status}, {self.command}, "
|
|
26
|
+
f"{image}, {self.image_protocol}, {self.is_error}"
|
|
25
27
|
)
|
|
26
28
|
|
|
27
29
|
@cached_property
|
|
@@ -37,7 +37,7 @@ _mycli_is_positional_db_arg() {
|
|
|
37
37
|
;;
|
|
38
38
|
--*=*)
|
|
39
39
|
;;
|
|
40
|
-
--host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options)
|
|
40
|
+
--host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options|--boundary-id)
|
|
41
41
|
expects_value=1
|
|
42
42
|
;;
|
|
43
43
|
-?*)
|
|
@@ -28,7 +28,7 @@ function _mycli_is_positional_db_arg
|
|
|
28
28
|
case --
|
|
29
29
|
set options_ended 1
|
|
30
30
|
case '--*=*'
|
|
31
|
-
case --host --hostname --port --user --username --socket --pass --password --password-file --vault-address --vault-mount --vault-secret --vault-password-field --vault-username-field --ssl-mode --ssl-ca --ssl-capath --ssl-cert --ssl-key --ssl-cipher --tls-version --database --dsn --completions --prompt --toolbar --logfile --checkpoint --myclirc --local-infile --login-path --execute --init-command --charset --character-set --batch --format --throttle --use-keyring --keepalive-ticks --ssh-jump --ssh-options
|
|
31
|
+
case --host --hostname --port --user --username --socket --pass --password --password-file --vault-address --vault-mount --vault-secret --vault-password-field --vault-username-field --ssl-mode --ssl-ca --ssl-capath --ssl-cert --ssl-key --ssl-cipher --tls-version --database --dsn --completions --prompt --toolbar --logfile --checkpoint --myclirc --local-infile --login-path --execute --init-command --charset --character-set --batch --format --throttle --use-keyring --keepalive-ticks --ssh-jump --ssh-options --boundary-id
|
|
32
32
|
set expects_value 1
|
|
33
33
|
case '-*'
|
|
34
34
|
set -l option_length (string length -- "$word")
|
|
@@ -35,7 +35,7 @@ _mycli_is_positional_db_arg() {
|
|
|
35
35
|
;;
|
|
36
36
|
--*=*)
|
|
37
37
|
;;
|
|
38
|
-
--host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options)
|
|
38
|
+
--host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options|--boundary-id)
|
|
39
39
|
expects_value=1
|
|
40
40
|
;;
|
|
41
41
|
-?*)
|