pgcli 4.6.0__tar.gz → 4.7.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.
- {pgcli-4.6.0 → pgcli-4.7.0}/.github/workflows/publish.yml +1 -1
- {pgcli-4.6.0 → pgcli-4.7.0}/AUTHORS +2 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/PKG-INFO +5 -2
- {pgcli-4.6.0 → pgcli-4.7.0}/README.rst +1 -1
- {pgcli-4.6.0 → pgcli-4.7.0}/changelog.rst +70 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/main.py +242 -16
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/parseutils/__init__.py +20 -5
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/prompt_utils.py +13 -4
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/pgclirc +7 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/pgexecute.py +12 -5
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli.egg-info/PKG-INFO +5 -2
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli.egg-info/SOURCES.txt +6 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli.egg-info/scm_file_list.json +6 -0
- pgcli-4.7.0/pgcli.egg-info/scm_version.json +8 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pyproject.toml +5 -1
- pgcli-4.7.0/tests/features/command_option.feature +38 -0
- pgcli-4.7.0/tests/features/file_option.feature +33 -0
- pgcli-4.7.0/tests/features/steps/command_option.py +144 -0
- pgcli-4.7.0/tests/features/steps/file_option.py +137 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/iocommands.py +10 -10
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/parseutils/test_parseutils.py +22 -0
- pgcli-4.7.0/tests/test_alias_dsn.py +90 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_main.py +358 -1
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_pgexecute.py +56 -0
- pgcli-4.7.0/tests/test_tuples_only.py +69 -0
- pgcli-4.6.0/pgcli.egg-info/scm_version.json +0 -8
- {pgcli-4.6.0 → pgcli-4.7.0}/.coveragerc +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.editorconfig +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.git-blame-ignore-revs +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.github/workflows/ci.yml +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.github/workflows/codeql.yml +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.github/workflows/codex-review.yml +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.gitignore +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/.pre-commit-config.yaml +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/CONTRIBUTING.rst +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/Dockerfile +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/LICENSE.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/MANIFEST.in +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/RELEASES.md +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/TODO +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/__init__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/__main__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/auth.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/completion_refresher.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/config.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/explain_output_formatter.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/key_bindings.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/magic.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/__init__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/formatter/__init__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/formatter/sqlformatter.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/parseutils/ctes.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/parseutils/meta.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/parseutils/tables.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/parseutils/utils.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/pgliterals/__init__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/pgliterals/main.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/pgliterals/pgliterals.json +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/prioritization.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/packages/sqlcompletion.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/pgbuffer.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/pgcompleter.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/pgstyle.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/pgtoolbar.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli/pyev.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli-completion.bash +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli.egg-info/dependency_links.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli.egg-info/entry_points.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli.egg-info/requires.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pgcli.egg-info/top_level.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/post-install +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/post-remove +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/pylintrc +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/release.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/sanity_checks.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/screenshots/image01.png +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/screenshots/image02.png +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/screenshots/kharkiv-destroyed.jpg +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/screenshots/pgcli.gif +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/setup.cfg +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/conftest.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/__init__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/auto_vertical.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/basic_commands.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/crud_database.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/crud_table.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/db_utils.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/environment.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/expanded.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/fixture_data/help.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/fixture_data/help_commands.txt +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/fixture_data/mock_pg_service.conf +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/fixture_utils.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/iocommands.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/named_queries.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/pgbouncer.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/specials.feature +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/__init__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/auto_vertical.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/basic_commands.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/crud_database.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/crud_table.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/expanded.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/named_queries.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/pgbouncer.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/specials.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/steps/wrappers.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/features/wrappager.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/formatter/__init__.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/formatter/test_sqlformatter.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/metadata.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/parseutils/test_ctes.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/parseutils/test_function_metadata.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/pytest.ini +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_application_name.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_auth.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_completion_refresher.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_config.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_fuzzy_completion.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_init_commands_simple.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_naive_completion.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_pgcompleter.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_pgspecial.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_prioritization.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_prompt_utils.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_rowlimit.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_smart_completion_multiple_schemata.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_smart_completion_public_schema_only.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_sqlcompletion.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_ssh_tunnel.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/test_trailing_comments.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tests/utils.py +0 -0
- {pgcli-4.6.0 → pgcli-4.7.0}/tox.ini +0 -0
|
@@ -145,6 +145,7 @@ Contributors:
|
|
|
145
145
|
* fbdb
|
|
146
146
|
* Charbel Jacquin (charbeljc)
|
|
147
147
|
* Jeronimo Garcia (bechampion)
|
|
148
|
+
* Diego
|
|
148
149
|
* Devadathan M B (devadathanmb)
|
|
149
150
|
* Charalampos Stratakis
|
|
150
151
|
* Laszlo Bimba (bimlas)
|
|
@@ -154,6 +155,7 @@ Contributors:
|
|
|
154
155
|
* Diego
|
|
155
156
|
* Chris (ChrisJr404)
|
|
156
157
|
* Pieter Ouwerkerk (pouwerkerk)
|
|
158
|
+
* VXNCXNX
|
|
157
159
|
|
|
158
160
|
Creator:
|
|
159
161
|
--------
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgcli
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.7.0
|
|
4
4
|
Summary: CLI for Postgres Database. With auto-completion and syntax highlighting.
|
|
5
5
|
Author-email: Pgcli Core Team <pgcli-dev@googlegroups.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
7
7
|
Project-URL: Homepage, https://pgcli.com
|
|
8
|
+
Project-URL: Changelog, https://github.com/dbcli/pgcli/blob/main/changelog.rst
|
|
9
|
+
Project-URL: Repository, https://github.com/dbcli/pgcli/
|
|
10
|
+
Project-URL: Issues, https://github.com/dbcli/pgcli/issues
|
|
8
11
|
Classifier: Intended Audience :: Developers
|
|
9
12
|
Classifier: Operating System :: Unix
|
|
10
13
|
Classifier: Programming Language :: Python
|
|
@@ -405,7 +408,7 @@ which is quite literally the backbone library, that made this app possible.
|
|
|
405
408
|
Jonathan has also provided valuable feedback and support during the development
|
|
406
409
|
of this app.
|
|
407
410
|
|
|
408
|
-
`Click <https://click.
|
|
411
|
+
`Click <https://click.palletsprojects.com/>`_ is used for command line option parsing
|
|
409
412
|
and printing error messages.
|
|
410
413
|
|
|
411
414
|
Thanks to `psycopg <https://www.psycopg.org/>`_ for providing a rock solid
|
|
@@ -352,7 +352,7 @@ which is quite literally the backbone library, that made this app possible.
|
|
|
352
352
|
Jonathan has also provided valuable feedback and support during the development
|
|
353
353
|
of this app.
|
|
354
354
|
|
|
355
|
-
`Click <https://click.
|
|
355
|
+
`Click <https://click.palletsprojects.com/>`_ is used for command line option parsing
|
|
356
356
|
and printing error messages.
|
|
357
357
|
|
|
358
358
|
Thanks to `psycopg <https://www.psycopg.org/>`_ for providing a rock solid
|
|
@@ -1,8 +1,70 @@
|
|
|
1
|
+
Upcoming
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
4.7.0 (2026-09-19)
|
|
7
|
+
==================
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
---------
|
|
11
|
+
* Add support for `single-command` to run a SQL command and exit.
|
|
12
|
+
* Command line option `-c` or `--command`.
|
|
13
|
+
* You can specify multiple times.
|
|
14
|
+
* Runs one statement at a time, like `-f`, and can be combined with `-f`:
|
|
15
|
+
both run, the same way psql does.
|
|
16
|
+
* Add support for forcing destructive commands without confirmation.
|
|
17
|
+
* Command line option `-y` or `--yes`.
|
|
18
|
+
* Skips the destructive command confirmation prompt when enabled.
|
|
19
|
+
* Useful for automated scripts and CI/CD pipelines.
|
|
20
|
+
* Add a ``-t``/``--tuples-only`` command line option that prints the rows and
|
|
21
|
+
nothing else, matching psql: no column headers, no title, no status footer
|
|
22
|
+
and no timing line. The configured table format is left untouched, so ``\T``
|
|
23
|
+
still reports it and can still change it mid-session.
|
|
24
|
+
* Add support for executing SQL commands from file and exit.
|
|
25
|
+
* Command line option `-f` or `--file`.
|
|
26
|
+
* Multiple files can be specified.
|
|
27
|
+
* Files run one statement at a time, like psql, so a ``\watch`` only
|
|
28
|
+
repeats its own statement (and a bare ``\watch`` re-runs the statement
|
|
29
|
+
before it), instead of re-running the whole file. A backslash command
|
|
30
|
+
spans only its own line, also like psql, so a metacommand followed by
|
|
31
|
+
SQL on the next line does not swallow the SQL.
|
|
32
|
+
|
|
33
|
+
Bug fixes:
|
|
34
|
+
----------
|
|
35
|
+
* Fix special commands being broken while explain mode (F5) is on. Every input
|
|
36
|
+
was prefixed with ``EXPLAIN (...)`` and sent to the server as SQL, including
|
|
37
|
+
backslash commands and the bare words ``exit``/``quit``, so ``\q``, ``\d``,
|
|
38
|
+
``\i``, named queries and ``\G`` all failed with ``syntax error at or near
|
|
39
|
+
"\"`` and there was no way to leave explain mode or quit. Special commands
|
|
40
|
+
are now detected first and the EXPLAIN prefix is applied only to real SQL.
|
|
41
|
+
* Fix ``-l``/``--list`` and ``--ping`` discarding the connection string. The
|
|
42
|
+
positional argument was unconditionally replaced with ``postgres``, which
|
|
43
|
+
also threw away a connection URI or ``key=value`` conninfo (host, user, port,
|
|
44
|
+
``sslmode``, everything) and silently fell back to a local socket connection
|
|
45
|
+
as the OS user. The database argument is now kept, like psql; only when no
|
|
46
|
+
database is given at all does the listing connect to ``postgres``.
|
|
47
|
+
|
|
1
48
|
4.6.0 (2026-08-26)
|
|
2
49
|
==================
|
|
3
50
|
|
|
51
|
+
Internal:
|
|
52
|
+
---------
|
|
53
|
+
* Make the external-editor behave scenario less flaky: the ``expect_exact``
|
|
54
|
+
timeouts in ``tests/features/steps/iocommands.py`` were as low as 1-2
|
|
55
|
+
seconds, which intermittently expired on loaded CI runners and reported
|
|
56
|
+
``Scenario: edit sql in file with external editor`` as an error. Raised to 10
|
|
57
|
+
seconds; passing runs are unaffected because pexpect returns as soon as the
|
|
58
|
+
expected text appears.
|
|
59
|
+
|
|
4
60
|
Bug fixes:
|
|
5
61
|
----------
|
|
62
|
+
* Fix ``--list-dsn`` and ``-D``/``--dsn`` not finding ``[alias_dsn]`` entries.
|
|
63
|
+
On a fresh install (no config written yet) ``--list-dsn`` printed a misleading
|
|
64
|
+
"Invalid DSNs found" error. ``--list-dsn`` now treats a missing config or
|
|
65
|
+
``[alias_dsn]`` section as simply nothing to list, without writing a config
|
|
66
|
+
file, and ``-D`` resolves the alias from the config already loaded at startup
|
|
67
|
+
instead of reading it again ([issue 1489](https://github.com/dbcli/pgcli/issues/1489)).
|
|
6
68
|
* Restore cursor shape behaviour for Emacs mode
|
|
7
69
|
* Fix ``TypeError: cannot use a string pattern on a bytes-like object`` when
|
|
8
70
|
completion metadata comes back as bytes (e.g. ``SQL_ASCII`` client encoding).
|
|
@@ -11,9 +73,17 @@ Bug fixes:
|
|
|
11
73
|
issues (CVE-2026-59893, CVE-2026-54284, CVE-2026-71491) and a string-escaping
|
|
12
74
|
bug (CVE-2026-59894); the previous ``<0.6`` cap prevented users from
|
|
13
75
|
installing the fixed release.
|
|
76
|
+
* Detect an unconditional ``UPDATE`` with ``sqlparse`` rather than splitting on whitespace, so a
|
|
77
|
+
``WHERE`` appearing inside a string literal no longer suppresses the destructive-statement warning.
|
|
14
78
|
|
|
15
79
|
Features:
|
|
16
80
|
---------
|
|
81
|
+
* Add a ``--timeout`` command line option and a ``connect_timeout`` config value
|
|
82
|
+
(default 30 seconds) for the connection timeout. Precedence, highest first:
|
|
83
|
+
``--timeout``, then a ``connect_timeout`` in the connection string, then
|
|
84
|
+
``$PGCONNECT_TIMEOUT``, then the config value. libpq's own default is 0,
|
|
85
|
+
which waits until the operating system gives up on the TCP connection, so an
|
|
86
|
+
unreachable host used to hang for minutes.
|
|
17
87
|
* Honor the ``PSQL_EDITOR`` environment variable when opening the external
|
|
18
88
|
editor (``\\e``, ``\\ev``, ``\\ef``, ``\\ne``), matching psql's precedence of
|
|
19
89
|
``PSQL_EDITOR``, then ``EDITOR``, then ``VISUAL`` ([issue 1398](https://github.com/dbcli/pgcli/issues/1398)).
|
|
@@ -119,7 +119,8 @@ MetaQuery.__new__.__defaults__ = ("", False, 0, 0, False, False, False, False)
|
|
|
119
119
|
|
|
120
120
|
OutputSettings = namedtuple(
|
|
121
121
|
"OutputSettings",
|
|
122
|
-
"table_format dcmlfmt floatfmt column_date_formats missingval expanded max_width case_function style_output
|
|
122
|
+
"table_format dcmlfmt floatfmt column_date_formats missingval expanded max_width case_function style_output "
|
|
123
|
+
"max_field_width tuples_only",
|
|
123
124
|
)
|
|
124
125
|
OutputSettings.__new__.__defaults__ = (
|
|
125
126
|
None,
|
|
@@ -132,6 +133,7 @@ OutputSettings.__new__.__defaults__ = (
|
|
|
132
133
|
lambda x: x,
|
|
133
134
|
None,
|
|
134
135
|
DEFAULT_MAX_FIELD_WIDTH,
|
|
136
|
+
False,
|
|
135
137
|
)
|
|
136
138
|
|
|
137
139
|
|
|
@@ -157,6 +159,35 @@ def get_editor():
|
|
|
157
159
|
return os.environ.get("PSQL_EDITOR") or os.environ.get("EDITOR") or os.environ.get("VISUAL") or None
|
|
158
160
|
|
|
159
161
|
|
|
162
|
+
def get_connect_timeout(explicit, dsn, kwargs, default):
|
|
163
|
+
"""Pick the connection timeout to apply, in seconds.
|
|
164
|
+
|
|
165
|
+
Precedence, highest first:
|
|
166
|
+
|
|
167
|
+
1. ``explicit``, i.e. ``--timeout`` on the command line
|
|
168
|
+
2. ``connect_timeout`` in the connection string, or in ``kwargs``
|
|
169
|
+
3. ``$PGCONNECT_TIMEOUT``
|
|
170
|
+
4. ``default``, the ``connect_timeout`` config value
|
|
171
|
+
|
|
172
|
+
Returns ``None`` when the user already stated a timeout by one of the
|
|
173
|
+
means we must not override, in which case the caller leaves the
|
|
174
|
+
connection parameters alone and libpq reads it from where it already is.
|
|
175
|
+
|
|
176
|
+
A default matters because libpq's own is 0, which waits until the
|
|
177
|
+
operating system gives up on the TCP connection, so an unreachable host
|
|
178
|
+
hangs for minutes.
|
|
179
|
+
"""
|
|
180
|
+
if explicit is not None:
|
|
181
|
+
return explicit
|
|
182
|
+
if "connect_timeout" in kwargs:
|
|
183
|
+
return None
|
|
184
|
+
if dsn and "connect_timeout" in conninfo_to_dict(dsn):
|
|
185
|
+
return None
|
|
186
|
+
if os.environ.get("PGCONNECT_TIMEOUT"):
|
|
187
|
+
return None
|
|
188
|
+
return default
|
|
189
|
+
|
|
190
|
+
|
|
160
191
|
class PGCli:
|
|
161
192
|
default_prompt = "\\u@\\h:\\d> "
|
|
162
193
|
max_len_prompt = 30
|
|
@@ -192,18 +223,22 @@ class PGCli:
|
|
|
192
223
|
application_name="pgcli",
|
|
193
224
|
single_connection=False,
|
|
194
225
|
less_chatty=None,
|
|
226
|
+
tuples_only=None,
|
|
195
227
|
prompt=None,
|
|
196
228
|
prompt_dsn=None,
|
|
197
229
|
auto_vertical_output=False,
|
|
198
230
|
warn=None,
|
|
199
231
|
ssh_tunnel_url: str | None = None,
|
|
232
|
+
connect_timeout: int | None = None,
|
|
200
233
|
log_file: str | None = None,
|
|
234
|
+
force_destructive: bool = False,
|
|
201
235
|
):
|
|
202
236
|
self.force_passwd_prompt = force_passwd_prompt
|
|
203
237
|
self.never_passwd_prompt = never_passwd_prompt
|
|
204
238
|
self.pgexecute = pgexecute
|
|
205
239
|
self.dsn_alias = None
|
|
206
240
|
self.watch_command = None
|
|
241
|
+
self.force_destructive = force_destructive
|
|
207
242
|
|
|
208
243
|
# Load config.
|
|
209
244
|
c = self.config = get_config(pgclirc_file)
|
|
@@ -250,6 +285,13 @@ class PGCli:
|
|
|
250
285
|
self.min_num_menu_lines = c["main"].as_int("min_num_menu_lines")
|
|
251
286
|
self.multiline_continuation_char = c["main"]["multiline_continuation_char"]
|
|
252
287
|
self.table_format = c["main"]["table_format"]
|
|
288
|
+
# psql's -t prints the rows and nothing else: no column headers, no
|
|
289
|
+
# title, no status footer and no timing line. The table format is left
|
|
290
|
+
# alone here and switched to an unadorned one at output time, so \T
|
|
291
|
+
# still reports (and can change) the configured format.
|
|
292
|
+
self.tuples_only = bool(tuples_only)
|
|
293
|
+
if self.tuples_only:
|
|
294
|
+
self.pgspecial.timing_enabled = False
|
|
253
295
|
self.syntax_style = c["main"]["syntax_style"]
|
|
254
296
|
self.cli_style = c["colors"]
|
|
255
297
|
self.wider_completion_menu = c["main"].as_bool("wider_completion_menu")
|
|
@@ -263,6 +305,9 @@ class PGCli:
|
|
|
263
305
|
self.prompt_format = prompt if prompt is not None else c["main"].get("prompt", self.default_prompt)
|
|
264
306
|
self.prompt_dsn_format = prompt_dsn
|
|
265
307
|
self.on_error = c["main"]["on_error"].upper()
|
|
308
|
+
# Connection timeout, in seconds. See connect() for the precedence.
|
|
309
|
+
self.connect_timeout = connect_timeout
|
|
310
|
+
self.default_connect_timeout = c["main"].as_int("connect_timeout")
|
|
266
311
|
self.decimal_format = c["data_formats"]["decimal"]
|
|
267
312
|
self.float_format = c["data_formats"]["float"]
|
|
268
313
|
self.column_date_formats = c["column_date_formats"]
|
|
@@ -553,7 +598,7 @@ class PGCli:
|
|
|
553
598
|
):
|
|
554
599
|
message = "Destructive statements must be run within a transaction. Command execution stopped."
|
|
555
600
|
return [(None, None, None, message)]
|
|
556
|
-
destroy = confirm_destructive_query(query, self.destructive_warning, self.dsn_alias)
|
|
601
|
+
destroy = confirm_destructive_query(query, self.destructive_warning, self.dsn_alias, self.force_destructive)
|
|
557
602
|
if destroy is False:
|
|
558
603
|
message = "Wise choice. Command execution stopped."
|
|
559
604
|
return [(None, None, None, message)]
|
|
@@ -676,6 +721,12 @@ class PGCli:
|
|
|
676
721
|
|
|
677
722
|
kwargs.setdefault("application_name", self.application_name)
|
|
678
723
|
|
|
724
|
+
# The resolved value is passed as a connection parameter rather than
|
|
725
|
+
# merged into the dsn, leaving the user's connection string untouched.
|
|
726
|
+
timeout = get_connect_timeout(self.connect_timeout, dsn, kwargs, self.default_connect_timeout)
|
|
727
|
+
if timeout is not None:
|
|
728
|
+
kwargs["connect_timeout"] = str(timeout)
|
|
729
|
+
|
|
679
730
|
# If password prompt is not forced but no password is provided, try
|
|
680
731
|
# getting it from environment variable.
|
|
681
732
|
if not self.force_passwd_prompt and not passwd:
|
|
@@ -871,11 +922,11 @@ class PGCli:
|
|
|
871
922
|
):
|
|
872
923
|
click.secho("Destructive statements must be run within a transaction.")
|
|
873
924
|
raise KeyboardInterrupt
|
|
874
|
-
destroy = confirm_destructive_query(text, self.destructive_warning, self.dsn_alias)
|
|
925
|
+
destroy = confirm_destructive_query(text, self.destructive_warning, self.dsn_alias, self.force_destructive)
|
|
875
926
|
if destroy is False:
|
|
876
927
|
click.secho("Wise choice!")
|
|
877
928
|
raise KeyboardInterrupt
|
|
878
|
-
elif destroy:
|
|
929
|
+
elif destroy and not self.force_destructive:
|
|
879
930
|
click.secho("Your call!")
|
|
880
931
|
|
|
881
932
|
output, query = self._evaluate_command(text)
|
|
@@ -1006,6 +1057,56 @@ class PGCli:
|
|
|
1006
1057
|
def run_cli(self):
|
|
1007
1058
|
logger = self.logger
|
|
1008
1059
|
|
|
1060
|
+
# Handle command mode (-c flag) - similar to psql behavior
|
|
1061
|
+
# Multiple -c options are executed sequentially
|
|
1062
|
+
if hasattr(self, 'commands') and self.commands:
|
|
1063
|
+
try:
|
|
1064
|
+
for command in self.commands:
|
|
1065
|
+
logger.debug("Running command: %s", command)
|
|
1066
|
+
# Statement by statement, like psql -c: \watch only repeats
|
|
1067
|
+
# its own statement, not the whole -c block.
|
|
1068
|
+
if not self._execute_statements(command):
|
|
1069
|
+
break
|
|
1070
|
+
except PgCliQuitError:
|
|
1071
|
+
# Normal exit from quit command
|
|
1072
|
+
sys.exit(0)
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
logger.error("Error executing command: %s", e)
|
|
1075
|
+
logger.error("traceback: %r", traceback.format_exc())
|
|
1076
|
+
click.secho(str(e), err=True, fg="red")
|
|
1077
|
+
sys.exit(1)
|
|
1078
|
+
# psql runs both -c and -f when they are given together, so only
|
|
1079
|
+
# exit here when there is no file left to run.
|
|
1080
|
+
if not (hasattr(self, 'input_files') and self.input_files):
|
|
1081
|
+
sys.exit(0)
|
|
1082
|
+
|
|
1083
|
+
# Handle file mode (-f flag) - similar to psql behavior
|
|
1084
|
+
# Multiple -f options are executed sequentially
|
|
1085
|
+
if hasattr(self, 'input_files') and self.input_files:
|
|
1086
|
+
try:
|
|
1087
|
+
for input_file in self.input_files:
|
|
1088
|
+
logger.debug("Reading commands from file: %s", input_file)
|
|
1089
|
+
with open(input_file, 'r', encoding='utf-8') as f:
|
|
1090
|
+
file_content = f.read()
|
|
1091
|
+
|
|
1092
|
+
if file_content.strip():
|
|
1093
|
+
logger.debug("Executing commands from file: %s", input_file)
|
|
1094
|
+
# Statement by statement, like psql -f: \watch only
|
|
1095
|
+
# repeats its own statement, not the whole file.
|
|
1096
|
+
if not self._execute_statements(file_content):
|
|
1097
|
+
break
|
|
1098
|
+
|
|
1099
|
+
except PgCliQuitError:
|
|
1100
|
+
# Normal exit from quit command
|
|
1101
|
+
sys.exit(0)
|
|
1102
|
+
except Exception as e:
|
|
1103
|
+
logger.error("Error executing command: %s", e)
|
|
1104
|
+
logger.error("traceback: %r", traceback.format_exc())
|
|
1105
|
+
click.secho(str(e), err=True, fg="red")
|
|
1106
|
+
sys.exit(1)
|
|
1107
|
+
# Exit successfully after executing all commands
|
|
1108
|
+
sys.exit(0)
|
|
1109
|
+
|
|
1009
1110
|
history_file = self.config["main"]["history_file"]
|
|
1010
1111
|
if history_file == "default":
|
|
1011
1112
|
history_file = config_location() + "history"
|
|
@@ -1082,6 +1183,43 @@ class PGCli:
|
|
|
1082
1183
|
query = self.execute_command(text)
|
|
1083
1184
|
|
|
1084
1185
|
self.query_history.append(query)
|
|
1186
|
+
return query
|
|
1187
|
+
|
|
1188
|
+
def _execute_statements(self, text):
|
|
1189
|
+
r"""Run a block of SQL the way psql -f does: one statement at a time.
|
|
1190
|
+
|
|
1191
|
+
get_watch_command()'s regex captures ALL the text before a \watch, so
|
|
1192
|
+
feeding a whole file to handle_watch_command would make \watch repeat
|
|
1193
|
+
every statement in it. Splitting first keeps \watch scoped to its own
|
|
1194
|
+
statement, and a bare \watch picks up the previous statement through
|
|
1195
|
+
query_history, exactly like psql.
|
|
1196
|
+
|
|
1197
|
+
A backslash command spans only its own line, like in psql, so a
|
|
1198
|
+
metacommand followed by SQL on the next line does not swallow the
|
|
1199
|
+
SQL (sqlparse only cuts at semicolons).
|
|
1200
|
+
|
|
1201
|
+
Honors on_error: with STOP, the first failed statement stops the run.
|
|
1202
|
+
Returns True when every statement succeeded.
|
|
1203
|
+
"""
|
|
1204
|
+
ok = True
|
|
1205
|
+
statements = sqlparse.split(text)
|
|
1206
|
+
while statements:
|
|
1207
|
+
statement = statements.pop(0)
|
|
1208
|
+
stripped = statement.strip()
|
|
1209
|
+
if not stripped:
|
|
1210
|
+
continue
|
|
1211
|
+
if stripped.startswith("\\") and "\n" in stripped:
|
|
1212
|
+
# psql's rule: a backslash command ends at its newline. Put
|
|
1213
|
+
# the rest back through the splitter.
|
|
1214
|
+
first_line, rest = stripped.split("\n", 1)
|
|
1215
|
+
statements = sqlparse.split(rest) + statements
|
|
1216
|
+
statement = first_line
|
|
1217
|
+
query = self.handle_watch_command(statement)
|
|
1218
|
+
if query is not None and not query.successful:
|
|
1219
|
+
ok = False
|
|
1220
|
+
if self.on_error != "RESUME":
|
|
1221
|
+
break
|
|
1222
|
+
return ok
|
|
1085
1223
|
|
|
1086
1224
|
def _build_cli(self, history):
|
|
1087
1225
|
key_bindings = pgcli_bindings(self)
|
|
@@ -1231,6 +1369,7 @@ class PGCli:
|
|
|
1231
1369
|
case_function=(self.completer.case if self.settings["case_column_headers"] else lambda x: x),
|
|
1232
1370
|
style_output=self.style_output,
|
|
1233
1371
|
max_field_width=self.max_field_width,
|
|
1372
|
+
tuples_only=self.tuples_only,
|
|
1234
1373
|
)
|
|
1235
1374
|
|
|
1236
1375
|
# Hide query text for named queries in quiet mode
|
|
@@ -1387,7 +1526,13 @@ class PGCli:
|
|
|
1387
1526
|
return len(lines) >= (self.prompt_app.output.get_size().rows - 4)
|
|
1388
1527
|
|
|
1389
1528
|
def echo_via_pager(self, text, color=None):
|
|
1390
|
-
|
|
1529
|
+
# Disable pager for -c/--command and -f/--file modes and \watch command
|
|
1530
|
+
if (
|
|
1531
|
+
self.pgspecial.pager_config == PAGER_OFF
|
|
1532
|
+
or self.watch_command
|
|
1533
|
+
or (hasattr(self, 'commands') and self.commands)
|
|
1534
|
+
or (hasattr(self, 'input_files') and self.input_files)
|
|
1535
|
+
):
|
|
1391
1536
|
click.echo(text, color=color)
|
|
1392
1537
|
elif self.pgspecial.pager_config == PAGER_LONG_OUTPUT and self.table_format != "csv":
|
|
1393
1538
|
lines = text.split("\n")
|
|
@@ -1425,6 +1570,13 @@ class PGCli:
|
|
|
1425
1570
|
help="Username to connect to the postgres database.",
|
|
1426
1571
|
)
|
|
1427
1572
|
@click.option("-u", "--user", "username_opt", help="Username to connect to the postgres database.")
|
|
1573
|
+
@click.option(
|
|
1574
|
+
"--timeout",
|
|
1575
|
+
"connect_timeout",
|
|
1576
|
+
type=click.INT,
|
|
1577
|
+
default=None,
|
|
1578
|
+
help="Seconds to wait for a connection before giving up (0 waits forever). Overrides the connection string and $PGCONNECT_TIMEOUT.",
|
|
1579
|
+
)
|
|
1428
1580
|
@click.option(
|
|
1429
1581
|
"-W",
|
|
1430
1582
|
"--password",
|
|
@@ -1490,6 +1642,14 @@ class PGCli:
|
|
|
1490
1642
|
default=False,
|
|
1491
1643
|
help="Skip intro on startup and goodbye on exit.",
|
|
1492
1644
|
)
|
|
1645
|
+
@click.option(
|
|
1646
|
+
"-t",
|
|
1647
|
+
"--tuples-only",
|
|
1648
|
+
"tuples_only",
|
|
1649
|
+
is_flag=True,
|
|
1650
|
+
default=False,
|
|
1651
|
+
help="Print rows only: no column headers, no status footer and no timing, like psql.",
|
|
1652
|
+
)
|
|
1493
1653
|
@click.option("--prompt", help='Prompt format (Default: "\\u@\\h:\\d> ").')
|
|
1494
1654
|
@click.option(
|
|
1495
1655
|
"--prompt-dsn",
|
|
@@ -1535,6 +1695,29 @@ class PGCli:
|
|
|
1535
1695
|
type=str,
|
|
1536
1696
|
help="SQL statement to execute after connecting.",
|
|
1537
1697
|
)
|
|
1698
|
+
@click.option(
|
|
1699
|
+
"-c",
|
|
1700
|
+
"--command",
|
|
1701
|
+
"commands",
|
|
1702
|
+
multiple=True,
|
|
1703
|
+
help="run command (SQL or internal) and exit. Multiple -c options are allowed.",
|
|
1704
|
+
)
|
|
1705
|
+
@click.option(
|
|
1706
|
+
"-y",
|
|
1707
|
+
"--yes",
|
|
1708
|
+
"force_destructive",
|
|
1709
|
+
is_flag=True,
|
|
1710
|
+
default=False,
|
|
1711
|
+
help="Force destructive commands without confirmation prompt.",
|
|
1712
|
+
)
|
|
1713
|
+
@click.option(
|
|
1714
|
+
"-f",
|
|
1715
|
+
"--file",
|
|
1716
|
+
"input_files",
|
|
1717
|
+
multiple=True,
|
|
1718
|
+
type=click.Path(exists=True, readable=True, dir_okay=False),
|
|
1719
|
+
help="execute commands from file, then exit. Multiple -f options are allowed.",
|
|
1720
|
+
)
|
|
1538
1721
|
@click.argument("dbname", default=lambda: None, envvar="PGDATABASE", nargs=1)
|
|
1539
1722
|
@click.argument("username", default=lambda: None, envvar="PGUSER", nargs=1)
|
|
1540
1723
|
def cli(
|
|
@@ -1553,6 +1736,7 @@ def cli(
|
|
|
1553
1736
|
row_limit,
|
|
1554
1737
|
application_name,
|
|
1555
1738
|
less_chatty,
|
|
1739
|
+
tuples_only,
|
|
1556
1740
|
prompt,
|
|
1557
1741
|
prompt_dsn,
|
|
1558
1742
|
list_databases,
|
|
@@ -1563,6 +1747,10 @@ def cli(
|
|
|
1563
1747
|
ssh_tunnel: str,
|
|
1564
1748
|
init_command: str,
|
|
1565
1749
|
log_file: str,
|
|
1750
|
+
commands: tuple,
|
|
1751
|
+
force_destructive: bool,
|
|
1752
|
+
input_files: tuple,
|
|
1753
|
+
connect_timeout: int | None,
|
|
1566
1754
|
):
|
|
1567
1755
|
if version:
|
|
1568
1756
|
print("Version:", __version__)
|
|
@@ -1585,9 +1773,14 @@ def cli(
|
|
|
1585
1773
|
config_full_path,
|
|
1586
1774
|
)
|
|
1587
1775
|
if list_dsn:
|
|
1776
|
+
config_file = get_config_filename(pgclirc)
|
|
1777
|
+
if not os.path.exists(config_file):
|
|
1778
|
+
# Nothing is configured yet, so there is nothing to list. Don't write
|
|
1779
|
+
# out the default config just to read it back for a read-only command.
|
|
1780
|
+
sys.exit(0)
|
|
1588
1781
|
try:
|
|
1589
|
-
cfg = load_config(
|
|
1590
|
-
for alias in cfg
|
|
1782
|
+
cfg = load_config(config_file)
|
|
1783
|
+
for alias in cfg.get("alias_dsn", {}):
|
|
1591
1784
|
click.secho(alias + " : " + cfg["alias_dsn"][alias])
|
|
1592
1785
|
sys.exit(0)
|
|
1593
1786
|
except Exception:
|
|
@@ -1615,14 +1808,22 @@ def cli(
|
|
|
1615
1808
|
application_name=application_name,
|
|
1616
1809
|
single_connection=single_connection,
|
|
1617
1810
|
less_chatty=less_chatty,
|
|
1811
|
+
tuples_only=tuples_only,
|
|
1618
1812
|
prompt=prompt,
|
|
1619
1813
|
prompt_dsn=prompt_dsn,
|
|
1620
1814
|
auto_vertical_output=auto_vertical_output,
|
|
1621
1815
|
warn=warn,
|
|
1622
1816
|
ssh_tunnel_url=ssh_tunnel,
|
|
1623
1817
|
log_file=log_file,
|
|
1818
|
+
force_destructive=force_destructive,
|
|
1819
|
+
connect_timeout=connect_timeout,
|
|
1624
1820
|
)
|
|
1625
1821
|
|
|
1822
|
+
# Store commands for -c option (can be multiple)
|
|
1823
|
+
pgcli.commands = commands if commands else None
|
|
1824
|
+
# Store file paths for -f option (can be multiple)
|
|
1825
|
+
pgcli.input_files = input_files if input_files else None
|
|
1826
|
+
|
|
1626
1827
|
# Choose which ever one has a valid value.
|
|
1627
1828
|
if dbname_opt and dbname:
|
|
1628
1829
|
# work as psql: when database is given as option and argument use the argument as user
|
|
@@ -1634,11 +1835,27 @@ def cli(
|
|
|
1634
1835
|
service = database[8:]
|
|
1635
1836
|
elif os.getenv("PGSERVICE") is not None:
|
|
1636
1837
|
service = os.getenv("PGSERVICE")
|
|
1637
|
-
# because option --ping, --list or -l are not supposed to have a db name
|
|
1838
|
+
# because option --ping, --list or -l are not supposed to have a db name.
|
|
1839
|
+
# A connection string is not a db name though: a URI or a key=value conninfo
|
|
1840
|
+
# carries the whole connection (host, user, port, sslmode, ...), so replacing
|
|
1841
|
+
# it with "postgres" would throw all of that away and fall back to a local
|
|
1842
|
+
# socket connection as the OS user. Only a plain db name is discarded here;
|
|
1843
|
+
# a connection string that names no database gets "postgres" for the
|
|
1844
|
+
# listing, since libpq would otherwise default to the OS user name.
|
|
1845
|
+
is_conn_string = "://" in database or ("=" in database and service is None)
|
|
1638
1846
|
if list_databases or ping_database:
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1847
|
+
if not database:
|
|
1848
|
+
database = "postgres"
|
|
1849
|
+
elif is_conn_string:
|
|
1850
|
+
try:
|
|
1851
|
+
if not conninfo_to_dict(database).get("dbname"):
|
|
1852
|
+
database = make_conninfo(database, dbname="postgres")
|
|
1853
|
+
except Exception:
|
|
1854
|
+
pass # invalid conninfo: let the connection attempt report it
|
|
1855
|
+
|
|
1856
|
+
# PGCli() already loaded (and, if needed, wrote) the config above, so reuse it
|
|
1857
|
+
# rather than reading the file a second time to resolve the -D alias.
|
|
1858
|
+
cfg = pgcli.config
|
|
1642
1859
|
if dsn != "":
|
|
1643
1860
|
try:
|
|
1644
1861
|
dsn_config = cfg["alias_dsn"][dsn]
|
|
@@ -1906,7 +2123,15 @@ def exception_formatter(e, verbose_errors: bool = False):
|
|
|
1906
2123
|
def format_output(title, cur, headers, status, settings, explain_mode=False):
|
|
1907
2124
|
output = []
|
|
1908
2125
|
expanded = settings.expanded or settings.table_format == "vertical"
|
|
1909
|
-
|
|
2126
|
+
if settings.tuples_only:
|
|
2127
|
+
# Rows and nothing else, so an unadorned format. This wins over
|
|
2128
|
+
# expanded output: with the headers suppressed there is no label
|
|
2129
|
+
# column left for the vertical formatter to lay out.
|
|
2130
|
+
table_format = "plain"
|
|
2131
|
+
elif settings.expanded:
|
|
2132
|
+
table_format = "vertical"
|
|
2133
|
+
else:
|
|
2134
|
+
table_format = settings.table_format
|
|
1910
2135
|
max_width = settings.max_width
|
|
1911
2136
|
case_function = settings.case_function
|
|
1912
2137
|
if explain_mode:
|
|
@@ -1964,11 +2189,12 @@ def format_output(title, cur, headers, status, settings, explain_mode=False):
|
|
|
1964
2189
|
dialect = "excel" if platform.system() == "Windows" else "unix"
|
|
1965
2190
|
output_kwargs["dialect"] = dialect
|
|
1966
2191
|
|
|
1967
|
-
|
|
2192
|
+
# The title is printed unless there is none, or -t asked for rows only.
|
|
2193
|
+
if title and not settings.tuples_only:
|
|
1968
2194
|
output.append(title)
|
|
1969
2195
|
|
|
1970
2196
|
if cur:
|
|
1971
|
-
headers = [case_function(x) for x in headers]
|
|
2197
|
+
headers = [] if settings.tuples_only else [case_function(x) for x in headers]
|
|
1972
2198
|
if max_width is not None:
|
|
1973
2199
|
cur = list(cur)
|
|
1974
2200
|
column_types = None
|
|
@@ -2002,8 +2228,8 @@ def format_output(title, cur, headers, status, settings, explain_mode=False):
|
|
|
2002
2228
|
|
|
2003
2229
|
output = itertools.chain(output, formatted)
|
|
2004
2230
|
|
|
2005
|
-
#
|
|
2006
|
-
if status:
|
|
2231
|
+
# Likewise the status footer.
|
|
2232
|
+
if status and not settings.tuples_only:
|
|
2007
2233
|
output = itertools.chain(output, [format_status(cur, status)])
|
|
2008
2234
|
|
|
2009
2235
|
return output
|
|
@@ -20,10 +20,25 @@ def query_starts_with(formatted_sql, prefixes):
|
|
|
20
20
|
return bool(formatted_sql) and formatted_sql.split()[0] in prefixes
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def query_is_unconditional_update(
|
|
24
|
-
"""Check if the query starts with UPDATE and contains no WHERE.
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
def query_is_unconditional_update(query):
|
|
24
|
+
"""Check if the query starts with UPDATE and contains no top-level WHERE clause.
|
|
25
|
+
|
|
26
|
+
Uses sqlparse's parse tree (rather than naive whitespace splitting) so that
|
|
27
|
+
the word "where" appearing inside a string literal, comment, or a nested
|
|
28
|
+
subquery doesn't get mistaken for an actual WHERE clause.
|
|
29
|
+
"""
|
|
30
|
+
statements = sqlparse.parse(query)
|
|
31
|
+
if not statements:
|
|
32
|
+
return False
|
|
33
|
+
statement = statements[0]
|
|
34
|
+
|
|
35
|
+
first_token = statement.token_first(skip_cm=True)
|
|
36
|
+
if first_token is None or first_token.ttype is not sqlparse.tokens.DML:
|
|
37
|
+
return False
|
|
38
|
+
if first_token.value.upper() != "UPDATE":
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
return not any(isinstance(token, sqlparse.sql.Where) for token in statement.tokens)
|
|
27
42
|
|
|
28
43
|
|
|
29
44
|
def is_destructive(queries, keywords):
|
|
@@ -31,7 +46,7 @@ def is_destructive(queries, keywords):
|
|
|
31
46
|
for query in sqlparse.split(queries):
|
|
32
47
|
if query:
|
|
33
48
|
formatted_sql = sqlparse.format(query.lower(), strip_comments=True).strip()
|
|
34
|
-
if "unconditional_update" in keywords and query_is_unconditional_update(
|
|
49
|
+
if "unconditional_update" in keywords and query_is_unconditional_update(query):
|
|
35
50
|
return True
|
|
36
51
|
if query_starts_with(formatted_sql, keywords):
|
|
37
52
|
return True
|
|
@@ -3,22 +3,31 @@ import click
|
|
|
3
3
|
from .parseutils import is_destructive
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def confirm_destructive_query(queries, keywords, alias):
|
|
6
|
+
def confirm_destructive_query(queries, keywords, alias, force=False):
|
|
7
7
|
"""Check if the query is destructive and prompts the user to confirm.
|
|
8
8
|
|
|
9
9
|
Returns:
|
|
10
10
|
* None if the query is non-destructive or we can't prompt the user.
|
|
11
|
-
* True if the query is destructive and the user wants to proceed
|
|
11
|
+
* True if the query is destructive and the user wants to proceed, or if the
|
|
12
|
+
confirmation was bypassed with `force`.
|
|
12
13
|
* False if the query is destructive and the user doesn't want to proceed.
|
|
13
14
|
|
|
14
15
|
"""
|
|
16
|
+
if not is_destructive(queries, keywords):
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
if force:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
if not sys.stdin.isatty():
|
|
23
|
+
return None
|
|
24
|
+
|
|
15
25
|
info = "You're about to run a destructive command"
|
|
16
26
|
if alias:
|
|
17
27
|
info += f" in {click.style(alias, fg='red')}"
|
|
18
28
|
|
|
19
29
|
prompt_text = f"{info}.\nDo you want to proceed?"
|
|
20
|
-
|
|
21
|
-
return confirm(prompt_text)
|
|
30
|
+
return confirm(prompt_text)
|
|
22
31
|
|
|
23
32
|
|
|
24
33
|
def confirm(*args, **kwargs):
|