Full Code of dbcli/pgcli for AI

main d0a6cc2fe351 cached
113 files
588.2 KB
147.6k tokens
838 symbols
1 requests
Download .txt
Showing preview only (621K chars total). Download the full file or copy to clipboard to get everything.
Repository: dbcli/pgcli
Branch: main
Commit: d0a6cc2fe351
Files: 113
Total size: 588.2 KB

Directory structure:
gitextract_46ndausx/

├── .coveragerc
├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── ci.yml
│       ├── codeql.yml
│       └── publish.yml
├── .gitignore
├── .pre-commit-config.yaml
├── AUTHORS
├── CONTRIBUTING.rst
├── Dockerfile
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── RELEASES.md
├── TODO
├── Vagrantfile
├── changelog.rst
├── pgcli/
│   ├── __init__.py
│   ├── __main__.py
│   ├── auth.py
│   ├── completion_refresher.py
│   ├── config.py
│   ├── explain_output_formatter.py
│   ├── key_bindings.py
│   ├── magic.py
│   ├── main.py
│   ├── packages/
│   │   ├── __init__.py
│   │   ├── formatter/
│   │   │   ├── __init__.py
│   │   │   └── sqlformatter.py
│   │   ├── parseutils/
│   │   │   ├── __init__.py
│   │   │   ├── ctes.py
│   │   │   ├── meta.py
│   │   │   ├── tables.py
│   │   │   └── utils.py
│   │   ├── pgliterals/
│   │   │   ├── __init__.py
│   │   │   ├── main.py
│   │   │   └── pgliterals.json
│   │   ├── prioritization.py
│   │   ├── prompt_utils.py
│   │   └── sqlcompletion.py
│   ├── pgbuffer.py
│   ├── pgclirc
│   ├── pgcompleter.py
│   ├── pgexecute.py
│   ├── pgstyle.py
│   ├── pgtoolbar.py
│   └── pyev.py
├── pgcli-completion.bash
├── post-install
├── post-remove
├── pylintrc
├── pyproject.toml
├── release.py
├── sanity_checks.txt
├── tests/
│   ├── conftest.py
│   ├── features/
│   │   ├── __init__.py
│   │   ├── auto_vertical.feature
│   │   ├── basic_commands.feature
│   │   ├── crud_database.feature
│   │   ├── crud_table.feature
│   │   ├── db_utils.py
│   │   ├── environment.py
│   │   ├── expanded.feature
│   │   ├── fixture_data/
│   │   │   ├── help.txt
│   │   │   ├── help_commands.txt
│   │   │   └── mock_pg_service.conf
│   │   ├── fixture_utils.py
│   │   ├── iocommands.feature
│   │   ├── named_queries.feature
│   │   ├── pgbouncer.feature
│   │   ├── specials.feature
│   │   ├── steps/
│   │   │   ├── __init__.py
│   │   │   ├── auto_vertical.py
│   │   │   ├── basic_commands.py
│   │   │   ├── crud_database.py
│   │   │   ├── crud_table.py
│   │   │   ├── expanded.py
│   │   │   ├── iocommands.py
│   │   │   ├── named_queries.py
│   │   │   ├── pgbouncer.py
│   │   │   ├── specials.py
│   │   │   └── wrappers.py
│   │   └── wrappager.py
│   ├── formatter/
│   │   ├── __init__.py
│   │   └── test_sqlformatter.py
│   ├── metadata.py
│   ├── parseutils/
│   │   ├── test_ctes.py
│   │   ├── test_function_metadata.py
│   │   └── test_parseutils.py
│   ├── pytest.ini
│   ├── test_application_name.py
│   ├── test_auth.py
│   ├── test_completion_refresher.py
│   ├── test_config.py
│   ├── test_fuzzy_completion.py
│   ├── test_init_commands_simple.py
│   ├── test_main.py
│   ├── test_naive_completion.py
│   ├── test_pgcompleter.py
│   ├── test_pgexecute.py
│   ├── test_pgspecial.py
│   ├── test_prioritization.py
│   ├── test_prompt_utils.py
│   ├── test_rowlimit.py
│   ├── test_smart_completion_multiple_schemata.py
│   ├── test_smart_completion_public_schema_only.py
│   ├── test_sqlcompletion.py
│   ├── test_ssh_tunnel.py
│   └── utils.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
source=pgcli


================================================
FILE: .editorconfig
================================================
# editorconfig.org
# Get your text editor plugin at:
# http://editorconfig.org/#download
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[travis.yml]
indent_size = 2


================================================
FILE: .git-blame-ignore-revs
================================================


================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
## Description
<!--- Describe your problem as fully as you can. -->

## Your environment
<!-- This gives us some more context to work with. -->

- [ ] Please provide your OS and version information.
- [ ] Please provide your CLI version.
- [ ] What is the output of ``pip freeze`` command.


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
<!--- Describe your changes in detail. -->



## Checklist
<!--- We appreciate your help and want to give you credit. Please take a moment to put an `x` in the boxes below as you complete them. -->
- [ ] I've added this contribution to the `changelog.rst`.
- [ ] I've added my name to the `AUTHORS` file (or it's already there).
<!-- We would appreciate if you comply with our code style guidelines. -->
- [ ] I installed pre-commit hooks (`pip install pre-commit && pre-commit install`).
- [x] Please squash merge this pull request (uncheck if you'd like us to merge as multiple commits)


================================================
FILE: .github/workflows/ci.yml
================================================
name: pgcli

on:
  push:
    branches:
      - main
  pull_request:
    paths-ignore:
      - '**.rst'

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

    services:
      postgres:
        image: postgres:10
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
            - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
        with:
          version: "latest"

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install pgbouncer
        run: |
            sudo apt install pgbouncer -y

            sudo chmod 666 /etc/pgbouncer/*.*

            cat <<EOF > /etc/pgbouncer/userlist.txt
            "postgres" "postgres"
            EOF

            cat <<EOF > /etc/pgbouncer/pgbouncer.ini
            [databases]
            * = host=localhost port=5432
            [pgbouncer]
            listen_port = 6432
            listen_addr = localhost
            auth_type = trust
            auth_file = /etc/pgbouncer/userlist.txt
            logfile = pgbouncer.log
            pidfile = pgbouncer.pid
            admin_users = postgres
            EOF

            sudo systemctl stop pgbouncer

            pgbouncer -d /etc/pgbouncer/pgbouncer.ini

            psql -h localhost -U postgres -p 6432 pgbouncer -c 'show help'

      - name: Install requirements
        run: uv sync --all-extras -p ${{ matrix.python-version }}

      - name: Run unit tests
        run: uv run tox -e py${{ matrix.python-version }}

      - name: Run integration tests
        env:
            PGUSER: postgres
            PGPASSWORD: postgres
            TERM: xterm

        run: uv run tox -e integration

      - name: Check changelog for ReST compliance
        run: uv run tox -e rest

      - name: Run style checks
        run: uv run tox -e style


================================================
FILE: .github/workflows/codeql.yml
================================================
name: "CodeQL"

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
  schedule:
    - cron: "29 13 * * 1"

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ python ]

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: ${{ matrix.language }}
          queries: +security-and-quality

      - name: Autobuild
        uses: github/codeql-action/autobuild@v2

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2
        with:
          category: "/language:${{ matrix.language }}"


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish Python Package

on:
  release:
    types: [created]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

    services:
      postgres:
        image: postgres:10
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
            - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
        with:
          version: "latest"

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: uv sync --all-extras -p ${{ matrix.python-version }}

      - name: Run unit tests
        env:
          LANG: en_US.UTF-8
        run: uv run tox -e py${{ matrix.python-version }}

      - name: Run Style Checks
        run: uv run tox -e style

  build:
    runs-on: ubuntu-latest
    needs: [test]

    steps:
    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
    - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
      with:
        version: "latest"

    - name: Set up Python
      uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
      with:
        python-version: '3.13'

    - name: Install dependencies
      run: uv sync --all-extras -p 3.13

    - name: Build
      run: uv build

    - name: Store the distribution packages
      uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
      with:
        name: python-packages
        path: dist/

  publish:
    name: Publish to PyPI
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    needs: [build]
    environment: release
    permissions:
      id-token: write
    steps:
    - name: Download distribution packages
      uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
      with:
        name: python-packages
        path: dist/
    - name: Publish to PyPI
      uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4

================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
pyvenv/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
.pytest_cache
tests/behave.ini

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# PyCharm
.idea/
*.iml

# Vagrant
.vagrant/

# Generated Packages
*.deb
*.rpm

.vscode/
venv/

.ropeproject/
uv.lock



================================================
FILE: .pre-commit-config.yaml
================================================
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
  # Ruff version.
  rev: v0.14.10
  hooks:
    # Run the linter.
    - id: ruff
      args: [ --fix ]
    # Run the formatter.
    - id: ruff-format


================================================
FILE: AUTHORS
================================================
Many thanks to the following contributors.

Project Lead:
-------------
    * Irina Truong

Core Devs:
----------
    * Amjith Ramanujam
    * Darik Gamble
    * Stuart Quin
    * Joakim Koljonen
    * Daniel Rocco
    * Karl-Aksel Puulmann
    * Dick Marinus

Contributors:
-------------
    * Brett
    * Étienne BERSAC (bersace)
    * Daniel Schwarz
    * inkn
    * Jonathan Slenders
    * xalley
    * TamasNo1
    * François Pietka
    * Michael Kaminsky
    * Alexander Kukushkin
    * Ludovic Gasc (GMLudo)
    * Marc Abramowitz
    * Nick Hahner
    * Jay Zeng
    * Dimitar Roustchev
    * Dhaivat Pandit
    * Matheus Rosa
    * Ali Kargın
    * Nathan Jhaveri
    * David Celis
    * Sven-Hendrik Haase
    * Çağatay Yüksel
    * Tiago Ribeiro
    * Vignesh Anand
    * Charlie Arnold
    * dwalmsley
    * Artur Dryomov
    * rrampage
    * while0pass
    * Eric Workman
    * xa
    * Hans Roman
    * Guewen Baconnier
    * Dionysis Grigoropoulos
    * Jacob Magnusson
    * Johannes Hoff
    * vinotheassassin
    * Jacek Wielemborek
    * Fabien Meghazi
    * Manuel Barkhau
    * Sergii V
    * Emanuele Gaifas
    * Owen Stephens
    * Russell Davies
    * AlexTes
    * Hraban Luyat
    * Jackson Popkin
    * Gustavo Castro
    * Alexander Schmolck
    * Donnell Muse
    * Andrew Speed
    * Dmitry B
    * Isank
    * Marcin Sztolcman
    * Bojan Delić
    * Chris Vaughn
    * Frederic Aoustin
    * Pierre Giraud
    * Andrew Kuchling
    * Dan Clark
    * Catherine Devlin
    * Jason Ribeiro
    * Rishi Ramraj
    * Matthieu Guilbert
    * Alexandr Korsak
    * Saif Hakim
    * Artur Balabanov
    * Kenny Do
    * Max Rothman
    * Daniel Egger
    * Ignacio Campabadal
    * Mikhail Elovskikh (wronglink)
    * Marcin Cieślak (saper)
    * easteregg (verfriemelt-dot-org)
    * Scott Brenstuhl (808sAndBR)
    * Nathan Verzemnieks
    * raylu
    * Zhaolong Zhu
    * Zane C. Bowers-Hadley
    * Telmo "Trooper" (telmotrooper)
    * Alexander Zawadzki
    * Pablo A. Bianchi (pabloab)
    * Sebastian Janko (sebojanko)
    * Pedro Ferrari (petobens)
    * Martin Matejek (mmtj)
    * Jonas Jelten
    * BrownShibaDog
    * George Thomas(thegeorgeous)
    * Yoni Nakache(lazydba247)
    * Gantsev Denis
    * Stephano Paraskeva
    * Panos Mavrogiorgos (pmav99)
    * Igor Kim (igorkim)
    * Anthony DeBarros (anthonydb)
    * Seungyong Kwak (GUIEEN)
    * Tom Caruso (tomplex)
    * Jan Brun Rasmussen (janbrunrasmussen)
    * Kevin Marsh (kevinmarsh)
    * Eero Ruohola (ruohola)
    * Miroslav Šedivý (eumiro)
    * Eric R Young (ERYoung11)
    * Paweł Sacawa (psacawa)
    * Bruno Inec (sweenu)
    * Daniele Varrazzo
    * Daniel Kukula (dkuku)
    * Kian-Meng Ang (kianmeng)
    * Liu Zhao (astroshot)
    * Rigo Neri (rigoneri)
    * Anna Glasgall (annathyst)
    * Andy Schoenberger (andyscho)
    * Damien Baty (dbaty)
    * blag
    * Rob Berry (rob-b)
    * Sharon Yogev (sharonyogev)
    * Hollis Wu (holi0317)
    * Antonio Aguilar (crazybolillo)
    * Andrew M. MacFie (amacfie)
    * saucoide
    * Chris Rose (offbyone/offby1)
    * Mathieu Dupuy (deronnax)
    * Chris Novakovic
    * Max Smolin (maximsmol)
    * Josh Lynch (josh-lynch)
    * Fabio (3ximus)
    * Doug Harris (dougharris)
    * Jay Knight (jay-knight)
    * fbdb
    * Charbel Jacquin (charbeljc)
    * Devadathan M B (devadathanmb)
    * Charalampos Stratakis

Creator:
--------
Amjith Ramanujam


================================================
FILE: CONTRIBUTING.rst
================================================
Development Guide
-----------------
This is a guide for developers who would like to contribute to this project.

GitHub Workflow
---------------

If you're interested in contributing to pgcli, first of all my heart felt
thanks. `Fork the project <https://github.com/dbcli/pgcli>`_ on github.  Then
clone your fork into your computer (``git clone <url-for-your-fork>``).  Make
the changes and create the commits in your local machine. Then push those
changes to your fork. Then click on the pull request icon on github and create
a new pull request. Add a description about the change and send it along. I
promise to review the pull request in a reasonable window of time and get back
to you.

In order to keep your fork up to date with any changes from mainline, add a new
git remote to your local copy called 'upstream' and point it to the main pgcli
repo.

::

   $ git remote add upstream git@github.com:dbcli/pgcli.git

Once the 'upstream' end point is added you can then periodically do a ``git
pull upstream main`` to update your local copy and then do a ``git push
origin main`` to keep your own fork up to date.

Check Github's `Understanding the GitHub flow guide
<https://guides.github.com/introduction/flow/>`_ for a more detailed
explanation of this process.

Local Setup
-----------

The installation instructions in the README file are intended for users of
pgcli. If you're developing pgcli, you'll need to install it in a slightly
different way so you can see the effects of your changes right away without
having to go through the install cycle every time you change the code.

Set up [uv](https://docs.astral.sh/uv/getting-started/installation/) for development:

::

    cd pgcli
    uv venv
    source ./pgcli-dev/bin/activate

Once the virtualenv is activated, install pgcli using pip as follows:

::

    $ uv pip install --editable .

    or

    $ uv pip install -e .

This will install the necessary dependencies as well as install pgcli from the
working folder into the virtualenv. By installing it using `pip install -e`
we've linked the pgcli installation with the working copy. Any changes made
to the code are immediately available in the installed version of pgcli. This
makes it easy to change something in the code, launch pgcli and check the
effects of your changes.

Adding PostgreSQL Special (Meta) Commands
-----------------------------------------

If you want to work on adding new meta-commands (such as `\dp`, `\ds`, `dy`),
you need to contribute to `pgspecial <https://github.com/dbcli/pgspecial/>`_
project.

Visual Studio Code Debugging
-----------------------------
To set up Visual Studio Code to debug pgcli requires a launch.json file.

Within the project, create a file: .vscode\\launch.json like below.

::

    {
        // Use IntelliSense to learn about possible attributes.
        // Hover to view descriptions of existing attributes.
        // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Python: Module",
                "type": "python",
                "request": "launch",
                "module": "pgcli.main",
                "justMyCode": false,
                "console": "externalTerminal",
                "env": {
                    "PGUSER": "postgres",
                    "PGPASS": "password",
                    "PGHOST": "localhost",
                    "PGPORT": "5432"
                }
            }
        ]
    }

Building RPM and DEB packages
-----------------------------

You will need Vagrant 1.7.2 or higher. In the project root there is a
Vagrantfile that is setup to do multi-vm provisioning. If you're setting things
up for the first time, then do:

::

    $ version=x.y.z vagrant up debian
    $ version=x.y.z vagrant up centos

If you already have those VMs setup and you're merely creating a new version of
DEB or RPM package, then you can do:

::

    $ version=x.y.z vagrant provision

That will create a .deb file and a .rpm file.

The deb package can be installed as follows:

::

    $ sudo dpkg -i pgcli*.deb   # if dependencies are available.

    or

    $ sudo apt-get install -f pgcli*.deb  # if dependencies are not available.


The rpm package can be installed as follows:

::

    $ sudo yum install pgcli*.rpm

Running the integration tests
-----------------------------

Integration tests use `behave package <https://behave.readthedocs.io/>`_ and
pytest.
Configuration settings for this package are provided via a ``behave.ini`` file
in the ``tests`` directory.  An example::

    [behave]
    stderr_capture = false

    [behave.userdata]
    pg_test_user = dbuser
    pg_test_host = db.example.com
    pg_test_port = 30000

First, install the requirements for testing:

::
    $ uv pip install ".[dev]"

Ensure that the database user has permissions to create and drop test databases
by checking your ``pg_hba.conf`` file. The default user should be ``postgres``
at ``localhost``. Make sure the authentication method is set to ``trust``. If
you made any changes to your ``pg_hba.conf`` make sure to restart the postgres
service for the changes to take effect.

::

    # ONLY IF YOU MADE CHANGES TO YOUR pg_hba.conf FILE
    $ sudo service postgresql restart

After that:

::

    $ cd pgcli/tests
    $ behave

Note that these ``behave`` tests do not currently work when developing on Windows due to pexpect incompatibility.

To see stdout/stderr, use the following command:

::

    $ behave --no-capture

Troubleshooting the integration tests
-------------------------------------

- Make sure postgres instance on localhost is running
- Check your ``pg_hba.conf`` file to verify local connections are enabled
- Check `this issue <https://github.com/dbcli/pgcli/issues/945>`_ for relevant information.
- `File an issue <https://github.com/dbcli/pgcli/issues/new>`_.

Running the unit tests
----------------------

The unit tests can be run with pytest:

::

    $ cd pgcli
    $ pytest


Coding Style
------------

``pgcli`` uses `ruff <https://github.com/astral-sh/ruff>`_ to format the source code.

Releases
--------

If you're the person responsible for releasing `pgcli`, `this guide <https://github.com/dbcli/pgcli/blob/main/RELEASES.md>`_ is for you.


================================================
FILE: Dockerfile
================================================
FROM python:3.8

COPY . /app
RUN cd /app && pip install -e .

CMD pgcli


================================================
FILE: LICENSE.txt
================================================
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.

* Neither the name of the {organization} nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: MANIFEST.in
================================================
include LICENSE.txt AUTHORS changelog.rst
recursive-include tests *.py *.txt *.feature *.ini


================================================
FILE: README.rst
================================================
We stand with Ukraine
---------------------

Ukrainian people are fighting for their country. A lot of civilians, women and children, are suffering. Hundreds were killed and injured, and thousands were displaced.

This is an image from my home town, Kharkiv. This place is right in the old city center.

.. image:: screenshots/kharkiv-destroyed.jpg

Picture by @fomenko_ph (Telegram).

Please consider donating or volunteering.

* https://bank.gov.ua/en/
* https://savelife.in.ua/en/donate/
* https://www.comebackalive.in.ua/donate
* https://www.globalgiving.org/projects/ukraine-crisis-relief-fund/
* https://www.savethechildren.org/us/where-we-work/ukraine
* https://www.facebook.com/donate/1137971146948461/
* https://donate.wck.org/give/393234#!/donation/checkout
* https://atlantaforukraine.com/


A REPL for Postgres
-------------------

|Build Status| |CodeCov| |PyPI| |netlify|

This is a postgres client that does auto-completion and syntax highlighting.

Home Page: http://pgcli.com

MySQL Equivalent: http://mycli.net

.. image:: screenshots/pgcli.gif
.. image:: screenshots/image01.png

Quick Start
-----------

If you already know how to install python packages, then you can simply do:

::

    $ pip install -U pgcli

    or

    $ sudo apt-get install pgcli # Only on Debian based Linux (e.g. Ubuntu, Mint, etc)
    $ brew install pgcli  # Only on macOS

If you don't know how to install python packages, please check the
`detailed instructions`_.

.. _`detailed instructions`: https://github.com/dbcli/pgcli#detailed-installation-instructions

Usage
-----

::

    $ pgcli [database_name]

    or

    $ pgcli postgresql://[user[:password]@][netloc][:port][/dbname][?extra=value[&other=other-value]]

Examples:

::

    $ pgcli local_database

    $ pgcli postgres://amjith:pa$$w0rd@example.com:5432/app_db?sslmode=verify-ca&sslrootcert=/myrootcert

For more details:

::

    $ pgcli --help

    Usage: pgcli [OPTIONS] [DBNAME] [USERNAME]

    Options:
      -h, --host TEXT            Host address of the postgres database.
      -p, --port INTEGER         Port number at which the postgres instance is
                                 listening.
      -U, --username TEXT        Username to connect to the postgres database.
      -u, --user TEXT            Username to connect to the postgres database.
      -W, --password             Force password prompt.
      -w, --no-password          Never prompt for password.
      --single-connection        Do not use a separate connection for completions.
      -v, --version              Version of pgcli.
      -d, --dbname TEXT          database name to connect to.
      --pgclirc FILE             Location of pgclirc file.
      -D, --dsn TEXT             Use DSN configured into the [alias_dsn] section
                                 of pgclirc file.
      --list-dsn                 list of DSN configured into the [alias_dsn]
                                 section of pgclirc file.
      --row-limit INTEGER        Set threshold for row limit prompt. Use 0 to
                                 disable prompt.
      --less-chatty              Skip intro on startup and goodbye on exit.
      --prompt TEXT              Prompt format (Default: "\u@\h:\d> ").
      --prompt-dsn TEXT          Prompt format for connections using DSN aliases
                                 (Default: "\u@\h:\d> ").
      -l, --list                 list available databases, then exit.
      --auto-vertical-output     Automatically switch to vertical output mode if
                                 the result is wider than the terminal width.
      --warn [all|moderate|off]  Warn before running a destructive query.
      --help                     Show this message and exit.

``pgcli`` also supports many of the same `environment variables`_ as ``psql`` for login options (e.g. ``PGHOST``, ``PGPORT``, ``PGUSER``, ``PGPASSWORD``, ``PGDATABASE``).

The SSL-related environment variables are also supported, so if you need to connect a postgres database via ssl connection, you can set set environment like this:

::

    export PGSSLMODE="verify-full"
    export PGSSLCERT="/your-path-to-certs/client.crt"
    export PGSSLKEY="/your-path-to-keys/client.key"
    export PGSSLROOTCERT="/your-path-to-ca/ca.crt"
    pgcli -h localhost -p 5432 -U username postgres

.. _environment variables: https://www.postgresql.org/docs/current/libpq-envars.html

Features
--------

The `pgcli` is written using prompt_toolkit_.

* Auto-completes as you type for SQL keywords as well as tables and
  columns in the database.
* Syntax highlighting using Pygments.
* Smart-completion (enabled by default) will suggest context-sensitive
  completion.

    - ``SELECT * FROM <tab>`` will only show table names.
    - ``SELECT * FROM users WHERE <tab>`` will only show column names.

* Primitive support for ``psql`` back-slash commands.
* Pretty prints tabular data.

.. _prompt_toolkit: https://github.com/jonathanslenders/python-prompt-toolkit
.. _tabulate: https://pypi.python.org/pypi/tabulate

Config
------
A config file is automatically created at ``~/.config/pgcli/config`` at first launch.
See the file itself for a description of all available options.

Contributions:
--------------

If you're interested in contributing to this project, first of all I would like
to extend my heartfelt gratitude. I've written a small doc to describe how to
get this running in a development setup.

https://github.com/dbcli/pgcli/blob/main/CONTRIBUTING.rst

Please feel free to reach out to us if you need help.

* Amjith, pgcli author: amjith.r@gmail.com, Twitter: `@amjithr <http://twitter.com/amjithr>`_
* Irina, pgcli maintainer: i.chernyavska@gmail.com, Twitter: `@irinatruong <http://twitter.com/irinatruong>`_

Detailed Installation Instructions:
-----------------------------------

macOS:
======

The easiest way to install pgcli is using Homebrew.

::

    $ brew install pgcli

Done!

Alternatively, you can install ``pgcli`` as a python package using a package
manager called called ``pip``. You will need postgres installed on your system
for this to work.

In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installation/

::

    $ which pip

If it is installed then you can do:

::

    $ pip install pgcli

If that fails due to permission issues, you might need to run the command with
sudo permissions.

::

    $ sudo pip install pgcli

If pip is not installed check if easy_install is available on the system.

::

    $ which easy_install

    $ sudo easy_install pgcli

Linux:
======

Many distributions have ``pgcli`` packages.
Refer to https://repology.org/project/pgcli/versions or your distribution to check the available versions.

Alternatively, you can use tools such as `pipx`_ or `uvx`_ to install the latest published package to an isolated virtual environment.

.. _pipx: https://pipx.pypa.io/
.. _uvx: https://docs.astral.sh/uv/guides/tools/

Run:

::

    $ pipx install pgcli

to install ``pgcli`` with ``pipx``, or run:

::

    $ uvx pgcli

to run ``pgcli`` by installing on the fly with ``uvx``.

Docker
======

Pgcli can be run from within Docker. This can be useful to try pgcli without
installing it, or any dependencies, system-wide.

To build the image:

::

    $ docker build -t pgcli .

To create a container from the image:

::

    $ docker run --rm -ti pgcli pgcli <ARGS>

To access postgresql databases listening on localhost, make sure to run the
docker in "host net mode". E.g. to access a database called "foo" on the
postgresql server running on localhost:5432 (the standard port):

::

    $ docker run --rm -ti --net host pgcli pgcli -h localhost foo

To connect to a locally running instance over a unix socket, bind the socket to
the docker container:

::

    $ docker run --rm -ti -v /var/run/postgres:/var/run/postgres pgcli pgcli foo


IPython
=======

Pgcli can be run from within `IPython <https://ipython.org>`_ console. When working on a query,
it may be useful to drop into a pgcli session without leaving the IPython console, iterate on a
query, then quit pgcli to find the query results in your IPython workspace.

Assuming you have IPython installed:

::

    $ pip install ipython-sql

After that, run ipython and load the ``pgcli.magic`` extension:

::

    $ ipython

    In [1]: %load_ext pgcli.magic


Connect to a database and construct a query:

::

    In [2]: %pgcli postgres://someone@localhost:5432/world
    Connected: someone@world
    someone@localhost:world> select * from city c where countrycode = 'USA' and population > 1000000;
    +------+--------------+---------------+--------------+--------------+
    | id   | name         | countrycode   | district     | population   |
    |------+--------------+---------------+--------------+--------------|
    | 3793 | New York     | USA           | New York     | 8008278      |
    | 3794 | Los Angeles  | USA           | California   | 3694820      |
    | 3795 | Chicago      | USA           | Illinois     | 2896016      |
    | 3796 | Houston      | USA           | Texas        | 1953631      |
    | 3797 | Philadelphia | USA           | Pennsylvania | 1517550      |
    | 3798 | Phoenix      | USA           | Arizona      | 1321045      |
    | 3799 | San Diego    | USA           | California   | 1223400      |
    | 3800 | Dallas       | USA           | Texas        | 1188580      |
    | 3801 | San Antonio  | USA           | Texas        | 1144646      |
    +------+--------------+---------------+--------------+--------------+
    SELECT 9
    Time: 0.003s


Exit out of pgcli session with ``Ctrl + D`` and find the query results:

::

    someone@localhost:world>
    Goodbye!
    9 rows affected.
    Out[2]:
    [(3793, u'New York', u'USA', u'New York', 8008278),
     (3794, u'Los Angeles', u'USA', u'California', 3694820),
     (3795, u'Chicago', u'USA', u'Illinois', 2896016),
     (3796, u'Houston', u'USA', u'Texas', 1953631),
     (3797, u'Philadelphia', u'USA', u'Pennsylvania', 1517550),
     (3798, u'Phoenix', u'USA', u'Arizona', 1321045),
     (3799, u'San Diego', u'USA', u'California', 1223400),
     (3800, u'Dallas', u'USA', u'Texas', 1188580),
     (3801, u'San Antonio', u'USA', u'Texas', 1144646)]

The results are available in special local variable ``_``, and can be assigned to a variable of your
choice:

::

    In [3]: my_result = _

Pgcli dropped support for:

* Python<3.8 as of 4.0.0.
* Python<3.9 as of 4.2.0.

Thanks:
-------

A special thanks to `Jonathan Slenders <https://twitter.com/jonathan_s>`_ for
creating `Python Prompt Toolkit <http://github.com/jonathanslenders/python-prompt-toolkit>`_,
which is quite literally the backbone library, that made this app possible.
Jonathan has also provided valuable feedback and support during the development
of this app.

`Click <http://click.pocoo.org/>`_ is used for command line option parsing
and printing error messages.

Thanks to `psycopg <https://www.psycopg.org/>`_ for providing a rock solid
interface to Postgres database.

Thanks to all the beta testers and contributors for your time and patience. :)


.. |Build Status| image:: https://github.com/dbcli/pgcli/actions/workflows/ci.yml/badge.svg?branch=main
    :target: https://github.com/dbcli/pgcli/actions/workflows/ci.yml

.. |CodeCov| image:: https://codecov.io/gh/dbcli/pgcli/branch/main/graph/badge.svg
   :target: https://codecov.io/gh/dbcli/pgcli
   :alt: Code coverage report

.. |Landscape| image:: https://landscape.io/github/dbcli/pgcli/main/landscape.svg?style=flat
   :target: https://landscape.io/github/dbcli/pgcli/main
   :alt: Code Health

.. |PyPI| image:: https://img.shields.io/pypi/v/pgcli.svg
    :target: https://pypi.python.org/pypi/pgcli/
    :alt: Latest Version

.. |netlify| image:: https://api.netlify.com/api/v1/badges/3a0a14dd-776d-445d-804c-3dd74fe31c4e/deploy-status
     :target: https://app.netlify.com/sites/pgcli/deploys
     :alt: Netlify


================================================
FILE: RELEASES.md
================================================
Releasing pgcli
---------------

You have been made the maintainer of `pgcli`? Congratulations!

To release a new version of the package, [create a new release](https://github.com/dbcli/pgcli/releases) in Github. This will trigger a Github action which will run all the tests, build the wheel and upload it to PyPI.

================================================
FILE: TODO
================================================
# vi: ft=vimwiki
* [ ] Add coverage.
* [ ] Refactor to sqlcompletion to consume the text from left to right and use a state machine to suggest cols or tables instead of relying on hacks.
* [ ] Add a few more special commands. (\l pattern, \dp, \ds, \dy, \z etc)
* [ ] Refactor pgspecial.py to a class. 
* [ ] Show/hide docs for a statement using a keybinding.
* [ ] Check how to add the name of the table before printing the table.
* [ ] Add a new trigger for M-/ that does naive completion.
* [ ] New Feature List - Write the current version to config file. At launch if the version has changed, display the changelog between the two versions.
* [ ] Add a test for 'select * from custom.abc where custom.abc.' should suggest columns from abc.
* [ ] pgexecute columns(), tables() etc can be just cursors instead of fetchall()
* [ ] Add colorschemes in config file.


================================================
FILE: Vagrantfile
================================================
# -*- mode: ruby -*-
# vi: set ft=ruby :
# 
#

Vagrant.configure(2) do |config|

  config.vm.synced_folder ".", "/pgcli"

  pgcli_version = ENV['version']
  pgcli_description = "Postgres CLI with autocompletion and syntax highlighting"

  config.vm.define "debian" do |debian|
    debian.vm.box = "bento/debian-10.8"
    debian.vm.provision "shell", inline: <<-SHELL
    echo "-> Building DEB on `lsb_release -d`"
    sudo apt-get update
    sudo apt-get install -y libpq-dev python-dev python-setuptools rubygems
    sudo apt install -y python3-pip
    sudo pip3 install --no-cache-dir virtualenv virtualenv-tools3
    sudo apt-get install -y ruby-dev
    sudo apt-get install -y git
    sudo apt-get install -y rpm librpmbuild8

    sudo gem install fpm

    echo "-> Cleaning up old workspace"
    sudo rm -rf build
    mkdir -p build/usr/share
    virtualenv build/usr/share/pgcli
    build/usr/share/pgcli/bin/pip install /pgcli

    echo "-> Cleaning Virtualenv"
    cd build/usr/share/pgcli
    virtualenv-tools --update-path /usr/share/pgcli > /dev/null
    cd /home/vagrant/

    echo "-> Removing compiled files"
    find build -iname '*.pyc' -delete
    find build -iname '*.pyo' -delete

    echo "-> Creating PgCLI deb"
    sudo fpm -t deb -s dir -C build -n pgcli -v #{pgcli_version} \
        -a all \
        -d libpq-dev \
        -d python-dev \
        -p /pgcli/ \
        --after-install /pgcli/post-install \
        --after-remove /pgcli/post-remove \
        --url https://github.com/dbcli/pgcli \
        --description "#{pgcli_description}" \
        --license 'BSD'

    SHELL
  end

  
# This is considerably more messy than the debian section.  I had to go off-standard to update
# some packages to get this to work.

  config.vm.define "centos" do |centos|

    centos.vm.box = "bento/centos-7.9"
    centos.vm.box_version = "202012.21.0"
    centos.vm.provision "shell", inline: <<-SHELL
    #!/bin/bash
    echo "-> Building RPM on `hostnamectl | grep "Operating System"`"
    export PATH=/usr/local/rvm/gems/ruby-2.6.3/bin:/usr/local/rvm/gems/ruby-2.6.3@global/bin:/usr/local/rvm/rubies/ruby-2.6.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/rvm/bin:/root/bin
    echo "PATH -> " $PATH

#####
### get base updates

    sudo yum install -y rpm-build gcc postgresql-devel python-devel  python3-pip git python3-devel

######
### install FPM, which we need to install to get an up-to-date version of ruby, which we need for git

    echo "-> Get FPM installed"
    # import the necessary GPG keys
    gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
    sudo gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
    # install RVM
    sudo curl -sSL https://get.rvm.io | sudo bash -s stable
    sudo usermod -aG rvm vagrant
    sudo usermod -aG rvm root
    sudo /usr/local/rvm/bin/rvm alias create default 2.6.3
    source /etc/profile.d/rvm.sh
    
    # install a newer version of ruby.  centos7 only comes with ruby2.0.0, which isn't good enough for git.
    sudo yum install -y ruby-devel
    sudo /usr/local/rvm/bin/rvm install 2.6.3
    
    #
    # yes,this gives an error about generating doc but we don't need the doc. 

    /usr/local/rvm/gems/ruby-2.6.3/wrappers/gem install fpm

######

    sudo pip3 install virtualenv virtualenv-tools3
    echo "-> Cleaning up old workspace"
    rm -rf build
    mkdir -p build/usr/share
    virtualenv build/usr/share/pgcli
    build/usr/share/pgcli/bin/pip install /pgcli

    echo "-> Cleaning Virtualenv"
    cd build/usr/share/pgcli
    virtualenv-tools --update-path /usr/share/pgcli > /dev/null
    cd /home/vagrant/

    echo "-> Removing compiled files"
    find build -iname '*.pyc' -delete
    find build -iname '*.pyo' -delete

    cd /home/vagrant
    echo "-> Creating PgCLI RPM"
    /usr/local/rvm/gems/ruby-2.6.3/gems/fpm-1.12.0/bin/fpm -t rpm -s dir -C build -n pgcli -v #{pgcli_version} \
        -a all \
        -d postgresql-devel \
        -d python-devel \
        -p /pgcli/ \
        --after-install /pgcli/post-install \
        --after-remove /pgcli/post-remove \
        --url https://github.com/dbcli/pgcli \
        --description "#{pgcli_description}" \
        --license 'BSD'


  SHELL


  end


end



================================================
FILE: changelog.rst
================================================
Upcoming (TBD)
==============

Features:
---------
* Add support for `\\T` prompt escape sequence to display transaction status (similar to psql's `%x`).

4.4.0 (2025-12-24)
==================

Features:
---------
* Add support for `init-command` to run when the connection is established.
    * Command line option `--init-command`
    * Provide `init-command` in the config file
    * Support dsn specific init-command in the config file
* Add suggestion when setting the search_path
* Allow per dsn_alias ssh tunnel selection

Internal:
---------

* Moderize the repository
  * Use uv instead of pip
  * Use github trusted publisher for pypi release
  * Update dev requirements and replace requirements-dev.txt with pyproject.toml
  * Use ruff instead of black

Bug fixes:
----------

* Improve display of larger durations when passed as floats

4.3.0 (2025-03-22)
==================

Features
--------
* The session time zone setting is set to the system time zone by default

4.2.0 (2025-03-06)
==================

Features
--------
* Add a `--ping` command line option; allows pgcli to replace `pg_isready`
* Changed the packaging metadata from setup.py to pyproject.toml
* Add bash completion for services defined in the service file `~/.pg_service.conf`
* Added support for per-column date/time formatting using `column_date_formats` in config

Bug fixes:
----------
* Avoid raising `NameError` when exiting unsuccessfully in some cases
* Use configured `alias_map_file` to generate table aliases if available.

Internal:
---------

* Drop support for Python 3.8 and add 3.13.

4.1.0 (2024-03-09)
==================

Features:
---------
* Support `PGAPPNAME` as an environment variable and `--application-name` as a command line argument.
* Add `verbose_errors` config and `\v` special command which enable the
  displaying of all Postgres error fields received.
* Show Postgres notifications.
* Support sqlparse 0.5.x
* Add `--log-file [filename]` cli argument and `\log-file [filename]` special commands to
  log to an external file in addition to the normal output

Bug fixes:
----------

* Fix display of "short host" in prompt (with `\h`) for IPv4 addresses ([issue 964](https://github.com/dbcli/pgcli/issues/964)).
* Fix backwards display of NOTICEs from a Function ([issue 1443](https://github.com/dbcli/pgcli/issues/1443))
* Fix psycopg errors when installing on Windows.  ([issue 1413](https://https://github.com/dbcli/pgcli/issues/1413))
* Use a home-made function to display query duration instead of relying on a third-party library (the general behaviour does not change), which fixes the installation of `pgcli` on 32-bit architectures ([issue 1451](https://github.com/dbcli/pgcli/issues/1451))

==================
4.0.1 (2023-10-30)
==================

Internal:
---------
* Allow stable version of pendulum.

==================
4.0.0 (2023-10-27)
==================

Features:
---------

* Ask for confirmation when quitting cli while a transaction is ongoing.
* New `destructive_statements_require_transaction` config option to refuse to execute a
  destructive SQL statement if outside a transaction. This option is off by default.
* Changed the `destructive_warning` config to be a list of commands that are considered
  destructive. This would allow you to be warned on `create`, `grant`, or `insert` queries.
* Destructive warnings will now include the alias dsn connection string name if provided (-D option).
* pgcli.magic will now work with connection URLs that use TLS client certificates for authentication
* Have config option to retry queries on operational errors like connections being lost.
  Also prevents getting stuck in a retry loop.
* Config option to not restart connection when cancelling a `destructive_warning` query. By default,
  it will now not restart.
* Config option to always run with a single connection.
* Add comment explaining default LESS environment variable behavior and change example pager setting.
* Added `\echo` & `\qecho` special commands. ([issue 1335](https://github.com/dbcli/pgcli/issues/1335)).

Bug fixes:
----------

* Fix `\ev` not producing a correctly quoted "schema"."view"
* Fix 'invalid connection option "dsn"' ([issue 1373](https://github.com/dbcli/pgcli/issues/1373)).
* Fix explain mode when used with `expand`, `auto_expand`, or `--explain-vertical-output` ([issue 1393](https://github.com/dbcli/pgcli/issues/1393)).
* Fix sql-insert format emits NULL as 'None' ([issue 1408](https://github.com/dbcli/pgcli/issues/1408)).
* Improve check for prompt-toolkit 3.0.6 ([issue 1416](https://github.com/dbcli/pgcli/issues/1416)).
* Allow specifying an `alias_map_file` in the config that will use
  predetermined table aliases instead of generating aliases programmatically on
  the fly
* Fixed SQL error when there is a comment on the first line: ([issue 1403](https://github.com/dbcli/pgcli/issues/1403))
* Fix wrong usage of prompt instead of confirm when confirm execution of destructive query

Internal:
---------

* Drop support for Python 3.7 and add 3.12.

3.5.0 (2022/09/15):
===================

Features:
---------

* New formatter is added to export query result to sql format (such as sql-insert, sql-update) like mycli.

Bug fixes:
----------

* Fix exception when retrieving password from keyring ([issue 1338](https://github.com/dbcli/pgcli/issues/1338)).
* Fix using comments with special commands ([issue 1362](https://github.com/dbcli/pgcli/issues/1362)).
* Small improvements to the Windows developer experience
* Fix submitting queries in safe multiline mode ([1360](https://github.com/dbcli/pgcli/issues/1360)).

Internal:
---------

* Port to psycopg3 (https://github.com/psycopg/psycopg).
* Fix typos

3.4.1 (2022/03/19)
==================

Bug fixes:
----------

* Fix the bug with Redshift not displaying word count in status ([related issue](https://github.com/dbcli/pgcli/issues/1320)).
* Show the error status for CSV output format.


3.4.0 (2022/02/21)
==================

Features:
---------

* Add optional support for automatically creating an SSH tunnel to a machine with access to the remote database ([related issue](https://github.com/dbcli/pgcli/issues/459)).

3.3.1 (2022/01/18)
==================

Bug fixes:
----------

* Prompt for password when -W is provided even if there is a password in keychain. Fixes #1307.
* Upgrade cli_helpers to 2.2.1

3.3.0 (2022/01/11)
==================

Features:
---------

* Add `max_field_width` setting to config, to enable more control over field truncation ([related issue](https://github.com/dbcli/pgcli/issues/1250)).
* Re-run last query via bare `\watch`. (Thanks: `Saif Hakim`_)

Bug fixes:
----------

* Pin the version of pygments to prevent breaking change

3.2.0
=====

Release date: 2021/08/23

Features:
---------

* Consider `update` queries destructive and issue a warning. Change
  `destructive_warning` setting to `all|moderate|off`, vs `true|false`. (#1239)
* Skip initial comment in .pg_session even if it doesn't start with '#'
* Include functions from schemas in search_path. (`Amjith Ramanujam`_)
* Easy way to show explain output under F5

Bug fixes:
----------

* Fix issue where `syntax_style` config value would not have any effect. (#1212)
* Fix crash because of not found `InputMode.REPLACE_SINGLE` with prompt-toolkit < 3.0.6
* Fix comments being lost in config when saving a named query. (#1240)
* Fix IPython magic for ipython-sql >= 0.4.0
* Fix pager not being used when output format is set to csv. (#1238)
* Add function literals random, generate_series, generate_subscripts
* Fix ANSI escape codes in first line make the cli choose expanded output incorrectly
* Fix pgcli crashing with virtual `pgbouncer` database. (#1093)

3.1.0
=====

Features:
---------

* Make the output more compact by removing the empty newline. (Thanks: `laixintao`_)
* Add support for using [pspg](https://github.com/okbob/pspg) as a pager (#1102)
* Update python version in Dockerfile
* Support setting color for null, string, number, keyword value
* Support Prompt Toolkit 2
* Support sqlparse 0.4.x
* Update functions, datatypes literals for auto-suggestion field
* Add suggestion for schema in function auto-complete

Bug fixes:
----------

* Minor typo fixes in `pgclirc`. (Thanks: `anthonydb`_)
* Fix for list index out of range when executing commands from a file (#1193). (Thanks: `Irina Truong`_)
* Move from `humanize` to `pendulum` for displaying query durations (#1015)
* More explicit error message when connecting using DSN alias and it is not found.

3.0.0
=====

Features:
---------

* Add `__main__.py` file to execute pgcli as a package directly (#1123).
* Add support for ANSI escape sequences for coloring the prompt (#1122).
* Add support for partitioned tables (relkind "p").
* Add support for `pg_service.conf` files
* Add config option show_bottom_toolbar.

Bug fixes:
----------

* Fix warning raised for using `is not` to compare string literal
* Close open connection in completion_refresher thread

Internal:
---------

* Drop Python2.7, 3.4, 3.5 support. (Thanks: `laixintao`_)
* Support Python3.8. (Thanks: `laixintao`_)
* Fix dead link in development guide. (Thanks: `BrownShibaDog`_)
* Upgrade python-prompt-toolkit to v3.0. (Thanks: `laixintao`_)


2.2.0:
======

Features:
---------

* Add `\\G` as a terminator to sql statements that will show the results in expanded mode. This feature is copied from mycli. (Thanks: `Amjith Ramanujam`_)
* Removed limit prompt and added automatic row limit on queries with no LIMIT clause (#1079) (Thanks: `Sebastian Janko`_)
* Function argument completions now take account of table aliases (#1048). (Thanks: `Owen Stephens`_)

Bug fixes:
----------

* Error connecting to PostgreSQL 12beta1 (#1058). (Thanks: `Irina Truong`_ and `Amjith Ramanujam`_)
* Empty query caused error message (#1019) (Thanks: `Sebastian Janko`_)
* History navigation bindings in multiline queries (#1004) (Thanks: `Pedro Ferrari`_)
* Can't connect to pgbouncer database (#1093). (Thanks: `Irina Truong`_)
* Fix broken multi-line history search (#1031). (Thanks: `Owen Stephens`_)
* Fix slow typing/movement when multi-line query ends in a semicolon (#994). (Thanks: `Owen Stephens`_)
* Fix for PQconninfo not available in libpq < 9.3 (#1110). (Thanks: `Irina Truong`_)

Internal:
---------

* Add optional but default squash merge request to PULL_REQUEST_TEMPLATE

2.1.1
=====

Bug fixes:
----------
* Escape switches to VI navigation mode when not canceling completion popup. (Thanks: `Nathan Verzemnieks`_)
* Allow application_name to be overridden. (Thanks: `raylu`_)
* Fix for "no attribute KeyringLocked" (#1040). (Thanks: `Irina Truong`_)
* Pgcli no longer works with password containing spaces (#1043). (Thanks: `Irina Truong`_)
* Load keyring only when keyring is enabled in the config file (#1041). (Thanks: `Zhaolong Zhu`_)
* No longer depend on sqlparse as being less than 0.3.0 with the release of sqlparse 0.3.0. (Thanks: `VVelox`_)
* Fix the broken support for pgservice . (Thanks: `Xavier Francisco`_)
* Connecting using socket is broken in current master. (#1053). (Thanks: `Irina Truong`_)
* Allow usage of newer versions of psycopg2 (Thanks: `Telmo "Trooper"`_)
* Update README in alignment with the usage of newer versions of psycopg2 (Thanks: `Alexander Zawadzki`_)

Internal:
---------

* Add python 3.7 to travis build matrix. (Thanks: `Irina Truong`_)
* Apply `black` to code. (Thanks: `Irina Truong`_)

2.1.0
=====

Features:
---------

* Keybindings for closing the autocomplete list. (Thanks: `easteregg`_)
* Reconnect automatically when server closes connection. (Thanks: `Scott Brenstuhl`_)

Bug fixes:
----------
* Avoid error message on the server side if hstore extension is not installed in the current database (#991). (Thanks: `Marcin Cieślak`_)
* All pexpect submodules have been moved into the pexpect package as of version 3.0. Use pexpect.TIMEOUT (Thanks: `Marcin Cieślak`_)
* Resizing pgcli terminal kills the connection to postgres in python 2.7 (Thanks: `Amjith Ramanujam`_)
* Fix crash retrieving server version with ``--single-connection``. (Thanks: `Irina Truong`_)
* Cannot quit application without reconnecting to database (#1014). (Thanks: `Irina Truong`_)
* Password authentication failed for user "postgres" when using non-default password (#1020). (Thanks: `Irina Truong`_)

Internal:
---------

* (Fixup) Clean up and add behave logging. (Thanks: `Marcin Cieślak`_, `Dick Marinus`_)
* Override VISUAL environment variable for behave tests. (Thanks: `Marcin Cieślak`_)
* Remove build dir before running sdist, remove stray files from wheel distribution. (Thanks: `Dick Marinus`_)
* Fix unit tests, unhashable formatted text since new python prompttoolkit  version. (Thanks: `Dick Marinus`_)

2.0.2:
======

Features:
---------

* Allows passing the ``-u`` flag to specify a username. (Thanks: `Ignacio Campabadal`_)
* Fix for lag in v2 (#979). (Thanks: `Irina Truong`_)
* Support for multihost connection string that is convenient if you have postgres cluster. (Thanks: `Mikhail Elovskikh`_)

Internal:
---------

* Added tests for special command completion. (Thanks: `Amjith Ramanujam`_)

2.0.1:
======

Bug fixes:
----------

* Tab press on an empty line increases the indentation instead of triggering
  the auto-complete pop-up. (Thanks: `Artur Balabanov`_)
* Fix for loading/saving named queries from provided config file (#938). (Thanks: `Daniel Egger`_)
* Set default port in `connect_uri` when none is given. (Thanks: `Daniel Egger`_)
* Fix for error listing databases (#951). (Thanks: `Irina Truong`_)
* Enable Ctrl-Z to suspend the app (Thanks: `Amjith Ramanujam`_).
* Fix StopIteration exception raised at runtime for Python 3.7 (Thanks: `Amjith Ramanujam`_).

Internal:
---------

* Clean up and add behave logging. (Thanks: `Dick Marinus`_)
* Require prompt_toolkit>=2.0.6. (Thanks: `Dick Marinus`_)
* Improve development guide. (Thanks: `Ignacio Campabadal`_)

2.0.0:
======

* Update to ``prompt-toolkit`` 2.0. (Thanks: `Jonathan Slenders`_, `Dick Marinus`_, `Irina Truong`_)

1.11.0
======

Features:
---------

* Respect `\pset pager on` and use pager when output is longer than terminal height (Thanks: `Max Rothman`_)

1.10.3
======

Bug fixes:
----------

* Adapt the query used to get functions metadata to PG11 (#919). (Thanks: `Lele Gaifax`_).
* Fix for error retrieving version in Redshift (#922). (Thanks: `Irina Truong`_)
* Fix for keyring not disabled properly (#920). (Thanks: `Irina Truong`_)

1.10.2
======

Features:
---------

* Make `keyring` optional (Thanks: `Dick Marinus`_)

1.10.1
======

Bug fixes:
----------

* Fix for missing keyring. (Thanks: `Kenny Do`_)
* Fix for "-l" Flag Throws Error (#909). (Thanks: `Irina Truong`_)

1.10.0
======

Features:
---------
* Add quit commands to the completion menu. (Thanks: `Jason Ribeiro`_)
* Add table formats to ``\T`` completion. (Thanks: `Jason Ribeiro`_)
* Support `\\ev``, ``\ef`` (#754). (Thanks: `Catherine Devlin`_)
* Add ``application_name`` to help identify pgcli connection to database (issue #868) (Thanks: `François Pietka`_)
* Add `--user` option, duplicate of `--username`, the same cli option like `psql` (Thanks: `Alexandr Korsak`_)

Internal changes:
-----------------

* Mark tests requiring a running database server as dbtest (Thanks: `Dick Marinus`_)
* Add an is_special command flag to MetaQuery (Thanks: `Rishi Ramraj`_)
* Ported Destructive Warning from mycli.
* Refactor Destructive Warning behave tests (Thanks: `Dick Marinus`_)

Bug Fixes:
----------
* Disable pager when using \watch (#837). (Thanks: `Jason Ribeiro`_)
* Don't offer to reconnect when we can't change a param in realtime (#807). (Thanks: `Amjith Ramanujam`_ and `Saif Hakim`_)
* Make keyring optional. (Thanks: `Dick Marinus`_)
* Fix ipython magic connection (#891). (Thanks: `Irina Truong`_)
* Fix not enough values to unpack. (Thanks: `Matthieu Guilbert`_)
* Fix unbound local error when destructive_warning is false. (Thanks: `Matthieu Guilbert`_)
* Render tab characters as 4 spaces instead of `^I`. (Thanks: `Artur Balabanov`_)

1.9.1:
======

Features:
---------

* Change ``\h`` format string in prompt to only return the first part of the hostname,
  up to the first '.' character.  Add ``\H`` that returns the entire hostname (#858).
  (Thanks: `Andrew Kuchling`_)
* Add Color of table by parameter. The color of table is function of syntax style

Internal changes:
-----------------

* Add tests, AUTHORS and changelog.rst to release. (Thanks: `Dick Marinus`_)

Bug Fixes:
----------
* Fix broken pgcli --list command line option (#850). (Thanks: `Dmitry B`_)

1.9.0
=====

Features:
---------

* manage pager by \pset pager and add enable_pager to the config file (Thanks: `Frederic Aoustin`_).
* Add support for `\T` command to change format output. (Thanks: `Frederic Aoustin`_).
* Add option list-dsn (Thanks: `Frederic Aoustin`_).


Internal changes:
-----------------

* Removed support for Python 3.3. (Thanks: `Irina Truong`_)

1.8.2
=====

Features:
---------

* Use other prompt (prompt_dsn) when connecting using --dsn parameter. (Thanks: `Marcin Sztolcman`_)
* Include username into password prompt. (Thanks: `Bojan Delić`_)

Internal changes:
-----------------
* Use temporary dir as config location in tests. (Thanks: `Dmitry B`_)
* Fix errors in the ``tee`` test (#795 and #797). (Thanks: `Irina Truong`_)
* Increase timeout for quitting pgcli. (Thanks: `Dick Marinus`_)

Bug Fixes:
----------
* Do NOT quote the database names in the completion menu (Thanks: `Amjith Ramanujam`_)
* Fix error in ``unix_socket_directories`` (#805). (Thanks: `Irina Truong`_)
* Fix the --list command line option tries to connect to 'personal' DB (#816). (Thanks: `Isank`_)

1.8.1
=====

Internal changes:
-----------------
* Remove shebang and git execute permission from pgcli/main.py. (Thanks: `Dick Marinus`_)
* Require cli_helpers 0.2.3 (fix #791). (Thanks: `Dick Marinus`_)

1.8.0
=====

Features:
---------

* Add fish-style auto-suggestion from history. (Thanks: `Amjith Ramanujam`_)
* Improved formatting of arrays in output (Thanks: `Joakim Koljonen`_)
* Don't quote identifiers that are non-reserved keywords. (Thanks: `Joakim Koljonen`_)
* Remove the ``...`` in the continuation prompt and use empty space instead. (Thanks: `Amjith Ramanujam`_)
* Add \conninfo and handle more parameters with \c (issue #716) (Thanks: `François Pietka`_)

Internal changes:
-----------------
* Preliminary work for a future change in outputting results that uses less memory. (Thanks: `Dick Marinus`_)
* Remove import workaround for OrderedDict, required for python < 2.7. (Thanks: `Andrew Speed`_)
* Use less memory when formatting results for display (Thanks: `Dick Marinus`_).
* Port auto_vertical feature test from mycli to pgcli. (Thanks: `Dick Marinus`_)
* Drop wcwidth dependency (Thanks: `Dick Marinus`_)

Bug Fixes:
----------

* Fix the way we get host when using DSN (issue #765) (Thanks: `François Pietka`_)
* Add missing keyword COLUMN after DROP (issue #769) (Thanks: `François Pietka`_)
* Don't include arguments in function suggestions for backslash commands (Thanks: `Joakim Koljonen`_)
* Optionally use POSTGRES_USER, POSTGRES_HOST POSTGRES_PASSWORD from environment (Thanks: `Dick Marinus`_)

1.7.0
=====

* Refresh completions after `COMMIT` or `ROLLBACK`. (Thanks: `Irina Truong`_)
* Fixed DSN aliases not being read from custom pgclirc (issue #717). (Thanks: `Irina Truong`_).
* Use dbcli's Homebrew tap for installing pgcli on macOS (issue #718) (Thanks: `Thomas Roten`_).
* Only set `LESS` environment variable if it's unset. (Thanks: `Irina Truong`_)
* Quote schema in `SET SCHEMA` statement (issue #469) (Thanks: `Irina Truong`_)
* Include arguments in function suggestions (Thanks: `Joakim Koljonen`_)
* Use CLI Helpers for pretty printing query results (Thanks: `Thomas Roten`_).
* Skip serial columns when expanding * for `INSERT INTO foo(*` (Thanks: `Joakim Koljonen`_).
* Command line option to list databases (issue #206) (Thanks: `François Pietka`_)

1.6.0
=====

Features:
---------
* Add time option for prompt (Thanks: `Gustavo Castro`_)
* Suggest objects from all schemas (not just those in search_path) (Thanks: `Joakim Koljonen`_)
* Casing for column headers (Thanks: `Joakim Koljonen`_)
* Allow configurable character to be used for multi-line query continuations. (Thanks: `Owen Stephens`_)
* Completions after ORDER BY and DISTINCT now take account of table aliases. (Thanks: `Owen Stephens`_)
* Narrow keyword candidates based on previous keyword. (Thanks: `Étienne Bersac`_)
* Opening an external editor will edit the last-run query. (Thanks: `Thomas Roten`_)
* Support query options in postgres URIs such as ?sslcert=foo.pem (Thanks: `Alexander Schmolck`_)

Bug fixes:
----------
* Fixed external editor bug (issue #668). (Thanks: `Irina Truong`_).
* Standardize command line option names. (Thanks: `Russell Davies`_)
* Improve handling of ``lock_not_available`` error (issue #700). (Thanks: `Jackson Popkin <https://github.com/jdpopkin>`_)
* Fixed user option precedence (issue #697). (Thanks: `Irina Truong`_).

Internal changes:
-----------------
* Run pep8 checks in travis (Thanks: `Irina Truong`_).
* Add pager wrapper for behave tests (Thanks: `Dick Marinus`_).
* Behave quit pgcli nicely (Thanks: `Dick Marinus`_).
* Behave test source command (Thanks: `Dick Marinus`_).
* Behave fix clean up. (Thanks: `Dick Marinus`_).
* Test using behave the tee command (Thanks: `Dick Marinus`_).
* Behave remove boiler plate code (Thanks: `Dick Marinus`_).
* Behave fix pgspecial update (Thanks: `Dick Marinus`_).
* Add behave to tox (Thanks: `Dick Marinus`_).

1.5.1
=====

Features:
---------
* Better suggestions when editing functions (Thanks: `Joakim Koljonen`_)
* Command line option for ``--less-chatty``. (Thanks: `tk`_)
* Added ``MATERIALIZED VIEW`` keywords. (Thanks: `Joakim Koljonen`_).

Bug fixes:
----------

* Support unicode chars in expanded mode. (Thanks: `Amjith Ramanujam`_)
* Fixed "set_session cannot be used inside a transaction" when using dsn. (Thanks: `Irina Truong`_).

1.5.0
=====

Features:
---------
* Upgraded pgspecial to 1.7.0. (See `pgspecial changelog <https://github.com/dbcli/pgspecial/blob/master/changelog.rst>`_ for list of fixes)
* Add a new config setting to allow expandable mode (Thanks: `Jonathan Boudreau <https://github.com/AGhost-7>`_)
* Make pgcli prompt width short when the prompt is too long (Thanks: `Jonathan Virga <https://github.com/jnth>`_)
* Add additional completion for ``ALTER`` keyword (Thanks: `Darik Gamble`_)
* Make the menu size configurable. (Thanks `Darik Gamble`_)

Bug Fixes:
----------
* Handle more connection failure cases. (Thanks: `Amjith Ramanujam`_)
* Fix the connection failure issues with latest psycopg2. (Thanks: `Amjith Ramanujam`_)

Internal Changes:
-----------------

* Add testing for Python 3.5 and 3.6. (Thanks: `Amjith Ramanujam`_)

1.4.0
=====

Features:
---------

* Search table suggestions using initialisms. (Thanks: `Joakim Koljonen`_).
* Support for table-qualifying column suggestions. (Thanks: `Joakim Koljonen`_).
* Display transaction status in the toolbar. (Thanks: `Joakim Koljonen`_).
* Display vi mode in the toolbar. (Thanks: `Joakim Koljonen`_).
* Added --prompt option. (Thanks: `Irina Truong`_).

Bug Fixes:
----------

* Fix scoping for columns from CTEs. (Thanks: `Joakim Koljonen`_)
* Fix crash after `with`. (Thanks: `Joakim Koljonen`_).
* Fix issue #603 (`\i` raises a TypeError). (Thanks: `Lele Gaifax`_).


Internal Changes:
-----------------

* Set default data_formatting to nothing. (Thanks: `Amjith Ramanujam`_).
* Increased minimum prompt_toolkit requirement to 1.0.9. (Thanks: `Irina Truong`_).


1.3.1
=====

Bug Fixes:
----------
* Fix a crashing bug due to sqlparse upgrade. (Thanks: `Darik Gamble`_)


1.3.0
=====

IMPORTANT: Python 2.6 is not officially supported anymore.

Features:
---------
* Add delimiters to displayed numbers. This can be configured via the config file. (Thanks: `Sergii`_).
* Fix broken 'SHOW ALL' in redshift. (Thanks: `Manuel Barkhau`_).
* Support configuring keyword casing preferences. (Thanks: `Darik Gamble`_).
* Add a new multi_line_mode option in config file. The values can be `psql` or `safe`. (Thanks: `Joakim Koljonen`_)
  Setting ``multi_line_mode = safe`` will make sure that a query will only be executed when Alt+Enter is pressed.

Bug Fixes:
----------
* Fix crash bug with leading parenthesis. (Thanks: `Joakim Koljonen`_).
* Remove cumulative addition of timing data. (Thanks: `Amjith Ramanujam`_).
* Handle unrecognized keywords gracefully. (Thanks: `Darik Gamble`_)
* Use raw strings in regex specifiers. This preemptively fixes a crash in Python 3.6. (Thanks `Lele Gaifax`_)

Internal Changes:
-----------------
* Set sqlparse version dependency to >0.2.0, <0.3.0. (Thanks: `Amjith Ramanujam`_).
* XDG_CONFIG_HOME support for config file location. (Thanks: `Fabien Meghazi`_).
* Remove Python 2.6 from travis test suite. (Thanks: `Amjith Ramanujam`_)

1.2.0
=====

Features:
---------

* Add more specifiers to pgcli prompt. (Thanks: `Julien Rouhaud`_).
   ``\p`` for port info ``\#`` for super user and ``\i`` for pid.
* Add `\watch` command to periodically execute a command. (Thanks: `Stuart Quin`_).
    ``> SELECT * FROM django_migrations; \watch 1  /* Runs the command every second */``
* Add command-line option --single-connection to prevent pgcli from using multiple connections. (Thanks: `Joakim Koljonen`_).
* Add priority to the suggestions to sort based on relevance. (Thanks: `Joakim Koljonen`_).
* Configurable null format via the config file. (Thanks: `Adrian Dries`_).
* Add support for CTE aware auto-completion. (Thanks: `Darik Gamble`_).
* Add host and user information to default pgcli prompt. (Thanks: `Lim H`_).
* Better scoping for tables in insert statements to improve suggestions. (Thanks: `Joakim Koljonen`_).

Bug Fixes:
----------

* Do not install setproctitle on cygwin. (Thanks: `Janus Troelsen`_).
* Work around sqlparse crashing after AS keyword. (Thanks: `Joakim Koljonen`_).
* Fix a crashing bug with named queries. (Thanks: `Joakim Koljonen`_).
* Replace  timestampz alias since AWS Redshift does not support it. (Thanks: `Tahir Butt`_).
* Prevent pgcli from hanging indefinitely when Postgres instance is not running. (Thanks: `Darik Gamble`_)

Internal Changes:
-----------------

* Upgrade to sqlparse-0.2.0. (Thanks: `Tiziano Müller`_).
* Upgrade to pgspecial 1.6.0. (Thanks: `Stuart Quin`_).


1.1.0
=====

Features:
---------

* Add support for ``\db`` command. (Thanks: `Irina Truong`_)

Bugs:
-----

* Fix the crash at startup while parsing the postgres url with port number. (Thanks: `Eric Wald`_)
* Fix the crash with Redshift databases. (Thanks: `Darik Gamble`_)

Internal Changes:
-----------------

* Upgrade pgspecial to 1.5.0 and above.

1.0.0
=====

Features:
---------

* Upgrade to prompt-toolkit 1.0.0. (Thanks: `Jonathan Slenders`_).
* Add support for `\o` command to redirect query output to a file. (Thanks: `Tim Sanders`_).
* Add `\i` path completion. (Thanks: `Anthony Lai`_).
* Connect to a dsn saved in config file. (Thanks: `Rodrigo Ramírez Norambuena`_).
* Upgrade sqlparse requirement to version 0.1.19. (Thanks: `Fernando L. Canizo`_).
* Add timestamptz to DATE custom extension. (Thanks: `Fernando Mora`_).
* Ensure target dir exists when copying config. (Thanks: `David Szotten`_).
* Handle dates that fall in the B.C. range. (Thanks: `Stuart Quin`_).
* Pager is selected from config file or else from environment variable. (Thanks: `Fernando Mora`_).
* Add support for Amazon Redshift. (Thanks: `Timothy Cleaver`_).
* Add support for Postgres 8.x. (Thanks: `Timothy Cleaver`_ and `Darik Gamble`_)
* Don't error when completing parameter-less functions. (Thanks: `David Szotten`_).
* Concat and return all available notices. (Thanks: `Stuart Quin`_).
* Handle unicode in record type. (Thanks: `Amjith Ramanujam`_).
* Added humanized time display. Connect #396. (Thanks: `Irina Truong`_).
* Add EXPLAIN keyword to the completion list. (Thanks: `Amjith Ramanujam`_).
* Added sdist upload to release script. (Thanks: `Irina Truong`_).
* Sort completions based on most recently used. (Thanks: `Darik Gamble`)
* Expand '*' into column list during completion. This can be triggered by hitting `<tab>` after the '*' character in the sql while typing. (Thanks: `Joakim Koljonen`_)
* Add a limit to the warning about too many rows. This is controlled by a new config value in ~/.config/pgcli/config. (Thanks: `Anže Pečar`_)
* Improved argument list in function parameter completions. (Thanks: `Joakim Koljonen`_)
* Column suggestions after the COLUMN keyword. (Thanks: `Darik Gamble`_)
* Filter out trigger implemented functions from the suggestion list. (Thanks: `Daniel Rocco`_)
* State of the art JOIN clause completions that suggest entire conditions. (Thanks: `Joakim Koljonen`_)
* Suggest fully formed JOIN clauses based on Foreign Key relations. (Thanks: `Joakim Koljonen`_)
* Add support for `\dx` meta command to list the installed extensions. (Thanks: `Darik Gamble`_)
* Add support for `\copy` command. (Thanks: `Catherine Devlin`_)

Bugs:
-----

* Fix bug where config writing would leave a '~' dir. (Thanks: `James Munson`_).
* Fix auto-completion breaking for table names with caps. (Thanks: `Anthony Lai`_).
* Fix lexical ordering bug. (Thanks: `Anthony Lai`_).
* Use lexical order to break ties when fuzzy matching. (Thanks: `Daniel Rocco`_).
* Fix the bug in auto-expand mode when there are no rows to display. (Thanks: `Amjith Ramanujam`_).
* Fix broken `\i` after #395. (Thanks: `David Szotten`_).
* Fix multi-way joins in auto-completion. (Thanks: `Darik Gamble`_)
* Display null values as <null> in expanded output. (Thanks: `Amjith Ramanujam`_).
* Robust support for Postgres version less than 9.x. (Thanks: `Darik Gamble`_)

Internal Changes:
-----------------

* Update config file location in README. (Thanks: `Ari Summer`_).
* Explicitly add wcwidth as a dependency. (Thanks: `Amjith Ramanujam`_).
* Add tests for the format_output. (Thanks: `Amjith Ramanujam`_).
* Lots of tests for pgcompleter. (Thanks: `Darik Gamble`_).
* Update pgspecial dependency to 1.4.0.


0.20.1
======

Bug Fixes:
----------
* Fixed logging in Windows by switching the location of log and history file based on OS. (Thanks: Amjith, `Darik Gamble`_, `Irina Truong`_).

0.20.0
======

Features:
---------
* Perform auto-completion refresh in background. (Thanks: Amjith, `Darik Gamble`_, `Irina Truong`_).
  When the auto-completion entries are refreshed, the update now happens in a
  background thread. This means large databases with thousands of tables are
  handled without blocking.
* Add ``CONCURRENTLY`` to keyword completion. (Thanks: `Johannes Hoff`_).
* Add support for ``\h`` command. (Thanks: `Stuart Quin`_).
  This is a huge deal. Users can now get help on an SQL command by typing:
  ``\h COMMAND_NAME`` in the pgcli prompt.
* Add support for ``\x auto``. (Thanks: `Stuart Quin`_).
  ``\\x auto`` will automatically switch to expanded mode if the output is wider
  than the display window.
* Don't hide functions from pg_catalog. (Thanks: `Darik Gamble`_).
* Suggest set-returning functions as tables. (Thanks: `Darik Gamble`_).
  Functions that return table like results will now be suggested in places of tables.
* Suggest fields from functions used as tables. (Thanks: `Darik Gamble`_).
* Using ``pgspecial`` as a separate module. (Thanks: `Irina Truong`_).
* Make "enter" key behave as "tab" key when the completion menu is displayed. (Thanks: `Matheus Rosa`_).
* Support different error-handling options when running multiple queries. (Thanks: `Darik Gamble`_).
  When ``on_error = STOP`` in the config file, pgcli will abort execution if one of the queries results in an error.
* Hide the password displayed in the process name in ``ps``. (Thanks: `Stuart Quin`_)

Bug Fixes:
----------
* Fix the ordering bug in `\\d+` display, this bug was displaying the wrong table name in the reference. (Thanks: `Tamas Boros`_).
* Only show expanded layout if valid list of headers provided. (Thanks: `Stuart Quin`_).
* Fix suggestions in compound join clauses. (Thanks: `Darik Gamble`_).
* Fix completion refresh in multiple query scenario. (Thanks: `Darik Gamble`_).
* Fix the broken timing information.
* Fix the removal of whitespaces in the output. (Thanks: `Jacek Wielemborek`_)
* Fix PyPI badge. (Thanks: `Artur Dryomov`_).

Improvements:
-------------
* Move config file to `~/.config/pgcli/config` instead of `~/.pgclirc` (Thanks: `inkn`_).
* Move literal definitions to standalone JSON files. (Thanks: `Darik Gamble`_).

Internal Changes:
-----------------
* Improvements to integration tests to make it more robust. (Thanks: `Irina Truong`_).

0.19.2
======

Features:
---------

* Autocompletion for database name in \c and \connect. (Thanks: `Darik Gamble`_).
* Improved multiline query support by correctly handling open quotes. (Thanks: `Darik Gamble`_).
* Added \pager command.
* Enhanced \i to run multiple queries and display the results for each of them
* Added keywords to suggestions after WHERE clause.
* Enabled autocompletion in named queries. (Thanks: `Irina Truong`_).
* Path to .pgclirc can be specified in command line. (Thanks: `Irina Truong`_).
* Added support for pg_service_conf file. (Thanks: `Irina Truong`_).
* Added custom styles. (Contributor: `Darik Gamble`_).

Internal Changes:
-----------------

* More completer test cases. (Thanks: `Darik Gamble`_).
* Updated sqlparse version from 0.1.14 to 0.1.16. (Thanks: `Darik Gamble`_).
* Upgraded to prompt_toolkit 0.46. (Thanks: `Jonathan Slenders`_).

BugFixes:
---------
* Fixed the completer crashing on invalid SQL. (Thanks: `Darik Gamble`_).
* Fixed unicode issues, updated tests and fixed broken tests.

0.19.1
======

BugFixes:
---------

* Fix an autocompletion bug that was crashing the completion engine when unknown keyword is entered. (Thanks: `Darik Gamble`_)

0.19.0
======

Features:
---------

* Wider completion menus can be enabled via the config file. (Thanks: `Jonathan Slenders`_)

  Open the config file (~/.pgclirc) and check if you have
  ``wider_completion_menu`` option available. If not add it in and set it to
  ``True``.

* Completion menu now has metadata information such as schema, table, column, view, etc., next to the suggestions. (Thanks: `Darik Gamble`_)
* Customizable history file location via config file. (Thanks: `Çağatay Yüksel`_)

  Add this line to your config file (~/.pgclirc) to customize where to store the history file.

::

  history_file = /path/to/history/file

* Add support for running queries from a file using ``\i`` special command. (Thanks: `Michael Kaminsky`_)

BugFixes:
---------

* Always use utf-8 for database encoding regardless of the default encoding used by the database.
* Fix for None dereference on ``\d schemaname.`` with sequence. (Thanks: `Nathan Jhaveri`_)
* Fix a crashing bug in the autocompletion engine for some ``JOIN`` queries.
* Handle KeyboardInterrupt in pager and not quit pgcli as a consequence.

Internal Changes:
-----------------

* Added more behaviorial tests (Thanks: `Irina Truong`_)
* Added code coverage to the tests. (Thanks: `Irina Truong`_)
* Run behaviorial tests as part of TravisCI (Thanks: `Irina Truong`_)
* Upgraded prompt_toolkit version to 0.45 (Thanks: `Jonathan Slenders`_)
* Update the minimum required version of click to 4.1.

0.18.0
======

Features:
---------

* Add fuzzy matching for the table names and column names.

  Matching very long table/column names are now easier with fuzzy matching. The
  fuzzy match works like the fuzzy open in SublimeText or Vim's Ctrl-P plugin.

  eg: Typing ``djmv`` will match `django_migration_views` since it is able to
  match parts of the input to the full table name.

* Change the timing information to seconds.

  The ``Command Time`` and ``Format Time`` are now displayed in seconds instead
  of a unitless number displayed in scientific notation.

* Support for named queries (favorite queries). (Thanks: `Brett Atoms`_)

  Frequently typed queries can now be saved and recalled using a name using
  newly added special commands (``\n[+]``, ``\ns``, ``\nd``).

  eg:

::

    # Save a query
    pgcli> \ns simple select * from foo
    saved

    # List all saved queries
    pgcli> \n+

    # Execute a saved query
    pgcli> \n simple

    # Delete a saved query
    pgcli> \nd simple

* Pasting queries into the pgcli repl is orders of magnitude faster. (Thanks: `Jonathan Slenders`_)

* Add support for PGPASSWORD environment variable to pass the password for the
  postgres database. (Thanks: `Irina Truong`_)

* Add the ability to manually refresh autocompletions by typing ``\#`` or
  ``\refresh``. This is useful if the database was updated by an external means
  and you'd like to refresh the auto-completions to pick up the new change.

Bug Fixes:
----------

* Fix an error when running ``\d table_name`` when running on a table with rules. (Thanks: `Ali Kargın`_)
* Fix a pgcli crash when entering non-ascii characters in Windows. (Thanks: `Darik Gamble`_, `Jonathan Slenders`_)
* Faster rendering of expanded mode output by making the horizontal separator a fixed length string.
* Completion suggestions for the ``\c`` command are not auto-escaped by default.

Internal Changes:
-----------------

* Complete refactor of handling the back-slash commands.
* Upgrade prompt_toolkit to 0.42. (Thanks: `Jonathan Slenders`_)
* Change the config file management to use ConfigObj.(Thanks: `Brett Atoms`_)
* Add integration tests using ``behave``. (Thanks: `Irina Truong`_)

0.17.0
======

Features:
---------

* Add support for auto-completing view names. (Thanks: `Darik Gamble`_)
* Add support for building RPM and DEB packages. (Thanks: dp_)
* Add subsequence matching for completion. (Thanks: `Daniel Rocco`_)
  Previously completions only matched a table name if it started with the
  partially typed word. Now completions will match even if the partially typed
  word is in the middle of a suggestion.
  eg: When you type 'mig', 'django_migrations' will be suggested.
* Completion for built-in tables and temporary tables are suggested after entering a prefix of ``pg_``. (Thanks: `Darik Gamble`_)
* Add place holder doc strings for special commands that are planned for implementation. (Thanks: `Irina Truong`_)
* Updated version of prompt_toolkit, now matching braces are highlighted. (Thanks: `Jonathan Slenders`_)
* Added support of ``\\e`` command. Queries can be edited in an external editor. (Thanks: `Irina Truong`_)
  eg: When you type ``SELECT * FROM \e`` it will be opened in an external editor.
* Add special command ``\dT`` to show datatypes. (Thanks: `Darik Gamble`_)
* Add auto-completion support for datatypes in CREATE, SELECT etc. (Thanks: `Darik Gamble`_)
* Improve the auto-completion in WHERE clause with logical operators. (Thanks: `Darik Gamble`_)
*

Bug Fixes:
----------

* Fix the table formatting while printing multi-byte characters (Chinese, Japanese etc). (Thanks: `蔡佳男`_)
* Fix a crash when pg_catalog was present in search path. (Thanks: `Darik Gamble`_)
* Fixed a bug that broke `\\e` when prompt_tookit was updated. (Thanks: `François Pietka`_)
* Fix the display of triggers as shown in the ``\d`` output. (Thanks: `Dimitar Roustchev`_)
* Fix broken auto-completion for INNER JOIN, LEFT JOIN etc. (Thanks: `Darik Gamble`_)
* Fix incorrect super() calls in pgbuffer, pgtoolbar and pgcompleter. No change in functionality but protects against future problems. (Thanks: `Daniel Rocco`_)
* Add missing schema completion for CREATE and DROP statements. (Thanks: `Darik Gamble`_)
* Minor fixes around cursor cleanup.

0.16.3
======

Bug Fixes:
----------
* Add more SQL keywords for auto-complete suggestion.
* Messages raised as part of stored procedures are no longer ignored.
* Use postgres flavored syntax highlighting instead of generic ANSI SQL.

0.16.2
======

Bug Fixes:
----------
* Fix a bug where the schema qualifier was ignored by the auto-completion.
  As a result the suggestions for tables vs functions are cleaner. (Thanks: `Darik Gamble`_)
* Remove scientific notation when formatting large numbers. (Thanks: `Daniel Rocco`_)
* Add the FUNCTION keyword to auto-completion.
* Display NULL values as <null> instead of empty strings.
* Fix the completion refresh when ``\connect`` is executed.

0.16.1
======

Bug Fixes:
----------
* Fix unicode issues with hstore.
* Fix a silent error when database is changed using \\c.

0.16.0
======

Features:
---------
* Add \ds special command to show sequences.
* Add Vi mode for keybindings. This can be enabled by adding 'vi = True' in ~/.pgclirc. (Thanks: `Jay Zeng`_)
* Add a -v/--version flag to pgcli.
* Add completion for TEMPLATE keyword and smart-completion for
  'CREATE DATABASE blah WITH TEMPLATE <tab>'. (Thanks: `Daniel Rocco`_)
* Add custom decoders to json/jsonb to emulate the behavior of psql. This
  removes the unicode prefix (eg: u'Éowyn') in the output. (Thanks: `Daniel Rocco`_)
* Add \df special command to show functions. (Thanks: `Darik Gamble`_)
* Make suggestions for special commands smarter. eg: \dn - only suggests schemas. (Thanks: `Darik Gamble`_)
* Print out the version and other meta info about pgcli at startup.

Bug Fixes:
----------
* Fix a rare crash caused by adding new schemas to a database. (Thanks: `Darik Gamble`_)
* Make \dt command honor the explicit schema specified in the arg. (Thanks: `Darik Gamble`_)
* Print BIGSERIAL type as Integer instead of Float.
* Show completions for special commands at the beginning of a statement. (Thanks: `Daniel Rocco`_)
* Allow special commands to work in a multi-statement case where multiple sql
  statements are separated by semi-colon in the same line.

0.15.4
======
* Dummy version to replace accidental PyPI entry deletion.

0.15.3
======
* Override the LESS options completely instead of appending to it.

0.15.2
======
* Revert back to using psycopg2 as the postgres adapter. psycopg2cffi fails for some tests in Python 3.

0.15.0
======

Features:
---------
* Add syntax color styles to config.
* Add auto-completion for COPY statements.
* Change Postgres adapter to psycopg2cffi, to make it PyPy compatible.
  Now pgcli can be run by PyPy.

Bug Fixes:
----------
* Treat boolean values as strings instead of ints.
* Make \di, \dv and \dt to be schema aware. (Thanks: `Darik Gamble`_)
* Make column name display unicode compatible.

0.14.0
======

Features:
---------
* Add alias completion support to ON keyword. (Thanks: `Irina Truong`_)
* Add LIMIT keyword to completion.
* Auto-completion for Postgres schemas. (Thanks: `Darik Gamble`_)
* Better unicode handling for datatypes, dbname and roles.
* Add \timing command to time the sql commands.
  This can be set via config file (~/.pgclirc) using `timing = True`.
* Add different table styles for displaying output.
  This can be changed via config file (~/.pgclirc) using `table_format = fancy_grid`.
* Add confirmation before printing results that have more than 1000 rows.

Bug Fixes:
----------

* Performance improvements to expanded view display (\x).
* Cast bytea files to text while displaying. (Thanks: `Daniel Rocco`_)
* Added a list of reserved words that should be auto-escaped.
* Auto-completion is now case-insensitive.
* Fix the broken completion for multiple sql statements. (Thanks: `Darik Gamble`_)

0.13.0
======

Features:
---------

* Add -d/--dbname option to the commandline.
  eg: pgcli -d database
* Add the username as an argument after the database.
  eg: pgcli dbname user

Bug Fixes:
----------
* Fix the crash when \c fails.
* Fix the error thrown by \d when triggers are present.
* Fix broken behavior on \?. (Thanks: `Darik Gamble`_)

0.12.0
======

Features:
---------

* Upgrade to prompt_toolkit version 0.26 (Thanks: https://github.com/macobo)
  * Adds Ctrl-left/right to move the cursor one word left/right respectively.
  * Internal API changes.
* IPython integration through `ipython-sql`_ (Thanks: `Darik Gamble`_)
  * Add an ipython magic extension to embed pgcli inside ipython.
  * Results from a pgcli query are sent back to ipython.
* Multiple sql statements in the same line separated by semi-colon. (Thanks: https://github.com/macobo)

.. _`ipython-sql`: https://github.com/catherinedevlin/ipython-sql

Bug Fixes:
----------

* Fix 'message' attribute not found exception in Python 3. (Thanks: https://github.com/GMLudo)
* Use the database username as the database name instead of defaulting to OS username. (Thanks: https://github.com/fpietka)
* Auto-completion for auto-escaped column/table names.
* Fix i-reverse-search to work in prompt_toolkit version 0.26.

0.11.0
======

Features:
---------

* Add \dn command. (Thanks: https://github.com/CyberDem0n)
* Add \x command. (Thanks: https://github.com/stuartquin)
* Auto-escape special column/table names. (Thanks: https://github.com/qwesda)
* Cancel a command using Ctrl+C. (Thanks: https://github.com/macobo)
* Faster startup by reading all columns and tables in a single query. (Thanks: https://github.com/macobo)
* Improved psql compliance with env vars and password prompting. (Thanks: `Darik Gamble`_)
* Pressing Alt-Enter will introduce a line break. This is a way to break up the query into multiple lines without switching to multi-line mode. (Thanks: https://github.com/pabloab).

Bug Fixes:
----------
* Fix the broken behavior of \d+. (Thanks: https://github.com/macobo)
* Fix a crash during auto-completion. (Thanks: https://github.com/Erethon)
* Avoid losing pre_run_callables on error in editing.  (Thanks: https://github.com/catherinedevlin)

Improvements:
-------------
* Faster test runs on TravisCI. (Thanks: https://github.com/macobo)
* Integration tests with Postgres!! (Thanks: https://github.com/macobo)

.. _`Amjith Ramanujam`: https://blog.amjith.com
.. _`Andrew Kuchling`: https://github.com/akuchling
.. _`Darik Gamble`: https://github.com/darikg
.. _`Daniel Rocco`: https://github.com/drocco007
.. _`Jay Zeng`:  https://github.com/jayzeng
.. _`蔡佳男`: https://github.com/xalley
.. _dp: https://github.com/ceocoder
.. _`Jonathan Slenders`: https://github.com/jonathanslenders
.. _`Dimitar Roustchev`: https://github.com/droustchev
.. _`François Pietka`: https://github.com/fpietka
.. _`Ali Kargın`: https://github.com/sancopanco
.. _`Brett Atoms`: https://github.com/brettatoms
.. _`Nathan Jhaveri`: https://github.com/nathanjhaveri
.. _`Çağatay Yüksel`: https://github.com/cagatay
.. _`Michael Kaminsky`: https://github.com/mikekaminsky
.. _`inkn`: inkn
.. _`Johannes Hoff`: Johannes Hoff
.. _`Matheus Rosa`: Matheus Rosa
.. _`Artur Dryomov`: https://github.com/ming13
.. _`Stuart Quin`: https://github.com/stuartquin
.. _`Tamas Boros`: https://github.com/TamasNo1
.. _`Jacek Wielemborek`: https://github.com/d33tah
.. _`Rodrigo Ramírez Norambuena`: https://github.com/roramirez
.. _`Anthony Lai`: https://github.com/ajlai
.. _`Ari Summer`: Ari Summer
.. _`David Szotten`: David Szotten
.. _`Fernando L. Canizo`: Fernando L. Canizo
.. _`Tim Sanders`: https://github.com/Gollum999
.. _`Irina Truong`: https://github.com/j-bennet
.. _`James Munson`: https://github.com/jmunson
.. _`Fernando Mora`: https://github.com/fernandomora
.. _`Timothy Cleaver`: Timothy Cleaver
.. _`gtxx`: gtxx
.. _`Joakim Koljonen`: https://github.com/koljonen
.. _`Anže Pečar`: https://github.com/Smotko
.. _`Catherine Devlin`: https://github.com/catherinedevlin
.. _`Eric Wald`: https://github.com/eswald
.. _`avdd`: https://github.com/avdd
.. _`Adrian Dries`: Adrian Dries
.. _`Julien Rouhaud`: https://github.com/rjuju
.. _`Lim H`: Lim H
.. _`Tahir Butt`: Tahir Butt
.. _`Tiziano Müller`: https://github.com/dev-zero
.. _`Janus Troelsen`: https://github.com/ysangkok
.. _`Fabien Meghazi`: https://github.com/amigrave
.. _`Manuel Barkhau`: https://github.com/mbarkhau
.. _`Sergii`: https://github.com/foxyterkel
.. _`Lele Gaifax`: https://github.com/lelit
.. _`tk`: https://github.com/kanet77
.. _`Owen Stephens`: https://github.com/owst
.. _`Russell Davies`: https://github.com/russelldavies
.. _`Dick Marinus`: https://github.com/meeuw
.. _`Étienne Bersac`: https://github.com/bersace
.. _`Thomas Roten`: https://github.com/tsroten
.. _`Gustavo Castro`: https://github.com/gustavo-castro
.. _`Alexander Schmolck`: https://github.com/aschmolck
.. _`Andrew Speed`: https://github.com/AndrewSpeed
.. _`Dmitry B`: https://github.com/oxitnik
.. _`Marcin Sztolcman`: https://github.com/msztolcman
.. _`Isank`: https://github.com/isank
.. _`Bojan Delić`: https://github.com/delicb
.. _`Frederic Aoustin`: https://github.com/fraoustin
.. _`Jason Ribeiro`: https://github.com/jrib
.. _`Rishi Ramraj`: https://github.com/RishiRamraj
.. _`Matthieu Guilbert`: https://github.com/gma2th
.. _`Alexandr Korsak`: https://github.com/oivoodoo
.. _`Saif Hakim`: https://github.com/saifelse
.. _`Artur Balabanov`: https://github.com/arturbalabanov
.. _`Kenny Do`: https://github.com/kennydo
.. _`Max Rothman`: https://github.com/maxrothman
.. _`Daniel Egger`: https://github.com/DanEEStar
.. _`Ignacio Campabadal`: https://github.com/igncampa
.. _`Mikhail Elovskikh`: https://github.com/wronglink
.. _`Marcin Cieślak`: https://github.com/saper
.. _`Scott Brenstuhl`: https://github.com/808sAndBR
.. _`easteregg`: https://github.com/verfriemelt-dot-org
.. _`Nathan Verzemnieks`: https://github.com/njvrzm
.. _`raylu`: https://github.com/raylu
.. _`Zhaolong Zhu`: https://github.com/zzl0
.. _`Xavier Francisco`: https://github.com/Qu4tro
.. _`VVelox`: https://github.com/VVelox
.. _`Telmo "Trooper"`: https://github.com/telmotrooper
.. _`Alexander Zawadzki`: https://github.com/zadacka
.. _`Sebastian Janko`: https://github.com/sebojanko
.. _`Pedro Ferrari`: https://github.com/petobens
.. _`BrownShibaDog`: https://github.com/BrownShibaDog
.. _`thegeorgeous`: https://github.com/thegeorgeous
.. _`laixintao`: https://github.com/laixintao
.. _`anthonydb`: https://github.com/anthonydb
.. _`Daniel Kukula`: https://github.com/dkuku


================================================
FILE: pgcli/__init__.py
================================================
__version__ = "4.4.0"


================================================
FILE: pgcli/__main__.py
================================================
"""
pgcli package main entry point
"""

from .main import cli


if __name__ == "__main__":
    cli()


================================================
FILE: pgcli/auth.py
================================================
import click
from textwrap import dedent


keyring = None  # keyring will be loaded later


keyring_error_message = dedent(
    """\
    {}
    {}
    To remove this message do one of the following:
    - prepare keyring as described at: https://keyring.readthedocs.io/en/stable/
    - uninstall keyring: pip uninstall keyring
    - disable keyring in our configuration: add keyring = False to [main]"""
)


def keyring_initialize(keyring_enabled, *, logger):
    """Initialize keyring only if explicitly enabled"""
    global keyring

    if keyring_enabled:
        # Try best to load keyring (issue #1041).
        import importlib

        try:
            keyring = importlib.import_module("keyring")
        except ModuleNotFoundError as e:  # ImportError for Python 2, ModuleNotFoundError for Python 3
            logger.warning("import keyring failed: %r.", e)


def keyring_get_password(key):
    """Attempt to get password from keyring"""
    # Find password from store
    passwd = ""
    try:
        passwd = keyring.get_password("pgcli", key) or ""
    except Exception as e:
        click.secho(
            keyring_error_message.format("Load your password from keyring returned:", str(e)),
            err=True,
            fg="red",
        )
    return passwd


def keyring_set_password(key, passwd):
    try:
        keyring.set_password("pgcli", key, passwd)
    except Exception as e:
        click.secho(
            keyring_error_message.format("Set password in keyring returned:", str(e)),
            err=True,
            fg="red",
        )


================================================
FILE: pgcli/completion_refresher.py
================================================
import threading
import os
from collections import OrderedDict

from .pgcompleter import PGCompleter


class CompletionRefresher:
    refreshers = OrderedDict()

    def __init__(self):
        self._completer_thread = None
        self._restart_refresh = threading.Event()

    def refresh(self, executor, special, callbacks, history=None, settings=None):
        """
        Creates a PGCompleter object and populates it with the relevant
        completion suggestions in a background thread.

        executor - PGExecute object, used to extract the credentials to connect
                   to the database.
        special - PGSpecial object used for creating a new completion object.
        settings - dict of settings for completer object
        callbacks - A function or a list of functions to call after the thread
                    has completed the refresh. The newly created completion
                    object will be passed in as an argument to each callback.
        """
        if executor.is_virtual_database():
            # do nothing
            return [(None, None, None, "Auto-completion refresh can't be started.")]

        if self.is_refreshing():
            self._restart_refresh.set()
            return [(None, None, None, "Auto-completion refresh restarted.")]
        else:
            self._completer_thread = threading.Thread(
                target=self._bg_refresh,
                args=(executor, special, callbacks, history, settings),
                name="completion_refresh",
            )
            self._completer_thread.daemon = True
            self._completer_thread.start()
            return [(None, None, None, "Auto-completion refresh started in the background.")]

    def is_refreshing(self):
        return self._completer_thread and self._completer_thread.is_alive()

    def _bg_refresh(self, pgexecute, special, callbacks, history=None, settings=None):
        settings = settings or {}
        completer = PGCompleter(smart_completion=True, pgspecial=special, settings=settings)

        if settings.get("single_connection"):
            executor = pgexecute
        else:
            # Create a new pgexecute method to populate the completions.
            executor = pgexecute.copy()
        # If callbacks is a single function then push it into a list.
        if callable(callbacks):
            callbacks = [callbacks]

        while 1:
            for refresher in self.refreshers.values():
                refresher(completer, executor)
                if self._restart_refresh.is_set():
                    self._restart_refresh.clear()
                    break
            else:
                # Break out of while loop if the for loop finishes natually
                # without hitting the break statement.
                break

            # Start over the refresh from the beginning if the for loop hit the
            # break statement.
            continue

        # Load history into pgcompleter so it can learn user preferences
        n_recent = 100
        if history:
            for recent in history.get_strings()[-n_recent:]:
                completer.extend_query_history(recent, is_init=True)

        for callback in callbacks:
            callback(completer)

        if not settings.get("single_connection") and executor.conn:
            # close connection established with pgexecute.copy()
            executor.conn.close()


def refresher(name, refreshers=CompletionRefresher.refreshers):
    """Decorator to populate the dictionary of refreshers with the current
    function.
    """

    def wrapper(wrapped):
        refreshers[name] = wrapped
        return wrapped

    return wrapper


@refresher("schemata")
def refresh_schemata(completer, executor):
    completer.set_search_path(executor.search_path())
    completer.extend_schemata(executor.schemata())


@refresher("tables")
def refresh_tables(completer, executor):
    completer.extend_relations(executor.tables(), kind="tables")
    completer.extend_columns(executor.table_columns(), kind="tables")
    completer.extend_foreignkeys(executor.foreignkeys())


@refresher("views")
def refresh_views(completer, executor):
    completer.extend_relations(executor.views(), kind="views")
    completer.extend_columns(executor.view_columns(), kind="views")


@refresher("types")
def refresh_types(completer, executor):
    completer.extend_datatypes(executor.datatypes())


@refresher("databases")
def refresh_databases(completer, executor):
    completer.extend_database_names(executor.databases())


@refresher("casing")
def refresh_casing(completer, executor):
    casing_file = completer.casing_file
    if not casing_file:
        return
    generate_casing_file = completer.generate_casing_file
    if generate_casing_file and not os.path.isfile(casing_file):
        casing_prefs = "\n".join(executor.casing())
        with open(casing_file, "w") as f:
            f.write(casing_prefs)
    if os.path.isfile(casing_file):
        with open(casing_file) as f:
            completer.extend_casing([line.strip() for line in f])


@refresher("functions")
def refresh_functions(completer, executor):
    completer.extend_functions(executor.functions())


================================================
FILE: pgcli/config.py
================================================
import shutil
import os
import platform
from os.path import expanduser, exists, dirname
import re
from typing import TextIO
from configobj import ConfigObj


def config_location():
    if "XDG_CONFIG_HOME" in os.environ:
        return "%s/pgcli/" % expanduser(os.environ["XDG_CONFIG_HOME"])
    elif platform.system() == "Windows":
        return os.getenv("USERPROFILE") + "\\AppData\\Local\\dbcli\\pgcli\\"
    else:
        return expanduser("~/.config/pgcli/")


def load_config(usr_cfg, def_cfg=None):
    # avoid config merges when possible. For writing, we need an umerged config instance.
    # see https://github.com/dbcli/pgcli/issues/1240 and https://github.com/DiffSK/configobj/issues/171
    if def_cfg:
        cfg = ConfigObj()
        cfg.merge(ConfigObj(def_cfg, interpolation=False))
        cfg.merge(ConfigObj(expanduser(usr_cfg), interpolation=False, encoding="utf-8"))
    else:
        cfg = ConfigObj(expanduser(usr_cfg), interpolation=False, encoding="utf-8")
    cfg.filename = expanduser(usr_cfg)
    return cfg


def ensure_dir_exists(path):
    parent_dir = expanduser(dirname(path))
    os.makedirs(parent_dir, exist_ok=True)


def write_default_config(source, destination, overwrite=False):
    destination = expanduser(destination)
    if not overwrite and exists(destination):
        return

    ensure_dir_exists(destination)

    shutil.copyfile(source, destination)


def upgrade_config(config, def_config):
    cfg = load_config(config, def_config)
    cfg.write()


def get_config_filename(pgclirc_file=None):
    return pgclirc_file or "%sconfig" % config_location()


def get_config(pgclirc_file=None):
    from pgcli import __file__ as package_root

    package_root = os.path.dirname(package_root)

    pgclirc_file = get_config_filename(pgclirc_file)

    default_config = os.path.join(package_root, "pgclirc")
    write_default_config(default_config, pgclirc_file)

    return load_config(pgclirc_file, default_config)


def get_casing_file(config):
    casing_file = config["main"]["casing_file"]
    if casing_file == "default":
        casing_file = config_location() + "casing"
    return casing_file


def skip_initial_comment(f_stream: TextIO) -> int:
    """
    Initial comment in ~/.pg_service.conf is not always marked with '#'
    which crashes the parser. This function takes a file object and
    "rewinds" it to the beginning of the first section,
    from where on it can be parsed safely

    :return: number of skipped lines
    """
    section_regex = r"\s*\["
    pos = f_stream.tell()
    lines_skipped = 0
    while True:
        line = f_stream.readline()
        if line == "":
            break
        if re.match(section_regex, line) is not None:
            f_stream.seek(pos)
            break
        else:
            pos += len(line)
            lines_skipped += 1
    return lines_skipped


================================================
FILE: pgcli/explain_output_formatter.py
================================================
from pgcli.pyev import Visualizer
import json


"""Explain response output adapter"""


class ExplainOutputFormatter:
    def __init__(self, max_width):
        self.max_width = max_width

    def format_output(self, cur, headers, **output_kwargs):
        # explain query results should always contain 1 row each
        [(data,)] = list(cur)
        explain_list = json.loads(data)
        visualizer = Visualizer(self.max_width)
        for explain in explain_list:
            visualizer.load(explain)
            yield visualizer.get_list()


================================================
FILE: pgcli/key_bindings.py
================================================
import logging
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.filters import (
    completion_is_selected,
    is_searching,
    has_completions,
    has_selection,
    vi_mode,
)

from .pgbuffer import buffer_should_be_handled, safe_multi_line_mode

_logger = logging.getLogger(__name__)


def pgcli_bindings(pgcli):
    """Custom key bindings for pgcli."""
    kb = KeyBindings()

    tab_insert_text = " " * 4

    @kb.add("f2")
    def _(event):
        """Enable/Disable SmartCompletion Mode."""
        _logger.debug("Detected F2 key.")
        pgcli.completer.smart_completion = not pgcli.completer.smart_completion

    @kb.add("f3")
    def _(event):
        """Enable/Disable Multiline Mode."""
        _logger.debug("Detected F3 key.")
        pgcli.multi_line = not pgcli.multi_line

    @kb.add("f4")
    def _(event):
        """Toggle between Vi and Emacs mode."""
        _logger.debug("Detected F4 key.")
        pgcli.vi_mode = not pgcli.vi_mode
        event.app.editing_mode = EditingMode.VI if pgcli.vi_mode else EditingMode.EMACS

    @kb.add("f5")
    def _(event):
        """Toggle between Vi and Emacs mode."""
        _logger.debug("Detected F5 key.")
        pgcli.explain_mode = not pgcli.explain_mode

    @kb.add("tab")
    def _(event):
        """Force autocompletion at cursor on non-empty lines."""

        _logger.debug("Detected <Tab> key.")

        buff = event.app.current_buffer
        doc = buff.document

        if doc.on_first_line or doc.current_line.strip():
            if buff.complete_state:
                buff.complete_next()
            else:
                buff.start_completion(select_first=True)
        else:
            buff.insert_text(tab_insert_text, fire_event=False)

    @kb.add("escape", filter=has_completions)
    def _(event):
        """Force closing of autocompletion."""
        _logger.debug("Detected <Esc> key.")

        event.current_buffer.complete_state = None
        event.app.current_buffer.complete_state = None

    @kb.add("c-space")
    def _(event):
        """
        Initialize autocompletion at cursor.

        If the autocompletion menu is not showing, display it with the
        appropriate completions for the context.

        If the menu is showing, select the next completion.
        """
        _logger.debug("Detected <C-Space> key.")

        b = event.app.current_buffer
        if b.complete_state:
            b.complete_next()
        else:
            b.start_completion(select_first=False)

    @kb.add("enter", filter=completion_is_selected)
    def _(event):
        """Makes the enter key work as the tab key only when showing the menu.

        In other words, don't execute query when enter is pressed in
        the completion dropdown menu, instead close the dropdown menu
        (accept current selection).

        """
        _logger.debug("Detected enter key during completion selection.")

        event.current_buffer.complete_state = None
        event.app.current_buffer.complete_state = None

    # When using multi_line input mode the buffer is not handled on Enter (a new line is
    # inserted instead), so we force the handling if we're not in a completion or
    # history search, and one of several conditions are True
    @kb.add(
        "enter",
        filter=~(completion_is_selected | is_searching) & buffer_should_be_handled(pgcli),
    )
    def _(event):
        _logger.debug("Detected enter key.")
        event.current_buffer.validate_and_handle()

    @kb.add("escape", "enter", filter=~vi_mode & ~safe_multi_line_mode(pgcli))
    def _(event):
        """Introduces a line break regardless of multi-line mode or not."""
        _logger.debug("Detected alt-enter key.")
        event.app.current_buffer.insert_text("\n")

    @kb.add("c-p", filter=~has_selection)
    def _(event):
        """Move up in history."""
        event.current_buffer.history_backward(count=event.arg)

    @kb.add("c-n", filter=~has_selection)
    def _(event):
        """Move down in history."""
        event.current_buffer.history_forward(count=event.arg)

    return kb


================================================
FILE: pgcli/magic.py
================================================
from .main import PGCli
import sql.parse
import sql.connection
import logging

_logger = logging.getLogger(__name__)


def load_ipython_extension(ipython):
    """This is called via the ipython command '%load_ext pgcli.magic'"""

    # first, load the sql magic if it isn't already loaded
    if not ipython.find_line_magic("sql"):
        ipython.run_line_magic("load_ext", "sql")

    # register our own magic
    ipython.register_magic_function(pgcli_line_magic, "line", "pgcli")


def pgcli_line_magic(line):
    _logger.debug("pgcli magic called: %r", line)
    parsed = sql.parse.parse(line, {})
    # "get" was renamed to "set" in ipython-sql:
    # https://github.com/catherinedevlin/ipython-sql/commit/f4283c65aaf68f961e84019e8b939e4a3c501d43
    if hasattr(sql.connection.Connection, "get"):
        conn = sql.connection.Connection.get(parsed["connection"])
    else:
        try:
            conn = sql.connection.Connection.set(parsed["connection"])
        # a new positional argument was added to Connection.set in version 0.4.0 of ipython-sql
        except TypeError:
            conn = sql.connection.Connection.set(parsed["connection"], False)

    try:
        # A corresponding pgcli object already exists
        pgcli = conn._pgcli
        _logger.debug("Reusing existing pgcli")
    except AttributeError:
        # I can't figure out how to get the underylying psycopg2 connection
        # from the sqlalchemy connection, so just grab the url and make a
        # new connection
        pgcli = PGCli()
        u = conn.session.engine.url
        _logger.debug("New pgcli: %r", str(u))

        pgcli.connect_uri(str(u._replace(drivername="postgres")))
        conn._pgcli = pgcli

    # For convenience, print the connection alias
    print(f"Connected: {conn.name}")

    try:
        pgcli.run_cli()
    except SystemExit:
        pass

    if not pgcli.query_history:
        return

    q = pgcli.query_history[-1]

    if not q.successful:
        _logger.debug("Unsuccessful query - ignoring")
        return

    if q.meta_changed or q.db_changed or q.path_changed:
        _logger.debug("Dangerous query detected -- ignoring")
        return

    ipython = get_ipython()
    return ipython.run_cell_magic("sql", line, q.query)


================================================
FILE: pgcli/main.py
================================================
from zoneinfo import ZoneInfoNotFoundError
from configobj import ConfigObj, ParseError
from pgspecial.namedqueries import NamedQueries
from .config import skip_initial_comment

import atexit
import os
import re
import sys
import traceback
import logging
import threading
import shutil
import functools
import datetime as dt
import itertools
import pathlib
import platform
from time import time, sleep
from typing import Optional

from cli_helpers.tabular_output import TabularOutputFormatter
from cli_helpers.tabular_output.preprocessors import (
    align_decimals,
    format_numbers,
    format_timestamps,
)
from cli_helpers.utils import strip_ansi
from .explain_output_formatter import ExplainOutputFormatter
import click
import tzlocal

try:
    import setproctitle
except ImportError:
    setproctitle = None
from prompt_toolkit.completion import DynamicCompleter, ThreadedCompleter
from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
from prompt_toolkit.shortcuts import PromptSession, CompleteStyle
from prompt_toolkit.document import Document
from prompt_toolkit.filters import HasFocus, IsDone
from prompt_toolkit.formatted_text import ANSI
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.layout.processors import (
    ConditionalProcessor,
    HighlightMatchingBracketProcessor,
    TabsProcessor,
)
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from pygments.lexers.sql import PostgresLexer

from pgspecial.main import PGSpecial, NO_QUERY, PAGER_OFF, PAGER_LONG_OUTPUT
import pgspecial as special

from . import auth
from .pgcompleter import PGCompleter
from .pgtoolbar import create_toolbar_tokens_func
from .pgstyle import style_factory, style_factory_output
from .pgexecute import PGExecute
from .completion_refresher import CompletionRefresher
from .config import (
    get_casing_file,
    load_config,
    config_location,
    ensure_dir_exists,
    get_config,
    get_config_filename,
)
from .key_bindings import pgcli_bindings
from .packages.formatter.sqlformatter import register_new_formatter
from .packages.prompt_utils import confirm, confirm_destructive_query
from .packages.parseutils import is_destructive
from .packages.parseutils import parse_destructive_warning
from .__init__ import __version__

click.disable_unicode_literals_warning = True

from urllib.parse import urlparse

from getpass import getuser

from psycopg import OperationalError, InterfaceError, Notify
from psycopg.conninfo import make_conninfo, conninfo_to_dict
from psycopg.errors import Diagnostic

from collections import namedtuple

try:
    import sshtunnel

    SSH_TUNNEL_SUPPORT = True
except ImportError:
    SSH_TUNNEL_SUPPORT = False


# Ref: https://stackoverflow.com/questions/30425105/filter-special-chars-such-as-color-codes-from-shell-output
COLOR_CODE_REGEX = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
DEFAULT_MAX_FIELD_WIDTH = 500

# Query tuples are used for maintaining history
MetaQuery = namedtuple(
    "Query",
    [
        "query",  # The entire text of the command
        "successful",  # True If all subqueries were successful
        "total_time",  # Time elapsed executing the query and formatting results
        "execution_time",  # Time elapsed executing the query
        "meta_changed",  # True if any subquery executed create/alter/drop
        "db_changed",  # True if any subquery changed the database
        "path_changed",  # True if any subquery changed the search path
        "mutated",  # True if any subquery executed insert/update/delete
        "is_special",  # True if the query is a special command
    ],
)
MetaQuery.__new__.__defaults__ = ("", False, 0, 0, False, False, False, False)

OutputSettings = namedtuple(
    "OutputSettings",
    "table_format dcmlfmt floatfmt column_date_formats missingval expanded max_width case_function style_output max_field_width",
)
OutputSettings.__new__.__defaults__ = (
    None,
    None,
    None,
    None,
    "<null>",
    False,
    None,
    lambda x: x,
    None,
    DEFAULT_MAX_FIELD_WIDTH,
)


class PgCliQuitError(Exception):
    pass


def notify_callback(notify: Notify):
    click.secho(
        'Notification received on channel "{}" (PID {}):\n{}'.format(notify.channel, notify.pid, notify.payload),
        fg="green",
    )


class PGCli:
    default_prompt = "\\u@\\h:\\d> "
    max_len_prompt = 30

    def set_default_pager(self, config):
        configured_pager = config["main"].get("pager")
        os_environ_pager = os.environ.get("PAGER")

        if configured_pager:
            self.logger.info('Default pager found in config file: "%s"', configured_pager)
            os.environ["PAGER"] = configured_pager
        elif os_environ_pager:
            self.logger.info(
                'Default pager found in PAGER environment variable: "%s"',
                os_environ_pager,
            )
            os.environ["PAGER"] = os_environ_pager
        else:
            self.logger.info("No default pager found in environment. Using os default pager")

        # Set default set of less recommended options, if they are not already set.
        # They are ignored if pager is different than less.
        if not os.environ.get("LESS"):
            os.environ["LESS"] = "-SRXF"

    def __init__(
        self,
        force_passwd_prompt=False,
        never_passwd_prompt=False,
        pgexecute=None,
        pgclirc_file=None,
        row_limit=None,
        application_name="pgcli",
        single_connection=False,
        less_chatty=None,
        prompt=None,
        prompt_dsn=None,
        auto_vertical_output=False,
        warn=None,
        ssh_tunnel_url: Optional[str] = None,
        log_file: Optional[str] = None,
    ):
        self.force_passwd_prompt = force_passwd_prompt
        self.never_passwd_prompt = never_passwd_prompt
        self.pgexecute = pgexecute
        self.dsn_alias = None
        self.watch_command = None

        # Load config.
        c = self.config = get_config(pgclirc_file)

        # at this point, config should be written to pgclirc_file if it did not exist. Read it.
        self.config_writer = load_config(get_config_filename(pgclirc_file))

        # make sure to use self.config_writer, not self.config
        NamedQueries.instance = NamedQueries.from_config(self.config_writer)

        self.logger = logging.getLogger(__name__)
        self.initialize_logging()

        self.set_default_pager(c)
        self.output_file = None
        self.pgspecial = PGSpecial()

        self.hide_named_query_text = "hide_named_query_text" in c["main"] and c["main"].as_bool("hide_named_query_text")
        self.explain_mode = False
        self.multi_line = c["main"].as_bool("multi_line")
        self.multiline_mode = c["main"].get("multi_line_mode", "psql")
        self.vi_mode = c["main"].as_bool("vi")
        self.auto_expand = auto_vertical_output or c["main"].as_bool("auto_expand")
        self.auto_retry_closed_connection = c["main"].as_bool("auto_retry_closed_connection")
        self.expanded_output = c["main"].as_bool("expand")
        self.pgspecial.timing_enabled = c["main"].as_bool("timing")
        if row_limit is not None:
            self.row_limit = row_limit
        else:
            self.row_limit = c["main"].as_int("row_limit")

        self.application_name = application_name

        # if not specified, set to DEFAULT_MAX_FIELD_WIDTH
        # if specified but empty, set to None to disable truncation
        # ellipsis will take at least 3 symbols, so this can't be less than 3 if specified and > 0
        max_field_width = c["main"].get("max_field_width", DEFAULT_MAX_FIELD_WIDTH)
        if max_field_width and max_field_width.lower() != "none":
            max_field_width = max(3, abs(int(max_field_width)))
        else:
            max_field_width = None
        self.max_field_width = max_field_width

        self.min_num_menu_lines = c["main"].as_int("min_num_menu_lines")
        self.multiline_continuation_char = c["main"]["multiline_continuation_char"]
        self.table_format = c["main"]["table_format"]
        self.syntax_style = c["main"]["syntax_style"]
        self.cli_style = c["colors"]
        self.wider_completion_menu = c["main"].as_bool("wider_completion_menu")
        self.destructive_warning = parse_destructive_warning(warn or c["main"].as_list("destructive_warning"))
        self.destructive_warning_restarts_connection = c["main"].as_bool("destructive_warning_restarts_connection")
        self.destructive_statements_require_transaction = c["main"].as_bool("destructive_statements_require_transaction")

        self.less_chatty = bool(less_chatty) or c["main"].as_bool("less_chatty")
        self.verbose_errors = "verbose_errors" in c["main"] and c["main"].as_bool("verbose_errors")
        self.null_string = c["main"].get("null_string", "<null>")
        self.prompt_format = prompt if prompt is not None else c["main"].get("prompt", self.default_prompt)
        self.prompt_dsn_format = prompt_dsn
        self.on_error = c["main"]["on_error"].upper()
        self.decimal_format = c["data_formats"]["decimal"]
        self.float_format = c["data_formats"]["float"]
        self.column_date_formats = c["column_date_formats"]
        auth.keyring_initialize(c["main"].as_bool("keyring"), logger=self.logger)
        self.show_bottom_toolbar = c["main"].as_bool("show_bottom_toolbar")

        self.pgspecial.pset_pager(self.config["main"].as_bool("enable_pager") and "on" or "off")

        self.style_output = style_factory_output(self.syntax_style, c["colors"])

        self.now = dt.datetime.today()

        self.completion_refresher = CompletionRefresher()

        self.query_history = []

        # Initialize completer
        smart_completion = c["main"].as_bool("smart_completion")
        keyword_casing = c["main"]["keyword_casing"]
        single_connection = single_connection or c["main"].as_bool("always_use_single_connection")
        self.settings = {
            "casing_file": get_casing_file(c),
            "generate_casing_file": c["main"].as_bool("generate_casing_file"),
            "generate_aliases": c["main"].as_bool("generate_aliases"),
            "asterisk_column_order": c["main"]["asterisk_column_order"],
            "qualify_columns": c["main"]["qualify_columns"],
            "case_column_headers": c["main"].as_bool("case_column_headers"),
            "search_path_filter": c["main"].as_bool("search_path_filter"),
            "single_connection": single_connection,
            "less_chatty": less_chatty,
            "keyword_casing": keyword_casing,
            "alias_map_file": c["main"]["alias_map_file"] or None,
        }

        completer = PGCompleter(smart_completion, pgspecial=self.pgspecial, settings=self.settings)
        self.completer = completer
        self._completer_lock = threading.Lock()
        self.register_special_commands()

        self.prompt_app = None

        self.dsn_ssh_tunnel_config = c.get("dsn ssh tunnels")
        self.ssh_tunnel_config = c.get("ssh tunnels")
        self.ssh_tunnel_url = ssh_tunnel_url
        self.ssh_tunnel = None

        if log_file:
            with open(log_file, "a+"):
                pass  # ensure writeable
        self.log_file = log_file

        # formatter setup
        self.formatter = TabularOutputFormatter(format_name=c["main"]["table_format"])
        register_new_formatter(self.formatter)

    def quit(self):
        raise PgCliQuitError

    def toggle_named_query_quiet(self):
        """Toggle hiding of named query text"""
        self.hide_named_query_text = not self.hide_named_query_text
        status = "ON" if self.hide_named_query_text else "OFF"
        message = f"Named query quiet mode: {status}"
        return [(None, None, None, message)]

    def _is_named_query_execution(self, text):
        """Check if the command is a named query execution (\n <name>)."""
        text = text.strip()
        return text.startswith("\\n ") and not text.startswith("\\ns ") and not text.startswith("\\nd ")

    def register_special_commands(self):
        self.pgspecial.register(
            self.toggle_named_query_quiet,
            "\\nq",
            "\\nq",
            "Toggle named query quiet mode (hide query text)",
            arg_type=NO_QUERY,
            case_sensitive=True,
        )

        self.pgspecial.register(
            self.change_db,
            "\\c",
            "\\c[onnect] database_name",
            "Change to a new database.",
            aliases=("use", "\\connect", "USE"),
        )

        def refresh_callback():
            return self.refresh_completions(persist_priorities="all")

        self.pgspecial.register(
            self.quit,
            "\\q",
            "\\q",
            "Quit pgcli.",
            arg_type=NO_QUERY,
            case_sensitive=True,
            aliases=(":q",),
        )
        self.pgspecial.register(
            self.quit,
            "quit",
            "quit",
            "Quit pgcli.",
            arg_type=NO_QUERY,
            case_sensitive=False,
            aliases=("exit",),
        )
        self.pgspecial.register(
            refresh_callback,
            "\\#",
            "\\#",
            "Refresh auto-completions.",
            arg_type=NO_QUERY,
        )
        self.pgspecial.register(
            refresh_callback,
            "\\refresh",
            "\\refresh",
            "Refresh auto-completions.",
            arg_type=NO_QUERY,
        )
        self.pgspecial.register(self.execute_from_file, "\\i", "\\i filename", "Execute commands from file.")
        self.pgspecial.register(
            self.write_to_file,
            "\\o",
            "\\o [filename]",
            "Send all query results to file.",
        )
        self.pgspecial.register(
            self.write_to_logfile,
            "\\log-file",
            "\\log-file [filename]",
            "Log all query results to a logfile, in addition to the normal output destination.",
        )
        self.pgspecial.register(self.info_connection, "\\conninfo", "\\conninfo", "Get connection details")
        self.pgspecial.register(
            self.change_table_format,
            "\\T",
            "\\T [format]",
            "Change the table format used to output results",
        )

        self.pgspecial.register(
            self.echo,
            "\\echo",
            "\\echo [string]",
            "Echo a string to stdout",
        )

        self.pgspecial.register(
            self.echo,
            "\\qecho",
            "\\qecho [string]",
            "Echo a string to the query output channel.",
        )

        self.pgspecial.register(
            self.toggle_verbose_errors,
            "\\v",
            "\\v [on|off]",
            "Toggle verbose errors.",
        )

    def toggle_verbose_errors(self, pattern, **_):
        flag = pattern.strip()

        if flag == "on":
            self.verbose_errors = True
        elif flag == "off":
            self.verbose_errors = False
        else:
            self.verbose_errors = not self.verbose_errors

        message = "Verbose errors " + "on." if self.verbose_errors else "off."
        return [(None, None, None, message)]

    def echo(self, pattern, **_):
        return [(None, None, None, pattern)]

    def change_table_format(self, pattern, **_):
        try:
            if pattern not in TabularOutputFormatter().supported_formats:
                raise ValueError()
            self.table_format = pattern
            yield (None, None, None, f"Changed table format to {pattern}")
        except ValueError:
            msg = f"Table format {pattern} not recognized. Allowed formats:"
            for table_type in TabularOutputFormatter().supported_formats:
                msg += f"\n\t{table_type}"
            msg += "\nCurrently set to: %s" % self.table_format
            yield (None, None, None, msg)

    def info_connection(self, **_):
        if self.pgexecute.host.startswith("/"):
            host = 'socket "%s"' % self.pgexecute.host
        else:
            host = 'host "%s"' % self.pgexecute.host

        yield (
            None,
            None,
            None,
            'You are connected to database "%s" as user '
            '"%s" on %s at port "%s".' % (self.pgexecute.dbname, self.pgexecute.user, host, self.pgexecute.port),
        )

    def change_db(self, pattern, **_):
        if pattern:
            # Get all the parameters in pattern, handling double quotes if any.
            infos = re.findall(r'"[^"]*"|[^"\'\s]+', pattern)
            # Now removing quotes.
            [s.strip('"') for s in infos]

            infos.extend([None] * (4 - len(infos)))
            db, user, host, port = infos
            try:
                self.pgexecute.connect(
                    database=db,
                    user=user,
                    host=host,
                    port=port,
                    **self.pgexecute.extra_args,
                )
            except OperationalError as e:
                click.secho(str(e), err=True, fg="red")
                click.echo("Previous connection kept")
        else:
            self.pgexecute.connect()

        yield (
            None,
            None,
            None,
            'You are now connected to database "%s" as user "%s"' % (self.pgexecute.dbname, self.pgexecute.user),
        )

    def execute_from_file(self, pattern, **_):
        if not pattern:
            message = "\\i: missing required argument"
            return [(None, None, None, message, "", False, True)]
        try:
            with open(os.path.expanduser(pattern), encoding="utf-8") as f:
                query = f.read()
        except OSError as e:
            return [(None, None, None, str(e), "", False, True)]

        if self.destructive_warning:
            if (
                self.destructive_statements_require_transaction
                and not self.pgexecute.valid_transaction()
                and is_destructive(query, self.destructive_warning)
            ):
                message = "Destructive statements must be run within a transaction. Command execution stopped."
                return [(None, None, None, message)]
            destroy = confirm_destructive_query(query, self.destructive_warning, self.dsn_alias)
            if destroy is False:
                message = "Wise choice. Command execution stopped."
                return [(None, None, None, message)]

        on_error_resume = self.on_error == "RESUME"
        return self.pgexecute.run(
            query,
            self.pgspecial,
            on_error_resume=on_error_resume,
            explain_mode=self.explain_mode,
        )

    def write_to_logfile(self, pattern, **_):
        if not pattern:
            self.log_file = None
            message = "Logfile capture disabled"
            return [(None, None, None, message, "", True, True)]

        log_file = pathlib.Path(pattern).expanduser().absolute()

        try:
            with open(log_file, "a+"):
                pass  # ensure writeable
        except OSError as e:
            self.log_file = None
            message = str(e) + "\nLogfile capture disabled"
            return [(None, None, None, message, "", False, True)]

        self.log_file = str(log_file)
        message = 'Writing to file "%s"' % self.log_file
        return [(None, None, None, message, "", True, True)]

    def write_to_file(self, pattern, **_):
        if not pattern:
            self.output_file = None
            message = "File output disabled"
            return [(None, None, None, message, "", True, True)]
        filename = os.path.abspath(os.path.expanduser(pattern))
        if not os.path.isfile(filename):
            try:
                open(filename, "w").close()
            except OSError as e:
                self.output_file = None
                message = str(e) + "\nFile output disabled"
                return [(None, None, None, message, "", False, True)]
        self.output_file = filename
        message = 'Writing to file "%s"' % self.output_file
        return [(None, None, None, message, "", True, True)]

    def initialize_logging(self):
        log_file = self.config["main"]["log_file"]
        if log_file == "default":
            log_file = config_location() + "log"
        ensure_dir_exists(log_file)
        log_level = self.config["main"]["log_level"]

        # Disable logging if value is NONE by switching to a no-op handler.
        # Set log level to a high value so it doesn't even waste cycles getting called.
        if log_level.upper() == "NONE":
            handler = logging.NullHandler()
        else:
            handler = logging.FileHandler(os.path.expanduser(log_file))

        level_map = {
            "CRITICAL": logging.CRITICAL,
            "ERROR": logging.ERROR,
            "WARNING": logging.WARNING,
            "INFO": logging.INFO,
            "DEBUG": logging.DEBUG,
            "NONE": logging.CRITICAL,
        }

        log_level = level_map[log_level.upper()]

        formatter = logging.Formatter("%(asctime)s (%(process)d/%(threadName)s) %(name)s %(levelname)s - %(message)s")

        handler.setFormatter(formatter)

        root_logger = logging.getLogger("pgcli")
        root_logger.addHandler(handler)
        root_logger.setLevel(log_level)

        root_logger.debug("Initializing pgcli logging.")
        root_logger.debug("Log file %r.", log_file)

        pgspecial_logger = logging.getLogger("pgspecial")
        pgspecial_logger.addHandler(handler)
        pgspecial_logger.setLevel(log_level)

    def connect_dsn(self, dsn, **kwargs):
        self.connect(dsn=dsn, **kwargs)

    def connect_service(self, service, user):
        service_config, file = parse_service_info(service)
        if service_config is None:
            click.secho(f"service '{service}' was not found in {file}", err=True, fg="red")
            sys.exit(1)
        self.connect(
            database=service_config.get("dbname"),
            host=service_config.get("host"),
            user=user or service_config.get("user"),
            port=service_config.get("port"),
            passwd=service_config.get("password"),
        )

    def connect_uri(self, uri):
        kwargs = conninfo_to_dict(uri)
        remap = {"dbname": "database", "password": "passwd"}
        kwargs = {remap.get(k, k): v for k, v in kwargs.items()}
        self.connect(**kwargs)

    def connect(self, database="", host="", user="", port="", passwd="", dsn="", **kwargs):
        # Connect to the database.

        if not user:
            user = getuser()

        if not database:
            database = user

        kwargs.setdefault("application_name", self.application_name)

        # If password prompt is not forced but no password is provided, try
        # getting it from environment variable.
        if not self.force_passwd_prompt and not passwd:
            passwd = os.environ.get("PGPASSWORD", "")

        # Prompt for a password immediately if requested via the -W flag. This
        # avoids wasting time trying to connect to the database and catching a
        # no-password exception.
        # If we successfully parsed a password from a URI, there's no need to
        # prompt for it, even with the -W flag
        if self.force_passwd_prompt and not passwd:
            passwd = click.prompt("Password for %s" % user, hide_input=True, show_default=False, type=str)

        key = f"{user}@{host}"

        if not passwd and auth.keyring:
            passwd = auth.keyring_get_password(key)

        def should_ask_for_password(exc):
            # Prompt for a password after 1st attempt to connect
            # fails. Don't prompt if the -w flag is supplied
            if self.never_passwd_prompt:
                return False
            error_msg = exc.args[0]
            if "no password supplied" in error_msg:
                return True
            if "password authentication failed" in error_msg:
                return True
            return False

        if dsn:
            parsed_dsn = conninfo_to_dict(dsn)
            if "host" in parsed_dsn:
                host = parsed_dsn["host"]
            if "port" in parsed_dsn:
                port = parsed_dsn["port"]

        if self.dsn_alias and self.dsn_ssh_tunnel_config and not self.ssh_tunnel_url:
            for dsn_regex, tunnel_url in self.dsn_ssh_tunnel_config.items():
                if re.search(dsn_regex, self.dsn_alias):
                    self.ssh_tunnel_url = tunnel_url
                    break

        if self.ssh_tunnel_config and not self.ssh_tunnel_url:
            for db_host_regex, tunnel_url in self.ssh_tunnel_config.items():
                if re.search(db_host_regex, host):
                    self.ssh_tunnel_url = tunnel_url
                    break

        if self.ssh_tunnel_url:
            # We add the protocol as urlparse doesn't find it by itself
            if "://" not in self.ssh_tunnel_url:
                self.ssh_tunnel_url = f"ssh://{self.ssh_tunnel_url}"

            tunnel_info = urlparse(self.ssh_tunnel_url)
            params = {
                "local_bind_address": ("127.0.0.1",),
                "remote_bind_address": (host, int(port or 5432)),
                "ssh_address_or_host": (tunnel_info.hostname, tunnel_info.port or 22),
                "logger": self.logger,
            }
            if tunnel_info.username:
                params["ssh_username"] = tunnel_info.username
            if tunnel_info.password:
                params["ssh_password"] = tunnel_info.password

            # Hack: sshtunnel adds a console handler to the logger, so we revert handlers.
            # We can remove this when https://github.com/pahaz/sshtunnel/pull/250 is merged.
            logger_handlers = self.logger.handlers.copy()
            try:
                self.ssh_tunnel = sshtunnel.SSHTunnelForwarder(**params)
                self.ssh_tunnel.start()
            except Exception as e:
                self.logger.handlers = logger_handlers
                self.logger.error("traceback: %r", traceback.format_exc())
                click.secho(str(e), err=True, fg="red")
                sys.exit(1)
            self.logger.handlers = logger_handlers

            atexit.register(self.ssh_tunnel.stop)
            host = "127.0.0.1"
            port = self.ssh_tunnel.local_bind_ports[0]

            if dsn:
                dsn = make_conninfo(dsn, host=host, port=port)

        # Attempt to connect to the database.
        # Note that passwd may be empty on the first attempt. If connection
        # fails because of a missing or incorrect password, but we're allowed to
        # prompt for a password (no -w flag), prompt for a passwd and try again.
        try:
            try:
                pgexecute = PGExecute(
                    database,
                    user,
                    passwd,
                    host,
                    port,
                    dsn,
                    notify_callback,
                    **kwargs,
                )
            except (OperationalError, InterfaceError) as e:
                if should_ask_for_password(e):
                    passwd = click.prompt(
                        "Password for %s" % user,
                        hide_input=True,
                        show_default=False,
                        type=str,
                    )
                    pgexecute = PGExecute(
                        database,
                        user,
                        passwd,
                        host,
                        port,
                        dsn,
                        notify_callback,
                        **kwargs,
                    )
                else:
                    raise e
            if passwd and auth.keyring:
                auth.keyring_set_password(key, passwd)

        except Exception as e:  # Connecting to a database could fail.
            self.logger.debug("Database connection failed: %r.", e)
            self.logger.error("traceback: %r", traceback.format_exc())
            click.secho(str(e), err=True, fg="red")
            sys.exit(1)

        self.pgexecute = pgexecute

    def handle_editor_command(self, text):
        r"""
        Editor command is any query that is prefixed or suffixed
        by a '\e'. The reason for a while loop is because a user
        might edit a query multiple times.
        For eg:
        "select * from \e"<enter> to edit it in vim, then come
        back to the prompt with the edited query "select * from
        blah where q = 'abc'\e" to edit it again.
        :param text: Document
        :return: Document
        """
        editor_command = special.editor_command(text)
        while editor_command:
            if editor_command == "\\e":
                filename = special.get_filename(text)
                query = special.get_editor_query(text) or self.get_last_query()
            else:  # \ev or \ef
                filename = None
                spec = text.split()[1]
                if editor_command == "\\ev":
                    query = self.pgexecute.view_definition(spec)
                elif editor_command == "\\ef":
                    query = self.pgexecute.function_definition(spec)
            sql, message = special.open_external_editor(filename, sql=query)
            if message:
                # Something went wrong. Raise an exception and bail.
                raise RuntimeError(message)
            while True:
                try:
                    text = self.prompt_app.prompt(default=sql)
                    break
                except KeyboardInterrupt:
                    sql = ""

            editor_command = special.editor_command(text)
        return text

    def execute_command(self, text, handle_closed_connection=True):
        logger = self.logger

        query = MetaQuery(query=text, successful=False)

        try:
            if self.destructive_warning:
                if (
                    self.destructive_statements_require_transaction
                    and not self.pgexecute.valid_transaction()
                    and is_destructive(text, self.destructive_warning)
                ):
                    click.secho("Destructive statements must be run within a transaction.")
                    raise KeyboardInterrupt
                destroy = confirm_destructive_query(text, self.destructive_warning, self.dsn_alias)
                if destroy is False:
                    click.secho("Wise choice!")
                    raise KeyboardInterrupt
                elif destroy:
                    click.secho("Your call!")

            output, query = self._evaluate_command(text)
        except KeyboardInterrupt:
            if self.destructive_warning_restarts_connection:
                # Restart connection to the database
                self.pgexecute.connect()
                logger.debug("cancelled query and restarted connection, sql: %r", text)
                click.secho("cancelled query and restarted connection", err=True, fg="red")
            else:
                logger.debug("cancelled query, sql: %r", text)
                click.secho("cancelled query", err=True, fg="red")
        except NotImplementedError:
            click.secho("Not Yet Implemented.", fg="yellow")
        except OperationalError as e:
            logger.error("sql: %r, error: %r", text, e)
            logger.error("traceback: %r", traceback.format_exc())
            click.secho(str(e), err=True, fg="red")
            if handle_closed_connection:
                self._handle_server_closed_connection(text)
        except (PgCliQuitError, EOFError):
            raise
        except Exception as e:
            logger.error("sql: %r, error: %r", text, e)
            logger.error("traceback: %r", traceback.format_exc())
            click.secho(str(e), err=True, fg="red")
        else:
            try:
                if self.output_file and not text.startswith(("\\o ", "\\log-file", "\\? ", "\\echo ")):
                    try:
                        with open(self.output_file, "a", encoding="utf-8") as f:
                            should_hide = (
                                self.hide_named_query_text
                                and query.is_special
                                and query.successful
                                and self._is_named_query_execution(text)
                            )
                            if not should_hide:
                                click.echo(text, file=f)
                            click.echo("\n".join(output), file=f)
                            click.echo("", file=f)  # extra newline
                    except OSError as e:
                        click.secho(str(e), err=True, fg="red")
                else:
                    if output:
                        self.echo_via_pager("\n".join(output))

                # Log to file in addition to normal output
                if self.log_file and not text.startswith(("\\o ", "\\log-file", "\\? ", "\\echo ")) and not text.strip() == "":
                    try:
                        with open(self.log_file, "a", encoding="utf-8") as f:
                            click.echo(dt.datetime.now().isoformat(), file=f)  # timestamp log
                            should_hide = (
                                self.hide_named_query_text
                                and query.is_special
                                and query.successful
                                and self._is_named_query_execution(text)
                            )
                            if not should_hide:
                                click.echo(text, file=f)
                            click.echo("\n".join(output), file=f)
                            click.echo("", file=f)  # extra newline
                    except OSError as e:
                        click.secho(str(e), err=True, fg="red")
            except KeyboardInterrupt:
                pass

            if self.pgspecial.timing_enabled:
                # Only add humanized time display if > 1 second
                if query.total_time > 1:
                    print(
                        "Time: %0.03fs (%s), executed in: %0.03fs (%s)"
                        % (
                            query.total_time,
                            duration_in_words(query.total_time),
                            query.execution_time,
                            duration_in_words(query.execution_time),
                        )
                    )
                else:
                    print("Time: %0.03fs" % query.total_time)

            # Check if we need to update completions, in order of most
            # to least drastic changes
            if query.db_changed:
                with self._completer_lock:
                    self.completer.reset_completions()
                self.refresh_completions(persist_priorities="keywords")
            elif query.meta_changed:
                self.refresh_completions(persist_priorities="all")
            elif query.path_changed:
                logger.debug("Refreshing search path")
                with self._completer_lock:
                    self.completer.set_search_path(self.pgexecute.search_path())
                logger.debug("Search path: %r", self.completer.search_path)
        return query

    def _check_ongoing_transaction_and_allow_quitting(self):
        """Return whether we can really quit, possibly by asking the
        user to confirm so if there is an ongoing transaction.
        """
        if not self.pgexecute.valid_transaction():
            return True
        while 1:
            try:
                choice = click.prompt(
                    "A transaction is ongoing. Choose `c` to COMMIT, `r` to ROLLBACK, `a` to abort exit.",
                    default="a",
                )
            except click.Abort:
                # Print newline if user aborts with `^C`, otherwise
                # pgcli's prompt will be printed on the same line
                # (just after the confirmation prompt).
                click.echo(None, err=False)
                choice = "a"
            choice = choice.lower()
            if choice == "a":
                return False  # do not quit
            if choice == "c":
                query = self.execute_command("commit")
                return query.successful  # quit only if query is successful
            if choice == "r":
                query = self.execute_command("rollback")
                return query.successful  # quit only if query is successful

    def run_cli(self):
        logger = self.logger

        history_file = self.config["main"]["history_file"]
        if history_file == "default":
            history_file = config_location() + "history"
        history = FileHistory(os.path.expanduser(history_file))
        self.refresh_completions(history=history, persist_priorities="none")

        self.prompt_app = self._build_cli(history)

        if not self.less_chatty:
            print("Server: PostgreSQL", self.pgexecute.server_version)
            print("Version:", __version__)
            print("Home: http://pgcli.com")

        try:
            while True:
                try:
                    text = self.prompt_app.prompt()
                except KeyboardInterrupt:
                    continue
                except EOFError:
                    if not self._check_ongoing_transaction_and_allow_quitting():
                        continue
                    raise

                try:
                    text = self.handle_editor_command(text)
                except RuntimeError as e:
                    logger.error("sql: %r, error: %r", text, e)
                    logger.error("traceback: %r", traceback.format_exc())
                    click.secho(str(e), err=True, fg="red")
                    continue

                try:
                    self.handle_watch_command(text)
                except PgCliQuitError:
                    if not self._check_ongoing_transaction_and_allow_quitting():
                        continue
                    raise

                self.now = dt.datetime.today()

                # Allow PGCompleter to learn user's preferred keywords, etc.
                with self._completer_lock:
                    self.completer.extend_query_history(text)

        except (PgCliQuitError, EOFError):
            if not self.less_chatty:
                print("Goodbye!")

    def handle_watch_command(self, text):
        # Initialize default metaquery in case execution fails
        self.watch_command, timing = special.get_watch_command(text)

        # If we run \watch without a command, apply it to the last query run.
        if self.watch_command is not None and not self.watch_command.strip():
            try:
                self.watch_command = self.query_history[-1].query
            except IndexError:
                click.secho("\\watch cannot be used with an empty query", err=True, fg="red")
                self.watch_command = None

        # If there's a command to \watch, run it in a loop.
        if self.watch_command:
            while self.watch_command:
                try:
                    query = self.execute_command(self.watch_command)
                    click.echo(f"Waiting for {timing} seconds before repeating")
                    sleep(timing)
                except KeyboardInterrupt:
                    self.watch_command = None

        # Otherwise, execute it as a regular command.
        else:
            query = self.execute_command(text)

        self.query_history.append(query)

    def _build_cli(self, history):
        key_bindings = pgcli_bindings(self)

        def get_message():
            if self.dsn_alias and self.prompt_dsn_format is not None:
                prompt_format = self.prompt_dsn_format
            else:
                prompt_format = self.prompt_format

            prompt = self.get_prompt(prompt_format)

            if prompt_format == self.default_prompt and len(prompt) > self.max_len_prompt:
                prompt = self.get_prompt("\\d> ")

            prompt = prompt.replace("\\x1b", "\x1b")
            return ANSI(prompt)

        def get_continuation(width, line_number, is_soft_wrap):
            continuation = self.multiline_continuation_char * (width - 1) + " "
            return [("class:continuation", continuation)]

        get_toolbar_tokens = create_toolbar_tokens_func(self)

        if self.wider_completion_menu:
            complete_style = CompleteStyle.MULTI_COLUMN
        else:
            complete_style = CompleteStyle.COLUMN

        with self._completer_lock:
            prompt_app = PromptSession(
                lexer=PygmentsLexer(PostgresLexer),
                reserve_space_for_menu=self.min_num_menu_lines,
                message=get_message,
                prompt_continuation=get_continuation,
                bottom_toolbar=get_toolbar_tokens if self.show_bottom_toolbar else None,
                complete_style=complete_style,
                input_processors=[
                    # Highlight matching brackets while editing.
                    ConditionalProcessor(
                        processor=HighlightMatchingBracketProcessor(chars="[](){}"),
                        filter=HasFocus(DEFAULT_BUFFER) & ~IsDone(),
                    ),
                    # Render \t as 4 spaces instead of "^I"
                    TabsProcessor(char1=" ", char2=" "),
                ],
                auto_suggest=AutoSuggestFromHistory(),
                tempfile_suffix=".sql",
                # N.b. pgcli's multi-line mode controls submit-on-Enter (which
                # overrides the default behaviour of prompt_toolkit) and is
                # distinct from prompt_toolkit's multiline mode here, which
                # controls layout/display of the prompt/buffer
                multiline=True,
                history=history,
                completer=ThreadedCompleter(DynamicCompleter(lambda: self.completer)),
                complete_while_typing=True,
                style=style_factory(self.syntax_style, self.cli_style),
                include_default_pygments_style=False,
                key_bindings=key_bindings,
                enable_open_in_editor=True,
                enable_system_prompt=True,
                enable_suspend=True,
                editing_mode=EditingMode.VI if self.vi_mode else EditingMode.EMACS,
                search_ignore_case=True,
            )

            return prompt_app

    def _should_limit_output(self, sql, cur):
        """returns True if the output should be truncated, False otherwise."""
        if self.explain_mode:
            return False
        if not is_select(sql):
            return False

        return not self._has_limit(sql) and self.row_limit != 0 and cur and cur.rowcount > self.row_limit

    def _has_limit(self, sql):
        if not sql:
            return False
        return "limit " in sql.lower()

    def _limit_output(self, cur):
        limit = min(self.row_limit, cur.rowcount)
        new_cur = itertools.islice(cur, limit)
        new_status = "SELECT " + str(limit)
        click.secho("The result was limited to %s rows" % limit, fg="red")

        return new_cur, new_status

    def _evaluate_command(self, text):
        """Used to run a command entered by the user during CLI operation
        (Puts the E in REPL)

        returns (results, MetaQuery)
        """
        logger = self.logger
        logger.debug("sql: %r", text)

        # set query to formatter in order to parse table name
        self.formatter.query = text
        all_success = True
        meta_changed = False  # CREATE, ALTER, DROP, etc
        mutated = False  # INSERT, DELETE, etc
        db_changed = False
        path_changed = False
        output = []
        total = 0
        execution = 0

        # Run the query.
        start = time()
        on_error_resume = self.on_error == "RESUME"
        res = self.pgexecute.run(
            text,
            self.pgspecial,
            lambda x: exception_formatter(x, self.verbose_errors),
            on_error_resume,
            explain_mode=self.explain_mode,
        )

        is_special = None

        for title, cur, headers, status, sql, success, is_special in res:
            logger.debug("headers: %r", headers)
            logger.debug("rows: %r", cur)
            logger.debug("status: %r", status)

            if self._should_limit_output(sql, cur):
                cur, status = self._limit_output(cur)

            if self.pgspecial.auto_expand or self.auto_expand:
                max_width = self.prompt_app.output.get_size().columns
            else:
                max_width = None

            expanded = self.pgspecial.expanded_output or self.expanded_output
            settings = OutputSettings(
                table_format=self.table_format,
                dcmlfmt=self.decimal_format,
                floatfmt=self.float_format,
                column_date_formats=self.column_date_formats,
                missingval=self.null_string,
                expanded=expanded,
                max_width=max_width,
                case_function=(self.completer.case if self.settings["case_column_headers"] else lambda x: x),
                style_output=self.style_output,
                max_field_width=self.max_field_width,
            )

            # Hide query text for named queries in quiet mode
            if (
                self.hide_named_query_text
                and is_special
                and success
                and self._is_named_query_execution(text)
                and title
                and title.startswith("> ")
            ):
                title = None

            execution = time() - start
            formatted = format_output(title, cur, headers, status, settings, self.explain_mode)

            output.extend(formatted)
            total = time() - start

            # Keep track of whether any of the queries are mutating or changing
            # the database
            if success:
                mutated = mutated or is_mutating(status)
                db_changed = db_changed or has_change_db_cmd(sql)
                meta_changed = meta_changed or has_meta_cmd(sql)
                path_changed = path_changed or has_change_path_cmd(sql)
            else:
                all_success = False

        meta_query = MetaQuery(
            text,
            all_success,
            total,
            execution,
            meta_changed,
            db_changed,
            path_changed,
            mutated,
            is_special,
        )

        return output, meta_query

    def _handle_server_closed_connection(self, text):
        """Used during CLI execution."""
        try:
            click.secho("Reconnecting...", fg="green")
            self.pgexecute.connect()
            click.secho("Reconnected!", fg="green")
        except OperationalError as e:
            click.secho("Reconnect Failed", fg="red")
            click.secho(str(e), err=True, fg="red")
        else:
            retry = self.auto_retry_closed_connection or confirm("Run the query from before reconnecting?")
            if retry:
                click.secho("Running query...", fg="green")
                # Don't get stuck in a retry loop
                self.execute_command(text, handle_closed_connection=False)

    def refresh_completions(self, history=None, persist_priorities="all"):
        """Refresh outdated completions

        :param history: A prompt_toolkit.history.FileHistory object. Used to
                        load keyword and identifier preferences

        :param persist_priorities: 'all' or 'keywords'
        """

        callback = functools.partial(self._on_completions_refreshed, persist_priorities=persist_priorities)
        return self.completion_refresher.refresh(
            self.pgexecute,
            self.pgspecial,
            callback,
            history=history,
            settings=self.settings,
        )

    def _on_completions_refreshed(self, new_completer, persist_priorities):
        self._swap_completer_objects(new_completer, persist_priorities)

        if self.prompt_app:
            # After refreshing, redraw the CLI to clear the statusbar
            # "Refreshing completions..." indicator
            self.prompt_app.app.invalidate()

    def _swap_completer_objects(self, new_completer, persist_priorities):
        """Swap the completer object with the newly created completer.

        persist_priorities is a string specifying how the old completer's
        learned prioritizer should be transferred to the new completer.

          'none'     - The new prioritizer is left in a new/clean state

          'all'      - The new prioritizer is updated to exactly reflect
                       the old one

          'keywords' - The new prioritizer is updated with old keyword
                       priorities, but not any other.

        """
        with self._completer_lock:
            old_completer = self.completer
            self.completer = new_completer

            if persist_priorities == "all":
                # Just swap over the entire prioritizer
                new_completer.prioritizer = old_completer.prioritizer
            elif persist_priorities == "keywords":
                # Swap over the entire prioritizer, but clear name priorities,
                # leaving learned keyword priorities alone
                new_completer.prioritizer = old_completer.prioritizer
                new_completer.prioritizer.clear_names()
            elif persist_priorities == "none":
                # Leave the new prioritizer as is
                pass
            self.completer = new_completer

    def get_completions(self, text, cursor_positition):
        with self._completer_lock:
            return self.completer.get_completions(Document(text=text, cursor_position=cursor_positition), None)

    def get_prompt(self, string):
        # should be before replacing \\d
        string = string.replace("\\dsn_alias", self.dsn_alias or "")
        string = string.replace("\\t", self.now.strftime("%x %X"))
        string = string.replace("\\u", self.pgexecute.user or "(none)")
        string = string.replace("\\H", self.pgexecute.host or "(none)")
        string = string.replace("\\h", self.pgexecute.short_host or "(none)")
        string = string.replace("\\d", self.pgexecute.dbname or "(none)")
        string = string.replace(
            "\\p",
            str(self.pgexecute.port) if self.pgexecute.port is not None else "5432",
        )
        string = string.replace("\\i", str(self.pgexecute.pid) or "(none)")
        string = string.replace("\\#", "#" if self.pgexecute.superuser else ">")
        string = string.replace("\\n", "\n")
        string = string.replace("\\T", self.pgexecute.transaction_indicator)
        return string

    def get_last_query(self):
        """Get the last query executed or None."""
        return self.query_history[-1][0] if self.query_history else None

    def is_too_wide(self, line):
        """Will this line be too wide to fit into terminal?"""
        if not self.prompt_app:
            return False
        return len(COLOR_CODE_REGEX.sub("", line)) > self.prompt_app.output.get_size().columns

    def is_too_tall(self, lines):
        """Are there too many lines to fit into terminal?"""
        if not self.prompt_app:
            return False
        return len(lines) >= (self.prompt_app.output.get_size().rows - 4)

    def echo_via_pager(self, text, color=None):
        if self.pgspecial.pager_config == PAGER_OFF or self.watch_command:
            click.echo(text, color=color)
        elif self.pgspecial.pager_config == PAGER_LONG_OUTPUT and self.table_format != "csv":
            lines = text.split("\n")

            # The last 4 lines are reserved for the pgcli menu and padding
            if self.is_too_tall(lines) or any(self.is_too_wide(l) for l in lines):
                click.echo_via_pager(text, color=color)
            else:
                click.echo(text, color=color)
        else:
            click.echo_via_pager(text, color)


@click.command()
# Default host is '' so psycopg can default to either localhost or unix socket
@click.option(
    "-h",
    "--host",
    default="",
    envvar="PGHOST",
    help="Host address of the postgres database.",
)
@click.option(
    "-p",
    "--port",
    default=5432,
    help="Port number at which the postgres instance is listening.",
    envvar="PGPORT",
    type=click.INT,
)
@click.option(
    "-U",
    "--username",
    "username_opt",
    help="Username to connect to the postgres database.",
)
@click.option("-u", "--user", "username_opt", help="Username to connect to the postgres database.")
@click.option(
    "-W",
    "--password",
    "prompt_passwd",
    is_flag=True,
    default=False,
    help="Force password prompt.",
)
@click.option(
    "-w",
    "--no-password",
    "never_prompt",
    is_flag=True,
    default=False,
    help="Never prompt for password.",
)
@click.option(
    "--single-connection",
    "single_connection",
    is_flag=True,
    default=False,
    help="Do not use a separate connection for completions.",
)
@click.option("-v", "--version", is_flag=True, help="Version of pgcli.")
@click.option("-d", "--dbname", "dbname_opt", help="database name to connect to.")
@click.option(
    "--pgclirc",
    default=config_location() + "config",
    envvar="PGCLIRC",
    help="Location of pgclirc file.",
    type=click.Path(dir_okay=False),
)
@click.option(
    "-D",
    "--dsn",
    default="",
    envvar="DSN",
    help="Use DSN configured into the [alias_dsn] section of pgclirc file.",
)
@click.option(
    "--list-dsn",
    "list_dsn",
    is_flag=True,
    help="list of DSN configured into the [alias_dsn] section of pgclirc file.",
)
@click.option(
    "--row-limit",
    default=None,
    envvar="PGROWLIMIT",
    type=click.INT,
    help="Set threshold for row limit prompt. Use 0 to disable prompt.",
)
@click.option(
    "--application-name",
    default="pgcli",
    envvar="PGAPPNAME",
    help="Application name for the connection.",
)
@click.option(
    "--less-chatty",
    "less_chatty",
    is_flag=True,
    default=False,
    help="Skip intro on startup and goodbye on exit.",
)
@click.option("--prompt", help='Prompt format (Default: "\\u@\\h:\\d> ").')
@click.option(
    "--prompt-dsn",
    help='Prompt format for connections using DSN aliases (Default: "\\u@\\h:\\d> ").',
)
@click.option(
    "-l",
    "--list",
    "list_databases",
    is_flag=True,
    help="list available databases, then exit.",
)
@click.option(
    "--ping",
    "ping_database",
    is_flag=True,
    default=False,
    help="Check database connectivity, then exit.",
)
@click.option(
    "--auto-vertical-output",
    is_flag=True,
    help="Automatically switch to vertical output mode if the result is wider than the terminal width.",
)
@click.option(
    "--warn",
    default=None,
    help="Warn before running a destructive query.",
)
@click.option(
    "--ssh-tunnel",
    default=None,
    help="Open an SSH tunnel to the given address and connect to the database from it.",
)
@click.option(
    "--log-file",
    default=None,
    help="Write all queries & output into a file, in addition to the normal output destination.",
)
@click.option(
    "--init-command",
    "init_command",
    type=str,
    help="SQL statement to execute after connecting.",
)
@click.argument("dbname", default=lambda: None, envvar="PGDATABASE", nargs=1)
@click.argument("username", default=lambda: None, envvar="PGUSER", nargs=1)
def cli(
    dbname,
    username_opt,
    host,
    port,
    prompt_passwd,
    never_prompt,
    single_connection,
    dbname_opt,
    username,
    version,
    pgclirc,
    dsn,
    row_limit,
    application_name,
    less_chatty,
    prompt,
    prompt_dsn,
    list_databases,
    ping_database,
    auto_vertical_output,
    list_dsn,
    warn,
    ssh_tunnel: str,
    init_command: str,
    log_file: str,
):
    if version:
        print("Version:", __version__)
        sys.exit(0)

    config_dir = os.path.dirname(config_location())
    if not os.path.exists(config_dir):
        os.makedirs(config_dir)

    # Migrate the config file from old location.
    config_full_path = config_location() + "config"
    if os.path.exists(os.path.expanduser("~/.pgclirc")):
        if not os.path.exists(config_full_path):
            shutil.move(os.path.expanduser("~/.pgclirc"), config_full_path)
            print("Config file (~/.pgclirc) moved to new location", config_full_path)
        else:
            print("Config file is now located at", config_full_path)
            print(
                "Please move the existing config file ~/.pgclirc to",
                config_full_path,
            )
    if list_dsn:
        try:
            cfg = load_config(pgclirc, config_full_path)
            for alias in cfg["alias_dsn"]:
                click.secho(alias + " : " + cfg["alias_dsn"][alias])
            sys.exit(0)
        except Exception:
            click.secho(
                "Invalid DSNs found in the config file. Please check the \"[alias_dsn]\" section in pgclirc.",
                err=True,
                fg="red",
            )
            sys.exit(1)

    if ssh_tunnel and not SSH_TUNNEL_SUPPORT:
        click.secho(
            'Cannot open SSH tunnel, "sshtunnel" package was not found. '
            "Please install pgcli with `pip install pgcli[sshtunnel]` if you want SSH tunnel support.",
            err=True,
            fg="red",
        )
        sys.exit(1)

    pgcli = PGCli(
        prompt_passwd,
        never_prompt,
        pgclirc_file=pgclirc,
        row_limit=row_limit,
        application_name=application_name,
        single_connection=single_connection,
        less_chatty=less_chatty,
        prompt=prompt,
        prompt_dsn=prompt_dsn,
        auto_vertical_output=auto_vertical_output,
        warn=warn,
        ssh_tunnel_url=ssh_tunnel,
        log_file=log_file,
    )

    # Choose which ever one has a valid value.
    if dbname_opt and dbname:
        # work as psql: when database is given as option and argument use the argument as user
        username = dbname
    database = dbname_opt or dbname or ""
    user = username_opt or username
    service = None
    if database.startswith("service="):
        service = database[8:]
    elif os.getenv("PGSERVICE") is not None:
        service = os.getenv("PGSERVICE")
    # because option --ping, --list or -l are not supposed to have a db name
    if list_databases or ping_database:
        database = "postgres"

    cfg = load_config(pgclirc, config_full_path)
    if dsn != "":
        try:
            dsn_config = cfg["alias_dsn"][dsn]
        except KeyError:
            click.secho(
                f"Could not find a DSN with alias {dsn}. Please check the \"[alias_dsn]\" section in pgclirc.",
                err=True,
                fg="red",
            )
            sys.exit(1)
        except Exception:
            click.secho(
                "Invalid DSNs found in the config file. Please check the \"[alias_dsn]\" section in pgclirc.",
                err=True,
                fg="red",
            )
            sys.exit(1)
        pgcli.dsn_alias = dsn
        pgcli.connect_uri(dsn_config)
    elif "://" in database:
        pgcli.connect_uri(database)
    elif "=" in database and service is None:
        pgcli.connect_dsn(database, user=user)
    elif service is not None:
        pgcli.connect_service(service, user)
    else:
        pgcli.connect(database, host, user, port)

    if "use_local_timezone" not in cfg["main"] or cfg["main"].as_bool("use_local_timezone"):
        server_tz = pgcli.pgexecute.get_timezone()

        def echo_error(msg: str):
            click.secho(
                "Failed to determine the local time zone",
                err=True,
                fg="yellow",
            )
            click.secho(
                msg,
                err=True,
                fg="yellow",
            )
            click.secho(
                f"Continuing with the default time zone as preset by the server ({server_tz})",
                err=True,
                fg="yellow",
            )
            click.secho(
                "Set `use_local_timezone = False` in the config to avoid trying to override the server time zone\n",
                err=True,
                dim=True,
            )

        local_tz = None
        try:
            local_tz = tzlocal.get_localzone_name()

            if local_tz is None:
                echo_error("No local time zone configuration found\n")
            else:
                click.secho(
                    f"Using local time zone {local_tz} (server uses {server_tz})",
                    fg="green",
                )
                click.secho(
                    "Use `set time zone <TZ>` to override, or set `use_local_timezone = False` in the config",
                    dim=True,
                )

                pgcli.pgexecute.set_timezone(local_tz)
        except ZoneInfoNotFoundError as e:
            # e.args[0] is the pre-formatted message which includes a list
            # of conflicting sources
            echo_error(e.args[0])

    # Merge init-commands: global, DSN-specific, then CLI-provided
    init_cmds = []
    # 1) Global init-commands
    global_section = pgcli.config.get("init-commands", {})
    for _, val in global_section.items():
        if isinstance(val, (list, tuple)):
            init_cmds.extend(val)
        elif val:
            init_cmds.append(val)
    # 2) DSN-specific init-commands
    if dsn:
        alias_section = pgcli.config.get("alias_dsn.init-commands", {})
        if dsn in alias_section:
            val = alias_section.get(dsn)
            if isinstance(val, (list, tuple)):
                init_cmds.extend(val)
            elif val:
                init_cmds.append(val)
    # 3) CLI-provided init-command
    if init_command:
        init_cmds.append(init_command)
    if init_cmds:
        click.echo("Running init commands: %s" % "; ".join(init_cmds))
        for cmd in init_cmds:
            # Execute each init command
            list(pgcli.pgexecute.run(cmd))

    if list_databases:
        cur, headers, status = pgcli.pgexecute.full_databases()

        title = "List of databases"
        settings = OutputSettings(table_format="ascii", missingval="<null>")
        formatted = format_output(title, cur, headers, status, settings)
        pgcli.echo_via_pager("\n".join(formatted))

        sys.exit(0)

    if ping_database:
        try:
            list(pgcli.pgexecute.run("SELECT 1"))
        except Exception:
            click.secho(
                "Could not connect to the database. Please check that the database is running.",
                err=True,
                fg="red",
            )
            sys.exit(1)
        else:
            click.echo("PONG")
            sys.exit(0)

    pgcli.logger.debug(
        "Launch Params: \n\tdatabase: %r\tuser: %r\thost: %r\tport: %r",
        database,
        user,
        host,
        port,
    )

    if setproctitle:
        obfuscate_process_password()

    pgcli.run_cli()


def obfuscate_process_password():
    process_title = setproctitle.getproctitle()
    if "://" in process_title:
        process_title = re.sub(r":(.*):(.*)@", r":\1:xxxx@", process_title)
    elif "=" in process_title:
        process_title = re.sub(r"password=(.+?)((\s[a-zA-Z]+=)|$)", r"password=xxxx\2", process_title)

    setproctitle.setproctitle(process_title)


def has_meta_cmd(query):
    """Determines if the completion needs a refresh by checking if the sql
    statement is an alter, create, drop, commit or rollback."""
    try:
        first_token = query.split()[0]
        if first_token.lower() in ("alter", "create", "drop", "commit", "rollback"):
            return True
    except Exception:
        return False

    return False


def has_change_db_cmd(query):
    """Determines if the statement is a database switch such as 'use' or '\\c'"""
    try:
        first_token = query.split()[0]
        if first_token.lower() in ("use", "\\c", "\\connect"):
            return True
    except Exception:
        return False

    return False


def has_change_path_cmd(sql):
    """Determines if the search_path should be refreshed by checking if the
    sql has 'set search_path'."""
    return "set search_path" in sql.lower()


def is_mutating(status):
    """Determines if the statement is mutating based on the status."""
    if not status:
        return False

    mutating = {"insert", "update", "delete"}
    return status.split(None, 1)[0].lower() in mutating


def is_select(status):
    """Returns true if the first word in status is 'select'."""
    if not status:
        return False
    return status.split(None, 1)[0].lower() == "select"


def diagnostic_output(diagnostic: Diagnostic) -> str:
    fields = []

    if diagnostic.severity is not None:
        fields.append("Severity: " + diagnostic.severity)

    if diagnostic.severity_nonlocalized is not None:
        fields.append("Severity (non-localized): " + diagnostic.severity_nonlocalized)

    if diagnostic.sqlstate is not None:
        fields.append("SQLSTATE code: " + diagnostic.sqlstate)

    if diagnostic.message_primary is not None:
        fields.append("Message: " + diagnostic.message_primary)

    if diagnostic.message_detail is not None:
        fields.append("Detail: " + diagnostic.message_detail)

    if diagnostic.message_hint is not None:
        fields.append("Hint: " + diagnostic.message_hint)

    if diagnostic.statement_position is not None:
        fields.append("Position: " + diagnostic.statement_position)

    if diagnostic.internal_position is not None:
        fields.append("Internal position: " + diagnostic.internal_position)

    if diagnostic.internal_query is not None:
        fields.append("Internal query: " + diagnostic.internal_query)

    if diagnostic.context is not None:
        fields.append("Where: " + diagnostic.context)

    if diagnostic.schema_name is not None:
        fields.append("Schema name: " + diagnostic.schema_name)

    if diagnostic.table_name is not None:
        fields.append("Table name: " + diagnostic.table_name)

    if diagnostic.column_name is not None:
        fields.append("Column name: " + diagnostic.column_name)

    if diagnostic.datatype_name is not None:
        fields.append("Data type name: " + diagnostic.datatype_name)

    if diagnostic.constraint_name is not None:
        fields.append("Constraint name: " + diagnostic.constraint_name)

    if diagnostic.source_file is not None:
        fields.append("File: " + diagnostic.source_file)

    if diagnostic.source_line is not None:
        fields.append("Line: " + diagnostic.source_line)

    if diagnostic.source_function is not None:
        fields.append("Routine: " + diagnostic.source_function)

    return "\n".join(fields)


def exception_formatter(e, verbose_errors: bool = False):
    s = str(e)
    if verbose_errors:
        s += "\n" + diagnostic_output(e.diag)
    return click.style(s, fg="red")


def format_output(title, cur, headers, status, settings, explain_mode=False):
    output = []
    expanded = settings.expanded or settings.table_format == "vertical"
    table_format = "vertical" if settings.expanded else settings.table_format
    max_width = settings.max_width
    case_function = settings.case_function
    if explain_mode:
        formatter = ExplainOutputFormatter(max_width or 100)
    else:
        formatter = TabularOutputFormatter(format_name=table_format)

    def format_array(val):
        if val is None:
            return settings.missingval
        if not isinstance(val, list):
            return val
        return "{" + ",".join(str(format_array(e)) for e in val) + "}"

    def format_arrays(data, headers, **_):
        data = list(data)
        for row in data:
            row[:] = [format_array(val) if isinstance(val, list) else val for val in row]

        return data, headers

    def format_status(cur, status):
        # redshift does not return rowcount as part of status.
        # See https://github.com/dbcli/pgcli/issues/1320
        if cur and hasattr(cur, "rowcount") and cur.rowcount is not None:
            if status and not status.endswith(str(cur.rowcount)):
                status += " %s" % cur.rowcount
        return status

    output_kwargs = {
        "sep_title": "RECORD {n}",
        "sep_character": "-",
        "sep_length": (1, 25),
        "missing_value": settings.missingval,
        "integer_format": settings.dcmlfmt,
        "float_format": settings.floatfmt,
        "column_date_formats": settings.column_date_formats,
        "preprocessors": (format_numbers, format_arrays),
        "disable_numparse": True,
        "preserve_whitespace": True,
        "style": settings.style_output,
        "max_field_width": settings.max_field_width,
    }
    if not settings.floatfmt:
        output_kwargs["preprocessors"] = (align_decimals,)

    if settings.column_date_formats:
        output_kwargs["preprocessors"] += (format_timestamps,)

    if table_format == "csv":
        # The default CSV dialect is "excel" which is not handling newline values correctly
        # Nevertheless, we want to keep on using "excel" on Windows since it uses '\r\n'
        # as the line terminator
        # https://github.com/dbcli/pgcli/issues/1102
        dialect = "excel" if platform.system() == "Windows" else "unix"
        output_kwargs["dialect"] = dialect

    if title:  # Only print the title if it's not None.
        output.append(title)

    if cur:
        headers = [case_function(x) for x in headers]
        if max_width is not None:
            cur = list(cur)
        column_types = None
        if hasattr(cur, "description"):
            column_types = []
            for d in cur.description:
                col_type = cur.adapters.types.get(d.type_code)
                type_name = col_type.name if col_type else None
                if type_name in ("numeric", "float4", "float8"):
                    column_types.append(float)
                if type_name in ("int2", "int4", "int8"):
                    column_types.append(int)
                else:
                    column_types.append(str)

        formatted = formatter.format_output(cur, headers, **output_kwargs)
        if isinstance(formatted, str):
            formatted = iter(formatted.splitlines())
        first_line = next(formatted)
        formatted = itertools.chain([first_line], formatted)
        if not explain_mode and not expanded and max_width and len(strip_ansi(first_line)) > max_width and headers:
            formatted = formatter.format_output(
                cur,
                headers,
                format_name="vertical",
                column_types=column_types,
                **output_kwargs,
            )
            if isinstance(formatted, str):
                formatted = iter(formatted.splitlines())

        output = itertools.chain(output, formatted)

    # Only print the status if it's not None
    if status:
        output = itertools.chain(output, [format_status(cur, status)])

    return output


def parse_service_info(service):
    service = service or os.getenv("PGSERVICE")
    service_file = os.getenv("PGSERVICEFILE")
    if not service_file:
        # try ~/.pg_service.conf (if that exists)
        if platform.system() == "Windows":
            service_file = os.getenv("PGSYSCONFDIR") + "\\pg_service.conf"
        elif os.getenv("PGSYSCONFDIR"):
            service_file = os.path.join(os.getenv("PGSYSCONFDIR"), ".pg_service.conf")
        else:
            service_file = os.path.expanduser("~/.pg_service.conf")
    if not service or not os.path.exists(service_file):
        # nothing to do
        return None, service_file
    with open(service_file, newline="") as f:
        skipped_lines = skip_initial_comment(f)
        try:
            service_file_config = ConfigObj(f)
        except ParseError as err:
            err.line_number += skipped_lines
            raise err
    if service not in service_file_config:
        return None, service_file
    service_conf = service_file_config.get(service)
    return service_conf, service_file


def duration_in_words(duration_in_seconds: float) -> str:
    if not duration_in_seconds:
        return "0 seconds"
    components = []
    hours, remainder = divmod(duration_in_seconds, 3600)
    if hours > 1:
        components.append(f"{int(hours)} hours")
    elif hours == 1:
        components.append("1 hour")
    minutes, seconds = divmod(remainder, 60)
    if minutes > 1:
        components.append(f"{int(minutes)} minutes")
    elif minutes == 1:
        components.append("1 minute")
    if seconds >= 2:
        components.append(f"{int(seconds)} seconds")
    elif seconds >= 1:
        components.append("1 second")
    elif seconds:
        components.append(f"{round(seconds, 3)} second")
    return " ".join(components)


if __name__ == "__main__":
    cli()


================================================
FILE: pgcli/packages/__init__.py
================================================


================================================
FILE: pgcli/packages/formatter/__init__.py
================================================
# coding=utf-8


================================================
FILE: pgcli/packages/formatter/sqlformatter.py
================================================
# coding=utf-8

from pgcli.packages.parseutils.tables import extract_tables


supported_formats = (
    "sql-insert",
    "sql-update",
    "sql-update-1",
    "sql-update-2",
)

preprocessors = ()


def escape_for_sql_statement(value):
    if value is None:
        return "NULL"

    if isinstance(value, bytes):
        return f"X'{value.hex()}'"

    return "'{}'".format(value)


def adapter(data, headers, table_format=None, **kwargs):
    tables = extract_tables(formatter.query)
    if len(tables) > 0:
        table = tables[0]
        if table[0]:
            table_name = "{}.{}".format(*table[:2])
        else:
            table_name = table[1]
    else:
        table_name = "DUAL"
    if table_format == "sql-insert":
        h = '", "'.join(headers)
        yield 'INSERT INTO "{}" ("{}") VALUES'.format(table_name, h)
        prefix = "  "
        for d in data:
            values = ", ".join(escape_for_sql_statement(v) for i, v in enumerate(d))
            yield "{}({})".format(prefix, values)
            if prefix == "  ":
                prefix = ", "
        yield ";"
    if table_format.startswith("sql-update"):
        s = table_format.split("-")
        keys = 1
        if len(s) > 2:
            keys = int(s[-1])
        for d in data:
            yield 'UPDATE "{}" SET'.format(table_name)
            prefix = "  "
            for i, v in enumerate(d[keys:], keys):
                yield '{}"{}" = {}'.format(prefix, headers[i], escape_for_sql_statement(v))
                if prefix == "  ":
                    prefix = ", "
            f = '"{}" = {}'
            where = (f.format(headers[i], escape_for_sql_statement(d[i])) for i in range(keys))
            yield "WHERE {};".format(" AND ".join(where))


def register_new_formatter(TabularOutputFormatter):
    global formatter
    formatter = TabularOutputFormatter
    for sql_format in supported_formats:
        TabularOutputFormatter.register_new_formatter(sql_format, adapter, preprocessors, {"table_format": sql_format})


================================================
FILE: pgcli/packages/parseutils/__init__.py
================================================
import sqlparse


BASE_KEYWORDS = [
    "drop",
    "shutdown",
    "delete",
    "truncate",
    "alter",
    "unconditional_update",
]
ALL_KEYWORDS = BASE_KEYWORDS + ["update"]


def query_starts_with(formatted_sql, prefixes):
    """Check if the query starts with any item from *prefixes*."""
    prefixes = [prefix.lower() for prefix in prefixes]
    return bool(formatted_sql) and formatted_sql.split()[0] in prefixes


def query_is_unconditional_update(formatted_sql):
    """Check if the query starts with UPDATE and contains no WHERE."""
    tokens = formatted_sql.split()
    return bool(tokens) and tokens[0] == "update" and "where" not in tokens


def is_destructive(queries, keywords):
    """Returns if any of the queries in *queries* is destructive."""
    for query in sqlparse.split(queries):
        if query:
            formatted_sql = sqlparse.format(query.lower(), strip_comments=True).strip()
            if "unconditional_update" in keywords and query_is_unconditional_update(formatted_sql):
                return True
            if query_starts_with(formatted_sql, keywords):
                return True
    return False


def parse_destructive_warning(warning_level):
    """Converts a deprecated destructive warning option to a list of command keywords."""
    if not warning_level:
        return []

    if not isinstance(warning_level, list):
        if "," in warning_level:
            return warning_level.split(",")
        warning_level = [warning_level]

    return {
        "true": ALL_KEYWORDS,
        "false": [],
        "all": ALL_KEYWORDS,
        "moderate": BASE_KEYWORDS,
        "off": [],
        "": [],
    }.get(warning_level[0], warning_level)


================================================
FILE: pgcli/packages/parseutils/ctes.py
================================================
from sqlparse import parse
from sqlparse.tokens import Keyword, CTE, DML
from sqlparse.sql import Identifier, IdentifierList, Parenthesis
from collections import namedtuple
from .meta import TableMetadata, ColumnMetadata


# TableExpression is a namedtuple representing a CTE, used internally
# name: cte alias assigned in the query
# columns: list of column names
# start: index into the original string of the left parens starting the CTE
# stop: index into the original string of the right parens ending the CTE
TableExpression = namedtuple("TableExpression", "name columns start stop")


def isolate_query_ctes(full_text, text_before_cursor):
    """Simplify a query by converting CTEs into table metadata objects"""

    if not full_text or not full_text.strip():
        return full_text, text_before_cursor, ()

    ctes, remainder = extract_ctes(full_text)
    if not ctes:
        return full_text, text_before_cursor, ()

    current_position = len(text_before_cursor)
    meta = []

    for cte in ctes:
        if cte.start < current_position < cte.stop:
            # Currently editing a cte - treat its body as the current full_text
            text_before_cursor = full_text[cte.start : current_position]
            full_text = full_text[cte.start : cte.stop]
            return full_text, text_before_cursor, meta

        # Append this cte to the list of available table metadata
        cols = (ColumnMetadata(name, None, ()) for name in cte.columns)
        meta.append(TableMetadata(cte.name, cols))

    # Editing past the last cte (ie the main body of the query)
    full_text = full_text[ctes[-1].stop :]
    text_before_cursor = text_before_cursor[ctes[-1].stop : current_position]

    return full_text, text_before_cursor, tuple(meta)


def extract_ctes(sql):
    """Extract constant table expresseions from a query

    Returns tuple (ctes, remainder_sql)

    ctes is a list of TableExpression namedtuples
    remainder_sql is the text from the original query after the CTEs have
    been stripped.
    """

    p = parse(sql)[0]

    # Make sure the first meaningful token is "WITH" which is necessary to
    # define CTEs
    idx, tok = p.token_next(-1, skip_ws=True, skip_cm=True)
    if not (tok and tok.ttype == CTE):
        return [], sql

    # Get the next (meaningful) token, which should be the first CTE
    idx, tok = p.token_next(idx)
    if not tok:
        return ([], "")
    start_pos = token_start_pos(p.tokens, idx)
    ctes = []

    if isinstance(tok, IdentifierList):
        # Multiple ctes
        for t in tok.get_identifiers():
            cte_start_offset = token_start_pos(tok.tokens, tok.token_index(t))
            cte = get_cte_from_token(t, start_pos + cte_start_offset)
            if not cte:
                continue
            ctes.append(cte)
    elif isinstance(tok, Identifier):
        # A single CTE
        cte = get_cte_from_token(tok, start_pos)
        if cte:
            ctes.append(cte)

    idx = p.token_index(tok) + 1

    # Collapse everything after the ctes into a remainder query
    remainder = "".join(str(tok) for tok in p.tokens[idx:])

    return ctes, remainder


def get_cte_from_token(tok, pos0):
    cte_name = tok.get_real_name()
    if not cte_name:
        return None

    # Find the start position of the opening parens enclosing the cte body
    idx, parens = tok.token_next_by(Parenthesis)
    if not parens:
        return None

    start_pos = pos0 + token_start_pos(tok.tokens, idx)
    cte_len = len(str(parens))  # includes parens
    stop_pos = start_pos + cte_len

    column_names = extract_column_names(parens)

    return TableExpression(cte_name, column_names, start_pos, stop_pos)


def extract_column_names(parsed):
    # Find the first DML token to check if it's a SELECT or INSERT/UPDATE/DELETE
    idx, tok = parsed.token_next_by(t=DML)
    tok_val = tok and tok.value.lower()

    if tok_val in ("insert", "update", "delete"):
        # Jump ahead to the RETURNING clause where the list of column names is
        idx, tok = parsed.token_next_by(idx, (Keyword, "returning"))
    elif not tok_val == "select":
        # Must be invalid CTE
        return ()

    # The next token should be either a column name, or a list of column names
    idx, tok = parsed.token_next(idx, skip_ws=True, skip_cm=True)
    return tuple(t.get_name() for t in _identifiers(tok))


def token_start_pos(tokens, idx):
    return sum(len(str(t)) for t in tokens[:idx])


def _identifiers(tok):
    if isinstance(tok, IdentifierList):
        for t in tok.get_identifiers():
            # NB: IdentifierList.get_identifiers() can return non-identifiers!
            if isinstance(t, Identifier):
                yield t
    elif isinstance(tok, Identifier):
        yield tok


================================================
FILE: pgcli/packages/parseutils/meta.py
================================================
from collections import namedtuple

_ColumnMetadata = namedtuple("ColumnMetadata", ["name", "datatype", "foreignkeys", "default", "has_default"])


def ColumnMetadata(name, datatype, foreignkeys=None, default=None, has_default=False):
    return _ColumnMetadata(name, datatype, foreignkeys or [], default, has_default)


ForeignKey = namedtuple(
    "ForeignKey",
    [
        "parentschema",
        "parenttable",
        "parentcolumn",
        "childschema",
        "childtable",
        "childcolumn",
    ],
)
TableMetadata = namedtuple("TableMetadata", "name columns")


def parse_defaults(defaults_string):
    """Yields default values for a function, given the string provided by
    pg_get_expr(pg_catalog.pg_proc.proargdefaults, 0)"""
    if not defaults_string:
        return
    current = ""
    in_quote = None
    for char in defaults_string:
        if current == "" and char == " ":
            # Skip space after comma separating default expressions
            continue
        if char == '"' or char == "'":
            if in_quote and char == in_quote:
                # End quote
                in_quote = None
            elif not in_quote:
                # Begin quote
                in_quote = char
        elif char == "," and not in_quote:
            # End of expression
            yield current
            current = ""
            continue
        current += char
    yield current


class FunctionMetadata:
    def __init__(
        self,
        schema_name,
        func_name,
        arg_names,
        arg_types,
        arg_modes,
        return_type,
        is_aggregate,
        is_window,
        is_set_returning,
        is_extension,
        arg_defaults,
    ):
        """Class for describing a postgresql function"""

        self.schema_name = schema_name
        self.func_name = func_name

        self.arg_modes = tuple(arg_modes) if arg_modes else None
        self.arg_names = tuple(arg_names) if arg_names else None

        # Be flexible in not requiring arg_types -- use None as a placeholder
        # for each arg. (Used for compatibility with old versions of postgresql
        # where such info is hard to get.
        if arg_types:
            self.arg_types = tuple(arg_types)
        elif arg_modes:
            self.arg_types = tuple([None] * len(arg_modes))
        elif arg_names:
            self.arg_types = tuple([None] * len(arg_names))
        else:
            self.arg_types = None

        self.arg_defaults = tuple(parse_defaults(arg_defaults))

        self.return_type = return_type.strip()
        self.is_aggregate = is_aggregate
        self.is_window = is_window
        self.is_set_returning = is_set_returning
        self.is_extension = bool(is_extension)
        self.is_public = self.schema_name and self.schema_name == "public"

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

    def __ne__(self, other):
        return not self.__eq__(other)

    def _signature(self):
        return (
            self.schema_name,
            self.func_name,
            self.arg_names,
            self.arg_types,
            self.arg_modes,
            self.return_type,
            self.is_aggregate,
            self.is_window,
            self.is_set_returning,
            self.is_extension,
            self.arg_defaults,
        )

    def __hash__(self):
        return hash(self._signature())

    def __repr__(self):
        return (
            "%s(schema_name=%r, func_name=%r, arg_names=%r, "
            "arg_types=%r, arg_modes=%r, return_type=%r, is_aggregate=%r, "
            "is_window=%r, is_set_returning=%r, is_extension=%r, arg_defaults=%r)"
        ) % ((self.__class__.__name__,) + self._signature())

    def has_variadic(self):
        return self.arg_modes and any(arg_mode == "v" for arg_mode in self.arg_modes)

    def args(self):
        """Returns a list of input-parameter ColumnMetadata namedtuples."""
        if not self.arg_names:
            return []
        modes = self.arg_modes or ["i"] * len(self.arg_names)
        args = [
            (name, typ)
            for name, typ, mode in zip(self.arg_names, self.arg_types, modes)
            if mode in ("i", "b", "v")  # IN, INOUT, VARIADIC
        ]

        def arg(name, typ, num):
            num_args = len(args)
            num_defaults = len(self.arg_defaults)
            has_default = num + num_defaults >= num_args
            default = self.arg_defaults[num - num_args + num_defaults] if has_default else None
            return ColumnMetadata(name, typ, [], default, has_default)

        return [arg(name, typ, num) for num, (name, typ) in enumerate(args)]

    def fields(self):
        """Returns a list of output-field ColumnMetadata namedtuples"""

        if self.return_type.lower() == "void":
            return []
        elif not self.arg_modes:
            # For functions  without output parameters, the function name
            # is used as the name of the output column.
            # E.g. 'SELECT unnest FROM unnest(...);'
            return [ColumnMetadata(self.func_name, self.return_type, [])]

        return [
            ColumnMetadata(name, typ, [])
            for name, typ, mode in zip(self.arg_names, self.arg_types, self.arg_modes)
            if mode in ("o", "b", "t")
        ]  # OUT, INOUT, TABLE


================================================
FILE: pgcli/packages/parseutils/tables.py
================================================
import sqlparse
from collections import namedtuple
from sqlparse.sql import IdentifierList, Identifier, Function
from sqlparse.tokens import Keyword, DML, Punctuation

TableReference = namedtuple("TableReference", ["schema", "name", "alias", "is_function"])
TableReference.ref = property(
    lambda self: self.alias or (self.name if self.name.islower() or self.name[0] == '"' else '"' + self.name + '"')
)


# This code is borrowed from sqlparse example script.
# <url>
def is_subselect(parsed):
    if not parsed.is_group:
        return False
    for item in parsed.tokens:
        if item.ttype is DML and item.value.upper() in (
            "SELECT",
            "INSERT",
            "UPDATE",
            "CREATE",
            "DELETE",
        ):
            return True
    return False


def _identifier_is_function(identifier):
    return any(isinstance(t, Function) for t in identifier.tokens)


def extract_from_part(parsed, stop_at_punctuation=True):
    tbl_prefix_seen = False
    for item in parsed.tokens:
        if tbl_prefix_seen:
            if is_subselect(item):
                yield from extract_from_part(item, stop_at_punctuation)
            elif stop_at_punctuation and item.ttype is Punctuation:
                return
            # An incomplete nested select won't be recognized correctly as a
            # sub-select. eg: 'SELECT * FROM (SELECT id FROM user'. This causes
            # the second FROM to trigger this elif condition resulting in a
            # `return`. So we need to ignore the keyword if the keyword
            # FROM.
            # Also 'SELECT * FROM abc JOIN def' will trigger this elif
            # condition. So we need to ignore the keyword JOIN and its variants
            # INNER JOIN, FULL OUTER JOIN, etc.
            elif item.ttype is Keyword and (not item.value.upper() == "FROM") and (not item.value.upper().endswith("JOIN")):
                tbl_prefix_seen = False
            else:
                yield item
        elif item.ttype is Keyword or item.ttype is Keyword.DML:
            item_val = item.value.upper()
            if item_val in (
                "COPY",
                "FROM",
                "INTO",
                "UPDATE",
                "TABLE",
            ) or item_val.endswith("JOIN"):
                tbl_prefix_seen = True
        # 'SELECT a, FROM abc' will detect FROM as part of the column list.
        # So this check here is necessary.
        elif isinstance(item, IdentifierList):
            for identifier in item.get_identifiers():
                if identifier.ttype is Keyword and identifier.value.upper() == "FROM":
                    tbl_prefix_seen = True
                    break


def extract_table_identifiers(token_stream, allow_functions=True):
    """yields tuples of TableReference namedtuples"""

    # We need to do some massaging of the names because postgres is case-
    # insensitive and '"Foo"' is not the same table as 'Foo' (while 'foo' is)
    def parse_identifier(item):
        name = item.get_real_name()
        schema_name = item.get_parent_name()
        alias = item.get_alias()
        if not name:
            schema_name = None
            name = item.get_name()
            alias = alias or name
        schema_quoted = schema_name and item.value[0] == '"'
        if schema_name and not schema_quoted:
            schema_name = schema_name.lower()
        quote_count = item.value.count('"')
        name_quoted = quote_count > 2 or (quote_count and not schema_quoted)
        alias_quoted = alias and item.value[-1] == '"'
        if alias_quoted or name_quoted and not alias and name.islower():
            alias = '"' + (alias or name) + '"'
        if name and not name_quoted and not name.islower():
            if not alias:
                alias = name
            name = name.lower()
        return schema_name, name, alias

    try:
        for item in token_stream:
            if isinstance(item, IdentifierList):
                for identifier in item.get_identifiers():
                    # Sometimes Keywords (such as FROM ) are classified as
                    # identifiers which don't have the get_real_name() method.
                    try:
                        schema_name = identifier.get_parent_name()
       
Download .txt
gitextract_46ndausx/

├── .coveragerc
├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── ci.yml
│       ├── codeql.yml
│       └── publish.yml
├── .gitignore
├── .pre-commit-config.yaml
├── AUTHORS
├── CONTRIBUTING.rst
├── Dockerfile
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── RELEASES.md
├── TODO
├── Vagrantfile
├── changelog.rst
├── pgcli/
│   ├── __init__.py
│   ├── __main__.py
│   ├── auth.py
│   ├── completion_refresher.py
│   ├── config.py
│   ├── explain_output_formatter.py
│   ├── key_bindings.py
│   ├── magic.py
│   ├── main.py
│   ├── packages/
│   │   ├── __init__.py
│   │   ├── formatter/
│   │   │   ├── __init__.py
│   │   │   └── sqlformatter.py
│   │   ├── parseutils/
│   │   │   ├── __init__.py
│   │   │   ├── ctes.py
│   │   │   ├── meta.py
│   │   │   ├── tables.py
│   │   │   └── utils.py
│   │   ├── pgliterals/
│   │   │   ├── __init__.py
│   │   │   ├── main.py
│   │   │   └── pgliterals.json
│   │   ├── prioritization.py
│   │   ├── prompt_utils.py
│   │   └── sqlcompletion.py
│   ├── pgbuffer.py
│   ├── pgclirc
│   ├── pgcompleter.py
│   ├── pgexecute.py
│   ├── pgstyle.py
│   ├── pgtoolbar.py
│   └── pyev.py
├── pgcli-completion.bash
├── post-install
├── post-remove
├── pylintrc
├── pyproject.toml
├── release.py
├── sanity_checks.txt
├── tests/
│   ├── conftest.py
│   ├── features/
│   │   ├── __init__.py
│   │   ├── auto_vertical.feature
│   │   ├── basic_commands.feature
│   │   ├── crud_database.feature
│   │   ├── crud_table.feature
│   │   ├── db_utils.py
│   │   ├── environment.py
│   │   ├── expanded.feature
│   │   ├── fixture_data/
│   │   │   ├── help.txt
│   │   │   ├── help_commands.txt
│   │   │   └── mock_pg_service.conf
│   │   ├── fixture_utils.py
│   │   ├── iocommands.feature
│   │   ├── named_queries.feature
│   │   ├── pgbouncer.feature
│   │   ├── specials.feature
│   │   ├── steps/
│   │   │   ├── __init__.py
│   │   │   ├── auto_vertical.py
│   │   │   ├── basic_commands.py
│   │   │   ├── crud_database.py
│   │   │   ├── crud_table.py
│   │   │   ├── expanded.py
│   │   │   ├── iocommands.py
│   │   │   ├── named_queries.py
│   │   │   ├── pgbouncer.py
│   │   │   ├── specials.py
│   │   │   └── wrappers.py
│   │   └── wrappager.py
│   ├── formatter/
│   │   ├── __init__.py
│   │   └── test_sqlformatter.py
│   ├── metadata.py
│   ├── parseutils/
│   │   ├── test_ctes.py
│   │   ├── test_function_metadata.py
│   │   └── test_parseutils.py
│   ├── pytest.ini
│   ├── test_application_name.py
│   ├── test_auth.py
│   ├── test_completion_refresher.py
│   ├── test_config.py
│   ├── test_fuzzy_completion.py
│   ├── test_init_commands_simple.py
│   ├── test_main.py
│   ├── test_naive_completion.py
│   ├── test_pgcompleter.py
│   ├── test_pgexecute.py
│   ├── test_pgspecial.py
│   ├── test_prioritization.py
│   ├── test_prompt_utils.py
│   ├── test_rowlimit.py
│   ├── test_smart_completion_multiple_schemata.py
│   ├── test_smart_completion_public_schema_only.py
│   ├── test_sqlcompletion.py
│   ├── test_ssh_tunnel.py
│   └── utils.py
└── tox.ini
Download .txt
SYMBOL INDEX (838 symbols across 63 files)

FILE: pgcli/auth.py
  function keyring_initialize (line 19) | def keyring_initialize(keyring_enabled, *, logger):
  function keyring_get_password (line 33) | def keyring_get_password(key):
  function keyring_set_password (line 48) | def keyring_set_password(key, passwd):

FILE: pgcli/completion_refresher.py
  class CompletionRefresher (line 8) | class CompletionRefresher:
    method __init__ (line 11) | def __init__(self):
    method refresh (line 15) | def refresh(self, executor, special, callbacks, history=None, settings...
    method is_refreshing (line 45) | def is_refreshing(self):
    method _bg_refresh (line 48) | def _bg_refresh(self, pgexecute, special, callbacks, history=None, set...
  function refresher (line 90) | def refresher(name, refreshers=CompletionRefresher.refreshers):
  function refresh_schemata (line 103) | def refresh_schemata(completer, executor):
  function refresh_tables (line 109) | def refresh_tables(completer, executor):
  function refresh_views (line 116) | def refresh_views(completer, executor):
  function refresh_types (line 122) | def refresh_types(completer, executor):
  function refresh_databases (line 127) | def refresh_databases(completer, executor):
  function refresh_casing (line 132) | def refresh_casing(completer, executor):
  function refresh_functions (line 147) | def refresh_functions(completer, executor):

FILE: pgcli/config.py
  function config_location (line 10) | def config_location():
  function load_config (line 19) | def load_config(usr_cfg, def_cfg=None):
  function ensure_dir_exists (line 32) | def ensure_dir_exists(path):
  function write_default_config (line 37) | def write_default_config(source, destination, overwrite=False):
  function upgrade_config (line 47) | def upgrade_config(config, def_config):
  function get_config_filename (line 52) | def get_config_filename(pgclirc_file=None):
  function get_config (line 56) | def get_config(pgclirc_file=None):
  function get_casing_file (line 69) | def get_casing_file(config):
  function skip_initial_comment (line 76) | def skip_initial_comment(f_stream: TextIO) -> int:

FILE: pgcli/explain_output_formatter.py
  class ExplainOutputFormatter (line 8) | class ExplainOutputFormatter:
    method __init__ (line 9) | def __init__(self, max_width):
    method format_output (line 12) | def format_output(self, cur, headers, **output_kwargs):

FILE: pgcli/key_bindings.py
  function pgcli_bindings (line 17) | def pgcli_bindings(pgcli):

FILE: pgcli/magic.py
  function load_ipython_extension (line 9) | def load_ipython_extension(ipython):
  function pgcli_line_magic (line 20) | def pgcli_line_magic(line):

FILE: pgcli/main.py
  class PgCliQuitError (line 136) | class PgCliQuitError(Exception):
  function notify_callback (line 140) | def notify_callback(notify: Notify):
  class PGCli (line 147) | class PGCli:
    method set_default_pager (line 151) | def set_default_pager(self, config):
    method __init__ (line 172) | def __init__(
    method quit (line 308) | def quit(self):
    method toggle_named_query_quiet (line 311) | def toggle_named_query_quiet(self):
    method _is_named_query_execution (line 318) | def _is_named_query_execution(self, text):
    method register_special_commands (line 323) | def register_special_commands(self):
    method toggle_verbose_errors (line 418) | def toggle_verbose_errors(self, pattern, **_):
    method echo (line 431) | def echo(self, pattern, **_):
    method change_table_format (line 434) | def change_table_format(self, pattern, **_):
    method info_connection (line 447) | def info_connection(self, **_):
    method change_db (line 461) | def change_db(self, pattern, **_):
    method execute_from_file (line 491) | def execute_from_file(self, pattern, **_):
    method write_to_logfile (line 522) | def write_to_logfile(self, pattern, **_):
    method write_to_file (line 542) | def write_to_file(self, pattern, **_):
    method initialize_logging (line 559) | def initialize_logging(self):
    method connect_dsn (line 599) | def connect_dsn(self, dsn, **kwargs):
    method connect_service (line 602) | def connect_service(self, service, user):
    method connect_uri (line 615) | def connect_uri(self, uri):
    method connect (line 621) | def connect(self, database="", host="", user="", port="", passwd="", d...
    method handle_editor_command (line 765) | def handle_editor_command(self, text):
    method execute_command (line 803) | def execute_command(self, text, handle_closed_connection=True):
    method _check_ongoing_transaction_and_allow_quitting (line 919) | def _check_ongoing_transaction_and_allow_quitting(self):
    method run_cli (line 947) | def run_cli(self):
    method handle_watch_command (line 999) | def handle_watch_command(self, text):
    method _build_cli (line 1027) | def _build_cli(self, history):
    method _should_limit_output (line 1094) | def _should_limit_output(self, sql, cur):
    method _has_limit (line 1103) | def _has_limit(self, sql):
    method _limit_output (line 1108) | def _limit_output(self, cur):
    method _evaluate_command (line 1116) | def _evaluate_command(self, text):
    method _handle_server_closed_connection (line 1217) | def _handle_server_closed_connection(self, text):
    method refresh_completions (line 1233) | def refresh_completions(self, history=None, persist_priorities="all"):
    method _on_completions_refreshed (line 1251) | def _on_completions_refreshed(self, new_completer, persist_priorities):
    method _swap_completer_objects (line 1259) | def _swap_completer_objects(self, new_completer, persist_priorities):
    method get_completions (line 1291) | def get_completions(self, text, cursor_positition):
    method get_prompt (line 1295) | def get_prompt(self, string):
    method get_last_query (line 1313) | def get_last_query(self):
    method is_too_wide (line 1317) | def is_too_wide(self, line):
    method is_too_tall (line 1323) | def is_too_tall(self, lines):
    method echo_via_pager (line 1329) | def echo_via_pager(self, text, color=None):
  function cli (line 1480) | def cli(
  function obfuscate_process_password (line 1722) | def obfuscate_process_password():
  function has_meta_cmd (line 1732) | def has_meta_cmd(query):
  function has_change_db_cmd (line 1745) | def has_change_db_cmd(query):
  function has_change_path_cmd (line 1757) | def has_change_path_cmd(sql):
  function is_mutating (line 1763) | def is_mutating(status):
  function is_select (line 1772) | def is_select(status):
  function diagnostic_output (line 1779) | def diagnostic_output(diagnostic: Diagnostic) -> str:
  function exception_formatter (line 1839) | def exception_formatter(e, verbose_errors: bool = False):
  function format_output (line 1846) | def format_output(title, cur, headers, status, settings, explain_mode=Fa...
  function parse_service_info (line 1952) | def parse_service_info(service):
  function duration_in_words (line 1979) | def duration_in_words(duration_in_seconds: float) -> str:

FILE: pgcli/packages/formatter/sqlformatter.py
  function escape_for_sql_statement (line 16) | def escape_for_sql_statement(value):
  function adapter (line 26) | def adapter(data, headers, table_format=None, **kwargs):
  function register_new_formatter (line 63) | def register_new_formatter(TabularOutputFormatter):

FILE: pgcli/packages/parseutils/__init__.py
  function query_starts_with (line 15) | def query_starts_with(formatted_sql, prefixes):
  function query_is_unconditional_update (line 21) | def query_is_unconditional_update(formatted_sql):
  function is_destructive (line 27) | def is_destructive(queries, keywords):
  function parse_destructive_warning (line 39) | def parse_destructive_warning(warning_level):

FILE: pgcli/packages/parseutils/ctes.py
  function isolate_query_ctes (line 16) | def isolate_query_ctes(full_text, text_before_cursor):
  function extract_ctes (line 47) | def extract_ctes(sql):
  function get_cte_from_token (line 94) | def get_cte_from_token(tok, pos0):
  function extract_column_names (line 113) | def extract_column_names(parsed):
  function token_start_pos (line 130) | def token_start_pos(tokens, idx):
  function _identifiers (line 134) | def _identifiers(tok):

FILE: pgcli/packages/parseutils/meta.py
  function ColumnMetadata (line 6) | def ColumnMetadata(name, datatype, foreignkeys=None, default=None, has_d...
  function parse_defaults (line 24) | def parse_defaults(defaults_string):
  class FunctionMetadata (line 51) | class FunctionMetadata:
    method __init__ (line 52) | def __init__(
    method __eq__ (line 95) | def __eq__(self, other):
    method __ne__ (line 98) | def __ne__(self, other):
    method _signature (line 101) | def _signature(self):
    method __hash__ (line 116) | def __hash__(self):
    method __repr__ (line 119) | def __repr__(self):
    method has_variadic (line 126) | def has_variadic(self):
    method args (line 129) | def args(self):
    method fields (line 149) | def fields(self):

FILE: pgcli/packages/parseutils/tables.py
  function is_subselect (line 14) | def is_subselect(parsed):
  function _identifier_is_function (line 29) | def _identifier_is_function(identifier):
  function extract_from_part (line 33) | def extract_from_part(parsed, stop_at_punctuation=True):
  function extract_table_identifiers (line 72) | def extract_table_identifiers(token_stream, allow_functions=True):
  function extract_tables (line 126) | def extract_tables(sql):

FILE: pgcli/packages/parseutils/utils.py
  function last_word (line 18) | def last_word(text, include="alphanum_underscore"):
  function find_prev_keyword (line 66) | def find_prev_keyword(sql, n_skip=0):
  function is_open_quote (line 105) | def is_open_quote(sql):
  function _parsed_is_open_quote (line 113) | def _parsed_is_open_quote(parsed):
  function parse_partial_identifier (line 118) | def parse_partial_identifier(word):

FILE: pgcli/packages/pgliterals/main.py
  function get_literals (line 11) | def get_literals(literal_type, type_=tuple):

FILE: pgcli/packages/prioritization.py
  function _compile_regex (line 11) | def _compile_regex(keyword):
  class PrevalenceCounter (line 22) | class PrevalenceCounter:
    method __init__ (line 23) | def __init__(self):
    method update (line 27) | def update(self, text):
    method update_names (line 31) | def update_names(self, text):
    method clear_names (line 37) | def clear_names(self):
    method update_keywords (line 40) | def update_keywords(self, text):
    method keyword_count (line 47) | def keyword_count(self, keyword):
    method name_count (line 50) | def name_count(self, name):

FILE: pgcli/packages/prompt_utils.py
  function confirm_destructive_query (line 6) | def confirm_destructive_query(queries, keywords, alias):
  function confirm (line 24) | def confirm(*args, **kwargs):
  function prompt (line 32) | def prompt(*args, **kwargs):

FILE: pgcli/packages/sqlcompletion.py
  class SqlStatement (line 49) | class SqlStatement:
    method __init__ (line 50) | def __init__(self, full_text, text_before_cursor):
    method is_insert (line 84) | def is_insert(self):
    method get_tables (line 87) | def get_tables(self, scope="full"):
    method get_previous_token (line 101) | def get_previous_token(self, token):
    method get_identifier_schema (line 104) | def get_identifier_schema(self):
    method reduce_to_prev_keyword (line 112) | def reduce_to_prev_keyword(self, n_skip=0):
  function suggest_type (line 117) | def suggest_type(full_text, text_before_cursor):
  function _strip_named_query (line 150) | def _strip_named_query(txt):
  function _find_function_body (line 165) | def _find_function_body(text):
  function _statement_from_function (line 170) | def _statement_from_function(full_text, text_before_cursor, statement):
  function _split_multiple_statements (line 183) | def _split_multiple_statements(full_text, text_before_cursor, parsed):
  function suggest_special (line 227) | def suggest_special(text):
  function suggest_based_on_last_token (line 279) | def suggest_based_on_last_token(token, stmt):
  function _suggest_expression (line 520) | def _suggest_expression(token_v, stmt):
  function identifies (line 544) | def identifies(table_id, ref):
  function _allow_join_condition (line 550) | def _allow_join_condition(statement):
  function _allow_join (line 570) | def _allow_join(statement):

FILE: pgcli/pgbuffer.py
  function _is_complete (line 11) | def _is_complete(sql):
  function safe_multi_line_mode (line 25) | def safe_multi_line_mode(pgcli):
  function buffer_should_be_handled (line 34) | def buffer_should_be_handled(pgcli):

FILE: pgcli/pgcompleter.py
  function SchemaObject (line 43) | def SchemaObject(name, schema=None, meta=None):
  function Candidate (line 50) | def Candidate(completion, prio=None, meta=None, synonyms=None, prio2=Non...
  function normalize_ref (line 58) | def normalize_ref(ref):
  function generate_alias (line 62) | def generate_alias(tbl, alias_map=None):
  class InvalidMapFile (line 80) | class InvalidMapFile(ValueError):
  function load_alias_map_file (line 84) | def load_alias_map_file(path):
  class PGCompleter (line 96) | class PGCompleter(Completer):
    method __init__ (line 105) | def __init__(self, smart_completion=True, pgspecial=None, settings=None):
    method escape_name (line 143) | def escape_name(self, name):
    method escape_schema (line 149) | def escape_schema(self, name):
    method unescape_name (line 152) | def unescape_name(self, name):
    method escaped_names (line 159) | def escaped_names(self, names):
    method extend_database_names (line 162) | def extend_database_names(self, databases):
    method extend_keywords (line 165) | def extend_keywords(self, additional_keywords):
    method extend_schemata (line 169) | def extend_schemata(self, schemata):
    method extend_casing (line 183) | def extend_casing(self, words):
    method extend_relations (line 191) | def extend_relations(self, data, kind):
    method extend_columns (line 213) | def extend_columns(self, column_data, kind):
    method extend_functions (line 235) | def extend_functions(self, func_data):
    method _refresh_arg_list_cache (line 254) | def _refresh_arg_list_cache(self):
    method extend_foreignkeys (line 268) | def extend_foreignkeys(self, fk_data):
    method extend_datatypes (line 288) | def extend_datatypes(self, type_data):
    method extend_query_history (line 299) | def extend_query_history(self, text, is_init=False):
    method set_search_path (line 307) | def set_search_path(self, search_path):
    method reset_completions (line 310) | def reset_completions(self):
    method find_matches (line 317) | def find_matches(self, text, collection, mode="fuzzy", meta=None):
    method case (line 454) | def case(self, word):
    method get_completions (line 457) | def get_completions(self, document, complete_event, smart_completion=N...
    method get_column_matches (line 487) | def get_column_matches(self, suggestion, word_before_cursor):
    method alias (line 547) | def alias(self, tbl, tbls):
    method get_join_matches (line 564) | def get_join_matches(self, suggestion, word_before_cursor):
    method get_join_condition_matches (line 605) | def get_join_condition_matches(self, suggestion, word_before_cursor):
    method get_function_matches (line 655) | def get_function_matches(self, suggestion, word_before_cursor, alias=F...
    method get_schema_matches (line 689) | def get_schema_matches(self, suggestion, word_before_cursor):
    method get_from_clause_item_matches (line 702) | def get_from_clause_item_matches(self, suggestion, word_before_cursor):
    method _arg_list (line 714) | def _arg_list(self, func, usage):
    method _format_arg (line 741) | def _format_arg(self, template, arg, arg_num, max_arg_len):
    method _make_cand (line 758) | def _make_cand(self, tbl, do_alias, suggestion, arg_mode=None):
    method get_table_matches (line 784) | def get_table_matches(self, suggestion, word_before_cursor, alias=False):
    method get_table_formats (line 795) | def get_table_formats(self, _, word_before_cursor):
    method get_view_matches (line 799) | def get_view_matches(self, suggestion, word_before_cursor, alias=False):
    method get_alias_matches (line 807) | def get_alias_matches(self, suggestion, word_before_cursor):
    method get_database_matches (line 811) | def get_database_matches(self, _, word_before_cursor):
    method get_keyword_matches (line 814) | def get_keyword_matches(self, suggestion, word_before_cursor):
    method get_path_matches (line 836) | def get_path_matches(self, _, word_before_cursor):
    method get_special_matches (line 842) | def get_special_matches(self, _, word_before_cursor):
    method get_datatype_matches (line 851) | def get_datatype_matches(self, suggestion, word_before_cursor):
    method get_namedquery_matches (line 863) | def get_namedquery_matches(self, _, word_before_cursor):
    method populate_scoped_cols (line 885) | def populate_scoped_cols(self, scoped_tbls, local_tbls=()):
    method _get_schemas (line 931) | def _get_schemas(self, obj_typ, schema):
    method _maybe_schema (line 943) | def _maybe_schema(self, schema, parent):
    method populate_schema_objects (line 946) | def populate_schema_objects(self, schema, obj_type):
    method populate_functions (line 959) | def populate_functions(self, schema, filter_func):

FILE: pgcli/pgexecute.py
  function remove_beginning_comments (line 24) | def remove_beginning_comments(command):
  function register_typecasters (line 40) | def register_typecasters(connection):
  class ProtocolSafeCursor (line 56) | class ProtocolSafeCursor(psycopg.Cursor):
    method __init__ (line 61) | def __init__(self, *args, **kwargs):
    method __iter__ (line 66) | def __iter__(self):
    method fetchall (line 71) | def fetchall(self):
    method fetchone (line 76) | def fetchone(self):
    method execute (line 85) | def execute(self, *args, **kwargs):
  class PGExecute (line 96) | class PGExecute:
    method __init__ (line 158) | def __init__(
    method is_virtual_database (line 183) | def is_virtual_database(self):
    method copy (line 188) | def copy(self):
    method connect (line 192) | def connect(
    method short_host (line 271) | def short_host(self):
    method _select_one (line 284) | def _select_one(self, cur, sql):
    method failed_transaction (line 294) | def failed_transaction(self):
    method valid_transaction (line 297) | def valid_transaction(self):
    method is_connection_closed (line 301) | def is_connection_closed(self):
    method transaction_indicator (line 305) | def transaction_indicator(self):
    method run (line 314) | def run(
    method _must_raise (line 423) | def _must_raise(self, e):
    method execute_normal_sql (line 437) | def execute_normal_sql(self, split_sql):
    method search_path (line 474) | def search_path(self):
    method view_definition (line 489) | def view_definition(self, spec):
    method function_definition (line 517) | def function_definition(self, spec):
    method schemata (line 530) | def schemata(self):
    method _relations (line 538) | def _relations(self, kinds=("r", "p", "f", "v", "m")):
    method tables (line 556) | def tables(self):
    method views (line 560) | def views(self):
    method _columns (line 567) | def _columns(self, kinds=("r", "p", "f", "v", "m")):
    method table_columns (line 625) | def table_columns(self):
    method view_columns (line 628) | def view_columns(self):
    method databases (line 631) | def databases(self):
    method full_databases (line 637) | def full_databases(self):
    method is_protocol_error (line 644) | def is_protocol_error(self):
    method get_socket_directory (line 651) | def get_socket_directory(self):
    method foreignkeys (line 658) | def foreignkeys(self):
    method functions (line 698) | def functions(self):
    method datatypes (line 788) | def datatypes(self):
    method casing (line 832) | def casing(self):
    method explain_prefix (line 881) | def explain_prefix(self):
    method get_timezone (line 884) | def get_timezone(self) -> str:
    method set_timezone (line 890) | def set_timezone(self, timezone: str):

FILE: pgcli/pgstyle.py
  function parse_pygments_style (line 49) | def parse_pygments_style(token_name, style_object, style_dict):
  function style_factory (line 65) | def style_factory(name, cli_style):
  function style_factory_output (line 93) | def style_factory_output(name, cli_style):

FILE: pgcli/pgtoolbar.py
  function _get_vi_mode (line 15) | def _get_vi_mode():
  function create_toolbar_tokens_func (line 19) | def create_toolbar_tokens_func(pgcli):

FILE: pgcli/pyev.py
  class Visualizer (line 27) | class Visualizer:
    method __init__ (line 28) | def __init__(self, terminal_width=100, color=True):
    method load (line 33) | def load(self, explain_dict):
    method process_all (line 39) | def process_all(self):
    method process_plan (line 44) | def process_plan(self, plan):
    method prefix_format (line 54) | def prefix_format(self, v):
    method tag_format (line 59) | def tag_format(self, v):
    method muted_format (line 64) | def muted_format(self, v):
    method bold_format (line 69) | def bold_format(self, v):
    method good_format (line 74) | def good_format(self, v):
    method warning_format (line 79) | def warning_format(self, v):
    method critical_format (line 84) | def critical_format(self, v):
    method output_format (line 89) | def output_format(self, v):
    method calculate_planner_estimate (line 94) | def calculate_planner_estimate(self, plan):
    method calculate_actuals (line 112) | def calculate_actuals(self, plan):
    method calculate_outlier_nodes (line 127) | def calculate_outlier_nodes(self, plan):
    method calculate_maximums (line 137) | def calculate_maximums(self, plan):
    method duration_to_string (line 159) | def duration_to_string(self, value):
    method format_details (line 177) | def format_details(self, plan):
    method format_tags (line 191) | def format_tags(self, plan):
    method get_terminator (line 205) | def get_terminator(self, index, plan):
    method wrap_string (line 217) | def wrap_string(self, line, width):
    method intcomma (line 222) | def intcomma(self, value):
    method output_fn (line 235) | def output_fn(self, current_prefix, string):
    method create_lines (line 238) | def create_lines(self, plan, prefix, depth, width, last_child):
    method generate_lines (line 397) | def generate_lines(self):
    method get_list (line 412) | def get_list(self):
    method print (line 415) | def print(self):

FILE: release.py
  function skip_step (line 17) | def skip_step():
  function run_step (line 29) | def run_step(*args):
  function version (line 47) | def version(version_file):
  function commit_for_release (line 56) | def commit_for_release(version_file, ver):
  function create_git_tag (line 62) | def create_git_tag(tag_name):
  function create_distribution_files (line 66) | def create_distribution_files():
  function upload_distribution_files (line 71) | def upload_distribution_files():
  function push_to_github (line 75) | def push_to_github():
  function push_tags_to_github (line 79) | def push_tags_to_github():
  function checklist (line 83) | def checklist(questions):

FILE: tests/conftest.py
  function connection (line 17) | def connection():
  function cursor (line 27) | def cursor(connection):
  function executor (line 33) | def executor(connection):
  function exception_formatter (line 46) | def exception_formatter():
  function temp_config (line 51) | def temp_config(tmpdir_factory):

FILE: tests/features/db_utils.py
  function create_db (line 4) | def create_db(hostname="localhost", username=None, password=None, dbname...
  function create_cn (line 28) | def create_cn(hostname, password, username, dbname, port):
  function pgbouncer_available (line 43) | def pgbouncer_available(hostname="localhost", password=None, username="p...
  function drop_db (line 56) | def drop_db(hostname="localhost", username=None, password=None, dbname=N...
  function close_cn (line 75) | def close_cn(cn=None):

FILE: tests/features/environment.py
  function before_all (line 15) | def before_all(context):
  function show_env_changes (line 113) | def show_env_changes(env_old, env_new):
  function after_all (line 125) | def after_all(context):
  function before_step (line 149) | def before_step(context, _):
  function is_known_problem (line 153) | def is_known_problem(scenario):
  function before_scenario (line 166) | def before_scenario(context, scenario):
  function after_scenario (line 188) | def after_scenario(context, scenario):

FILE: tests/features/fixture_utils.py
  function read_fixture_lines (line 5) | def read_fixture_lines(filename):
  function read_fixture_files (line 17) | def read_fixture_files():

FILE: tests/features/steps/auto_vertical.py
  function step_run_cli_with_arg (line 7) | def step_run_cli_with_arg(context, arg):
  function step_execute_small_query (line 12) | def step_execute_small_query(context):
  function step_execute_large_query (line 17) | def step_execute_large_query(context):
  function step_see_small_results (line 22) | def step_see_small_results(context):
  function step_see_large_results (line 40) | def step_see_large_results(context):

FILE: tests/features/steps/basic_commands.py
  function step_list_databases (line 17) | def step_list_databases(context):
  function step_see_list_databases (line 23) | def step_see_list_databases(context):
  function step_ping_database (line 30) | def step_ping_database(context):
  function step_get_pong_response (line 36) | def step_get_pong_response(context):
  function step_run_cli (line 43) | def step_run_cli(context):
  function step_run_cli_using_arg (line 48) | def step_run_cli_using_arg(context, arg):
  function step_wait_prompt (line 69) | def step_wait_prompt(context):
  function step_ctrl_d (line 74) | def step_ctrl_d(context):
  function step_try_to_ctrl_d (line 84) | def step_try_to_ctrl_d(context):
  function step_ctrl_c (line 97) | def step_ctrl_c(context):
  function step_see_cancelled_query_warning (line 103) | def step_see_cancelled_query_warning(context):
  function step_see_ongoing_transaction_error (line 111) | def step_see_ongoing_transaction_error(context):
  function step_send_sleep_15_seconds (line 119) | def step_send_sleep_15_seconds(context):
  function step_check_for_active_sleep_queries (line 127) | def step_check_for_active_sleep_queries(context):
  function step_no_active_sleep_queries (line 137) | def step_no_active_sleep_queries(context):
  function step_send_help (line 158) | def step_send_help(context):
  function step_send_partial_select_command (line 166) | def step_send_partial_select_command(context):
  function step_see_error_message (line 174) | def step_see_error_message(context):
  function step_send_source_command (line 179) | def step_send_source_command(context):
  function step_check_application_name (line 188) | def step_check_application_name(context):
  function step_see_found (line 193) | def step_see_found(context):
  function step_resppond_to_destructive_command (line 214) | def step_resppond_to_destructive_command(context, response):
  function step_send_password (line 225) | def step_send_password(context):
  function step_send_text (line 231) | def step_send_text(context, text):

FILE: tests/features/steps/crud_database.py
  function step_db_create (line 14) | def step_db_create(context):
  function step_db_drop (line 24) | def step_db_drop(context):
  function step_db_connect_test (line 32) | def step_db_connect_test(context):
  function step_db_connect_dbserver (line 41) | def step_db_connect_dbserver(context):
  function step_wait_exit (line 50) | def step_wait_exit(context):
  function step_see_prompt (line 58) | def step_see_prompt(context):
  function step_see_help (line 68) | def step_see_help(context):
  function step_see_db_created (line 74) | def step_see_db_created(context):
  function step_see_db_dropped (line 82) | def step_see_db_dropped(context):
  function step_see_db_connected (line 90) | def step_see_db_connected(context):

FILE: tests/features/steps/crud_table.py
  function step_create_table (line 17) | def step_create_table(context):
  function step_insert_into_table (line 25) | def step_insert_into_table(context):
  function step_update_table (line 33) | def step_update_table(context):
  function step_select_from_table (line 41) | def step_select_from_table(context):
  function step_delete_from_table (line 49) | def step_delete_from_table(context):
  function step_drop_table (line 57) | def step_drop_table(context):
  function step_alter_table (line 65) | def step_alter_table(context):
  function step_begin_transaction (line 73) | def step_begin_transaction(context):
  function step_rollback_transaction (line 81) | def step_rollback_transaction(context):
  function step_see_table_created (line 89) | def step_see_table_created(context):
  function step_see_record_inserted (line 97) | def step_see_record_inserted(context):
  function step_see_record_updated (line 105) | def step_see_record_updated(context):
  function step_see_data_selected (line 113) | def step_see_data_selected(context, data):
  function step_see_no_data_selected (line 135) | def step_see_no_data_selected(context):
  function step_see_data_deleted (line 155) | def step_see_data_deleted(context):
  function step_see_table_dropped (line 163) | def step_see_table_dropped(context):
  function step_see_transaction_began (line 171) | def step_see_transaction_began(context):
  function step_see_transaction_rolled_back (line 179) | def step_see_transaction_rolled_back(context):

FILE: tests/features/steps/expanded.py
  function step_prepare_data (line 14) | def step_prepare_data(context):
  function step_set_expanded (line 32) | def step_set_expanded(context, mode):
  function step_see_data (line 40) | def step_see_data(context, which):

FILE: tests/features/steps/iocommands.py
  function step_edit_file (line 9) | def step_edit_file(context):
  function step_edit_type_sql (line 20) | def step_edit_type_sql(context):
  function step_edit_quit (line 28) | def step_edit_quit(context):
  function step_edit_done_sql (line 34) | def step_edit_done_sql(context):
  function step_tee_ouptut (line 46) | def step_tee_ouptut(context):
  function step_query_select_123456 (line 58) | def step_query_select_123456(context):
  function step_notee_output (line 63) | def step_notee_output(context):
  function step_see_123456_in_ouput (line 69) | def step_see_123456_in_ouput(context):

FILE: tests/features/steps/named_queries.py
  function step_save_named_query (line 12) | def step_save_named_query(context):
  function step_use_named_query (line 20) | def step_use_named_query(context):
  function step_delete_named_query (line 28) | def step_delete_named_query(context):
  function step_see_named_query_saved (line 36) | def step_see_named_query_saved(context):
  function step_see_named_query_executed (line 44) | def step_see_named_query_executed(context):
  function step_see_named_query_deleted (line 53) | def step_see_named_query_deleted(context):

FILE: tests/features/steps/pgbouncer.py
  function step_send_help_command (line 12) | def step_send_help_command(context):
  function see_pgbouncer_help (line 17) | def see_pgbouncer_help(context):

FILE: tests/features/steps/specials.py
  function step_refresh_completions (line 12) | def step_refresh_completions(context):
  function step_see_refresh_started (line 20) | def step_see_refresh_started(context):

FILE: tests/features/steps/wrappers.py
  function expect_exact (line 8) | def expect_exact(context, expected, timeout):
  function expect_pager (line 37) | def expect_pager(context, expected, timeout):
  function run_cli (line 48) | def run_cli(context, run_args=None, prompt_check=True, currentdb=None):
  function wait_prompt (line 64) | def wait_prompt(context):

FILE: tests/features/wrappager.py
  function wrappager (line 5) | def wrappager(boundary):

FILE: tests/formatter/test_sqlformatter.py
  function test_escape_for_sql_statement_bytes (line 9) | def test_escape_for_sql_statement_bytes():
  function test_escape_for_sql_statement_number (line 15) | def test_escape_for_sql_statement_number():
  function test_escape_for_sql_statement_str (line 21) | def test_escape_for_sql_statement_str():
  function test_output_sql_insert (line 27) | def test_output_sql_insert():
  function test_output_sql_update (line 68) | def test_output_sql_update():

FILE: tests/metadata.py
  function escape (line 15) | def escape(name):
  function completion (line 21) | def completion(display_meta, text, pos=0):
  function function (line 25) | def function(text, pos=0, display=None):
  function get_result (line 29) | def get_result(completer, text, position=None):
  function result_set (line 34) | def result_set(completer, text, position=None):
  function wildcard_expansion (line 54) | def wildcard_expansion(cols, pos=-1):
  class MetaData (line 58) | class MetaData:
    method __init__ (line 59) | def __init__(self, metadata):
    method builtin_functions (line 62) | def builtin_functions(self, pos=0):
    method builtin_datatypes (line 65) | def builtin_datatypes(self, pos=0):
    method keywords (line 68) | def keywords(self, pos=0):
    method specials (line 71) | def specials(self, pos=0):
    method columns (line 74) | def columns(self, tbl, parent="public", typ="tables", pos=0):
    method datatypes (line 82) | def datatypes(self, parent="public", pos=0):
    method tables (line 85) | def tables(self, parent="public", pos=0):
    method views (line 88) | def views(self, parent="public", pos=0):
    method functions (line 91) | def functions(self, parent="public", pos=0):
    method schemas (line 104) | def schemas(self, pos=0):
    method functions_and_keywords (line 108) | def functions_and_keywords(self, parent="public", pos=0):
    method columns_functions_and_keywords (line 112) | def columns_functions_and_keywords(self, tbl, parent="public", typ="ta...
    method from_clause_items (line 115) | def from_clause_items(self, parent="public", pos=0):
    method schemas_and_from_clause_items (line 118) | def schemas_and_from_clause_items(self, parent="public", pos=0):
    method types (line 121) | def types(self, parent="public", pos=0):
    method completer (line 125) | def completer(self):
    method get_completers (line 128) | def get_completers(self, casing):
    method _make_col (line 163) | def _make_col(self, sch, tbl, col):
    method get_completer (line 167) | def get_completer(self, settings=None, casing=None):

FILE: tests/parseutils/test_ctes.py
  function extract_column_names (line 10) | def extract_column_names(sql):
  function test_token_str_pos (line 15) | def test_token_str_pos():
  function test_single_column_name_extraction (line 27) | def test_single_column_name_extraction():
  function test_aliased_single_column_name_extraction (line 32) | def test_aliased_single_column_name_extraction():
  function test_aliased_expression_name_extraction (line 37) | def test_aliased_expression_name_extraction():
  function test_multiple_column_name_extraction (line 42) | def test_multiple_column_name_extraction():
  function test_missing_column_name_handled_gracefully (line 47) | def test_missing_column_name_handled_gracefully():
  function test_aliased_multiple_column_name_extraction (line 55) | def test_aliased_multiple_column_name_extraction():
  function test_table_qualified_column_name_extraction (line 60) | def test_table_qualified_column_name_extraction():
  function test_extract_column_names_from_returning_clause (line 73) | def test_extract_column_names_from_returning_clause(sql):
  function test_simple_cte_extraction (line 77) | def test_simple_cte_extraction():
  function test_cte_extraction_around_comments (line 87) | def test_cte_extraction_around_comments():
  function test_multiple_cte_extraction (line 105) | def test_multiple_cte_extraction():

FILE: tests/parseutils/test_function_metadata.py
  function test_function_metadata_eq (line 4) | def test_function_metadata_eq():

FILE: tests/parseutils/test_parseutils.py
  function test_empty_string (line 12) | def test_empty_string():
  function test_simple_select_single_table (line 17) | def test_simple_select_single_table():
  function test_simple_select_single_table_schema_qualified_quoted_table (line 23) | def test_simple_select_single_table_schema_qualified_quoted_table(sql):
  function test_simple_select_single_table_schema_qualified (line 29) | def test_simple_select_single_table_schema_qualified(sql):
  function test_simple_select_single_table_double_quoted (line 34) | def test_simple_select_single_table_double_quoted():
  function test_simple_select_multiple_tables (line 39) | def test_simple_select_multiple_tables():
  function test_simple_select_multiple_tables_double_quoted (line 44) | def test_simple_select_multiple_tables_double_quoted():
  function test_simple_select_single_table_deouble_quoted_aliased (line 49) | def test_simple_select_single_table_deouble_quoted_aliased():
  function test_simple_select_multiple_tables_deouble_quoted_aliased (line 54) | def test_simple_select_multiple_tables_deouble_quoted_aliased():
  function test_simple_select_multiple_tables_schema_qualified (line 59) | def test_simple_select_multiple_tables_schema_qualified():
  function test_simple_select_with_cols_single_table (line 64) | def test_simple_select_with_cols_single_table():
  function test_simple_select_with_cols_single_table_schema_qualified (line 69) | def test_simple_select_with_cols_single_table_schema_qualified():
  function test_simple_select_with_cols_multiple_tables (line 74) | def test_simple_select_with_cols_multiple_tables():
  function test_simple_select_with_cols_multiple_qualified_tables (line 79) | def test_simple_select_with_cols_multiple_qualified_tables():
  function test_select_with_hanging_comma_single_table (line 84) | def test_select_with_hanging_comma_single_table():
  function test_select_with_hanging_comma_multiple_tables (line 89) | def test_select_with_hanging_comma_multiple_tables():
  function test_select_with_hanging_period_multiple_tables (line 94) | def test_select_with_hanging_period_multiple_tables():
  function test_simple_insert_single_table (line 99) | def test_simple_insert_single_table():
  function test_simple_insert_single_table_schema_qualified (line 110) | def test_simple_insert_single_table_schema_qualified():
  function test_simple_update_table_no_schema (line 115) | def test_simple_update_table_no_schema():
  function test_simple_update_table_with_schema (line 120) | def test_simple_update_table_with_schema():
  function test_join_table (line 126) | def test_join_table(join_type):
  function test_join_table_schema_qualified (line 132) | def test_join_table_schema_qualified():
  function test_incomplete_join_clause (line 137) | def test_incomplete_join_clause():
  function test_join_as_table (line 145) | def test_join_as_table():
  function test_multiple_joins (line 150) | def test_multiple_joins():
  function test_subselect_tables (line 164) | def test_subselect_tables():
  function test_extract_no_tables (line 171) | def test_extract_no_tables(text):
  function test_simple_function_as_table (line 177) | def test_simple_function_as_table(arg_list):
  function test_simple_schema_qualified_function_as_table (line 183) | def test_simple_schema_qualified_function_as_table(arg_list):
  function test_simple_aliased_function_as_table (line 189) | def test_simple_aliased_function_as_table(arg_list):
  function test_simple_table_and_function (line 194) | def test_simple_table_and_function():
  function test_complex_table_and_function (line 199) | def test_complex_table_and_function():
  function test_find_prev_keyword_using (line 207) | def test_find_prev_keyword_using():
  function test_find_prev_keyword_where (line 221) | def test_find_prev_keyword_where(sql):
  function test_find_prev_keyword_open_parens (line 227) | def test_find_prev_keyword_open_parens(sql):
  function test_is_open_quote__closed (line 244) | def test_is_open_quote__closed(sql):
  function test_is_open_quote__open (line 262) | def test_is_open_quote__open(sql):
  function test_is_destructive (line 284) | def test_is_destructive(sql, keywords, expected):
  function test_parse_destructive_warning (line 305) | def test_parse_destructive_warning(warning_level, expected):

FILE: tests/test_application_name.py
  function test_application_name_in_env (line 9) | def test_application_name_in_env():

FILE: tests/test_auth.py
  function test_keyring_initialize (line 7) | def test_keyring_initialize(enabled, call_count):
  function test_keyring_get_password_ok (line 15) | def test_keyring_get_password_ok():
  function test_keyring_get_password_exception (line 21) | def test_keyring_get_password_exception():
  function test_keyring_set_password_ok (line 27) | def test_keyring_set_password_ok():
  function test_keyring_set_password_exception (line 33) | def test_keyring_set_password_exception():

FILE: tests/test_completion_refresher.py
  function refresher (line 7) | def refresher():
  function test_ctor (line 13) | def test_ctor(refresher):
  function test_refresh_called_once (line 33) | def test_refresh_called_once(refresher):
  function test_refresh_called_twice (line 52) | def test_refresh_called_twice(refresher):
  function test_refresh_with_callbacks (line 81) | def test_refresh_with_callbacks(refresher):

FILE: tests/test_config.py
  function test_ensure_file_parent (line 10) | def test_ensure_file_parent(tmpdir):
  function test_ensure_existing_dir (line 16) | def test_ensure_existing_dir(tmpdir):
  function test_ensure_other_create_error (line 23) | def test_ensure_other_create_error(tmpdir):
  function test_skip_initial_comment (line 42) | def test_skip_initial_comment(text, skipped_lines):

FILE: tests/test_fuzzy_completion.py
  function completer (line 5) | def completer():
  function test_ranking_ignores_identifier_quotes (line 11) | def test_ranking_ignores_identifier_quotes(completer):
  function test_ranking_based_on_shortest_match (line 31) | def test_ranking_based_on_shortest_match(completer):
  function test_should_break_ties_using_lexical_order (line 56) | def test_should_break_ties_using_lexical_order(completer, collection):
  function test_matching_should_be_case_insensitive (line 76) | def test_matching_should_be_case_insensitive(completer):

FILE: tests/test_init_commands_simple.py
  function dummy_exec (line 8) | def dummy_exec(monkeypatch, tmp_path):
  function test_init_command_option (line 36) | def test_init_command_option(dummy_exec):
  function test_init_commands_from_config (line 50) | def test_init_commands_from_config(dummy_exec, tmp_path):
  function test_init_commands_option_and_config (line 68) | def test_init_commands_option_and_config(dummy_exec, tmp_path):

FILE: tests/test_main.py
  function test_obfuscate_process_password (line 32) | def test_obfuscate_process_password():
  function test_format_output (line 62) | def test_format_output():
  function test_column_date_formats (line 77) | def test_column_date_formats():
  function test_no_column_date_formats (line 105) | def test_no_column_date_formats():
  function test_format_output_truncate_on (line 128) | def test_format_output_truncate_on():
  function test_format_output_truncate_off (line 147) | def test_format_output_truncate_off():
  function test_format_array_output (line 156) | def test_format_array_output(executor):
  function test_format_array_output_expanded (line 179) | def test_format_array_output_expanded(executor):
  function test_format_output_auto_expand (line 203) | def test_format_output_auto_expand():
  function pset_pager_mocks (line 259) | def pset_pager_mocks():
  function test_pset_pager_off (line 271) | def test_pset_pager_off(term_height, term_width, text, pset_pager_mocks):
  function test_pset_pager_always (line 283) | def test_pset_pager_always(term_height, term_width, text, pset_pager_moc...
  function test_pset_pager_on (line 298) | def test_pset_pager_on(term_height, term_width, text, use_pager, pset_pa...
  function test_color_pattern (line 324) | def test_color_pattern(text, expected_length):
  function test_i_works (line 329) | def test_i_works(tmpdir, executor):
  function test_toggle_verbose_errors (line 339) | def test_toggle_verbose_errors(executor):
  function test_echo_works (line 357) | def test_echo_works(executor):
  function test_qecho_works (line 365) | def test_qecho_works(executor):
  function test_logfile_works (line 373) | def test_logfile_works(executor):
  function test_logfile_unwriteable_file (line 387) | def test_logfile_unwriteable_file(executor):
  function test_watch_works (line 397) | def test_watch_works(executor):
  function test_missing_rc_dir (line 440) | def test_missing_rc_dir(tmpdir):
  function test_quoted_db_uri (line 447) | def test_quoted_db_uri(tmpdir):
  function test_pg_service_file (line 454) | def test_pg_service_file(tmpdir):
  function test_ssl_db_uri (line 507) | def test_ssl_db_uri(tmpdir):
  function test_port_db_uri (line 525) | def test_port_db_uri(tmpdir):
  function test_multihost_db_uri (line 532) | def test_multihost_db_uri(tmpdir):
  function test_application_name_db_uri (line 545) | def test_application_name_db_uri(tmpdir):
  function test_duration_in_words (line 576) | def test_duration_in_words(duration_in_seconds, words):
  function test_get_prompt_with_transaction_status (line 589) | def test_get_prompt_with_transaction_status(transaction_indicator, expec...
  function test_get_prompt_transaction_status_in_full_prompt (line 605) | def test_get_prompt_transaction_status_in_full_prompt():
  function test_notifications (line 622) | def test_notifications(executor):

FILE: tests/test_naive_completion.py
  function completer (line 8) | def completer():
  function complete_event (line 15) | def complete_event():
  function test_empty_string_completion (line 21) | def test_empty_string_completion(completer, complete_event):
  function test_select_keyword_completion (line 28) | def test_select_keyword_completion(completer, complete_event):
  function test_function_name_completion (line 35) | def test_function_name_completion(completer, complete_event):
  function test_column_name_completion (line 52) | def test_column_name_completion(completer, complete_event):
  function test_alter_well_known_keywords_completion (line 59) | def test_alter_well_known_keywords_completion(completer, complete_event):
  function test_special_name_completion (line 77) | def test_special_name_completion(completer, complete_event):
  function test_datatype_name_completion (line 85) | def test_datatype_name_completion(completer, complete_event):

FILE: tests/test_pgcompleter.py
  function test_load_alias_map_file_missing_file (line 7) | def test_load_alias_map_file_missing_file():
  function test_load_alias_map_file_invalid_json (line 15) | def test_load_alias_map_file_invalid_json(tmp_path):
  function test_generate_alias_uses_upper_case_letters_from_name (line 30) | def test_generate_alias_uses_upper_case_letters_from_name(table_name, al...
  function test_generate_alias_uses_first_char_and_every_preceded_by_underscore (line 42) | def test_generate_alias_uses_first_char_and_every_preceded_by_underscore...
  function test_generate_alias_can_use_alias_map (line 53) | def test_generate_alias_can_use_alias_map(table_name, alias_map, alias):
  function test_pgcompleter_alias_uses_configured_alias_map (line 63) | def test_pgcompleter_alias_uses_configured_alias_map(table_name, alias_m...
  function test_generate_alias_prefers_alias_over_upper_case_name (line 82) | def test_generate_alias_prefers_alias_over_upper_case_name(table_name, a...
  function test_generate_alias_prefers_upper_case_name_over_underscore_name (line 93) | def test_generate_alias_prefers_upper_case_name_over_underscore_name(tab...

FILE: tests/test_pgexecute.py
  function function_meta_data (line 14) | def function_meta_data(
  function test_conn (line 43) | def test_conn(executor):
  function test_copy (line 58) | def test_copy(executor):
  function test_bools_are_treated_as_strings (line 74) | def test_bools_are_treated_as_strings(executor):
  function test_expanded_slash_G (line 89) | def test_expanded_slash_G(executor, pgspecial):
  function test_schemata_table_views_and_columns_query (line 98) | def test_schemata_table_views_and_columns_query(executor):
  function test_foreign_key_query (line 139) | def test_foreign_key_query(executor):
  function test_functions_query (line 152) | def test_functions_query(executor):
  function test_datatypes_query (line 202) | def test_datatypes_query(executor):
  function test_database_list (line 210) | def test_database_list(executor):
  function test_invalid_syntax (line 216) | def test_invalid_syntax(executor, exception_formatter):
  function test_invalid_syntax_verbose (line 227) | def test_invalid_syntax_verbose(executor):
  function test_invalid_column_name (line 247) | def test_invalid_column_name(executor, exception_formatter):
  function expanded (line 253) | def expanded(request):
  function test_unicode_support_in_output (line 258) | def test_unicode_support_in_output(executor, expanded):
  function test_not_is_special (line 267) | def test_not_is_special(executor, pgspecial):
  function test_execute_from_file_no_arg (line 277) | def test_execute_from_file_no_arg(executor, pgspecial):
  function test_execute_from_file_io_error (line 288) | def test_execute_from_file_io_error(os, executor, pgspecial):
  function test_execute_from_commented_file_that_executes_another_file (line 302) | def test_execute_from_commented_file_that_executes_another_file(executor...
  function test_execute_commented_first_line_and_special (line 320) | def test_execute_commented_first_line_and_special(executor, pgspecial, t...
  function test_execute_commented_first_line_and_normal (line 418) | def test_execute_commented_first_line_and_normal(executor, pgspecial, tm...
  function test_multiple_queries_same_line (line 509) | def test_multiple_queries_same_line(executor):
  function test_multiple_queries_with_special_command_same_line (line 517) | def test_multiple_queries_with_special_command_same_line(executor, pgspe...
  function test_multiple_queries_same_line_syntaxerror (line 526) | def test_multiple_queries_same_line_syntaxerror(executor, exception_form...
  function pgspecial (line 537) | def pgspecial():
  function test_special_command_help (line 542) | def test_special_command_help(executor, pgspecial):
  function test_bytea_field_support_in_output (line 549) | def test_bytea_field_support_in_output(executor):
  function test_unicode_support_in_unknown_type (line 557) | def test_unicode_support_in_unknown_type(executor):
  function test_unicode_support_in_enum_type (line 562) | def test_unicode_support_in_enum_type(executor):
  function test_json_renders_without_u_prefix (line 570) | def test_json_renders_without_u_prefix(executor, expanded):
  function test_jsonb_renders_without_u_prefix (line 579) | def test_jsonb_renders_without_u_prefix(executor, expanded):
  function test_date_time_types (line 588) | def test_date_time_types(executor):
  function test_large_numbers_render_directly (line 609) | def test_large_numbers_render_directly(executor, value):
  function test_describe_special (line 619) | def test_describe_special(executor, command, verbose, pattern, pgspecial):
  function test_raises_with_no_formatter (line 628) | def test_raises_with_no_formatter(executor, sql):
  function test_on_error_resume (line 634) | def test_on_error_resume(executor, exception_formatter):
  function test_on_error_stop (line 641) | def test_on_error_stop(executor, exception_formatter):
  function test_nonexistent_function_definition (line 655) | def test_nonexistent_function_definition(executor):
  function test_function_definition (line 661) | def test_function_definition(executor):
  function test_function_notice_order (line 677) | def test_function_notice_order(executor):
  function test_view_definition (line 709) | def test_view_definition(executor):
  function test_nonexistent_view_definition (line 722) | def test_nonexistent_view_definition(executor):
  function test_short_host (line 730) | def test_short_host(executor):
  class VirtualCursor (line 743) | class VirtualCursor:
    method __init__ (line 746) | def __init__(self):
    method execute (line 753) | def execute(self, *args, **kwargs):
  function test_exit_without_active_connection (line 759) | def test_exit_without_active_connection(executor):
  function test_virtual_database (line 783) | def test_virtual_database(executor):

FILE: tests/test_pgspecial.py
  function test_slash_suggests_special (line 14) | def test_slash_suggests_special():
  function test_slash_d_suggests_special (line 19) | def test_slash_d_suggests_special():
  function test_dn_suggests_schemata (line 24) | def test_dn_suggests_schemata():
  function test_d_suggests_tables_views_and_schemas (line 32) | def test_d_suggests_tables_views_and_schemas():
  function test_d_dot_suggests_schema_qualified_tables_or_views (line 40) | def test_d_dot_suggests_schema_qualified_tables_or_views():
  function test_df_suggests_schema_or_function (line 48) | def test_df_suggests_schema_or_function():
  function test_leading_whitespace_ok (line 56) | def test_leading_whitespace_ok():
  function test_dT_suggests_schema_or_datatypes (line 63) | def test_dT_suggests_schema_or_datatypes():
  function test_schema_qualified_dT_suggests_datatypes (line 69) | def test_schema_qualified_dT_suggests_datatypes():
  function test_c_suggests_databases (line 76) | def test_c_suggests_databases(command):

FILE: tests/test_prioritization.py
  function test_prevalence_counter (line 4) | def test_prevalence_counter():

FILE: tests/test_prompt_utils.py
  function test_confirm_destructive_query_notty (line 6) | def test_confirm_destructive_query_notty():
  function test_confirm_destructive_query_with_alias (line 13) | def test_confirm_destructive_query_with_alias():

FILE: tests/test_rowlimit.py
  function default_pgcli_obj (line 12) | def default_pgcli_obj():
  function DEFAULT (line 17) | def DEFAULT(default_pgcli_obj):
  function LIMIT (line 22) | def LIMIT(DEFAULT):
  function over_default (line 27) | def over_default(DEFAULT):
  function over_limit (line 34) | def over_limit(LIMIT):
  function low_count (line 41) | def low_count():
  function test_row_limit_with_LIMIT_clause (line 47) | def test_row_limit_with_LIMIT_clause(LIMIT, over_limit):
  function test_row_limit_without_LIMIT_clause (line 59) | def test_row_limit_without_LIMIT_clause(LIMIT, over_limit):
  function test_row_limit_on_non_select (line 71) | def test_row_limit_on_non_select(over_limit):

FILE: tests/test_smart_completion_multiple_schemata.py
  function test_suggested_column_names_from_shadowed_visible_table (line 125) | def test_suggested_column_names_from_shadowed_visible_table(completer, t...
  function test_suggested_column_names_from_qualified_shadowed_table (line 138) | def test_suggested_column_names_from_qualified_shadowed_table(completer,...
  function test_suggested_column_names_from_cte (line 145) | def test_suggested_column_names_from_cte(completer, text):
  function test_suggested_join_conditions (line 160) | def test_suggested_join_conditions(completer, text):
  function test_suggested_joins (line 183) | def test_suggested_joins(completer, query, tbl):
  function test_suggested_column_names_from_schema_qualifed_table (line 191) | def test_suggested_column_names_from_schema_qualifed_table(completer):
  function test_suggested_columns_with_insert (line 206) | def test_suggested_columns_with_insert(completer, text):
  function test_suggested_column_names_in_function (line 211) | def test_suggested_column_names_in_function(completer):
  function test_suggested_table_names_with_schema_dot (line 222) | def test_suggested_table_names_with_schema_dot(completer, text, use_lead...
  function test_suggested_table_names_with_schema_dot2 (line 236) | def test_suggested_table_names_with_schema_dot2(completer, text, use_lea...
  function test_suggested_column_names_with_qualified_alias (line 248) | def test_suggested_column_names_with_qualified_alias(completer):
  function test_suggested_multiple_column_names (line 254) | def test_suggested_multiple_column_names(completer):
  function test_suggested_multiple_column_names_with_alias (line 260) | def test_suggested_multiple_column_names_with_alias(completer):
  function test_suggestions_after_on (line 273) | def test_suggestions_after_on(completer, text):
  function test_suggested_aliases_after_on_right_side (line 286) | def test_suggested_aliases_after_on_right_side(completer):
  function test_table_names_after_from (line 293) | def test_table_names_after_from(completer):
  function test_schema_qualified_function_name (line 300) | def test_schema_qualified_function_name(completer):
  function test_schema_qualified_function_name_after_from (line 310) | def test_schema_qualified_function_name_after_from(completer):
  function test_unqualified_function_name_not_returned (line 319) | def test_unqualified_function_name_not_returned(completer):
  function test_unqualified_function_name_in_search_path (line 326) | def test_unqualified_function_name_in_search_path(completer):
  function test_schema_qualified_type_name (line 345) | def test_schema_qualified_type_name(completer, text):
  function test_suggest_columns_from_aliased_set_returning_function (line 351) | def test_suggest_columns_from_aliased_set_returning_function(completer):
  function test_wildcard_column_expansion_with_function (line 365) | def test_wildcard_column_expansion_with_function(completer, text):
  function test_wildcard_column_expansion_with_alias_qualifier (line 377) | def test_wildcard_column_expansion_with_alias_qualifier(completer):
  function test_wildcard_column_expansion_with_insert (line 422) | def test_wildcard_column_expansion_with_insert(completer, text):
  function test_wildcard_column_expansion_with_table_qualifier (line 431) | def test_wildcard_column_expansion_with_table_qualifier(completer):
  function test_wildcard_column_expansion_with_two_tables (line 444) | def test_wildcard_column_expansion_with_two_tables(completer):
  function test_wildcard_column_expansion_with_two_tables_and_parent (line 456) | def test_wildcard_column_expansion_with_two_tables_and_parent(completer):
  function test_suggest_columns_from_unquoted_table (line 480) | def test_suggest_columns_from_unquoted_table(completer, text):
  function test_suggest_columns_from_quoted_table (line 488) | def test_suggest_columns_from_quoted_table(completer, text):
  function test_schema_or_visible_table_completion (line 499) | def test_schema_or_visible_table_completion(completer, text):
  function test_table_aliases (line 506) | def test_table_aliases(completer, text):
  function test_aliases_with_casing (line 522) | def test_aliases_with_casing(completer, text):
  function test_table_casing (line 538) | def test_table_casing(completer, text):
  function test_alias_search_without_aliases2 (line 553) | def test_alias_search_without_aliases2(completer):
  function test_alias_search_without_aliases1 (line 560) | def test_alias_search_without_aliases1(completer):
  function test_alias_search_with_aliases2 (line 567) | def test_alias_search_with_aliases2(completer):
  function test_alias_search_with_aliases1 (line 574) | def test_alias_search_with_aliases1(completer):
  function test_join_alias_search_with_aliases1 (line 581) | def test_join_alias_search_with_aliases1(completer):
  function test_join_alias_search_without_aliases1 (line 591) | def test_join_alias_search_without_aliases1(completer):
  function test_join_alias_search_with_aliases2 (line 601) | def test_join_alias_search_with_aliases2(completer):
  function test_join_alias_search_without_aliases2 (line 608) | def test_join_alias_search_without_aliases2(completer):
  function test_function_alias_search_without_aliases (line 615) | def test_function_alias_search_without_aliases(completer):
  function test_function_alias_search_with_aliases (line 625) | def test_function_alias_search_with_aliases(completer):
  function test_column_alias_search (line 635) | def test_column_alias_search(completer):
  function test_column_alias_search_qualified (line 642) | def test_column_alias_search_qualified(completer):
  function test_schema_object_order (line 649) | def test_schema_object_order(completer):
  function test_all_schema_objects (line 655) | def test_all_schema_objects(completer):
  function test_all_schema_objects_with_casing (line 664) | def test_all_schema_objects_with_casing(completer):
  function test_all_schema_objects_with_aliases (line 673) | def test_all_schema_objects_with_aliases(completer):
  function test_set_schema (line 682) | def test_set_schema(completer):

FILE: tests/test_smart_completion_public_schema_only.py
  function test_function_column_name (line 121) | def test_function_column_name(completer):
  function test_drop_alter_function (line 131) | def test_drop_alter_function(completer, action):
  function test_empty_string_completion (line 136) | def test_empty_string_completion(completer):
  function test_select_keyword_completion (line 142) | def test_select_keyword_completion(completer):
  function test_builtin_function_name_completion (line 148) | def test_builtin_function_name_completion(completer):
  function test_builtin_function_matches_only_at_start (line 164) | def test_builtin_function_matches_only_at_start(completer):
  function test_user_function_name_completion (line 173) | def test_user_function_name_completion(completer):
  function test_user_function_name_completion_matches_anywhere (line 189) | def test_user_function_name_completion_matches_anywhere(completer):
  function test_list_functions_for_special (line 200) | def test_list_functions_for_special(completer):
  function test_suggested_column_names_from_visible_table (line 206) | def test_suggested_column_names_from_visible_table(completer):
  function test_suggested_cased_column_names (line 212) | def test_suggested_cased_column_names(completer):
  function test_suggested_auto_qualified_column_names (line 221) | def test_suggested_auto_qualified_column_names(text, completer):
  function test_suggested_auto_qualified_column_names_two_tables (line 236) | def test_suggested_auto_qualified_column_names_two_tables(text, completer):
  function test_no_column_qualification (line 246) | def test_no_column_qualification(text, completer):
  function test_suggested_cased_always_qualified_column_names (line 253) | def test_suggested_cased_always_qualified_column_names(completer):
  function test_suggested_column_names_in_function (line 262) | def test_suggested_column_names_in_function(completer):
  function test_suggested_column_names_with_table_dot (line 268) | def test_suggested_column_names_with_table_dot(completer):
  function test_suggested_column_names_with_alias (line 274) | def test_suggested_column_names_with_alias(completer):
  function test_suggested_multiple_column_names (line 280) | def test_suggested_multiple_column_names(completer):
  function test_suggested_multiple_column_names_with_alias (line 286) | def test_suggested_multiple_column_names_with_alias(completer):
  function test_suggested_cased_column_names_with_alias (line 292) | def test_suggested_cased_column_names_with_alias(completer):
  function test_suggested_multiple_column_names_with_dot (line 298) | def test_suggested_multiple_column_names_with_dot(completer):
  function test_suggest_columns_after_three_way_join (line 308) | def test_suggest_columns_after_three_way_join(completer):
  function test_suggested_join_conditions (line 337) | def test_suggested_join_conditions(completer, text):
  function test_cased_join_conditions (line 344) | def test_cased_join_conditions(completer, text):
  function test_suggested_join_conditions_with_same_table_twice (line 361) | def test_suggested_join_conditions_with_same_table_twice(completer, text):
  function test_suggested_join_conditions_with_invalid_qualifier (line 377) | def test_suggested_join_conditions_with_invalid_qualifier(completer, text):
  function test_suggested_join_conditions_with_invalid_table (line 390) | def test_suggested_join_conditions_with_invalid_table(completer, text, r...
  function test_suggested_joins_fuzzy (line 405) | def test_suggested_joins_fuzzy(completer, text):
  function test_suggested_joins (line 430) | def test_suggested_joins(completer, text):
  function test_cased_joins (line 444) | def test_cased_joins(completer, text):
  function test_aliased_joins (line 459) | def test_aliased_joins(completer, text):
  function test_suggested_joins_quoted_schema_qualified_table (line 483) | def test_suggested_joins_quoted_schema_qualified_table(completer, text):
  function test_suggested_aliases_after_on (line 498) | def test_suggested_aliases_after_on(completer, text):
  function test_suggested_aliases_after_on_right_side (line 517) | def test_suggested_aliases_after_on_right_side(completer, text):
  function test_suggested_tables_after_on (line 531) | def test_suggested_tables_after_on(completer, text):
  function test_suggested_tables_after_on_right_side (line 550) | def test_suggested_tables_after_on_right_side(completer, text):
  function test_join_using_suggests_common_columns (line 564) | def test_join_using_suggests_common_columns(completer, text):
  function test_join_using_suggests_from_last_table (line 579) | def test_join_using_suggests_from_last_table(completer, text):
  function test_join_using_suggests_columns_after_first_column (line 593) | def test_join_using_suggests_columns_after_first_column(completer, text):
  function test_table_names_after_from (line 607) | def test_table_names_after_from(completer, text):
  function test_auto_escaped_col_names (line 627) | def test_auto_escaped_col_names(completer):
  function test_allow_leading_double_quote_in_last_word (line 633) | def test_allow_leading_double_quote_in_last_word(completer):
  function test_suggest_datatype (line 651) | def test_suggest_datatype(text, completer):
  function test_suggest_columns_from_escaped_table_alias (line 657) | def test_suggest_columns_from_escaped_table_alias(completer):
  function test_suggest_columns_from_set_returning_function (line 663) | def test_suggest_columns_from_set_returning_function(completer):
  function test_suggest_columns_from_aliased_set_returning_function (line 669) | def test_suggest_columns_from_aliased_set_returning_function(completer):
  function test_join_functions_using_suggests_common_columns (line 675) | def test_join_functions_using_suggests_common_columns(completer):
  function test_join_functions_on_suggests_columns_and_join_conditions (line 683) | def test_join_functions_on_suggests_columns_and_join_conditions(completer):
  function test_learn_keywords (line 693) | def test_learn_keywords(completer):
  function test_learn_table_names (line 705) | def test_learn_table_names(completer):
  function test_columns_before_keywords (line 720) | def test_columns_before_keywords(completer):
  function test_wildcard_column_expansion (line 741) | def test_wildcard_column_expansion(completer, text):
  function test_wildcard_column_expansion_with_alias (line 763) | def test_wildcard_column_expansion_with_alias(completer, text):
  function test_wildcard_column_expansion_with_table_qualifier (line 788) | def test_wildcard_column_expansion_with_table_qualifier(completer, text,...
  function test_wildcard_column_expansion_with_two_tables (line 799) | def test_wildcard_column_expansion_with_two_tables(completer):
  function test_wildcard_column_expansion_with_two_tables_and_parent (line 811) | def test_wildcard_column_expansion_with_two_tables_and_parent(completer):
  function test_suggest_columns_from_unquoted_table (line 828) | def test_suggest_columns_from_unquoted_table(completer, text):
  function test_suggest_columns_from_quoted_table (line 835) | def test_suggest_columns_from_quoted_table(completer):
  function test_schema_or_visible_table_completion (line 842) | def test_schema_or_visible_table_completion(completer, text):
  function test_table_aliases (line 849) | def test_table_aliases(completer, text):
  function test_duplicate_table_aliases (line 856) | def test_duplicate_table_aliases(completer, text):
  function test_duplicate_aliases_with_casing (line 881) | def test_duplicate_aliases_with_casing(completer, text):
  function test_aliases_with_casing (line 904) | def test_aliases_with_casing(completer, text):
  function test_table_casing (line 911) | def test_table_casing(completer, text):
  function test_insert (line 926) | def test_insert(completer, text):
  function test_suggest_cte_names (line 933) | def test_suggest_cte_names(completer):
  function test_suggest_columns_from_cte (line 948) | def test_suggest_columns_from_cte(completer):
  function test_cte_qualified_columns (line 970) | def test_cte_qualified_columns(completer, text):
  function test_keyword_casing_upper (line 985) | def test_keyword_casing_upper(keyword_casing, expected, texts):
  function test_keyword_after_alter (line 993) | def test_keyword_after_alter(completer):
  function test_set_schema (line 1001) | def test_set_schema(completer):
  function test_special_name_completion (line 1009) | def test_special_name_completion(completer):

FILE: tests/test_sqlcompletion.py
  function cols_etc (line 21) | def cols_etc(table, schema=None, alias=None, is_function=False, parent=N...
  function test_select_suggests_cols_with_visible_table_scope (line 34) | def test_select_suggests_cols_with_visible_table_scope():
  function test_select_suggests_cols_with_qualified_table_scope (line 39) | def test_select_suggests_cols_with_qualified_table_scope():
  function test_cte_does_not_crash (line 44) | def test_cte_does_not_crash():
  function test_where_suggests_columns_functions_quoted_table (line 51) | def test_where_suggests_columns_functions_quoted_table(expression):
  function test_where_suggests_columns_functions (line 74) | def test_where_suggests_columns_functions(expression):
  function test_where_in_suggests_columns (line 83) | def test_where_in_suggests_columns(expression):
  function test_after_as (line 89) | def test_after_as(expression):
  function test_where_equals_any_suggests_columns_or_keywords (line 94) | def test_where_equals_any_suggests_columns_or_keywords():
  function test_lparen_suggests_cols_and_funcs (line 100) | def test_lparen_suggests_cols_and_funcs():
  function test_select_suggests_cols_and_funcs (line 109) | def test_select_suggests_cols_and_funcs():
  function test_suggests_tables_views_and_schemas (line 119) | def test_suggests_tables_views_and_schemas(expression):
  function test_suggest_tables_views_schemas_and_functions (line 125) | def test_suggest_tables_views_schemas_and_functions(expression):
  function test_suggest_after_join_with_two_tables (line 137) | def test_suggest_after_join_with_two_tables(expression):
  function test_suggest_after_join_with_one_table (line 148) | def test_suggest_after_join_with_one_table(expression):
  function test_suggest_qualified_tables_and_views (line 159) | def test_suggest_qualified_tables_and_views(expression):
  function test_suggest_qualified_aliasable_tables_and_views (line 165) | def test_suggest_qualified_aliasable_tables_and_views(expression):
  function test_suggest_qualified_tables_views_and_functions (line 180) | def test_suggest_qualified_tables_views_and_functions(expression):
  function test_suggest_qualified_tables_views_functions_and_joins (line 186) | def test_suggest_qualified_tables_views_functions_and_joins(expression):
  function test_truncate_suggests_tables_and_schemas (line 195) | def test_truncate_suggests_tables_and_schemas():
  function test_truncate_suggests_qualified_tables (line 200) | def test_truncate_suggests_qualified_tables():
  function test_distinct_suggests_cols (line 206) | def test_distinct_suggests_cols(text):
  function test_distinct_and_order_by_suggestions_with_aliases (line 226) | def test_distinct_and_order_by_suggestions_with_aliases(text, text_befor...
  function test_distinct_and_order_by_suggestions_with_alias_given (line 252) | def test_distinct_and_order_by_suggestions_with_alias_given(text, text_b...
  function test_function_arguments_with_alias_given (line 266) | def test_function_arguments_with_alias_given():
  function test_col_comma_suggests_cols (line 281) | def test_col_comma_suggests_cols():
  function test_table_comma_suggests_tables_and_schemas (line 290) | def test_table_comma_suggests_tables_and_schemas():
  function test_into_suggests_tables_and_schemas (line 295) | def test_into_suggests_tables_and_schemas():
  function test_insert_into_lparen_suggests_cols (line 301) | def test_insert_into_lparen_suggests_cols(text):
  function test_insert_into_lparen_partial_text_suggests_cols (line 306) | def test_insert_into_lparen_partial_text_suggests_cols():
  function test_insert_into_lparen_comma_suggests_cols (line 311) | def test_insert_into_lparen_comma_suggests_cols():
  function test_partially_typed_col_name_suggests_col_names (line 316) | def test_partially_typed_col_name_suggests_col_names():
  function test_dot_suggests_cols_of_a_table_or_schema_qualified_table (line 321) | def test_dot_suggests_cols_of_a_table_or_schema_qualified_table():
  function test_dot_suggests_cols_of_an_alias (line 340) | def test_dot_suggests_cols_of_an_alias(sql):
  function test_dot_suggests_cols_of_an_alias_where (line 359) | def test_dot_suggests_cols_of_an_alias_where(sql):
  function test_dot_col_comma_suggests_cols_or_schema_qualified_table (line 369) | def test_dot_col_comma_suggests_cols_or_schema_qualified_table():
  function test_sub_select_suggests_keyword (line 387) | def test_sub_select_suggests_keyword(expression):
  function test_sub_select_partial_text_suggests_keyword (line 400) | def test_sub_select_partial_text_suggests_keyword(expression):
  function test_outer_table_reference_in_exists_subquery_suggests_columns (line 405) | def test_outer_table_reference_in_exists_subquery_suggests_columns():
  function test_sub_select_table_name_completion (line 417) | def test_sub_select_table_name_completion(expression):
  function test_sub_select_table_name_completion_with_outer_table (line 429) | def test_sub_select_table_name_completion_with_outer_table(expression):
  function test_sub_select_col_name_completion (line 435) | def test_sub_select_col_name_completion():
  function test_sub_select_multiple_col_name_completion (line 445) | def test_sub_select_multiple_col_name_completion():
  function test_sub_select_dot_col_name_completion (line 450) | def test_sub_select_dot_col_name_completion():
  function test_join_suggests_tables_and_schemas (line 462) | def test_join_suggests_tables_and_schemas(tbl_alias, join_type):
  function test_left_join_with_comma (line 473) | def test_left_join_with_comma():
  function test_join_alias_dot_suggests_cols1 (line 489) | def test_join_alias_dot_suggests_cols1(sql):
  function test_join_alias_dot_suggests_cols2 (line 508) | def test_join_alias_dot_suggests_cols2(sql):
  function test_on_suggests_aliases_and_join_conditions (line 533) | def test_on_suggests_aliases_and_join_conditions(sql):
  function test_on_suggests_tables_and_join_conditions (line 549) | def test_on_suggests_tables_and_join_conditions(sql):
  function test_on_suggests_aliases_right_side (line 565) | def test_on_suggests_aliases_right_side(sql):
  function test_on_suggests_tables_and_join_conditions_right_side (line 577) | def test_on_suggests_tables_and_join_conditions_right_side(sql):
  function test_join_using_suggests_common_columns (line 598) | def test_join_using_suggests_common_columns(text):
  function test_suggest_columns_after_multiple_joins (line 603) | def test_suggest_columns_after_multiple_joins():
  function test_2_statements_2nd_current (line 613) | def test_2_statements_2nd_current():
  function test_2_statements_1st_current (line 629) | def test_2_statements_1st_current():
  function test_3_statements_2nd_current (line 637) | def test_3_statements_2nd_current():
  function test_statements_in_function_body (line 695) | def test_statements_in_function_body(text):
  function test_statements_with_cursor_after_function_body (line 724) | def test_statements_with_cursor_after_function_body(text):
  function test_statements_with_cursor_before_function_body (line 730) | def test_statements_with_cursor_before_function_body(text):
  function test_create_db_with_template (line 735) | def test_create_db_with_template():
  function test_specials_included_for_initial_completion (line 742) | def test_specials_included_for_initial_completion(initial_text):
  function test_drop_schema_qualified_table_suggests_only_tables (line 748) | def test_drop_schema_qualified_table_suggests_only_tables():
  function test_handle_pre_completion_comma_gracefully (line 755) | def test_handle_pre_completion_comma_gracefully(text):
  function test_drop_schema_suggests_schemas (line 761) | def test_drop_schema_suggests_schemas():
  function test_cast_operator_suggests_types (line 767) | def test_cast_operator_suggests_types(text):
  function test_cast_operator_suggests_schema_qualified_types (line 776) | def test_cast_operator_suggests_schema_qualified_types(text):
  function test_alter_column_type_suggests_types (line 783) | def test_alter_column_type_suggests_types():
  function test_identifier_suggests_types_in_parentheses (line 807) | def test_identifier_suggests_types_in_parentheses(text):
  function test_alias_suggests_keywords (line 827) | def test_alias_suggests_keywords(text):
  function test_invalid_sql (line 832) | def test_invalid_sql():
  function test_suggest_where_keyword (line 843) | def test_suggest_where_keyword(text):
  function test_named_query_completion (line 874) | def test_named_query_completion(text, before, expected):
  function test_select_suggests_fields_from_function (line 879) | def test_select_suggests_fields_from_function():
  function test_leading_parenthesis (line 885) | def test_leading_parenthesis(sql):
  function test_ignore_leading_double_quotes (line 891) | def test_ignore_leading_double_quotes(sql):
  function test_column_keyword_suggests_columns (line 905) | def test_column_keyword_suggests_columns(sql):
  function test_handle_unrecognized_kw_generously (line 910) | def test_handle_unrecognized_kw_generously():
  function test_keyword_after_alter (line 919) | def test_keyword_after_alter(sql):
  function test_suggestion_when_setting_search_path (line 923) | def test_suggestion_when_setting_search_path():

FILE: tests/test_ssh_tunnel.py
  function mock_ssh_tunnel_forwarder (line 14) | def mock_ssh_tunnel_forwarder() -> MagicMock:
  function mock_pgexecute (line 24) | def mock_pgexecute() -> MagicMock:
  function test_ssh_tunnel (line 29) | def test_ssh_tunnel(mock_ssh_tunnel_forwarder: MagicMock, mock_pgexecute...
  function test_cli_with_tunnel (line 116) | def test_cli_with_tunnel() -> None:
  function test_config (line 126) | def test_config(tmpdir: os.PathLike, mock_ssh_tunnel_forwarder: MagicMoc...

FILE: tests/utils.py
  function db_connection (line 12) | def db_connection(dbname=None):
  function create_db (line 47) | def create_db(dbname):
  function drop_tables (line 55) | def drop_tables(conn):
  function run (line 66) | def run(executor, sql, join=False, expanded=False, pgspecial=None, excep...
  function completions_to_set (line 80) | def completions_to_set(completions):
Condensed preview — 113 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (641K chars).
[
  {
    "path": ".coveragerc",
    "chars": 19,
    "preview": "[run]\nsource=pgcli\n"
  },
  {
    "path": ".editorconfig",
    "chars": 266,
    "preview": "# editorconfig.org\n# Get your text editor plugin at:\n# http://editorconfig.org/#download\nroot = true\n\n[*]\ncharset = utf-"
  },
  {
    "path": ".git-blame-ignore-revs",
    "chars": 0,
    "preview": ""
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 290,
    "preview": "## Description\n<!--- Describe your problem as fully as you can. -->\n\n## Your environment\n<!-- This gives us some more co"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 604,
    "preview": "## Description\n<!--- Describe your changes in detail. -->\n\n\n\n## Checklist\n<!--- We appreciate your help and want to give"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2420,
    "preview": "name: pgcli\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    paths-ignore:\n      - '**.rst'\n\njobs:\n  build:\n "
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 834,
    "preview": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n  schedule:\n    - cron: \"2"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 2605,
    "preview": "name: Publish Python Package\n\non:\n  release:\n    types: [created]\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    runs"
  },
  {
    "path": ".gitignore",
    "chars": 845,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 209,
    "preview": "repos:\n- repo: https://github.com/astral-sh/ruff-pre-commit\n  # Ruff version.\n  rev: v0.14.10\n  hooks:\n    # Run the lin"
  },
  {
    "path": "AUTHORS",
    "chars": 3413,
    "preview": "Many thanks to the following contributors.\n\nProject Lead:\n-------------\n    * Irina Truong\n\nCore Devs:\n----------\n    * "
  },
  {
    "path": "CONTRIBUTING.rst",
    "chars": 6300,
    "preview": "Development Guide\n-----------------\nThis is a guide for developers who would like to contribute to this project.\n\nGitHub"
  },
  {
    "path": "Dockerfile",
    "chars": 72,
    "preview": "FROM python:3.8\n\nCOPY . /app\nRUN cd /app && pip install -e .\n\nCMD pgcli\n"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1455,
    "preview": "All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted pro"
  },
  {
    "path": "MANIFEST.in",
    "chars": 93,
    "preview": "include LICENSE.txt AUTHORS changelog.rst\nrecursive-include tests *.py *.txt *.feature *.ini\n"
  },
  {
    "path": "README.rst",
    "chars": 11937,
    "preview": "We stand with Ukraine\n---------------------\n\nUkrainian people are fighting for their country. A lot of civilians, women "
  },
  {
    "path": "RELEASES.md",
    "chars": 315,
    "preview": "Releasing pgcli\n---------------\n\nYou have been made the maintainer of `pgcli`? Congratulations!\n\nTo release a new versio"
  },
  {
    "path": "TODO",
    "chars": 865,
    "preview": "# vi: ft=vimwiki\n* [ ] Add coverage.\n* [ ] Refactor to sqlcompletion to consume the text from left to right and use a st"
  },
  {
    "path": "Vagrantfile",
    "chars": 4421,
    "preview": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n# \n#\n\nVagrant.configure(2) do |config|\n\n  config.vm.synced_folder \".\", \"/pgcli\""
  },
  {
    "path": "changelog.rst",
    "chars": 50038,
    "preview": "Upcoming (TBD)\n==============\n\nFeatures:\n---------\n* Add support for `\\\\T` prompt escape sequence to display transaction"
  },
  {
    "path": "pgcli/__init__.py",
    "chars": 22,
    "preview": "__version__ = \"4.4.0\"\n"
  },
  {
    "path": "pgcli/__main__.py",
    "chars": 101,
    "preview": "\"\"\"\npgcli package main entry point\n\"\"\"\n\nfrom .main import cli\n\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "pgcli/auth.py",
    "chars": 1568,
    "preview": "import click\nfrom textwrap import dedent\n\n\nkeyring = None  # keyring will be loaded later\n\n\nkeyring_error_message = dede"
  },
  {
    "path": "pgcli/completion_refresher.py",
    "chars": 5212,
    "preview": "import threading\nimport os\nfrom collections import OrderedDict\n\nfrom .pgcompleter import PGCompleter\n\n\nclass CompletionR"
  },
  {
    "path": "pgcli/config.py",
    "chars": 2867,
    "preview": "import shutil\nimport os\nimport platform\nfrom os.path import expanduser, exists, dirname\nimport re\nfrom typing import Tex"
  },
  {
    "path": "pgcli/explain_output_formatter.py",
    "chars": 546,
    "preview": "from pgcli.pyev import Visualizer\nimport json\n\n\n\"\"\"Explain response output adapter\"\"\"\n\n\nclass ExplainOutputFormatter:\n  "
  },
  {
    "path": "pgcli/key_bindings.py",
    "chars": 4170,
    "preview": "import logging\nfrom prompt_toolkit.enums import EditingMode\nfrom prompt_toolkit.key_binding import KeyBindings\nfrom prom"
  },
  {
    "path": "pgcli/magic.py",
    "chars": 2262,
    "preview": "from .main import PGCli\nimport sql.parse\nimport sql.connection\nimport logging\n\n_logger = logging.getLogger(__name__)\n\n\nd"
  },
  {
    "path": "pgcli/main.py",
    "chars": 73496,
    "preview": "from zoneinfo import ZoneInfoNotFoundError\nfrom configobj import ConfigObj, ParseError\nfrom pgspecial.namedqueries impor"
  },
  {
    "path": "pgcli/packages/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pgcli/packages/formatter/__init__.py",
    "chars": 15,
    "preview": "# coding=utf-8\n"
  },
  {
    "path": "pgcli/packages/formatter/sqlformatter.py",
    "chars": 2019,
    "preview": "# coding=utf-8\n\nfrom pgcli.packages.parseutils.tables import extract_tables\n\n\nsupported_formats = (\n    \"sql-insert\",\n  "
  },
  {
    "path": "pgcli/packages/parseutils/__init__.py",
    "chars": 1698,
    "preview": "import sqlparse\n\n\nBASE_KEYWORDS = [\n    \"drop\",\n    \"shutdown\",\n    \"delete\",\n    \"truncate\",\n    \"alter\",\n    \"uncondit"
  },
  {
    "path": "pgcli/packages/parseutils/ctes.py",
    "chars": 4771,
    "preview": "from sqlparse import parse\nfrom sqlparse.tokens import Keyword, CTE, DML\nfrom sqlparse.sql import Identifier, Identifier"
  },
  {
    "path": "pgcli/packages/parseutils/meta.py",
    "chars": 5383,
    "preview": "from collections import namedtuple\n\n_ColumnMetadata = namedtuple(\"ColumnMetadata\", [\"name\", \"datatype\", \"foreignkeys\", \""
  },
  {
    "path": "pgcli/packages/parseutils/tables.py",
    "chars": 6391,
    "preview": "import sqlparse\nfrom collections import namedtuple\nfrom sqlparse.sql import IdentifierList, Identifier, Function\nfrom sq"
  },
  {
    "path": "pgcli/packages/parseutils/utils.py",
    "chars": 4427,
    "preview": "import re\nimport sqlparse\nfrom sqlparse.sql import Identifier\nfrom sqlparse.tokens import Token, Error\n\ncleanup_regex = "
  },
  {
    "path": "pgcli/packages/pgliterals/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pgcli/packages/pgliterals/main.py",
    "chars": 386,
    "preview": "import os\nimport json\n\nroot = os.path.dirname(__file__)\nliteral_file = os.path.join(root, \"pgliterals.json\")\n\nwith open("
  },
  {
    "path": "pgcli/packages/pgliterals/pgliterals.json",
    "chars": 12998,
    "preview": "{\n    \"keywords\": {\n        \"ACCESS\": [],\n        \"ADD\": [],\n        \"ALL\": [],\n        \"ALTER\": [\n            \"AGGREGAT"
  },
  {
    "path": "pgcli/packages/prioritization.py",
    "chars": 1532,
    "preview": "import re\nimport sqlparse\nfrom sqlparse.tokens import Name\nfrom collections import defaultdict\nfrom .pgliterals.main imp"
  },
  {
    "path": "pgcli/packages/prompt_utils.py",
    "chars": 1125,
    "preview": "import sys\nimport click\nfrom .parseutils import is_destructive\n\n\ndef confirm_destructive_query(queries, keywords, alias)"
  },
  {
    "path": "pgcli/packages/sqlcompletion.py",
    "chars": 22768,
    "preview": "import re\nimport sqlparse\nfrom collections import namedtuple\nfrom sqlparse.sql import Comparison, Identifier, Where\nfrom"
  },
  {
    "path": "pgcli/pgbuffer.py",
    "chars": 1761,
    "preview": "import logging\n\nfrom prompt_toolkit.enums import DEFAULT_BUFFER\nfrom prompt_toolkit.filters import Condition\nfrom prompt"
  },
  {
    "path": "pgcli/pgclirc",
    "chars": 10304,
    "preview": "# vi: ft=dosini\n[main]\n\n# Enables context sensitive auto-completion. If this is disabled, all\n# possible completions wil"
  },
  {
    "path": "pgcli/pgcompleter.py",
    "chars": 41392,
    "preview": "import json\nimport logging\nimport re\nfrom itertools import count, chain\nimport operator\nfrom collections import namedtup"
  },
  {
    "path": "pgcli/pgexecute.py",
    "chars": 34627,
    "preview": "import ipaddress\nimport logging\nimport traceback\nfrom collections import namedtuple\nimport re\nimport pgspecial as specia"
  },
  {
    "path": "pgcli/pgstyle.py",
    "chars": 4872,
    "preview": "import logging\n\nimport pygments.styles\nfrom pygments.token import string_to_tokentype, Token\nfrom pygments.style import "
  },
  {
    "path": "pgcli/pgtoolbar.py",
    "chars": 2281,
    "preview": "from prompt_toolkit.key_binding.vi_state import InputMode\nfrom prompt_toolkit.application import get_app\n\nvi_modes = {\n "
  },
  {
    "path": "pgcli/pyev.py",
    "chars": 15225,
    "preview": "import textwrap\nimport re\nfrom click import style as color\n\nDESCRIPTIONS = {\n    \"Append\": \"Used in a UNION to merge mul"
  },
  {
    "path": "pgcli-completion.bash",
    "chars": 2092,
    "preview": "_pg_databases()\n{\n    # -w was introduced in 8.4, https://launchpad.net/bugs/164772\n    # \"Access privileges\" in output "
  },
  {
    "path": "post-install",
    "chars": 103,
    "preview": "#!/bin/bash\n\necho \"Setting up symlink to pgcli\"\nln -sf /usr/share/pgcli/bin/pgcli /usr/local/bin/pgcli\n"
  },
  {
    "path": "post-remove",
    "chars": 70,
    "preview": "#!/bin/bash\n\necho \"Removing symlink to pgcli\"\nrm /usr/local/bin/pgcli\n"
  },
  {
    "path": "pylintrc",
    "chars": 57,
    "preview": "[MESSAGES CONTROL]\ndisable=missing-docstring,invalid-name"
  },
  {
    "path": "pyproject.toml",
    "chars": 3869,
    "preview": "[project]\nname = \"pgcli\"\nauthors = [{ name = \"Pgcli Core Team\", email = \"pgcli-dev@googlegroups.com\" }]\nlicense = { text"
  },
  {
    "path": "release.py",
    "chars": 3063,
    "preview": "#!/usr/bin/env python\n\"\"\"A script to publish a release of pgcli to PyPI.\"\"\"\n\nimport io\nimport re\nimport subprocess\nimpor"
  },
  {
    "path": "sanity_checks.txt",
    "chars": 1158,
    "preview": "# vi: ft=vimwiki\n\n* Launch pgcli with different inputs. \n    * pgcli test_db\n    * pgcli postgres://localhost/test_db\n  "
  },
  {
    "path": "tests/conftest.py",
    "chars": 1174,
    "preview": "import os\nimport pytest\nfrom utils import (\n    POSTGRES_HOST,\n    POSTGRES_PORT,\n    POSTGRES_USER,\n    POSTGRES_PASSWO"
  },
  {
    "path": "tests/features/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/features/auto_vertical.feature",
    "chars": 407,
    "preview": "Feature: auto_vertical mode:\n  on, off\n\n  Scenario: auto_vertical on with small query\n    When we run dbcli with --auto-"
  },
  {
    "path": "tests/features/basic_commands.feature",
    "chars": 2390,
    "preview": "Feature: run the cli,\n  call the help command,\n  exit the cli\n\n  Scenario: run \"\\?\" command\n     When we send \"\\?\" comma"
  },
  {
    "path": "tests/features/crud_database.feature",
    "chars": 566,
    "preview": "Feature: manipulate databases:\n  create, drop, connect, disconnect\n\n  Scenario: create and drop temporary database\n     "
  },
  {
    "path": "tests/features/crud_table.feature",
    "chars": 1608,
    "preview": "Feature: manipulate tables:\n  create, insert, update, select, delete from, drop\n\n  Scenario: create, insert, select from"
  },
  {
    "path": "tests/features/db_utils.py",
    "chars": 2048,
    "preview": "from psycopg import connect\n\n\ndef create_db(hostname=\"localhost\", username=None, password=None, dbname=None, port=None):"
  },
  {
    "path": "tests/features/environment.py",
    "chars": 7127,
    "preview": "import copy\nimport os\nimport shutil\nimport signal\nimport sys\nimport tempfile\n\nimport db_utils as dbutils\nimport fixture_"
  },
  {
    "path": "tests/features/expanded.feature",
    "chars": 852,
    "preview": "Feature: expanded mode:\n  on, off, auto\n\n  Scenario: expanded on\n    When we prepare the test data\n      and we set expa"
  },
  {
    "path": "tests/features/fixture_data/help.txt",
    "chars": 1950,
    "preview": "+--------------------------+------------------------------------------------+\n| Command                  | Description  "
  },
  {
    "path": "tests/features/fixture_data/help_commands.txt",
    "chars": 1254,
    "preview": "Command\nDescription\n\\#\nRefresh auto-completions.\n\\?\nShow Commands.\n\\T [format]\nChange the table format used to output re"
  },
  {
    "path": "tests/features/fixture_data/mock_pg_service.conf",
    "chars": 61,
    "preview": "[mock_postgres]\ndbname=postgres\nhost=localhost\nuser=postgres\n"
  },
  {
    "path": "tests/features/fixture_utils.py",
    "chars": 798,
    "preview": "import os\nimport codecs\n\n\ndef read_fixture_lines(filename):\n    \"\"\"\n    Read lines of text from file.\n    :param filenam"
  },
  {
    "path": "tests/features/iocommands.feature",
    "chars": 513,
    "preview": "Feature: I/O commands\n\n  Scenario: edit sql in file with external editor\n     When we start external editor providing a "
  },
  {
    "path": "tests/features/named_queries.feature",
    "chars": 334,
    "preview": "Feature: named queries:\n  save, use and delete named queries\n\n  Scenario: save, use and delete named queries\n     When w"
  },
  {
    "path": "tests/features/pgbouncer.feature",
    "chars": 280,
    "preview": "@pgbouncer\nFeature: run pgbouncer,\n  call the help command,\n  exit the cli\n\n  Scenario: run \"show help\" command\n     Whe"
  },
  {
    "path": "tests/features/specials.feature",
    "chars": 167,
    "preview": "Feature: Special commands\n\n  Scenario: run refresh command\n     When we refresh completions\n      and we wait for prompt"
  },
  {
    "path": "tests/features/steps/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/features/steps/auto_vertical.py",
    "chars": 2276,
    "preview": "from textwrap import dedent\nfrom behave import then, when\nimport wrappers\n\n\n@when(\"we run dbcli with {arg}\")\ndef step_ru"
  },
  {
    "path": "tests/features/steps/basic_commands.py",
    "chars": 6669,
    "preview": "\"\"\"\nSteps for behavioral style tests are defined in this module.\nEach step is defined by the string decorating it.\nThis "
  },
  {
    "path": "tests/features/steps/crud_database.py",
    "chars": 2280,
    "preview": "\"\"\"\nSteps for behavioral style tests are defined in this module.\nEach step is defined by the string decorating it.\nThis "
  },
  {
    "path": "tests/features/steps/crud_table.py",
    "chars": 3944,
    "preview": "\"\"\"\nSteps for behavioral style tests are defined in this module.\nEach step is defined by the string decorating it.\nThis "
  },
  {
    "path": "tests/features/steps/expanded.py",
    "chars": 2035,
    "preview": "\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string decorating it. This "
  },
  {
    "path": "tests/features/steps/iocommands.py",
    "chars": 2611,
    "preview": "import os\nimport os.path\n\nfrom behave import when, then\nimport wrappers\n\n\n@when(\"we start external editor providing a fi"
  },
  {
    "path": "tests/features/steps/named_queries.py",
    "chars": 1293,
    "preview": "\"\"\"\nSteps for behavioral style tests are defined in this module.\nEach step is defined by the string decorating it.\nThis "
  },
  {
    "path": "tests/features/steps/pgbouncer.py",
    "chars": 558,
    "preview": "\"\"\"\nSteps for behavioral style tests are defined in this module.\nEach step is defined by the string decorating it.\nThis "
  },
  {
    "path": "tests/features/steps/specials.py",
    "chars": 724,
    "preview": "\"\"\"\nSteps for behavioral style tests are defined in this module.\nEach step is defined by the string decorating it.\nThis "
  },
  {
    "path": "tests/features/steps/wrappers.py",
    "chars": 1938,
    "preview": "import re\nimport pexpect\nimport textwrap\n\nfrom io import StringIO\n\n\ndef expect_exact(context, expected, timeout):\n    ti"
  },
  {
    "path": "tests/features/wrappager.py",
    "chars": 272,
    "preview": "#!/usr/bin/env python\nimport sys\n\n\ndef wrappager(boundary):\n    print(boundary)\n    while 1:\n        buf = sys.stdin.rea"
  },
  {
    "path": "tests/formatter/__init__.py",
    "chars": 15,
    "preview": "# coding=utf-8\n"
  },
  {
    "path": "tests/formatter/test_sqlformatter.py",
    "chars": 3549,
    "preview": "# coding=utf-8\n\nfrom pgcli.packages.formatter.sqlformatter import escape_for_sql_statement\n\nfrom cli_helpers.tabular_out"
  },
  {
    "path": "tests/metadata.py",
    "chars": 8308,
    "preview": "from functools import partial\nfrom itertools import product\nfrom pgcli.packages.parseutils.meta import FunctionMetadata,"
  },
  {
    "path": "tests/parseutils/test_ctes.py",
    "chars": 3580,
    "preview": "import pytest\nfrom sqlparse import parse\nfrom pgcli.packages.parseutils.ctes import (\n    token_start_pos,\n    extract_c"
  },
  {
    "path": "tests/parseutils/test_function_metadata.py",
    "chars": 554,
    "preview": "from pgcli.packages.parseutils.meta import FunctionMetadata\n\n\ndef test_function_metadata_eq():\n    f1 = FunctionMetadata"
  },
  {
    "path": "tests/parseutils/test_parseutils.py",
    "chars": 9922,
    "preview": "import pytest\nfrom pgcli.packages.parseutils import (\n    is_destructive,\n    parse_destructive_warning,\n    BASE_KEYWOR"
  },
  {
    "path": "tests/pytest.ini",
    "chars": 43,
    "preview": "[pytest]\naddopts=--capture=sys --showlocals"
  },
  {
    "path": "tests/test_application_name.py",
    "chars": 488,
    "preview": "from unittest.mock import patch\n\nfrom click.testing import CliRunner\n\nfrom pgcli.main import cli\nfrom pgcli.pgexecute im"
  },
  {
    "path": "tests/test_auth.py",
    "chars": 1437,
    "preview": "import pytest\nfrom unittest import mock\nfrom pgcli import auth\n\n\n@pytest.mark.parametrize(\"enabled,call_count\", [(True, "
  },
  {
    "path": "tests/test_completion_refresher.py",
    "chars": 2633,
    "preview": "import time\nimport pytest\nfrom unittest.mock import Mock, patch\n\n\n@pytest.fixture\ndef refresher():\n    from pgcli.comple"
  },
  {
    "path": "tests/test_config.py",
    "chars": 988,
    "preview": "import io\nimport os\nimport stat\n\nimport pytest\n\nfrom pgcli.config import ensure_dir_exists, skip_initial_comment\n\n\ndef t"
  },
  {
    "path": "tests/test_fuzzy_completion.py",
    "chars": 2823,
    "preview": "import pytest\n\n\n@pytest.fixture\ndef completer():\n    import pgcli.pgcompleter as pgcompleter\n\n    return pgcompleter.PGC"
  },
  {
    "path": "tests/test_init_commands_simple.py",
    "chars": 3281,
    "preview": "import pytest\nfrom click.testing import CliRunner\n\nfrom pgcli.main import cli, PGCli\n\n\n@pytest.fixture\ndef dummy_exec(mo"
  },
  {
    "path": "tests/test_main.py",
    "chars": 22105,
    "preview": "import os\nimport platform\nimport re\nimport tempfile\nimport datetime\nfrom unittest import mock\n\nimport pytest\n\ntry:\n    i"
  },
  {
    "path": "tests/test_naive_completion.py",
    "chars": 4003,
    "preview": "import pytest\nfrom prompt_toolkit.completion import Completion\nfrom prompt_toolkit.document import Document\nfrom utils i"
  },
  {
    "path": "tests/test_pgcompleter.py",
    "chars": 2833,
    "preview": "import json\nimport pytest\nfrom pgcli import pgcompleter\nimport tempfile\n\n\ndef test_load_alias_map_file_missing_file():\n "
  },
  {
    "path": "tests/test_pgexecute.py",
    "chars": 24729,
    "preview": "import re\nfrom textwrap import dedent\n\nimport psycopg\nimport pytest\nfrom unittest.mock import patch, MagicMock\nfrom pgsp"
  },
  {
    "path": "tests/test_pgspecial.py",
    "chars": 2367,
    "preview": "import pytest\nfrom pgcli.packages.sqlcompletion import (\n    suggest_type,\n    Special,\n    Database,\n    Schema,\n    Ta"
  },
  {
    "path": "tests/test_prioritization.py",
    "chars": 657,
    "preview": "from pgcli.packages.prioritization import PrevalenceCounter\n\n\ndef test_prevalence_counter():\n    counter = PrevalenceCou"
  },
  {
    "path": "tests/test_prompt_utils.py",
    "chars": 523,
    "preview": "import click\n\nfrom pgcli.packages.prompt_utils import confirm_destructive_query\n\n\ndef test_confirm_destructive_query_not"
  },
  {
    "path": "tests/test_rowlimit.py",
    "chars": 1964,
    "preview": "import pytest\nfrom unittest.mock import Mock\n\nfrom pgcli.main import PGCli\n\n\n# We need this fixtures because we need PGC"
  },
  {
    "path": "tests/test_smart_completion_multiple_schemata.py",
    "chars": 24344,
    "preview": "import itertools\nfrom metadata import (\n    MetaData,\n    alias,\n    name_join,\n    fk_join,\n    join,\n    schema,\n    t"
  },
  {
    "path": "tests/test_smart_completion_public_schema_only.py",
    "chars": 35446,
    "preview": "from metadata import (\n    MetaData,\n    alias,\n    name_join,\n    fk_join,\n    join,\n    keyword,\n    schema,\n    table"
  },
  {
    "path": "tests/test_sqlcompletion.py",
    "chars": 28979,
    "preview": "from pgcli.packages.sqlcompletion import (\n    suggest_type,\n    Special,\n    Database,\n    Schema,\n    Table,\n    Colum"
  },
  {
    "path": "tests/test_ssh_tunnel.py",
    "chars": 6075,
    "preview": "import os\nfrom unittest.mock import patch, MagicMock, ANY\n\nimport pytest\nfrom configobj import ConfigObj\nfrom click.test"
  },
  {
    "path": "tests/utils.py",
    "chars": 2370,
    "preview": "import pytest\nimport psycopg\nfrom pgcli.main import format_output, OutputSettings\nfrom os import getenv\n\nPOSTGRES_USER ="
  },
  {
    "path": "tox.ini",
    "chars": 575,
    "preview": "[tox]\nenvlist = py\n\n[testenv]\nskip_install = true\ndeps = uv\ncommands = uv pip install -e .[dev]\n        coverage run -m "
  }
]

About this extraction

This page contains the full source code of the dbcli/pgcli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 113 files (588.2 KB), approximately 147.6k tokens, and a symbol index with 838 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!