Showing preview only (721K chars total). Download the full file or copy to clipboard to get everything.
Repository: dbcli/mycli
Branch: main
Commit: 93ec867cf488
Files: 115
Total size: 685.8 KB
Directory structure:
gitextract_8u893jm8/
├── .coveragerc
├── .git-blame-ignore-revs
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── ci.yml
│ ├── codex-review.yml
│ ├── lint.yml
│ ├── publish.yml
│ └── typecheck.yml
├── .gitignore
├── AUTHORS.rst
├── CONTRIBUTING.md
├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── SPONSORS.rst
├── changelog.md
├── doc/
│ ├── key_bindings.rst
│ └── llm.md
├── mycli/
│ ├── AUTHORS
│ ├── SPONSORS
│ ├── TIPS
│ ├── __init__.py
│ ├── clibuffer.py
│ ├── clistyle.py
│ ├── clitoolbar.py
│ ├── compat.py
│ ├── completion_refresher.py
│ ├── config.py
│ ├── constants.py
│ ├── key_bindings.py
│ ├── lexer.py
│ ├── magic.py
│ ├── main.py
│ ├── myclirc
│ ├── packages/
│ │ ├── __init__.py
│ │ ├── checkup.py
│ │ ├── completion_engine.py
│ │ ├── filepaths.py
│ │ ├── hybrid_redirection.py
│ │ ├── paramiko_stub/
│ │ │ └── __init__.py
│ │ ├── parseutils.py
│ │ ├── prompt_utils.py
│ │ ├── shortcuts.py
│ │ ├── special/
│ │ │ ├── __init__.py
│ │ │ ├── dbcommands.py
│ │ │ ├── delimitercommand.py
│ │ │ ├── favoritequeries.py
│ │ │ ├── iocommands.py
│ │ │ ├── llm.py
│ │ │ ├── main.py
│ │ │ └── utils.py
│ │ ├── sqlresult.py
│ │ ├── string_utils.py
│ │ ├── tabular_output/
│ │ │ ├── __init__.py
│ │ │ └── sql_format.py
│ │ └── toolkit/
│ │ ├── __init__.py
│ │ ├── fzf.py
│ │ ├── history.py
│ │ └── utils.py
│ ├── sqlcompleter.py
│ └── sqlexecute.py
├── pyproject.toml
├── pytest.ini
├── test/
│ ├── __init__.py
│ ├── conftest.py
│ ├── features/
│ │ ├── __init__.py
│ │ ├── auto_vertical.feature
│ │ ├── basic_commands.feature
│ │ ├── connection.feature
│ │ ├── crud_database.feature
│ │ ├── crud_table.feature
│ │ ├── db_utils.py
│ │ ├── environment.py
│ │ ├── fixture_data/
│ │ │ ├── help.txt
│ │ │ └── help_commands.txt
│ │ ├── fixture_utils.py
│ │ ├── iocommands.feature
│ │ ├── named_queries.feature
│ │ ├── specials.feature
│ │ ├── steps/
│ │ │ ├── __init__.py
│ │ │ ├── auto_vertical.py
│ │ │ ├── basic_commands.py
│ │ │ ├── connection.py
│ │ │ ├── crud_database.py
│ │ │ ├── crud_table.py
│ │ │ ├── iocommands.py
│ │ │ ├── named_queries.py
│ │ │ ├── specials.py
│ │ │ ├── utils.py
│ │ │ └── wrappers.py
│ │ └── wrappager.py
│ ├── myclirc
│ ├── mylogin.cnf
│ ├── test.txt
│ ├── test_clistyle.py
│ ├── test_clitoolbar.py
│ ├── test_completion_engine.py
│ ├── test_completion_refresher.py
│ ├── test_config.py
│ ├── test_dbspecial.py
│ ├── test_llm_special.py
│ ├── test_main.py
│ ├── test_naive_completion.py
│ ├── test_parseutils.py
│ ├── test_plan.wiki
│ ├── test_prompt_utils.py
│ ├── test_smart_completion_public_schema_only.py
│ ├── test_special_iocommands.py
│ ├── test_sqlexecute.py
│ ├── test_tabular_output.py
│ └── utils.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .coveragerc
================================================
[run]
source = mycli
================================================
FILE: .git-blame-ignore-revs
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
<!-- You can delete any parts of this template not applicable to your issue. -->
### Suggested troubleshooting steps for bug reports
* [ ] Upgraded to the latest mycli if possible.
* [ ] Ran `mycli --checkup`, if supported.
### Expected Behavior
### Actual Behavior
### Steps to Reproduce
### System
* mycli version:
* OS/version:
### Discussion
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
<!--- Describe your changes in detail. -->
## Checklist
<!--- We appreciate your help and want to give you credit. Place an `x` in the boxes below as you complete them. -->
- [ ] I added this contribution to the `changelog.md` file.
- [ ] I added my name to the `AUTHORS` file (or it's already there).
- [ ] To lint and format the code, I ran
```bash
uv run ruff check && uv run ruff format && uv run mypy --install-types .
```
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
pull_request:
paths-ignore:
- '**.md'
- '**.rst'
- 'LICENSE.txt'
- 'doc/**/*.txt'
- '**/AUTHORS'
- '**/SPONSORS'
- '**/TIPS'
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: "latest"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Start MySQL
run: |
sudo /etc/init.d/mysql start
- name: Install dependencies
run: uv sync --all-extras -p ${{ matrix.python-version }}
- name: Wait for MySQL connection
run: |
while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do
sleep 5
done
- name: Pytest / behave
env:
PYTEST_PASSWORD: root
PYTEST_HOST: 127.0.0.1
TERM: xterm
run: |
uv run tox -e py${{ matrix.python-version }}
test-no-extras:
name: Tests Without Extras
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: "latest"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'
- name: Start MySQL
run: |
sudo /etc/init.d/mysql start
- name: Install dependencies
run: uv sync --extra dev -p python3.13
- name: Wait for MySQL connection
run: |
while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do
sleep 5
done
- name: Pytest / behave
env:
PYTEST_PASSWORD: root
PYTEST_HOST: 127.0.0.1
TERM: xterm
run: |
uv run tox -e py3.13
================================================
FILE: .github/workflows/codex-review.yml
================================================
name: Codex Review
on:
pull_request_target:
types: [opened, labeled, reopened, ready_for_review]
paths-ignore:
- '**.md'
- '**.rst'
- 'LICENSE.txt'
- 'doc/**/*.txt'
- '**/AUTHORS'
- '**/SPONSORS'
- '**/TIPS'
jobs:
codex-review:
if: github.event.pull_request.draft == false || (github.event.action == 'labeled' && contains(github.event.pull_request.labels.*.name, 'codex'))
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
final_message: ${{ steps.run_codex.outputs.final-message }}
steps:
- name: Check out PR merge commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Fetch base and head refs
run: |
git fetch --no-tags origin \
${{ github.event.pull_request.base.ref }} \
+refs/pull/${{ github.event.pull_request.number }}/head
- name: Run Codex review
id: run_codex
uses: openai/codex-action@v1
env:
# Use env variables to handle untrusted metadata safely
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt: |
You are reviewing PR #${{ github.event.pull_request.number }} for ${{ github.repository }}.
Only review changes introduced by this PR:
git log --oneline ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}
Focus on:
- correctness bugs and regressions
- security concerns
- missing tests or edge cases
Keep feedback concise and actionable.
Pull request title and body:
----
$PR_TITLE
$PR_BODY
post-feedback:
runs-on: ubuntu-latest
needs: codex-review
if: needs.codex-review.outputs.final_message != ''
permissions:
issues: write
pull-requests: write
steps:
- name: Post Codex review as PR comment
uses: actions/github-script@v8
env:
CODEX_FINAL_MESSAGE: ${{ needs.codex-review.outputs.final_message }}
with:
github-token: ${{ github.token }}
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: process.env.CODEX_FINAL_MESSAGE,
});
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on:
pull_request:
paths-ignore:
- '**.md'
- '**.rst'
- 'LICENSE.txt'
- 'doc/**/*.txt'
- '**/AUTHORS'
- '**/SPONSORS'
- '**/TIPS'
jobs:
linters:
name: Linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run ruff check
uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1
- name: Run ruff format
uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1
with:
args: 'format --check'
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish Python Package
on:
release:
types: [created]
permissions:
contents: read
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Require release changelog form
run: |
if grep -q TBD changelog.md; then false; fi
test:
runs-on: ubuntu-latest
needs: [docs]
continue-on-error: true
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: "latest"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Start MySQL
run: |
sudo /etc/init.d/mysql start
- name: Install dependencies
run: uv sync --all-extras -p ${{ matrix.python-version }}
- name: Wait for MySQL connection
run: |
while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do
sleep 5
done
- name: Pytest / behave
env:
PYTEST_PASSWORD: root
PYTEST_HOST: 127.0.0.1
run: |
uv run tox -e py${{ matrix.python-version }}
# arguably this should be made identical to CI for PRs
- name: Run Style Checks
run: uv run tox -e style
build:
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: "latest"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-packages
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
================================================
FILE: .github/workflows/typecheck.yml
================================================
name: Typecheck
on:
pull_request:
paths-ignore:
- '**.md'
- '**.rst'
- 'LICENSE.txt'
- 'doc/**/*.txt'
- '**/AUTHORS'
- '**/SPONSORS'
- '**/TIPS'
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: 'latest'
- name: Install dependencies
run: uv sync --all-extras
- name: Run mypy
run: |
uv run --no-sync --frozen -- python -m ensurepip
uv run --no-sync --frozen -- python -m mypy --no-pretty --install-types --non-interactive .
================================================
FILE: .gitignore
================================================
.idea/
.vscode/
/build
/dist
/mycli.egg-info
/src
/test/behave.ini
.vagrant
*.pyc
*.deb
.cache/
.coverage
.coverage.*
.venv/
venv/
.myclirc
uv.lock
================================================
FILE: AUTHORS.rst
================================================
Check out our `AUTHORS`_.
.. _AUTHORS: mycli/AUTHORS
================================================
FILE: CONTRIBUTING.md
================================================
# Development Guide
This is a guide for developers who would like to contribute to this project.
If you're interested in contributing to mycli, thank you. We'd love your help!
You'll always get credit for your work.
## GitHub Workflow
1. [Fork the repository](https://github.com/dbcli/mycli) on GitHub.
2. Clone your fork locally:
```bash
$ git clone <url-for-your-fork>
```
3. Add the official repository (`upstream`) as a remote repository:
```bash
$ git remote add upstream git@github.com:dbcli/mycli.git
```
4. Set up [uv](https://docs.astral.sh/uv/getting-started/installation/)
for development:
```bash
$ cd mycli
$ uv sync --extra dev
```
We've just created a virtual environment and installed all the dependencies
and tools we need to work on mycli.
5. Create a branch for your bugfix or feature based off the `main` branch:
```bash
$ git checkout -b <name-of-bugfix-or-feature> main
```
6. While you work on your bugfix or feature, be sure to pull the latest changes from `upstream`. This ensures that your local codebase is up-to-date:
```bash
$ git pull upstream main
```
7. When your work is ready for the mycli team to review it, push your branch to your fork:
```bash
$ git push origin <name-of-bugfix-or-feature>
```
8. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)
on GitHub.
## Running mycli
To run mycli with your local changes:
```bash
$ uv run mycli
```
## Running the Tests
While you work on mycli, it's important to run the tests to make sure your code
hasn't broken any existing functionality. To run the tests, just type in:
```bash
$ uv run tox
```
### Test Database Credentials
Some tests require a database connection to work. You can tell the tests which
credentials to use by setting the applicable environment variables:
```bash
$ export PYTEST_HOST=localhost
$ export PYTEST_USER=mycli
$ export PYTEST_PASSWORD=myclirocks
$ export PYTEST_PORT=3306
$ export PYTEST_CHARSET=utf8mb4
```
The default values are `localhost`, `root`, no password, `3306`, and `utf8mb4`.
You only need to set the values that differ from the defaults.
If you would like to run the tests as a user with only the necessary privileges,
create a `mycli` user and run the following grant statements.
```sql
GRANT ALL PRIVILEGES ON `mycli_%`.* TO 'mycli'@'localhost';
GRANT SELECT ON mysql.* TO 'mycli'@'localhost';
GRANT SELECT ON performance_schema.* TO 'mycli'@'localhost';
```
### CLI Tests
Some CLI tests expect the program `ex` to be a symbolic link to `vim`.
In some systems (e.g. Arch Linux) `ex` is a symbolic link to `vi`, which will
change the output and therefore make some tests fail.
You can check this by running:
```bash
$ readlink -f $(which ex)
```
# Github PR checklist
- add the contribution to the `changelog.md`
- add your name to the `AUTHORS` file (or it's already there).
- run `uv run ruff check && uv run ruff format && uv run mypy --install-types .`
## Releasing a new version of mycli
Create a new [release](https://github.com/dbcli/mycli/releases) in Github. This will trigger a Github action which will run all the tests, build the wheel and upload it to PyPI.
================================================
FILE: LICENSE.txt
================================================
Copyright (c) 2015-2026, mycli maintainers
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 mycli 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 *.md *.rst screenshots/*
include tasks.py .coveragerc tox.ini
recursive-include test *.cnf
recursive-include test *.feature
recursive-include test *.py
recursive-include test *.txt
================================================
FILE: README.md
================================================
# mycli
[](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli)
A command line client for MySQL that can do auto-completion and syntax highlighting.
Homepage: [https://mycli.net](https://mycli.net)
Documentation: [https://mycli.net/docs](https://mycli.net/docs)


Postgres Equivalent: [https://pgcli.com](https://pgcli.com)
Quick Start
-----------
If you already know how to install Python packages, then you can install it via `pip`:
You might need sudo on Linux.
```bash
pip install -U 'mycli[all]'
```
or
```bash
brew update && brew install mycli # Only on macOS
```
or
```bash
sudo apt-get install mycli # Only on Debian or Ubuntu
```
### Usage
See
```bash
mycli --help
```
Features
--------
`mycli` is written using [prompt_toolkit](https://github.com/jonathanslenders/python-prompt-toolkit/).
* Auto-completion as you type for SQL keywords as well as tables, views and
columns in the database.
* Fuzzy history search using [fzf](https://github.com/junegunn/fzf).
* 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.
* Support for multiline queries.
* Favorite queries with optional positional parameters. Save a query using
`\fs <alias> <query>` and execute it with `\f <alias>`.
* Timing of sql statements and table rendering.
* Log every query and its results to a file (disabled by default).
* Pretty print tabular data (with colors!).
* Support for SSL connections
* Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
* Support for querying LLMs with context derived from your schema.
* Support for storing passwords in the system keyring.
Mycli creates a config file `~/.myclirc` on first run; you can use the
options in that file to configure the above features, and more.
Some features are only exposed as [key bindings](doc/key_bindings.rst).
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/mycli/blob/main/CONTRIBUTING.md
## Additional Install Instructions:
These are some alternative ways to install mycli that are not managed by our team but provided by OS package maintainers. These packages could be slightly out of date and take time to release the latest version.
### Arch, Manjaro
You can install the mycli package available in the AUR:
```
yay -S mycli
```
### Debian, Ubuntu
On Debian, Ubuntu distributions, you can easily install the mycli package using apt:
```
sudo apt-get install mycli
```
### Fedora
Fedora has a package available for mycli, install it using dnf:
```
sudo dnf install mycli
```
### Windows
#### Option 1: Native Windows
Install the `less` pager, for example by `scoop install less`.
Follow the instructions on this blogpost: https://web.archive.org/web/20221006045208/https://www.codewall.co.uk/installing-using-mycli-on-windows/
**Mycli is not tested on Windows**, but the libraries used in the app are Windows-compatible.
This means it should work without any modifications, but isn't supported.
PRs to add native Windows testing to Mycli CI would be welcome!
#### Option 2: WSL
Everything should work as expected in WSL. This is a good option for using
Mycli on Windows.
### Thanks:
This project was funded through kickstarter. My thanks to the [backers](https://mycli.net/sponsors) who supported the project.
A special thanks to [Jonathan Slenders](https://twitter.com/jonathan_s) for
creating [Python Prompt Toolkit](https://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](https://palletsprojects.com/projects/click) is used for command line option parsing
and printing error messages.
Thanks to [PyMysql](https://github.com/PyMySQL/PyMySQL) for a pure python adapter to MySQL database.
### Compatibility
Mycli is tested on macOS and Linux, and requires Python 3.10 or better.
To connect to MySQL versions earlier than 5.5, you may need to set the following in `~/.myclirc`:
```
# character set for connections without --charset being set at the CLI
default_character_set = utf8
```
or set `--charset=utf8` when invoking MyCLI.
### Configuration and Usage
For more information on using and configuring mycli, [check out our documentation](https://mycli.net/docs).
Common topics include:
- [Configuring mycli](https://mycli.net/config)
- [Using/Disabling the pager](https://mycli.net/pager)
- [Syntax colors](https://mycli.net/syntax)
================================================
FILE: SPONSORS.rst
================================================
Check out our `SPONSORS`_.
.. _SPONSORS: mycli/SPONSORS
================================================
FILE: changelog.md
================================================
1.65.2 (2026/03/19)
==============
Security
--------
* Harden `codex-review` workflow against script injection from untrusted PR metadata.
1.65.1 (2026/03/18)
==============
Bug Fixes
---------
* Require `sqlglot` 30.x.
1.65.0 (2026/03/16)
==============
Features
---------
* Add prompt format string for literal backslash.
* Add collation completions, and complete charsets in more positions.
Bug Fixes
---------
* Suppress warnings when `sqlglotrs` is installed.
* Improve completions after operators, by recognizing more operators.
1.64.0 (2026/03/13)
==============
Features
---------
* Add `-r` raw mode to `system` command.
* Set timeouts, show exit codes, and improve formatting for `system` commands.
* Add a dependencies section to `--checkup`.
Bug Fixes
---------
* Require `sqlglot` 29.x, suppressing a deprecation warning.
1.63.0 (2026/03/12)
==============
Features
---------
* Make short toolbar message show after one prompt.
Internal
---------
* Migrate more repeated values to `constants.py`.
* Support `sqlglot` 28 and 29.
1.62.0 (2026/03/07)
==============
Features
---------
* Dynamic terminal titles based on prompt format strings.
* Ability to turn off the toolbar.
* Add completions for introducers on literals.
* Load whole-line autosuggest candidates in a background thread for speed.
Bug Fixes
---------
* Improve query cancellation on control-c.
* Improve refresh of some format strings in the toolbar.
* Improve keyring storage, requiring re-entering most keyring passwords.
* Improve sentinel value for `--password` without argument.
Internal
---------
* Require a more recent version of the `wcwidth` library.
* Make `safe_invalidate_display` function safer.
1.61.0 (2026/03/07)
==============
Features
---------
* Allow shorter timeout lengths after pressing Esc, for vi-mode.
* Let tab and control-space behaviors be configurable.
* Add short hostname prompt format string.
1.60.0 (2026/03/05)
==============
Features
---------
* Prioritize common functions in the "value" position.
* Improve value-position keywords.
* Allow warning-count in status output to be styled.
Bug Fixes
---------
* Fix crash for completion edge case (#1668).
* Update to a `cli_helpers` version with a `tabulate` bugfix.
1.59.0 (2026/03/03)
==============
Features
---------
* Offer filename completions on more special commands, such as `\edit`.
* Allow styling of status and timings text.
* Set up customization of prompt/continuation colors in `~/.myclirc`.
* Allow customization of the toolbar with prompt format strings.
* Add warnings-count prompt format strings: `\w` and `\W`.
* Handle/document more attributes in the `[colors]` section of `~/.myclirc`.
* Enable customization of table border color/attributes in `~/.myclirc`.
* Complete much more precisely in the "value" position.
Bug Fixes
---------
* Make toolbar widths consistent on toggle actions.
* Don't write ANSI prompt escapes to `tee` output.
Internal
---------
* Use prompt_toolkit's `bell()`.
* Refactor `SQLResult` dataclass.
* Avoid depending on string matches into host info.
* Add more URL constants.
* Set `$VISUAL` whenever `$EDITOR` is set.
* Fix tempfile leak in test suite.
* Avoid refreshing the prompt unless needed.
1.58.0 (2026/02/28)
==============
Features
---------
* Add `\bug` command.
* Let the `F1` key open a browser to mycli.net/docs and emit help text.
* Add documentation index URL to inline help.
* Rewrite bottom toolbar, showing more statuses, but staying compact.
* Let `help <keyword>` list similar keywords when not found.
* Optionally highlight fuzzy search previews.
* Make `\edit` synonymous with the `\e` command.
* Add environment variable section to `--checkup`.
Bug Fixes
---------
* Force a prompt_toolkit refresh after fzf history search to avoid display glitches.
* Include `status` footer in paged output.
* Ensure fullscreen in fuzzy history search.
Documentation
---------
* Add `help <keyword>` to TIPS.
* Refine inline help descriptions.
* Add `$VISUAL` environment variable hint to TIPS.
Internal
---------
* Better tests for `null_string` configuration option.
* Better cleanup of resources in the test suite.
* Simplify prettify/unprettify handlers.
* Make prettify/unprettify logic more robust.
1.57.0 (2026/02/25)
==============
Features
---------
* Add extra error output on connection failure for possible SSL mismatch (#1584).
* Bind alternate terminal sequences for function keys F2 - F4.
* Add `llm help` subcommand.
* Rewrite `help` table.
* Remove "info" counter from fzf history-search UI.
Bug Fixes
---------
* Let interactive changes to the prompt format respect dynamically-computed values.
* Better handle arguments to `system cd`.
* Fix missing keepalives in `\e` prompt loop.
* Always strip trailing newlines with `\e`.
* Fix `\llm` without arguments, and remove debug output.
Documentation
---------
* Startup tips: add right-arrow key binding.
* Startup tips: add control-space and the `min_completion_trigger` setting.
* Startup tips: add history-search bindings.
* Prefer `https` protocol over `http` in documentation.
Internal
---------
* Remove outdated email address in `pyproject.toml`.
* Set well-known URL values in `pyproject.toml`.
1.56.0 (2026/02/23)
==============
Features
---------
* Let the `--dsn` argument accept literal DSNs as well as aliases.
* Accept `--character-set` as an alias for `--charset` at the CLI.
* Add SSL/TLS version to `status` output.
* Accept `socket` as a DSN query parameter.
* Accept new-style `ssl_mode` in DSN URI query parameters, to match CLI argument.
* Fully deprecate the built-in SSH functionality.
* Let `--keepalive-ticks` be set per-connection, as a CLI option or DSN parameter.
* Accept `character_set` as a DSN query parameter.
* Don't attempt SSL for local socket connections when in "auto" SSL mode.
* Add prompt format string for SSL/TLS version of the connection.
* Add prompt format strings for displaying uptime.
* Add batch mode to startup tips.
* Update startup tips with new options.
Bug Fixes
---------
* Make `--ssl-capath` argument a directory.
* Allow users to use empty passwords without prompting or any configuration (#1584).
* Check the existence of a socket more directly in `status`.
* Allow multi-line SQL statements in batch mode on the standard input.
* Fix extraneous prompt refresh on every keystroke.
1.55.0 (2026/02/20)
==============
Features
---------
* `--checkup` now checks for external executables.
Bug Fixes
---------
* Improve completion suggestions within backticks.
* Watch command now returns correct time when run as part of a multi-part query (#1565).
* Don't diagnose free-entry sections such as `[favorite_queries]` in `--checkup`.
* When accepting a filename completion, fill in leading `./` if given.
Internal
--------
* Bump `cli_helpers` to non-yanked version.
1.54.1 (2026/02/17)
==============
Bug Fixes
--------
* Don't offer autocomplete suggestions when the cursor is within a string.
* Catch `getpwuid` error on unknown socket owner.
Internal
--------
* Tune Codex reviews.
* Refactor `is_inside_quotes()` detection.
1.54.0 (2026/02/16)
==============
Features
--------
* Add many CLI flags to startup tips.
* Accept all special commands without trailing semicolons in multi-line mode.
* Add prompt format strings for socket connections.
* Optionally defer auto-completions until a minimum number of characters is typed.
* Make the completion interface more responsive using a background thread.
* Option to suppress control-d exit behavior.
* Better support Truecolor terminals.
* Ability to send app-layer keepalive pings to the server.
* Add `WITH`, `EXPLAIN`, and `LEFT JOIN` to favorite keyword suggestions.
* Let the Escape key cancel completion popups.
Bug Fixes
---------
* Correct parameterization for completion queries.
* Grammar nits in help display.
Internal
--------
* Prefer `yield from` over yielding in a loop.
* Update `ruff` linter and CI.
* Update `LICENSE.txt` for dates and GitHub detection.
* Update key feature list in `README.md`, syncing with web.
* Sync prompt format string commentary with web.
* Add a GitHub Actions workflow to run Codex review on pull requests.
* Remove vim-style exit sequence which had no effect.
* Pin dependencies more tightly in `pyproject.toml`.
* Exclude more documentation files from CI.
1.53.0 (2026/02/12)
==============
Features
--------
* Add all `~/.myclirc` entries/sections to startup tips.
Bug Fixes
---------
* Fix `\dt+ table_name` returning empty results.
* Further bulletproof generating completions on stored procedures.
Internal
--------
* Add GitHub Issue templates.
1.52.0 (2026/02/11)
==============
Features
--------
* Suggest tables/views that contain the given columns first when provided in a SELECT query.
Bug Fixes
--------
* Reduce duplicated `--checkup` output.
* Handle errors generating completions on stored procedures.
* Fix whitespace/inline comments breaking destructive `UPDATE … WHERE` statement detection.
Internal
--------
* Let CI ignore additional documentation files.
* Upgrade `cli_helpers` library to v2.10.0.
* Organize startup tips.
1.51.1 (2026/02/09)
==============
Features
--------
* Options to limit size of LLM prompts; cache LLM prompt data.
* Add startup usage tips.
* Move `main.ssl_mode` config option to `connection.default_ssl_mode`.
* Add "unsupported" and "deprecated" `--checkup` sections.
Bug Fixes
--------
* Correct mangled schema info sent in LLM prompts.
* Give destructive warning on multi-table `UPDATE`s.
1.50.0 (2026/02/07)
==============
Features
--------
* Deprecate reading configuration values from `my.cnf` files.
* Add `--checkup` mode to show unconfigured new features.
* Add `binary_display` configuration option.
Bug Fixes
--------
* Link to `--ssl`/`--no-ssl` GitHub issue in deprecation warning.
* Don't emit keyring-updated message unless needed.
* Include port and socket in keyring identifier.
1.49.0 (2026/02/02)
==============
Features
--------
* "Eager" completions for the `source` command, limited to `*.sql` files.
* Suggest column names from all tables in the current database after SELECT (#212).
* Put fuzzy completions more often to the bottom of the suggestion list.
* Store and retrieve passwords using the system keyring.
Bug Fixes
--------
* Refactor completions for special commands, with minor casing fixes.
* Raise `--password-file` higher in the precedence of password specification.
* Fix regression: show username in password prompt.
Internal
--------
* Remove `align_decimals` preprocessor, which had no effect.
* Fix TLS deprecation warning in test suite.
* Convert importlib read_text and open_text uses to newer files() syntax.
* Update Pull Request template.
1.48.0 (2026/01/27)
==============
Features
--------
* Right-align numeric columns, and make the behavior configurable.
* Add completions for stored procedures.
* Escape database completions.
* Offer completions on `CREATE TABLE ... LIKE`.
* Use 0x-style hex literals for binaries in SQL output formats.
Bug Fixes
--------
* Better respect case when `keyword_casing` is `auto`.
* Fix error when selecting from an empty table.
* Let favorite queries contain special commands.
* Render binary values more consistently as hex literals.
* Offer format completions on special command `\Tr`/`redirectformat`.
1.47.0 (2026/01/24)
==============
Features
--------
* Add a `--checkpoint=` argument to log successful queries in batch mode.
* Add `--throttle` option for batch mode.
Bug Fixes
--------
* Fix timediff output when the result is a negative value (#1113).
* Don't offer completions for numeric text.
1.46.0 (2026/01/22)
==============
Features
--------
* Add `--unbuffered` mode which fetches rows as needed, to save memory.
* Default to standards-compliant `utf8mb4` character set.
* Stream input from STDIN to consume less memory, adding `--noninteractive` and `--format=` CLI arguments.
* Remove suggested quoting on completions for identifiers with uppercase.
* Allow table names to be completed with leading schema names.
* Soft deprecate the built-in SSH features.
* Add true fuzzy-match completions with rapidfuzz.
Bug Fixes
--------
* Fix CamelCase fuzzy matching.
* Place special commands first in the list of completion candidates, and remove duplicates.
1.45.0 (2026/01/20)
==============
Features
--------
* Make password options also function as flags. Reworked password logic to prompt user as early as possible (#341).
* More complete and up-to-date set of MySQL reserved words for completions.
* Place exact-leading completions first.
* Allow history file location to be configured.
* Make destructive-warning keywords configurable.
* Smarter fuzzy completion matches.
Bug Fixes
--------
* Respect `--logfile` when using `--execute` or standard input at the shell CLI.
* Gracefully catch Paramiko parsing errors on `--list-ssh-config`.
* Downgrade to Paramiko 3.5.1 to avoid crashing on DSA SSH keys.
* Offer schema name completions in `GRANT ... ON` forms.
1.44.2 (2026/01/13)
==============
Bug Fixes
--------
* Update watch query output to display the correct execution time on all iterations (#763).
* Use correct database (if applicable) when reconnecting after a connection loss (#1437).
Internal
--------
* Create new data class to handle SQL/command results to make further code improvements easier.
1.44.1 (2026/01/10)
==============
Bug Fixes
--------
* Let `sqlparse` accept arbitrarily-large queries.
1.44.0 (2026/01/08)
==============
Features
--------
* Add enum value completions for WHERE/HAVING clauses. (#790)
* Add `show_favorite_query` config option to control query printing when running favorite queries. (#1118)
1.43.1 (2026/01/03)
==============
Bug Fixes
--------
* Prompt for password within SSL-auto retry flow.
1.43.0 (2026/01/02)
==============
Features
--------
* Update query processing functions to allow automatic show_warnings to work for more code paths like DDL.
* Add new ssl_mode config / --ssl-mode CLI option to control SSL connection behavior. This setting will supercede the
existing --ssl/--no-ssl CLI options, which are deprecated and will be removed in a future release.
* Rework reconnect logic to actually reconnect or create a new connection instead of simply changing the database (#746).
* Configurable string for missing values (NULLs) in outputs.
Bug Fixes
--------
* Update the prompt display logic to handle an edge case where a socket is used without
a host being parsed from any other method (#707).
Internal
--------
* Refine documentation for Windows.
* Target Python 3.10 for linting.
* Use fully-qualified pymysql exception classes.
1.42.0 (2025/12/20)
==============
Features
--------
* Add support for the automatic displaying of warnings after a SQL statement is executed.
May be set with the commands \W and \w, in the config file with show_warnings, or
with --show-warnings/--no-show-warnings on the command line.
Internal
--------
* Improve robustness for flaky tests when publishing.
* Improve type annotations for latest mypy/type stubs.
* Set mypy version more strictly.
1.41.2 (2025/11/24)
==============
Bug Fixes
--------
* Close connection to server properly to avoid "Aborted connection" warnings in server logs.
Internal
--------
* Add ruff to developement dependencies.
* Update contributing guidelines to match GitHub pull request checklist.
1.41.1 (2025/11/15)
==============
Bug Fixes
--------
* Upgrade `click` to v8.3.1, resolving a longstanding pager bug.
Internal
--------
* Include LLM dependencies in tox configuration.
1.41.0 (2025/11/01)
==============
Features
--------
* Make LLM dependencies an optional extra.
Bug Fixes
--------
* Let LLM commands respect show-timing configuration.
Internal
--------
* Add mypy to Pull Request template.
* Enable flake8-bugbear lint rules.
* Fix flaky editor-command tests in CI.
* Require release format of `changelog.md` when making a release.
* Improve type annotations on LLM driver.
1.40.0 (2025/10/14)
==============
Features
--------
* Support reconnecting to mysql server when the server restarts.
Internal
--------
* Test on Python 3.14.
* Switch from pyaes to pycryptodomex as it seems to be more actively maintained.
1.39.1 (2025/10/06)
==============
Bug Fixes
--------
* Don't require `--ssl` argument when other SSL arguments are given.
1.39.0 (2025/09/30)
==============
Features
--------
* Support only Python 3.10+.
Bug Fixes
--------
* Fixes use of incorrect ssl config after retrying connection with prompted password.
* Fix ssl_context always created.
Internal
--------
Typing fix for `pymysql.connect()`.
1.38.4 (2025/09/06)
==============
Bug Fixes
--------
* Limit Alt-R bindings to Emacs mode.
* Fix timing being printed twice.
Internal
--------
* Only read "my" configuration files once, rather than once per call to read_my_cnf_files.
1.38.3 (2025/08/21)
==============
Bug Fixes
--------
* Fix the infinite looping when `\llm` is called without args.
1.38.2 (2025/08/19)
======================
Bug Fixes
--------
* Fix failure to save Favorite Queries.
1.38.1 (2025/08/19)
======================
Bug Fixes
--------
* Partially fix Favorite Query completion crash.
Internal
--------
* Improve CI workflow naming.
1.38.0 (2025/08/16)
======================
Features
--------
* Add LLM support.
Bug Fixes
--------
* Improve missing ssh-extras message.
* Fix repeated control-r in traditional reverse isearch.
* Fix spelling of `ssl-verify-server-cert` option.
* Improve handling of `ssl-verify-server-cert` False values.
* Guard against missing contributors file on startup.
* Friendlier errors on password-file failures.
* Better handle empty-string passwords.
* Permit empty-string passwords at the interactive prompt.
Internal
--------
* Improve pull request template lint commands.
* Complete typehinting the non-test codebase.
* Modernization: conversion to f-strings.
* Modernization: remove more Python 2 compatibility logic.
1.37.1 (2025/07/28)
======================
Internal
--------
* Align LICENSE with SPDX format.
* Fix deprecated `license` specification format in `pyproject.toml`.
1.37.0 (2025/07/28)
======================
Features
--------
* Show username in password prompt.
* Add `mysql` and `mysql_unicode` table formats.
Bug Fixes
--------
* Help Windows installations find a working default pager.
Internal
--------
* Support only Python 3.9+ in `pyproject.toml`.
* Add linting suggestion to pull request template.
* Make CI names and properties more consistent.
* Enable typechecking for most of the non-test codebase.
* CI: turn off fail-fast matrix strategy.
* Remove unused Python 2 compatibility code.
* Also run CI tests without installing SSH extra dependencies.
* Update `cli_helpers` dependency, and list of table formats.
1.36.0 (2025/07/19)
======================
Features
--------
* Make control-r reverse search style configurable.
* Make fzf search key bindings more compatible with traditional isearch.
Bug Fixes
--------
* Better reset after pipe command failures.
Internal
--------
* Add limited typechecking to CI.
1.35.0 (2025/07/18)
======================
Features
--------
* Support chained pipe operators such as `select first_name from users $| grep '^J' $| head -10`.
* Support trailing file redirects after pipe operators, such as `select 10 $| tail -1 $> ten.txt`.
1.34.4 (2025/07/15)
======================
Bug Fixes
--------
* Fix old-style `\pipe_once`.
1.34.3 (2025/07/14)
======================
Bug Fixes
--------
* Use only `communicate()` to communicate with subprocess.
1.34.2 (2025/07/12)
======================
Bug Fixes
--------
* Use plain `print()` to communicate with subprocess.
1.34.1 (2025/07/12)
======================
Internal
--------
* Bump cli_helpers dependency for corrected output formats.
1.34.0 (2025/07/11)
======================
Features
--------
* Post-save command hook for redirected output.
Internal
--------
* Documentation cleanup.
* Bump cli_helpers dependency for more output formats.
1.33.0 (2025/07/07)
======================
Features
--------
* Keybindings to insert current date/datetime.
* Improve feedback when running external commands.
* Independent format for redirected output.
* Trailing shell-style redirect syntax.
Internal
--------
* Remove `requirements-dev.txt` in favor of uv/`pyproject.toml`.
1.32.0 (2025/07/04)
======================
Features
--------
* Support SSL query parameters on DSNs.
* More information and care on KeyboardInterrupt.
Internal
--------
* Work on passing `ruff check` linting.
* Relax expectation for unreliable test.
* Bump sqlglot version to v26 and add rs extras.
1.31.2 (2025/05/01)
===================
Bug Fixes
---------
* Let table-name extraction work on multi-statement inputs.
Internal
--------
* Work on passing `ruff check` linting.
* Remove backward-compatibility hacks.
* Pin more GitHub Actions and add Dependabot support.
* Enable xpassing test.
1.31.1 (2025/04/25)
===================
Internal
--------
* skip style checks on Publish action
1.31.0 (NEVER RELEASED)
===================
Features
--------
* Added explicit error handle to get_password_from_file with EAFP.
* Use the "history" scheme for fzf searches.
* Deduplicate history in fzf searches.
* Add a preview window to fzf history searches.
Internal
--------
* New Project Lead: [Roland Walker](https://github.com/rolandwalker)
* Update sqlparse to <=0.6.0
* Typing/lint fixes.
1.30.0 (2025/04/19)
===================
Features
--------
* DSN specific init-command in myclirc. Fixes (#1195)
* Add `\\g` to force the horizontal output.
1.29.2 (2024/12/11)
===================
Internal
--------
* Exclude tests from the python package.
1.29.1 (2024/12/11)
===================
Internal
--------
* Fix the GH actions to publish a new version.
1.29.0 (NEVER RELEASED)
=======================
Bug Fixes
----------
* fix SSL through SSH jump host by using a true python socket for a tunnel
* Fix mycli crash when connecting to Vitess
Internal
---------
* Modernize to use PEP-621. Use `uv` instead of `pip` in GH actions.
* Remove Python 3.8 and add Python 3.13 in test matrix.
1.28.0 (2024/11/10)
======================
Features
---------
* Added fzf history search functionality. The feature can switch between the old implementation and the new one based on the presence of the fzf binary.
Bug Fixes
----------
* Fixes `Database connection failed: error('unpack requires a buffer of 4 bytes')`
* Only show keyword completions after *
* Enable fuzzy matching for keywords
1.27.2 (2024/04/03)
===================
Bug Fixes
----------
* Don't use default prompt when one is not supplied to the --prompt option.
1.27.1 (2024/03/28)
===================
Bug Fixes
----------
* Don't install tests.
* Do not ignore the socket passed with the -S option, even when no port is passed
* Fix unexpected exception when using dsn without username & password (Thanks: [Will Wang])
* Let the `--prompt` option act normally with its predefined default value
Internal
---------
* paramiko is newer than 2.11.0 now, remove version pinning `cryptography`.
* Drop support for Python 3.7
1.27.0 (2023/08/11)
===================
Features
---------
* Detect TiDB instance, show in the prompt, and use additional keywords.
* Fix the completion order to show more commonly-used keywords at the top.
Bug Fixes
----------
* Better handle empty statements in un/prettify
* Remove vi-mode bindings for prettify/unprettify.
* Honor `\G` when executing from commandline with `-e`.
* Correctly report the version of TiDB.
* Revised `botton` spelling mistakes with `bottom` in `mycli/clitoolbar.py`
1.26.1 (2022/09/01)
===================
Bug Fixes
----------
* Require Python 3.7 in `setup.py`
1.26.0 (2022/09/01)
===================
Features
---------
* Add `--ssl` flag to enable ssl/tls.
* Add `pager` option to `~/.myclirc`, for instance `pager = 'pspg --csv'` (Thanks: [BuonOmo])
* Add prettify/unprettify keybindings to format the current statement using `sqlglot`.
Features
---------
* Add `--tls-version` option to control the tls version used.
Internal
---------
* Pin `cryptography` to suppress `paramiko` warning, helping CI complete and presumably affecting some users.
* Upgrade some dev requirements
* Change tests to always use databases prefixed with 'mycli_' for better security
Bug Fixes
----------
* Support for some MySQL compatible databases, which may not implement connection_id().
* Fix the status command to work with missing 'Flush_commands' (mariadb)
* Ignore the user of the system [myslqd] config.
1.25.0 (2022/04/02)
===================
Features
---------
* Add `beep_after_seconds` option to `~/.myclirc`, to ring the terminal bell after long queries.
1.24.4 (2022/03/30)
===================
Internal
---------
* Upgrade Ubuntu VM for runners as Github has deprecated it
Bug Fixes
----------
* Change in main.py - Replace the `click.get_terminal_size()` with `shutil.get_terminal_size()`
1.24.3 (2022/01/20)
===================
Bug Fixes
----------
* Upgrade cli_helpers to workaround Pygments regression.
1.24.2 (2022/01/11)
===================
Bug Fixes
----------
* Fix autocompletion for more than one JOIN
* Fix the status command when connected to TiDB or other servers that don't implement 'Threads\_connected'
* Pin pygments version to avoid a breaking change
1.24.1
=======
Bug Fixes
---------
* Restore dependency on cryptography for the interactive password prompt
Internal
---------
* Deprecate Python mock
1.24.0
======
Bug Fixes
----------
* Allow `FileNotFound` exception for SSH config files.
* Fix startup error on MySQL < 5.0.22
* Check error code rather than message for Access Denied error
* Fix login with ~/.my.cnf files
Features
---------
* Add `-g` shortcut to option `--login-path`.
* Alt-Enter dispatches the command in multi-line mode.
* Allow to pass a file or FIFO path with --password-file when password is not specified or is failing (as suggested in this best-practice <https://www.netmeister.org/blog/passing-passwords.html>)
Internal
---------
* Remove unused function is_open_quote()
* Use importlib, instead of file links, to locate resources
* Test various host-port combinations in command line arguments
* Switched from Cryptography to pyaes for decrypting mylogin.cnf
1.23.2
======
Bug Fixes
----------
* Ensure `--port` is always an int.
1.23.1
======
Bug Fixes
----------
* Allow `--host` without `--port` to make a TCP connection.
1.23.0
======
Bug Fixes
----------
* Fix config file include logic
Features
---------
* Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]).
* Use InputMode.REPLACE_SINGLE
* Add support for ANSI escape sequences for coloring the prompt.
* Allow customization of Pygments SQL syntax-highlighting styles.
* Add a `\clip` special command to copy queries to the system clipboard.
* Add a special command `\pipe_once` to pipe output to a subprocess.
* Add an option `--charset` to set the default charset when connect database.
Bug Fixes
----------
* Fixed compatibility with sqlparse 0.4 (Thanks: [mtorromeo]).
* Fixed iPython magic (Thanks: [mwcm]).
* Send "Connecting to socket" message to the standard error.
* Respect empty string for prompt_continuation via `prompt_continuation = ''` in `.myclirc`
* Fix \once -o to overwrite output whole, instead of line-by-line.
* Dispatch lines ending with `\e` or `\clip` on return, even in multiline mode.
* Restore working local `--socket=<UDS>` (Thanks: [xeron]).
* Allow backtick quoting around the database argument to the `use` command.
* Avoid opening `/dev/tty` when `--no-warn` is given.
* Fixed some typo errors in `README.md`.
1.22.2
======
Bug Fixes
----------
* Make the `pwd` module optional.
1.22.1
======
Bug Fixes
----------
* Fix the breaking change introduced in PyMySQL 0.10.0. (Thanks: [Amjith]).
Features
---------
* Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file.
* Add an option `--list-ssh-config` to list ssh configurations.
* Add an option `--ssh-config-path` to choose ssh configuration path.
Bug Fixes
----------
* Fix specifying empty password with `--password=''` when config file has a password set (Thanks: [Zach DeCook]).
1.21.1
======
Bug Fixes
----------
* Fix broken auto-completion for favorite queries (Thanks: [Amjith]).
* Fix undefined variable exception when running with --no-warn (Thanks: [Georgy Frolov])
* Support setting color for null value (Thanks: [laixintao])
1.21.0
======
Features
---------
* Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]).
* Mark `update` without `where`-clause as destructive query (Thanks: [Klaus Wünschel]).
* Added DELIMITER command (Thanks: [Georgy Frolov])
* Added clearer error message when failing to connect to the default socket.
* Extend main.is_dropping_database check with create after delete statement.
* Search `${XDG_CONFIG_HOME}/mycli/myclirc` after `${HOME}/.myclirc` and before `/etc/myclirc` (Thanks: [Takeshi D. Itoh])
Bug Fixes
----------
* Allow \o command more than once per session (Thanks: [Georgy Frolov])
* Fixed crash when the query dropping the current database starts with a comment (Thanks: [Georgy Frolov])
Internal
---------
* deprecate python versions 2.7, 3.4, 3.5; support python 3.8
1.20.1
======
Bug Fixes
----------
* Fix an error when using login paths with an explicit database name (Thanks: [Thomas Roten]).
1.20.0
======
Features
----------
* Auto find alias dsn when `://` not in `database` (Thanks: [QiaoHou Peng]).
* Mention URL encoding as escaping technique for special characters in connection DSN (Thanks: [Aljosha Papsch]).
* 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: [Amjith Ramanujam]).
* Use a generator to stream the output to the pager (Thanks: [Dick Marinus]).
Bug Fixes
----------
* Fix the missing completion for special commands (Thanks: [Amjith Ramanujam]).
* Fix favorites queries being loaded/stored only from/in default config file and not --myclirc (Thanks: [Matheus Rosa])
* Fix automatic vertical output with native syntax style (Thanks: [Thomas Roten]).
* Update `cli_helpers` version, this will remove quotes from batch output like the official client (Thanks: [Dick Marinus])
* Update `setup.py` to no longer require `sqlparse` to be less than 0.3.0 as that just came out and there are no notable changes. ([VVelox])
* workaround for ConfigObj parsing strings containing "," as lists (Thanks: [Mike Palandra])
Internal
---------
* fix unhashable FormattedText from prompt toolkit in unit tests (Thanks: [Dick Marinus]).
1.19.0
======
Internal
---------
* Add Python 3.7 trove classifier (Thanks: [Thomas Roten]).
* Fix pytest in Fedora mock (Thanks: [Dick Marinus]).
* Require `prompt_toolkit>=2.0.6` (Thanks: [Dick Marinus]).
Features
---------
* Add Token.Prompt/Continuation (Thanks: [Dick Marinus]).
* Don't reconnect when switching databases using use (Thanks: [Angelo Lupo]).
* Handle MemoryErrors while trying to pipe in large files and exit gracefully with an error (Thanks: [Amjith Ramanujam])
Bug Fixes
----------
* Enable Ctrl-Z to suspend the app (Thanks: [Amjith Ramanujam]).
1.18.2
======
Bug Fixes
----------
* Fixes database reconnecting feature (Thanks: [Yang Zou]).
Internal
---------
* Update Twine version to 1.12.1 (Thanks: [Thomas Roten]).
* Fix warnings for running tests on Python 3.7 (Thanks: [Dick Marinus]).
* Clean up and add behave logging (Thanks: [Dick Marinus]).
1.18.1
======
Features
---------
* Add Keywords: TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT (Thanks: [QiaoHou Peng]).
Internal
---------
* Update prompt toolkit (Thanks: [Jonathan Slenders], [Irina Truong], [Dick Marinus]).
1.18.0
======
Features
---------
* Display server version in welcome message (Thanks: [Irina Truong]).
* Set `program_name` connection attribute (Thanks: [Dick Marinus]).
* Use `return` to terminate a generator for better Python 3.7 support (Thanks: [Zhongyang Guan]).
* Add `SAVEPOINT` to SQLCompleter (Thanks: [Huachao Mao]).
* Connect using a SSH transport (Thanks: [Dick Marinus]).
* Add `FROM_UNIXTIME` and `UNIX_TIMESTAMP` to SQLCompleter (Thanks: [QiaoHou Peng])
* Search `${PWD}/.myclirc`, then `${HOME}/.myclirc`, lastly `/etc/myclirc` (Thanks: [QiaoHao Peng])
Bug Fixes
----------
* When DSN is used, allow overrides from mycli arguments (Thanks: [Dick Marinus]).
* A DSN without password should be allowed (Thanks: [Dick Marinus])
Bug Fixes
----------
* Convert `sql_format` to unicode strings for py27 compatibility (Thanks: [Dick Marinus]).
* Fixes mycli compatibility with pbr (Thanks: [Thomas Roten]).
* Don't align decimals for `sql_format` (Thanks: [Dick Marinus]).
Internal
---------
* Use fileinput (Thanks: [Dick Marinus]).
* Enable tests for Python 3.7 (Thanks: [Thomas Roten]).
* Remove `*.swp` from gitignore (Thanks: [Dick Marinus]).
1.17.0
=======
Features
----------
* Add `CONCAT` to SQLCompleter and remove unused code (Thanks: [caitinggui])
* Do not quit when aborting a confirmation prompt (Thanks: [Thomas Roten]).
* Add option list-dsn (Thanks: [Frederic Aoustin]).
* Add verbose option for list-dsn, add tests and clean up code (Thanks: [Dick Marinus]).
Bug Fixes
----------
* Add enable_pager to the config file (Thanks: [Frederic Aoustin]).
* Mark `test_sql_output` as a dbtest (Thanks: [Dick Marinus]).
* Don't crash if the log/history file directories don't exist (Thanks: [Thomas Roten]).
* Unquote dsn username and password (Thanks: [Dick Marinus]).
* Output `Password:` prompt to stderr (Thanks: [ushuz]).
* Mark `alter` as a destructive query (Thanks: [Dick Marinus]).
* Quote CSV fields (Thanks: [Thomas Roten]).
* Fix `thanks_picker` (Thanks: [Dick Marinus]).
Internal
---------
* Refactor Destructive Warning behave tests (Thanks: [Dick Marinus]).
1.16.0
=======
Features
---------
* Add DSN aliases to the config file (Thanks: [Frederic Aoustin]).
Bug Fixes
----------
* Do not try to connect to a unix socket on Windows (Thanks: [Thomas Roten]).
1.15.0
=======
Features
---------
* Add sql-update/insert output format. (Thanks: [Dick Marinus]).
* Also complete aliases in WHERE. (Thanks: [Dick Marinus]).
1.14.0
=======
Features
---------
* Add `watch [seconds] query` command to repeat a query every [seconds] seconds (by default 5). (Thanks: [David Caro](https://github.com/Terseus))
* Default to unix socket connection if host and port are unspecified. This simplifies authentication on some systems and matches mysql behaviour.
* Add support for positional parameters to favorite queries. (Thanks: [Scrappy Soft](https://github.com/scrappysoft))
Bug Fixes
----------
* Fix source command for script in current working directory. (Thanks: [Dick Marinus]).
* Fix issue where the `tee` command did not work on Python 2.7 (Thanks: [Thomas Roten]).
Internal Changes
-----------------
* Drop support for Python 3.3 (Thanks: [Thomas Roten]).
* Make tests more compatible between different build environments. (Thanks: [David Caro])
* Merge `_on_completions_refreshed` and `_swap_completer_objects` functions (Thanks: [Dick Marinus]).
1.13.1
=======
Bug Fixes
----------
* Fix keyword completion suggestion for `SHOW` (Thanks: [Thomas Roten]).
* Prevent mycli from crashing when failing to read login path file (Thanks: [Thomas Roten]).
Internal Changes
-----------------
* Make tests ignore user config files (Thanks: [Thomas Roten]).
1.13.0
=======
Features
---------
* Add file name completion for source command (issue #500). (Thanks: [Irina Truong]).
Bug Fixes
----------
* Fix UnicodeEncodeError when editing sql command in external editor (Thanks: Klaus Wünschel).
* Fix MySQL4 version comment retrieval (Thanks: [François Pietka])
* Fix error that occurred when outputting JSON and NULL data (Thanks: [Thomas Roten]).
1.12.1
=======
Bug Fixes
----------
* Prevent missing MySQL help database from causing errors in completions (Thanks: [Thomas Roten]).
* Fix mycli from crashing with small terminal windows under Python 2 (Thanks: [Thomas Roten]).
* Prevent an error from displaying when you drop the current database (Thanks: [Thomas Roten]).
Internal Changes
-----------------
* Use less memory when formatting results for display (Thanks: [Dick Marinus]).
* Preliminary work for a future change in outputting results that uses less memory (Thanks: [Dick Marinus]).
1.12.0
=======
Features
---------
* Add fish-style auto-suggestion from history. (Thanks: [Amjith Ramanujam])
1.11.0
=======
Features
---------
* Handle reserved space for completion menu better in small windows. (Thanks: [Thomas Roten]).
* Display current vi mode in toolbar. (Thanks: [Thomas Roten]).
* Opening an external editor will edit the last-run query. (Thanks: [Thomas Roten]).
* Output once special command. (Thanks: [Dick Marinus]).
* Add special command to show create table statement. (Thanks: [Ryan Smith])
* Display all result sets returned by stored procedures (Thanks: [Thomas Roten]).
* Add current time to prompt options (Thanks: [Thomas Roten]).
* Output status text in a more intuitive way (Thanks: [Thomas Roten]).
* Add colored/styled headers and odd/even rows (Thanks: [Thomas Roten]).
* Keyword completion casing (upper/lower/auto) (Thanks: [Irina Truong]).
Bug Fixes
----------
* Fixed incorrect timekeeping when running queries from a file. (Thanks: [Thomas Roten]).
* Do not display time and empty line for blank queries (Thanks: [Thomas Roten]).
* Fixed issue where quit command would sometimes not work (Thanks: [Thomas Roten]).
* Remove shebang from main.py (Thanks: [Dick Marinus]).
* Only use pager if output doesn't fit. (Thanks: [Dick Marinus]).
* Support tilde user directory for output file names (Thanks: [Thomas Roten]).
* Auto vertical output is a little bit better at its calculations (Thanks: [Thomas Roten]).
Internal Changes
-----------------
* Rename tests/ to test/. (Thanks: [Dick Marinus]).
* Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []).
* Switch from pycryptodome to cryptography (Thanks: [Thomas Roten]).
* Add pager wrapper for behave tests (Thanks: [Dick Marinus]).
* Behave test source command (Thanks: [Dick Marinus]).
* Test using behave the tee command (Thanks: [Dick Marinus]).
* Behave fix clean up. (Thanks: [Dick Marinus]).
* Remove output formatter code in favor of CLI Helpers dependency (Thanks: [Thomas Roten]).
* Better handle common before/after scenarios in behave. (Thanks: [Dick Marinus])
* Added a regression test for sqlparse >= 0.2.3 (Thanks: [Dick Marinus]).
* Reverted removal of temporary hack for sqlparse (Thanks: [Dick Marinus]).
* Add setup.py commands to simplify development tasks (Thanks: [Thomas Roten]).
* Add behave tests to tox (Thanks: [Dick Marinus]).
* Add missing @dbtest to tests (Thanks: [Dick Marinus]).
* Standardizes punctuation/grammar for help strings (Thanks: [Thomas Roten]).
1.10.0
=======
Features
---------
* Add ability to specify alternative myclirc file. (Thanks: [Dick Marinus]).
* Add new display formats for pretty printing query results. (Thanks: [Amjith
Ramanujam], [Dick Marinus], [Thomas Roten]).
* Add logic to shorten the default prompt if it becomes too long once generated. (Thanks: [John Sterling]).
Bug Fixes
----------
* Fix external editor bug (issue #377). (Thanks: [Irina Truong]).
* Fixed bug so that favorite queries can include unicode characters. (Thanks:
[Thomas Roten]).
* Fix requirements and remove old compatibility code (Thanks: [Dick Marinus])
* Fix bug where mycli would not start due to the thanks/credit intro text.
(Thanks: [Thomas Roten]).
* Use pymysql default conversions (issue #375). (Thanks: [Dick Marinus]).
Internal Changes
-----------------
* Upload mycli distributions in a safer manner (using twine). (Thanks: [Thomas
Roten]).
* Test mycli using pexpect/python-behave (Thanks: [Dick Marinus]).
* Run pep8 checks in travis (Thanks: [Irina Truong]).
* Remove temporary hack for sqlparse (Thanks: [Dick Marinus]).
1.9.0
======
Features
---------
* Add tee/notee commands for outputing results to a file. (Thanks: [Dick Marinus]).
* Add date, port, and whitespace options to prompt configuration. (Thanks: [Matheus Rosa]).
* Allow user to specify LESS pager flags. (Thanks: [John Sterling]).
* Add support for auto-reconnect. (Thanks: [Jialong Liu]).
* Add CSV batch output. (Thanks: [Matheus Rosa]).
* Add `auto_vertical_output` config to myclirc. (Thanks: [Matheus Rosa]).
* Improve Fedora install instructions. (Thanks: [Dick Marinus]).
Bug Fixes
----------
* Fix crashes occuring from commands starting with #. (Thanks: [Zhidong]).
* Fix broken PyMySQL link in README. (Thanks: [Daniël van Eeden]).
* Add various missing keywords for highlighting and autocompletion. (Thanks: [zer09]).
* Add the missing REGEXP keyword for highlighting and autocompletion. (Thanks: [cxbig]).
* Fix duplicate username entries in completion list. (Thanks: [John Sterling]).
* Remove extra spaces in TSV table format output. (Thanks: [Dick Marinus]).
* Kill running query when interrupted via Ctrl-C. (Thanks: [chainkite]).
* Read the `smart_completion` config from myclirc. (Thanks: [Thomas Roten]).
Internal Changes
-----------------
* Improve handling of test database credentials. (Thanks: [Dick Marinus]).
* Add Python 3.6 to test environments and PyPI metadata. (Thanks: [Thomas Roten]).
* Drop Python 2.6 support. (Thanks: [Thomas Roten]).
* Swap pycrypto dependency for pycryptodome. (Thanks: [Michał Górny]).
* Bump sqlparse version so pgcli and mycli can be installed together. (Thanks: [darikg]).
1.8.1
======
Bug Fixes
----------
* Remove duplicate listing of DISTINCT keyword. (Thanks: [Amjith Ramanujam]).
* Add an try/except for AS keyword crash. (Thanks: [Amjith Ramanujam]).
* Support python-sqlparse 0.2. (Thanks: [Dick Marinus]).
* Fallback to the raw object for invalid time values. (Thanks: [Amjith Ramanujam]).
* Reset the show items when completion is refreshed. (Thanks: [Amjith Ramanujam]).
Internal Changes
-----------------
* Make the dependency of sqlparse slightly more liberal. (Thanks: [Amjith Ramanujam]).
1.8.0
======
Features
---------
* Add support for --execute/-e commandline arg. (Thanks: [Matheus Rosa]).
* Add `less_chatty` config option to skip the intro messages. (Thanks: [Scrappy Soft]).
* Support `MYCLI_HISTFILE` environment variable to specify where to write the history file. (Thanks: [Scrappy Soft]).
* Add `prompt_continuation` config option to allow configuring the continuation prompt for multi-line queries. (Thanks: [Scrappy Soft]).
* Display login-path instead of host in prompt. (Thanks: [Irina Truong]).
Bug Fixes
----------
* Pin sqlparse to version 0.1.19 since the new version is breaking completion. (Thanks: [Amjith Ramanujam]).
* Remove unsupported keywords. (Thanks: [Matheus Rosa]).
* Fix completion suggestion inside functions with operands. (Thanks: [Irina Truong]).
1.7.0
======
Features
---------
* Add stdin batch mode. (Thanks: [Thomas Roten]).
* Add warn/no-warn command-line options. (Thanks: [Thomas Roten]).
* Upgrade sqlparse dependency to 0.1.19. (Thanks: [Amjith Ramanujam]).
* Update features list in README.md. (Thanks: [Matheus Rosa]).
* Remove extra \n in features list in README.md. (Thanks: [Matheus Rosa]).
Bug Fixes
----------
* Enable history search via <C-r>. (Thanks: [Amjith Ramanujam]).
Internal Changes
-----------------
* Upgrade `prompt_toolkit` to 1.0.0. (Thanks: [Jonathan Slenders])
1.6.0
======
Features
---------
* Change continuation prompt for multi-line mode to match default mysql.
* Add `status` command to match mysql's `status` command. (Thanks: [Thomas Roten]).
* Add SSL support for `mycli`. (Thanks: [Artem Bezsmertnyi]).
* Add auto-completion and highlight support for OFFSET keyword. (Thanks: [Matheus Rosa]).
* Add support for `MYSQL_TEST_LOGIN_FILE` env variable to specify alternate login file. (Thanks: [Thomas Roten]).
* Add support for `--auto-vertical-output` to automatically switch to vertical output if the output doesn't fit in the table format.
* Add support for system-wide config. Now /etc/myclirc will be honored. (Thanks: [Thomas Roten]).
* Add support for `nopager` and `\n` to turn off the pager. (Thanks: [Thomas Roten]).
* Add support for `--local-infile` command-line option. (Thanks: [Thomas Roten]).
Bug Fixes
----------
* Remove -S from `less` option which was clobbering the scroll back in history. (Thanks: [Thomas Roten]).
* Make system command work with Python 3. (Thanks: [Thomas Roten]).
* Support \G terminator for \f queries. (Thanks: [Terseus]).
Internal Changes
-----------------
* Upgrade `prompt_toolkit` to 0.60.
* Add Python 3.5 to test environments. (Thanks: [Thomas Roten]).
* Remove license meta-data. (Thanks: [Thomas Roten]).
* Skip binary tests if PyMySQL version does not support it. (Thanks: [Thomas Roten]).
* Refactor pager handling. (Thanks: [Thomas Roten])
* Capture warnings to log file. (Thanks: [Mikhail Borisov]).
* Make `syntax_style` a tiny bit more intuitive. (Thanks: [Phil Cohen]).
1.5.2
======
Bug Fixes
----------
* Protect against port number being None when no port is specified in command line.
1.5.1
======
Bug Fixes
----------
* Cast the value of port read from my.cnf to int.
1.5.0
======
Features
---------
* Make a config option to enable `audit_log`. (Thanks: [Matheus Rosa]).
* Add support for reading .mylogin.cnf to get user credentials. (Thanks: [Thomas Roten]).
This feature is only available when `pycrypto` package is installed.
* Register the special command `prompt` with the `\R` as alias. (Thanks: [Matheus Rosa]).
Users can now change the mysql prompt at runtime using `prompt` command.
eg:
```
mycli> prompt \u@\h>
Changed prompt format to \u@\h>
Time: 0.001s
amjith@localhost>
```
* Perform completion refresh in a background thread. Now mycli can handle
databases with thousands of tables without blocking.
* Add support for `system` command. (Thanks: [Matheus Rosa]).
Users can now run a system command from within mycli as follows:
```
amjith@localhost:(none)>system cat tmp.sql
select 1;
select * from django_migrations;
```
* Caught and hexed binary fields in MySQL. (Thanks: [Daniel West]).
Geometric fields stored in a database will be displayed as hexed strings.
* Treat enter key as tab when the suggestion menu is open. (Thanks: [Matheus Rosa])
* Add "delete" and "truncate" as destructive commands. (Thanks: [Martijn Engler]).
* Change \dt syntax to add an optional table name. (Thanks: [Shoma Suzuki]).
`\dt [tablename]` will describe the columns in a table.
* Add TRANSACTION related keywords.
* Treat DESC and EXPLAIN as DESCRIBE. (Thanks: [spacewander]).
Bug Fixes
----------
* Fix the removal of whitespace from table output.
* Add ability to make suggestions for compound join clauses. (Thanks: [Matheus Rosa]).
* Fix the incorrect reporting of command time.
* Add type validation for port argument. (Thanks [Matheus Rosa])
Internal Changes
-----------------
* Make pycrypto optional and only install it in \*nix systems. (Thanks: [Irina Truong]).
* Add badge for PyPI version to README. (Thanks: [Shoma Suzuki]).
* Updated release script with a --dry-run and --confirm-steps option. (Thanks: [Irina Truong]).
* Adds support for PyMySQL 0.6.2 and above. This is useful for debian package builders. (Thanks: [Thomas Roten]).
* Disable click warning.
1.4.0
======
Features
---------
* Add `source` command. This allows running sql statement from a file.
eg:
```
mycli> source filename.sql
```
* Added a config option to make the warning before destructive commands optional. (Thanks: [Daniel West](https://github.com/danieljwest))
In the config file ~/.myclirc set `destructive_warning = False` which will
disable the warning before running `DROP` commands.
* Add completion support for CHANGE TO and other master/slave commands. This is
still preliminary and it will be enhanced in the future.
* Add custom styles to color the menus and toolbars.
* Upgrade `prompt_toolkit` to 0.46. (Thanks: [Jonathan Slenders])
Multi-line queries are automatically indented.
Bug Fixes
----------
* Fix keyword completion after the `WHERE` clause.
* Add `\g` and `\G` as valid query terminators. Previously in multi-line mode
ending a query with a `\G` wouldn't run the query. This is now fixed.
1.3.0
======
Features
---------
* Add a new special command (\T) to change the table format on the fly. (Thanks: [Jonathan Bruno](https://github.com/brewneaux))
eg:
```
mycli> \T tsv
```
* Add `--defaults-group-suffix` to the command line. This lets the user specify
a group to use in the my.cnf files. (Thanks: [Irina Truong](https://github.com/j-bennet))
In the my.cnf file a user can specify credentials for different databases and
invoke mycli with the group name to use the appropriate credentials.
eg:
```
# my.cnf
[client]
user = 'root'
socket = '/tmp/mysql.sock'
pager = 'less -RXSF'
database = 'account'
[clientamjith]
user = 'amjith'
database = 'user_management'
$ mycli --defaults-group-suffix=amjith # uses the [clientamjith] section in my.cnf
```
* Add `--defaults-file` option to the command line. This allows specifying a
`my.cnf` to use at launch. This also makes it play nice with mysql sandbox.
* Make `-p` and `--password` take the password in commandline. This makes mycli
a drop in replacement for mysql.
1.2.0
======
Features
---------
* Add support for wider completion menus in the config file.
Add `wider_completion_menu = True` in the config file (~/.myclirc) to enable this feature.
Bug Fixes
---------
* Prevent Ctrl-C from quitting mycli while the pager is active.
* Refresh auto-completions after the database is changed via a CONNECT command.
Internal Changes
-----------------
* Upgrade `prompt_toolkit` dependency version to 0.45.
* Added Travis CI to run the tests automatically.
1.1.1
======
Bug Fixes
----------
* Change dictonary comprehension used in mycnf reader to list comprehension to make it compatible with Python 2.6.
1.1.0
======
Features
---------
* Fuzzy completion is now case-insensitive. (Thanks: [bjarnagin](https://github.com/bjarnagin))
* Added new-line (`\n`) to the list of special characters to use in prompt. (Thanks: [brewneaux](https://github.com/brewneaux))
* Honor the `pager` setting in my.cnf files. (Thanks: [Irina Truong](https://github.com/j-bennet))
Bug Fixes
----------
* Fix a crashing bug in completion engine for cross joins.
* Make `<null>` value consistent between tabular and vertical output.
Internal Changes
-----------------
* Changed pymysql version to be greater than 0.6.6.
* Upgrade `prompt_toolkit` version to 0.42. (Thanks: [Yasuhiro Matsumoto](https://github.com/mattn))
* Removed the explicit dependency on six.
2015/06/10
===========
Features
---------
* Customizable prompt. (Thanks [Steve Robbins](https://github.com/steverobbins))
* Make `\G` formatting to behave more like mysql.
Bug Fixes
----------
* Formatting issue in \G for really long column values.
2015/06/07
===========
Features
---------
* Upgrade `prompt_toolkit` to 0.38. This improves the performance of pasting long queries.
* Add support for reading my.cnf files.
* Add editor command \e.
* Replace ConfigParser with ConfigObj.
* Add \dt to show all tables.
* Add fuzzy completion for table names and column names.
* Automatically reconnect when connection is lost to the database.
Bug Fixes
----------
* Fix a bug with reconnect failure.
* Fix the issue with `use` command not changing the prompt.
* Fix the issue where `\\r` shortcut was not recognized.
2015/05/24
==========
Features
---------
* Add support for connecting via socket.
* Add completion for SQL functions.
* Add completion support for SHOW statements.
* Made the timing of sql statements human friendly.
* Automatically prompt for a password if needed.
Bug Fixes
----------
* Fixed the installation issues with PyMySQL dependency on case-sensitive file systems.
[Amjith Ramanujam]: https://blog.amjith.com
[Artem Bezsmertnyi]: https://github.com/mrdeathless
[BuonOmo]: https://github.com/BuonOmo
[Daniel West]: https://github.com/danieljwest
[Dick Marinus]: https://github.com/meeuw
[François Pietka]: https://github.com/fpietka
[Frederic Aoustin]: https://github.com/fraoustin
[Georgy Frolov]: https://github.com/pasenor
[Irina Truong]: https://github.com/j-bennet
[Jonathan Slenders]: https://github.com/jonathanslenders
[laixintao]: https://github.com/laixintao
[Martijn Engler]: https://github.com/martijnengler
[Matheus Rosa]: https://github.com/mdsrosa
[Mikhail Borisov]: https://github.com/borman
[mtorromeo]: https://github.com/mtorromeo
[mwcm]: https://github.com/mwcm
[Phil Cohen]: https://github.com/phlipper
[Scrappy Soft]: https://github.com/scrappysoft
[Shoma Suzuki]: https://github.com/shoma
[spacewander]: https://github.com/spacewander
[Terseus]: https://github.com/Terseus
[Thomas Roten]: https://github.com/tsroten
[xeron]: https://github.com/xeron
[Zach DeCook]: https://zachdecook.com
[Will Wang]: https://github.com/willww64
================================================
FILE: doc/key_bindings.rst
================================================
*************
Key Bindings:
*************
Most key bindings are simply inherited from `prompt-toolkit <https://python-prompt-toolkit.readthedocs.io/en/master/index.html>`_ .
The following key bindings are special to mycli:
###
F1
###
Open documentation index in a browser tab.
###
F2
###
Enable/Disable SmartCompletion Mode.
###
F3
###
Enable/Disable Multiline Mode.
###
F4
###
Toggle between Vi and Emacs mode.
###
Tab
###
Force autocompletion at cursor.
#######
C-space
#######
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.
#########
ESC Enter
#########
Introduce a line break in multi-line mode, or dispatch the command in single-line mode.
The sequence ESC-Enter is often sent by Alt-Enter.
##################
C-x p (Emacs-mode)
##################
Prettify and indent current statement, usually into multiple lines.
Only accepts buffers containing single SQL statements.
##################
C-x u (Emacs-mode)
##################
Unprettify and dedent current statement, usually into one line.
Only accepts buffers containing single SQL statements.
##################
C-o d (Emacs-mode)
##################
Insert the current date at cursor, defined by NOW() on the server.
####################
C-o C-d (Emacs-mode)
####################
Insert the quoted current date at cursor.
##################
C-o t (Emacs-mode)
##################
Insert the current datetime at cursor.
####################
C-o C-t (Emacs-mode)
####################
Insert the quoted current datetime at cursor.
================================================
FILE: doc/llm.md
================================================
# Using the \llm Command (AI-assisted SQL)
The `\llm` special command lets you ask natural-language questions and get SQL proposed for you. It uses the open‑source `llm` CLI under the hood and enriches your prompt with database context (schema and one sample row per table) so answers can include runnable SQL.
Alias: `\ai` works the same as `\llm`.
---
## Quick Start
1) Make sure mycli is installed with the `[llm]` extras, like
```bash
pip install 'mycli[llm]'
```
or that the `llm` dependency is installed separately:
```bash
pip install llm
```
2) From the mycli prompt, configure your API key (only needed for remote providers like OpenAI):
```text
\llm keys set openai
```
3) Ask a question. The response’s SQL (inside a ```sql fenced block) is extracted and pre-filled at the prompt:
```text
World> \llm "Capital of India?"
-- Answer text from the model...
-- ```sql
-- SELECT ...;
-- ```
-- Your prompt is prefilled with the SQL above.
```
You can now hit Enter to run, or edit the query first.
---
## What Context Is Sent
When you ask a plain question via `\llm "..."`, mycli:
- Sends your question.
- Adds your current database schema: table names with column types.
- Adds one sample row (if available) from each table.
This helps the model propose SQL that fits your schema. Follow‑ups using `-c` continue the same conversation and do not re-send the DB context (see “Continue Conversation (-c)”).
Note: Context is gathered from the current connection. If you are not connected, using contextual mode will fail — connect first.
---
## Using `llm` Subcommands from mycli
You can run any `llm` CLI subcommand by prefixing it with `\llm` inside mycli. Examples:
- List models:
```text
\llm models
```
- Set the default model:
```text
\llm models default gpt-5
```
- Set provider API key:
```text
\llm keys set openai
```
- Install a plugin (e.g., local models via Ollama):
```text
\llm install llm-ollama
```
After installing or uninstalling plugins, mycli will restart to pick up new commands.
Tab completion works for `\llm` subcommands, and even for model IDs under `models default`.
Aside: <https://ollama.com/> for using local models.
---
## Ask Questions With DB Context (default)
Ask your question in quotes. mycli sends database context and extracts a SQL block if present.
```text
World> \llm "Most visited urls?"
```
Behavior:
- Response is printed in the output pane.
- If the response contains a ```sql fenced block, mycli extracts the SQL and pre-fills it at your prompt.
---
## Continue Conversation (-c)
Use `-c` to ask a follow‑up that continues the previous conversation with the model. This does not re-send the DB context; it relies on the ongoing thread.
```text
World> \llm "Top 10 customers by spend"
-- model returns analysis and a ```sql block; SQL is prefilled
World> \llm -c "Now include each customer's email and order count"
```
Behavior:
- Continues the last conversation in the `llm` history.
- Database context is not re-sent on follow‑ups.
- If the response includes a ```sql block, the SQL is pre-filled at your prompt.
---
## Examples
- List available models:
```text
World> \llm models
```
- Change default model:
```text
World> \llm models default llama3
```
- Set API key (for providers that require it):
```text
World> \llm keys set openai
```
- Ask a question with context:
```text
World> \llm "Capital of India?"
```
- Use a local model (after installing a plugin such as `llm-ollama`):
```text
World> \llm install llm-ollama
World> \llm models default llama3
World> \llm "Top 10 customers by spend"
```
See: <https://ollama.com/> for details.
---
## Customize the Prompt Template
mycli uses a saved `llm` template named `mycli-llm-template` for contextual questions. You can view or edit it:
```text
World> \llm templates edit mycli-llm-template
```
Tip: After first use, mycli ensures this template exists. To just view it without editing, use:
```text
World> \llm templates show mycli-llm-template
```
---
## Troubleshooting
- No SQL pre-fill: Ensure the model’s response includes a ```sql fenced block. The built‑in prompt encourages this, but some models may omit it; try asking the model to include SQL in a ```sql block.
- Not connected to a database: Contextual questions require a live connection. Connect first. Follow‑ups with `-c` only help after a successful contextual call.
- Plugin changes not recognized: After `\llm install` or `\llm uninstall`, mycli restarts automatically to load new commands.
- Provider/API issues: Use `\llm keys list` and `\llm keys set <provider>` to check credentials. Use `\llm models` to confirm available models.
---
## Notes and Safety
- Data sent: Contextual questions send schema (table/column names and types) and a single sample row per table. Review your data sensitivity policies before using remote models; prefer local models (such as ollama) if needed.
- Help: Running `\llm` with no arguments shows a short usage message.
## Turning Off LLM Support
To turn off LLM support even when the `llm` dependency is installed, set the `MYCLI_LLM_OFF` environment variable:
```bash
export MYCLI_LLM_OFF=1
```
This may be desirable for faster startup times.
---
## Learn More
- `llm` project docs: https://llm.datasette.io/
- `llm` plugin directory: https://llm.datasette.io/en/stable/plugins/directory.html
================================================
FILE: mycli/AUTHORS
================================================
Project Lead:
-------------
* Roland Walker
Core Developers:
----------------
* Thomas Roten
* Irina Truong
* Matheus Rosa
* Darik Gamble
* Dick Marinus
* Amjith Ramanujam
Contributors:
-------------
* 0xflotus
* Abirami P
* Adam Chainz
* Aljosha Papsch
* Allrob
* Andy Teijelo Pérez
* Angelo Lupo
* Artem Bezsmertnyi
* bitkeen
* bjarnagin
* BuonOmo
* caitinggui
* Carlos Afonso
* Casper Langemeijer
* chainkite
* Claude Becker
* Colin Caine
* cxbig
* Daniel Black
* Daniel West
* Daniël van Eeden
* Fabrizio Gennari
* FatBoyXPC
* François Pietka
* Frederic Aoustin
* Georgy Frolov
* Heath Naylor
* Huachao Mao
* Ishaan Bhimwal
* Jakub Boukal
* jbruno
* Jerome Provensal
* Jialong Liu
* Johannes Hoff
* John Sterling
* Jonathan Bruno
* Jonathan Lloyd
* Jonathan Slenders
* Kacper Kwapisz
* Karthikeyan Singaravelan
* kevinhwang91
* KITAGAWA Yasutaka
* Klaus Wünschel
* laixintao
* Lennart Weller
* Martijn Engler
* Massimiliano Torromeo
* Michał Górny
* Mike Palandra
* Mikhail Borisov
* Miodrag Tokić
* Morgan Mitchell
* mrdeathless
* Nathan Huang
* Nicolas Palumbo
* Phil Cohen
* QiaoHou Peng
* Roland Walker
* Ryan Smith
* Scrappy Soft
* Seamile
* Shoma Suzuki
* spacewander
* Steve Robbins
* Takeshi D. Itoh
* Terje Røsten
* Terseus
* Tyler Kuipers
* ushuz
* William GARCIA
* xeron
* Yang Zou
* Yasuhiro Matsumoto
* Yuanchun Shang
* Zach DeCook
* Zane C. Bowers-Hadley
* zer09
* Zhaolong Zhu
* Zhidong
* Zhongyang Guan
* Arvind Mishra
* Kevin Schmeichel
* Mel Dafert
* Thomas Copper
* Will Wang
* Alfred Wingate
* Zhanze Wang
* Houston Wong
* Mohamed Rezk
* Ryosuke Kazami
* Cornel Cruceru
* Sherlock Holo
* keltaklo
* 924060929
* tmijieux
* Scott Nemes
* Angelino Storm
* Abhay Kumar
Created by:
-----------
Amjith Ramanujam
================================================
FILE: mycli/SPONSORS
================================================
Many thanks to the following Kickstarter backers.
* Tech Blue Software
* jweiland.net
# Silver Sponsors
* Whitane Tech
* Open Query Pty Ltd
* Prathap Ramamurthy
* Lincoln Loop
# Sponsors
* Nathan Taggart
* Iryna Cherniavska
* Sudaraka Wijesinghe
* www.mysqlfanboy.com
* Steve Robbins
* Norbert Spichtig
* orpharion bestheneme
* Daniel Black
* Anonymous
* Magnus udd
* Anonymous
* Lewis Peckover
* Cyrille Tabary
* Heath Naylor
* Ted Pennings
* Chris Anderton
* Jonathan Slenders
# Other Donors
* OpenAI
================================================
FILE: mycli/TIPS
================================================
###
### CLI arguments
###
check your ~/.myclirc settings using the --checkup flag!
list your aliased DSNs with the --list-dsn flag!
log every query and result with the --logfile option!
the --checkpoint option helps track successful queries in batch mode!
the --format option helps set the output format in batch mode!
the --throttle option helps slow down queries in batch mode!
the --password-file option can be used with a FIFO to avoid saving creds to a file!
the --character-set option sets the character set for a single session!
the --unbuffered flag can save memory when in batch mode!
--use-keyring=true lets you access the system keyring for passwords!
--use-keyring=reset resets a password saved to the system keyring!
the --myclirc option can change the config file location for a single session!
the --execute option lets you execute a single line of SQL!
the --auto-vertical-output flag lets you automatically switch to vertical output!
the --show-warnings flag turns on warnings from the MySQL server!
the --no-warn flag turns off warnings befor running a destructive query!
the --init-command option lets you execute initialization SQL before a session!
the --login-path option lets you work with login-path files!
--keepalive-ticks=<num> sets keepalive pings for a single session!
###
### commands
###
interact with an LLM using the \llm command!
copy a query to the clipboard using \clip at the end of the query!
\dt lists tables; \dt <table> describes <table>!
edit a query in an external editor using <query>\edit!
edit a query in an external editor using \edit <filename>!
set "export VISUAL='code --wait'" in your shell to `\edit` queries using VS Code!
\f lists favorite queries; \f <name> executes a favorite!
\fs <name> <query> saves a favorite query!
\fd <name> deletes a saved favorite query!
\l lists databases!
\once <filename> appends the next result to <filename>!
\| <command> sends the next result to a subprocess!
\t toggles timing of commands!
\r or "connect" reconnects to the server!
\delimiter changes the SQL delimiter!
\q, "quit", or "exit" exits from the prompt!
\? or "help" for help!
"help <keyword>" for help on SQL keywords!
\n or "nopager" to disable the pager!
use "tee"/"notee" to write/stop-writing results to a output file!
\W or "warnings" enables automatic warnings display!
\w or "nowarnings" disables automatic warnings display!
\P or "pager" sets the pager. Try "pager less"!
\R or "prompt" changes the prompt format!
\Tr or "redirectformat" changes the table format for redirects!
\# or "rehash" refreshes autocompletions!
\. or "source" executes queries from a file!
\s or "status" requests status information from the server!
use "system <command>" to execute a shell command!
\T or "tableformat" changes the interactive table format!
\u or "use" changes to a new database!
the "watch" command executes a query every N seconds!
use \bug to file a bug on GitHub!
###
### general
###
display query output vertically using \G at the end of a query!
run SQL scripts in batch mode using the standard input!
###
### keystrokes
###
edit a query in an external editor using keystrokes control-x + control-e!
open a documentation browser using keystroke F1!
toggle smart completion using keystroke F2!
toggle multi-line mode using keystroke F3!
toggle vi mode using keystroke F4!
complete at cursor using the tab key!
summon completion candidates using control-space!
control-space works well with "min_completion_trigger" in ~/.myclirc!
prettify a query using keystrokes control-x + p!
un-prettify a query using keystrokes control-x + u!
insert the current date using keystrokes control-o + d!
insert the quoted current date using keystrokes control-o + control-d!
insert the current datetime using keystrokes control-o + t!
insert the quoted current date using keystrokes control-o + control-t!
search query history using keystroke control-r!
use keystroke control-g to cancel completion popups!
use keystroke right-arrow to accept a full-line suggestion from your history!
cancel history search using keystrokes Escape or control-g!
###
### myclirc options
###
set "less_chatty = True" in ~/.myclirc to turn off these tips!
set a fancy table format like "table_format = psql_unicode" in ~/.myclirc!
change the string for NULLs with "null_string = <null>" in ~/.myclirc!
choose a color theme with "syntax_style" in ~/.myclirc!
design a prompt with the "prompt" option in ~/.myclirc!
turn off multi-line prompt indentation with "prompt_continuation = ''" in ~/.myclirc!
save passwords in the system keyring with "use_keyring" in ~/.myclirc!
enable SHOW WARNINGS with "show warnings" in ~/.myclirc!
turn off smart completions with "smart_completion" in ~/.myclirc!
turn on multi-line mode with "multi_line" in ~/.myclirc!
turn off destructive warnings with "destructive_warning" in ~/.myclirc!
control destructive warnings with "destructive_keywords" in ~/.myclirc!
move the history file locattion with "history_file" in ~/.myclirc!
enable an audit log with "audit_log" in ~/.myclirc!
disable timing of SQL statements with "timiing" in ~/.myclirc!
disable display of SQL when running a favorite with "show_favorite_query" in ~/.myclirc!
notify after a long query by setting "beep_after_seconds" in ~/.myclirc!
control alignment with "numeric_alignment" in ~/.myclirc!
control binary value display with "binary_display" in ~/.myclirc!
set vi key bindings with "key_bindings" in ~/.myclirc!
show more suggestions with "wider_completion_menu" in ~/.myclirc!
use the host alias in the prompt with "login_path_as_host" in ~/.myclirc!
auto-display wide results vertically with "auto_vertical_output" in ~/.myclirc!
control keyword casing in completions using "keyword_casing" in ~/.myclirc!
disable pager on startup using "enable_pager" in ~/.myclirc!
choose a pager command with "pager" in ~/.myclirc!
customize colors using the "[colors]" section in ~/.myclirc!
customize LLM commands using the "[llm]" section in ~/.myclirc!
customize history search using "control_r" in ~/.myclirc!
edit favorite queries directly using the "[favorite_queries]" section in ~/.myclirc!
set up initial commands using the "[init-commands]" section in ~/.myclirc!
create DSN shortcuts using the "[alias_dsn]" section in ~/.myclirc!
set up per-DSN initial commands using the "[alias_dsn.init-commands]" section in ~/.myclirc!
set up connection defaults using the "[connection]" section in ~/.myclirc!
use "min_completion_trigger" in ~/.myclirc to defer completions!
colorize search previews with "highlight_preview" in ~/.myclirc!
###
### redirection
###
redirect query output to a shell command with "$| <command>"!
redirect query output to a file with "$> <filename>"!
append query output to a file with "$>> <filename>"!
run a command after shell redirects with "post_redirect_command" in ~/.myclirc!
================================================
FILE: mycli/__init__.py
================================================
import importlib.metadata
__version__: str = importlib.metadata.version("mycli")
================================================
FILE: mycli/clibuffer.py
================================================
from prompt_toolkit.application import get_app
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.filters import Condition, Filter
from mycli.packages.special import iocommands
from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
def cli_is_multiline(mycli) -> Filter:
@Condition
def cond():
doc = get_app().layout.get_buffer_by_name(DEFAULT_BUFFER).document
if not mycli.multi_line:
return False
else:
return not _multiline_exception(doc.text)
return cond
def _multiline_exception(text: str) -> bool:
orig = text
text = text.strip()
first_word = text.split(' ')[0]
# Multi-statement favorite query is a special case. Because there will
# be a semicolon separating statements, we can't consider semicolon an
# EOL. Let's consider an empty line an EOL instead.
if first_word.startswith("\\fs"):
return orig.endswith("\n")
return (
# Special Command
first_word.startswith("\\")
or text.endswith((
# Ended with the current delimiter (usually a semi-column)
iocommands.get_current_delimiter(),
# or ended with certain commands
"\\g",
"\\G",
r"\e",
r"\edit",
r"\clip",
))
or
# non-backslashed special commands such as "exit" or "help" don't need semicolon
first_word in SPECIAL_COMMANDS
or
# uppercase variants accepted
first_word.lower() in SPECIAL_COMMANDS
or
# just a plain enter without any text
(first_word == "")
)
================================================
FILE: mycli/clistyle.py
================================================
import logging
from prompt_toolkit.styles import Style, merge_styles
from prompt_toolkit.styles.pygments import style_from_pygments_cls
from prompt_toolkit.styles.style import _MergedStyle
from pygments.style import Style as PygmentsStyle
import pygments.styles
from pygments.token import Token, string_to_tokentype
from pygments.util import ClassNotFound
logger = logging.getLogger(__name__)
# map Pygments tokens (ptk 1.0) to class names (ptk 2.0).
TOKEN_TO_PROMPT_STYLE: dict[Token, str] = {
Token.Menu.Completions.Completion.Current: "completion-menu.completion.current",
Token.Menu.Completions.Completion: "completion-menu.completion",
Token.Menu.Completions.Meta.Current: "completion-menu.meta.completion.current",
Token.Menu.Completions.Meta: "completion-menu.meta.completion",
Token.Menu.Completions.MultiColumnMeta: "completion-menu.multi-column-meta",
Token.Menu.Completions.ProgressButton: "scrollbar.arrow", # best guess
Token.Menu.Completions.ProgressBar: "scrollbar", # best guess
Token.SelectedText: "selected",
Token.SearchMatch: "search",
Token.SearchMatch.Current: "search.current",
Token.Toolbar: "bottom-toolbar",
Token.Toolbar.Off: "bottom-toolbar.off",
Token.Toolbar.On: "bottom-toolbar.on",
Token.Toolbar.Search: "search-toolbar",
Token.Toolbar.Search.Text: "search-toolbar.text",
Token.Toolbar.System: "system-toolbar",
Token.Toolbar.Arg: "arg-toolbar",
Token.Toolbar.Arg.Text: "arg-toolbar.text",
Token.Toolbar.Transaction.Valid: "bottom-toolbar.transaction.valid",
Token.Toolbar.Transaction.Failed: "bottom-toolbar.transaction.failed",
Token.Output.TableSeparator: "output.table-separator",
Token.Output.Header: "output.header",
Token.Output.OddRow: "output.odd-row",
Token.Output.EvenRow: "output.even-row",
Token.Output.Null: "output.null",
Token.Output.Status: "output.status",
Token.Output.Status.WarningCount: "output.status.warning-count",
Token.Output.Timing: "output.timing",
Token.Warnings.TableSeparator: "warnings.table-separator",
Token.Warnings.Header: "warnings.header",
Token.Warnings.OddRow: "warnings.odd-row",
Token.Warnings.EvenRow: "warnings.even-row",
Token.Warnings.Null: "warnings.null",
Token.Warnings.Status: "warnings.status",
Token.Warnings.Status.WarningCount: "warnings.status.warning-count",
Token.Warnings.Timing: "warnings.timing",
Token.Prompt: "prompt",
Token.Continuation: "continuation",
}
# reverse dict for cli_helpers, because they still expect Pygments tokens.
PROMPT_STYLE_TO_TOKEN: dict[str, Token] = {v: k for k, v in TOKEN_TO_PROMPT_STYLE.items()}
# all tokens that the Pygments MySQL lexer can produce
OVERRIDE_STYLE_TO_TOKEN: dict[str, Token] = {
"sql.comment": Token.Comment,
"sql.comment.multi-line": Token.Comment.Multiline,
"sql.comment.single-line": Token.Comment.Single,
"sql.comment.optimizer-hint": Token.Comment.Special,
"sql.escape": Token.Error,
"sql.keyword": Token.Keyword,
"sql.datatype": Token.Keyword.Type,
"sql.literal": Token.Literal,
"sql.literal.date": Token.Literal.Date,
"sql.symbol": Token.Name,
"sql.quoted-schema-object": Token.Name.Quoted,
"sql.quoted-schema-object.escape": Token.Name.Quoted.Escape,
"sql.constant": Token.Name.Constant,
"sql.function": Token.Name.Function,
"sql.variable": Token.Name.Variable,
"sql.number": Token.Number,
"sql.number.binary": Token.Number.Bin,
"sql.number.float": Token.Number.Float,
"sql.number.hex": Token.Number.Hex,
"sql.number.integer": Token.Number.Integer,
"sql.operator": Token.Operator,
"sql.punctuation": Token.Punctuation,
"sql.string": Token.String,
"sql.string.double-quouted": Token.String.Double,
"sql.string.escape": Token.String.Escape,
"sql.string.single-quoted": Token.String.Single,
"sql.whitespace": Token.Text,
}
def parse_pygments_style(
token_name: str,
style_object: PygmentsStyle | str,
style_dict: dict[str, str],
) -> tuple[Token, str]:
"""Parse token type and style string.
:param token_name: str name of Pygments token. Example: "Token.String"
:param style_object: pygments.style.Style instance to use as base
:param style_dict: dict of token names and their styles, customized to this cli
"""
token_type = string_to_tokentype(token_name)
if isinstance(style_object, PygmentsStyle):
# When a Pygments Style class is passed, use its "styles" mapping.
other_token_type = string_to_tokentype(style_dict[token_name])
return token_type, style_object.styles[other_token_type]
else:
return token_type, style_dict[token_name]
def is_valid_pygments(name: str) -> bool:
try:
class TestStyle(PygmentsStyle):
default_style = ''
styles = {Token.Default: name}
return True
except AssertionError:
# can't emit error because some styles are valid pygments and not valid ptoolkit
return False
def is_valid_ptoolkit(name: str) -> bool:
try:
_s = Style([("default", name)])
return True
except ValueError:
# can't emit error because some styles are valid pygments and not valid ptoolkit
return False
def style_factory_toolkit(name: str, cli_style: dict[str, str]) -> _MergedStyle:
try:
style: PygmentsStyle = pygments.styles.get_style_by_name(name)
except ClassNotFound:
style = pygments.styles.get_style_by_name("native")
prompt_styles: list[tuple[str, str]] = []
# prompt-toolkit used pygments tokens for styling before, switched to style
# names in 2.0. Convert old token types to new style names, for backwards compatibility.
for token in cli_style:
if token.startswith("Token."):
# treat as pygments token (1.0)
token_type, style_value = parse_pygments_style(token, style, cli_style)
if token_type in TOKEN_TO_PROMPT_STYLE:
prompt_style = TOKEN_TO_PROMPT_STYLE[token_type]
if is_valid_ptoolkit(style_value):
prompt_styles.append((prompt_style, style_value))
else:
# we don't want to support tokens anymore
logger.error("Unhandled style / class name: %s", token)
else:
# treat as prompt style name (2.0). See default style names here:
# https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/styles/defaults.py
if is_valid_ptoolkit(cli_style[token]):
prompt_styles.append((token, cli_style[token]))
override_style: Style = Style([("bottom-toolbar", "noreverse")])
return merge_styles([style_from_pygments_cls(style), override_style, Style(prompt_styles)])
def style_factory_helpers(
name: str,
cli_style: dict[str, str],
warnings: bool = False,
) -> PygmentsStyle:
try:
style: dict[PygmentsStyle | str, str] = pygments.styles.get_style_by_name(name).styles
except ClassNotFound:
style = pygments.styles.get_style_by_name("native").styles
for token in cli_style:
if token.startswith("Token."):
token_type, style_value = parse_pygments_style(token, style, cli_style)
if is_valid_pygments(style_value):
style.update({token_type: style_value})
elif token in PROMPT_STYLE_TO_TOKEN:
token_type = PROMPT_STYLE_TO_TOKEN[token]
if is_valid_pygments(cli_style[token]):
style.update({token_type: cli_style[token]})
elif token in OVERRIDE_STYLE_TO_TOKEN:
token_type = OVERRIDE_STYLE_TO_TOKEN[token]
if is_valid_pygments(cli_style[token]):
style.update({token_type: cli_style[token]})
else:
# TODO: cli helpers will have to switch to ptk.Style
logger.error("Unhandled style / class name: %s", token)
if warnings:
for warning_token in list(style.keys()):
if 'Warnings' not in str(warning_token):
continue
warning_str = str(warning_token)
output_str = warning_str.replace('Warnings', 'Output')
output_token = string_to_tokentype(output_str)
style[output_token] = style[warning_token]
class OutputStyle(PygmentsStyle):
default_style = ""
styles = style
return OutputStyle
================================================
FILE: mycli/clitoolbar.py
================================================
from typing import Callable
from prompt_toolkit.application import get_app
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.formatted_text import to_formatted_text
from prompt_toolkit.key_binding.vi_state import InputMode
from mycli.packages import special
def create_toolbar_tokens_func(mycli, show_initial_toolbar_help: Callable, format_string: str | None) -> Callable:
"""Return a function that generates the toolbar tokens."""
def get_toolbar_tokens() -> list[tuple[str, str]]:
divider = ('class:bottom-toolbar', ' │ ')
result = [("class:bottom-toolbar", "[Tab] Complete")]
dynamic = []
result.append(divider)
result.append(("class:bottom-toolbar", "[F1] Help"))
if mycli.completer.smart_completion:
result.append(divider)
result.append(("class:bottom-toolbar", "[F2] Smart-complete:"))
result.append(("class:bottom-toolbar.on", "ON "))
else:
result.append(divider)
result.append(("class:bottom-toolbar", "[F2] Smart-complete:"))
result.append(("class:bottom-toolbar.off", "OFF"))
if mycli.multi_line:
result.append(divider)
result.append(("class:bottom-toolbar", "[F3] Multiline:"))
result.append(("class:bottom-toolbar.on", "ON "))
else:
result.append(divider)
result.append(("class:bottom-toolbar", "[F3] Multiline:"))
result.append(("class:bottom-toolbar.off", "OFF"))
if mycli.prompt_app.editing_mode == EditingMode.VI:
result.append(divider)
result.append(("class:bottom-toolbar", "Vi:"))
result.append(("class:bottom-toolbar.on", _get_vi_mode()))
if mycli.toolbar_error_message:
dynamic.append(divider)
dynamic.append(("class:bottom-toolbar.transaction.failed", mycli.toolbar_error_message))
mycli.toolbar_error_message = None
if mycli.multi_line:
delimiter = special.get_current_delimiter()
if delimiter != ';' or show_initial_toolbar_help():
dynamic.append(divider)
dynamic.append(('class:bottom-toolbar', '"'))
dynamic.append(('class:bottom-toolbar.on', delimiter))
dynamic.append(('class:bottom-toolbar', '" ends a statement'))
if show_initial_toolbar_help():
dynamic.append(divider)
dynamic.append(("class:bottom-toolbar", "right-arrow accepts full-line suggestion"))
if mycli.completion_refresher.is_refreshing():
dynamic.append(divider)
dynamic.append(("class:bottom-toolbar", "Refreshing completions…"))
if format_string and format_string != r'\B':
if format_string.startswith(r'\B'):
amended_format = format_string[2:]
result.extend(dynamic)
dynamic = []
result.append(('class:bottom-toolbar', '\n'))
else:
amended_format = format_string
result = []
formatted = to_formatted_text(mycli.get_custom_toolbar(amended_format), style='class:bottom-toolbar')
result.extend([*formatted]) # coerce to list for mypy
result.extend(dynamic)
return result
return get_toolbar_tokens
def _get_vi_mode() -> str:
"""Get the current vi mode for display."""
return {
InputMode.INSERT: "I",
InputMode.NAVIGATION: "N",
InputMode.REPLACE: "R",
InputMode.REPLACE_SINGLE: "R",
InputMode.INSERT_MULTIPLE: "M",
}[get_app().vi_state.input_mode]
================================================
FILE: mycli/compat.py
================================================
"""Platform and Python version compatibility support."""
import sys
WIN: bool = sys.platform in ("win32", "cygwin")
================================================
FILE: mycli/completion_refresher.py
================================================
import threading
from typing import Callable
from mycli.packages.special.main import COMMANDS
from mycli.packages.sqlresult import SQLResult
from mycli.sqlcompleter import SQLCompleter
from mycli.sqlexecute import ServerSpecies, SQLExecute
class CompletionRefresher:
refreshers: dict = {}
def __init__(self) -> None:
self._completer_thread: threading.Thread | None = None
self._restart_refresh = threading.Event()
def refresh(
self,
executor: SQLExecute,
callbacks: Callable | list[Callable],
completer_options: dict | None = None,
) -> list[SQLResult]:
"""Creates a SQLCompleter object and populates it with the relevant
completion suggestions in a background thread.
executor - SQLExecute object, used to extract the credentials to connect
to the database.
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.
completer_options - dict of options to pass to SQLCompleter.
"""
if completer_options is None:
completer_options = {}
if self.is_refreshing():
self._restart_refresh.set()
return [SQLResult(status="Auto-completion refresh restarted.")]
else:
self._completer_thread = threading.Thread(
target=self._bg_refresh, args=(executor, callbacks, completer_options), name="completion_refresh"
)
self._completer_thread.daemon = True
self._completer_thread.start()
return [SQLResult(status="Auto-completion refresh started in the background.")]
def is_refreshing(self) -> bool:
return bool(self._completer_thread and self._completer_thread.is_alive())
def _bg_refresh(
self,
sqlexecute: SQLExecute,
callbacks: Callable | list[Callable],
completer_options: dict,
) -> None:
completer = SQLCompleter(**completer_options)
# Create a new sqlexecute method to populate the completions.
e = sqlexecute
executor = SQLExecute(
e.dbname,
e.user,
e.password,
e.host,
e.port,
e.socket,
e.character_set,
e.local_infile,
e.ssl,
e.ssh_user,
e.ssh_host,
e.ssh_port,
e.ssh_password,
e.ssh_key_filename,
)
# 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
for callback in callbacks:
callback(completer)
executor.close()
def refresher(name: str, refreshers: dict = CompletionRefresher.refreshers) -> Callable:
"""Decorator to add the decorated function to the dictionary of
refreshers. Any function decorated with a @refresher will be executed as
part of the completion refresh routine."""
def wrapper(wrapped):
refreshers[name] = wrapped
return wrapped
return wrapper
@refresher("databases")
def refresh_databases(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_database_names(executor.databases())
@refresher("schemata")
def refresh_schemata(completer: SQLCompleter, executor: SQLExecute) -> None:
# schemata - In MySQL Schema is the same as database. But for mycli
# schemata will be the name of the current database.
completer.extend_schemata(executor.dbname)
completer.set_dbname(executor.dbname)
@refresher("tables")
def refresh_tables(completer: SQLCompleter, executor: SQLExecute) -> None:
table_columns_dbresult = list(executor.table_columns())
completer.extend_relations(table_columns_dbresult, kind="tables")
completer.extend_columns(table_columns_dbresult, kind="tables")
@refresher("enum_values")
def refresh_enum_values(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_enum_values(executor.enum_values())
@refresher("users")
def refresh_users(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_users(executor.users())
# @refresher('views')
# def refresh_views(completer: SQLCompleter, executor: SQLExecute) -> None:
# completer.extend_relations(executor.views(), kind='views')
# completer.extend_columns(executor.view_columns(), kind='views')
@refresher("functions")
def refresh_functions(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_functions(executor.functions())
if executor.server_info and executor.server_info.species == ServerSpecies.TiDB:
completer.extend_functions(completer.tidb_functions, builtin=True)
@refresher("procedures")
def refresh_procedures(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_procedures(executor.procedures())
@refresher("character_sets")
def refresh_character_sets(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_character_sets(executor.character_sets())
@refresher("collations")
def refresh_collations(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_collations(executor.collations())
@refresher("special_commands")
def refresh_special(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_special_commands(list(COMMANDS.keys()))
@refresher("show_commands")
def refresh_show_commands(completer: SQLCompleter, executor: SQLExecute) -> None:
completer.extend_show_items(executor.show_candidates())
@refresher("keywords")
def refresh_keywords(completer: SQLCompleter, executor: SQLExecute) -> None:
if executor.server_info and executor.server_info.species == ServerSpecies.TiDB:
completer.extend_keywords(completer.tidb_keywords, replace=True)
================================================
FILE: mycli/config.py
================================================
from copy import copy
from importlib import resources
from io import BytesIO, TextIOWrapper
import logging
import os
from os.path import exists
import struct
import sys
from typing import IO, BinaryIO, Literal
from configobj import ConfigObj, ConfigObjError
from Cryptodome.Cipher import AES
logger = logging.getLogger(__name__)
def log(logger: logging.Logger, level: int, message: str) -> None:
"""Logs message to stderr if logging isn't initialized."""
if logger.parent and logger.parent.name == "root":
print(message, file=sys.stderr)
logger.log(level, message)
def read_config_file(f: str | IO[str], list_values: bool = True) -> ConfigObj | None:
"""Read a config file.
*list_values* set to `True` is the default behavior of ConfigObj.
Disabling it causes values to not be parsed for lists,
(e.g. 'a,b,c' -> ['a', 'b', 'c']. Additionally, the config values are
not unquoted. We are disabling list_values when reading MySQL config files
so we can correctly interpret commas in passwords.
"""
if isinstance(f, str):
f = os.path.expanduser(f)
try:
config = ConfigObj(f, interpolation=False, encoding="utf8", list_values=list_values)
except ConfigObjError as e:
log(logger, logging.WARNING, "Unable to parse line {0} of config file '{1}'.".format(e.line_number, f))
log(logger, logging.WARNING, "Using successfully parsed config values.")
return e.config
except (IOError, OSError) as e:
log(logger, logging.WARNING, "You don't have permission to read config file '{0}'.".format(e.filename))
return None
return config
def get_included_configs(config_file: str | IO[str]) -> list[str | IO[str]]:
"""Get a list of configuration files that are included into config_path
with !includedir directive.
"Normal" configs should be passed as file paths. The only exception
is .mylogin which is decoded into a stream. However, it never
contains include directives and so will be ignored by this
function.
"""
if not isinstance(config_file, str) or not os.path.isfile(config_file):
return []
included_configs: list[str | IO[str]] = []
try:
with open(config_file) as f:
include_directives = filter(lambda s: s.startswith("!includedir"), f)
dirs_split = (s.strip().split()[-1] for s in include_directives)
dirs = filter(os.path.isdir, dirs_split)
for dir_ in dirs:
for filename in os.listdir(dir_):
if filename.endswith(".cnf"):
included_configs.append(os.path.join(dir_, filename))
except (PermissionError, UnicodeDecodeError):
pass
return included_configs
def read_config_files(
files: list[str | IO[str]],
list_values: bool = True,
ignore_package_defaults: bool = False,
ignore_user_options: bool = False,
) -> ConfigObj:
"""Read and merge a list of config files."""
if ignore_package_defaults:
config = ConfigObj()
else:
config = create_default_config(list_values=list_values)
if ignore_user_options:
return config
_files = copy(files)
while _files:
_file = _files.pop(0)
_config = read_config_file(_file, list_values=list_values)
# expand includes only if we were able to parse config
# (otherwise we'll just encounter the same errors again)
if config is not None:
_files = get_included_configs(_file) + _files
if _config is not None:
config.merge(_config)
config.filename = _config.filename
return config
def create_default_config(list_values: bool = True) -> ConfigObj:
import mycli
default_config_file = resources.files(mycli).joinpath("myclirc").open('r')
return read_config_file(default_config_file, list_values=list_values)
def write_default_config(destination: str, overwrite: bool = False) -> None:
import mycli
with resources.files(mycli).joinpath("myclirc").open('r') as f:
default_config = f.read()
destination = os.path.expanduser(destination)
if not overwrite and exists(destination):
return
with open(destination, "w") as f:
f.write(default_config)
def get_mylogin_cnf_path() -> str | None:
"""Return the path to the login path file or None if it doesn't exist."""
mylogin_cnf_path = os.getenv("MYSQL_TEST_LOGIN_FILE")
if mylogin_cnf_path is None:
app_data = os.getenv("APPDATA")
default_dir = os.path.join(app_data, "MySQL") if app_data else "~"
mylogin_cnf_path = os.path.join(default_dir, ".mylogin.cnf")
mylogin_cnf_path = os.path.expanduser(mylogin_cnf_path)
if exists(mylogin_cnf_path):
logger.debug("Found login path file at '{0}'".format(mylogin_cnf_path))
return mylogin_cnf_path
return None
def open_mylogin_cnf(name: str) -> TextIOWrapper | None:
"""Open a readable version of .mylogin.cnf.
Returns the file contents as a TextIOWrapper object.
:param str name: The pathname of the file to be opened.
:return: the login path file or None
"""
try:
with open(name, "rb") as f:
plaintext = read_and_decrypt_mylogin_cnf(f)
except (OSError, IOError, ValueError):
logger.error("Unable to open login path file.")
return None
if not isinstance(plaintext, BytesIO):
logger.error("Unable to read login path file.")
return None
return TextIOWrapper(plaintext)
# TODO reuse code between encryption an decryption
def encrypt_mylogin_cnf(plaintext: IO[str]) -> BytesIO:
"""Encryption of .mylogin.cnf file, analogous to calling
mysql_config_editor.
Code is based on the python implementation by Kristian Koehntopp
https://github.com/isotopp/mysql-config-coder
"""
def realkey(key: bytes) -> bytes:
"""Create the AES key from the login key."""
rkey = bytearray(16)
for i in range(len(key)):
rkey[i % 16] ^= key[i]
return bytes(rkey)
def encode_line(plaintext: str, real_key: bytes, buf_len: int) -> bytes:
aes = AES.new(real_key, AES.MODE_ECB)
text_len = len(plaintext)
pad_len = buf_len - text_len
pad_chr = bytes(chr(pad_len), "utf8")
plaintext_b = plaintext.encode() + pad_chr * pad_len
encrypted_text = b"".join([aes.encrypt(plaintext_b[i : i + 16]) for i in range(0, len(plaintext_b), 16)])
return encrypted_text
LOGIN_KEY_LENGTH = 20
key = os.urandom(LOGIN_KEY_LENGTH)
real_key = realkey(key)
outfile = BytesIO()
outfile.write(struct.pack("i", 0))
outfile.write(key)
while True:
line = plaintext.readline()
if not line:
break
real_len = len(line)
pad_len = (int(real_len / 16) + 1) * 16
outfile.write(struct.pack("i", pad_len))
x = encode_line(line, real_key, pad_len)
outfile.write(x)
outfile.seek(0)
return outfile
def read_and_decrypt_mylogin_cnf(f: BinaryIO) -> BytesIO | None:
"""Read and decrypt the contents of .mylogin.cnf.
This decryption algorithm mimics the code in MySQL's
mysql_config_editor.cc.
The login key is 20-bytes of random non-printable ASCII.
It is written to the actual login path file. It is used
to generate the real key used in the AES cipher.
:param f: an I/O object opened in binary mode
:return: the decrypted login path file
:rtype: io.BytesIO or None
"""
# Number of bytes used to store the length of ciphertext.
MAX_CIPHER_STORE_LEN = 4
LOGIN_KEY_LEN = 20
# Move past the unused buffer.
buf = f.read(4)
if not buf or len(buf) != 4:
logger.error("Login path file is blank or incomplete.")
return None
# Read the login key.
key = f.read(LOGIN_KEY_LEN)
# Generate the real key.
rkey = [0] * 16
for i in range(LOGIN_KEY_LEN):
try:
rkey[i % 16] ^= ord(key[i : i + 1])
except TypeError:
# ord() was unable to get the value of the byte.
logger.error("Unable to generate login path AES key.")
return None
rkey_b = struct.pack("16B", *rkey)
# Create a bytes buffer to hold the plaintext.
plaintext = BytesIO()
aes = AES.new(rkey_b, AES.MODE_ECB)
while True:
# Read the length of the ciphertext.
len_buf = f.read(MAX_CIPHER_STORE_LEN)
if len(len_buf) < MAX_CIPHER_STORE_LEN:
break
(cipher_len,) = struct.unpack("<i", len_buf)
# Read cipher_len bytes from the file and decrypt.
cipher = f.read(cipher_len)
plain = _remove_pad(b"".join([aes.decrypt(cipher[i : i + 16]) for i in range(0, cipher_len, 16)]))
if plain is False:
continue
plaintext.write(plain)
if plaintext.tell() == 0:
logger.error("No data successfully decrypted from login path file.")
return None
plaintext.seek(0)
return plaintext
def str_to_bool(s: str | bool) -> bool:
"""Convert a string value to its corresponding boolean value."""
if isinstance(s, bool):
return s
elif not isinstance(s, str):
raise TypeError("argument must be a string")
true_values = ("true", "on", "1")
false_values = ("false", "off", "0")
if s.lower() in true_values:
return True
elif s.lower() in false_values:
return False
else:
raise ValueError(f'not a recognized boolean value: {s}')
def strip_matching_quotes(s: str) -> str:
"""Remove matching, surrounding quotes from a string.
This is the same logic that ConfigObj uses when parsing config
values.
"""
if isinstance(s, str) and len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"):
s = s[1:-1]
return s
def _remove_pad(line: bytes) -> bytes | Literal[False]:
"""Remove the pad from the *line*."""
try:
# Determine pad length.
pad_length = ord(line[-1:])
except TypeError:
# ord() was unable to get the value of the byte.
logger.warning("Unable to remove pad.")
return False
if pad_length > len(line) or len(set(line[-pad_length:])) != 1:
# Pad length should be less than or equal to the length of the
# plaintext. The pad should have a single unique byte.
logger.warning("Invalid pad found in login path file.")
return False
return line[:-pad_length]
================================================
FILE: mycli/constants.py
================================================
HOME_URL = 'https://mycli.net'
REPO_URL = 'https://github.com/dbcli/mycli'
DOCS_URL = f'{HOME_URL}/docs'
ISSUES_URL = f'{REPO_URL}/issues'
DEFAULT_CHARSET = 'utf8mb4'
DEFAULT_DATABASE = 'mysql'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 3306
DEFAULT_USER = 'root'
TEST_DATABASE = 'mycli_test_db'
================================================
FILE: mycli/key_bindings.py
================================================
import logging
import webbrowser
import prompt_toolkit
from prompt_toolkit.application.current import get_app
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.filters import (
Condition,
completion_is_selected,
control_is_searchable,
emacs_mode,
)
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.selection import SelectionType
from mycli.constants import DOCS_URL
from mycli.packages import shortcuts
from mycli.packages.toolkit.fzf import search_history
from mycli.packages.toolkit.utils import safe_invalidate_display
_logger = logging.getLogger(__name__)
@Condition
def ctrl_d_condition() -> bool:
"""Ctrl-D exit binding is only active when the buffer is empty."""
app = get_app()
return not app.current_buffer.text
@Condition
def in_completion() -> bool:
app = get_app()
return bool(app.current_buffer.complete_state)
def print_f1_help():
app = get_app()
app.print_text('\n')
app.print_text([
('', 'Inline help — type "'),
('bold', 'help'),
('', '" or "'),
('bold', r'\?'),
('', '"\n'),
])
app.print_text([
('', 'Docs index — '),
('bold', DOCS_URL),
('', '\n'),
])
app.print_text('\n')
def mycli_bindings(mycli) -> KeyBindings:
"""Custom key bindings for mycli."""
kb = KeyBindings()
@kb.add('f1')
def _(event: KeyPressEvent) -> None:
"""Open browser to documentation index."""
_logger.debug('Detected F1 key.')
webbrowser.open_new_tab(DOCS_URL)
prompt_toolkit.application.run_in_terminal(print_f1_help)
safe_invalidate_display(event.app)
@kb.add('escape', '[', 'P')
def _(event: KeyPressEvent) -> None:
"""Open browser to documentation index."""
_logger.debug("Detected alternate F1 key sequence.")
webbrowser.open_new_tab(DOCS_URL)
prompt_toolkit.application.run_in_terminal(print_f1_help)
safe_invalidate_display(event.app)
@kb.add("f2")
def _(_event: KeyPressEvent) -> None:
"""Enable/Disable SmartCompletion Mode."""
_logger.debug("Detected F2 key.")
mycli.completer.smart_completion = not mycli.completer.smart_completion
@kb.add('escape', '[', 'Q')
def _(_event: KeyPressEvent) -> None:
"""Enable/Disable SmartCompletion Mode."""
_logger.debug("Detected alternate F2 key sequence.")
mycli.completer.smart_completion = not mycli.completer.smart_completion
@kb.add("f3")
def _(_event: KeyPressEvent) -> None:
"""Enable/Disable Multiline Mode."""
_logger.debug("Detected F3 key.")
mycli.multi_line = not mycli.multi_line
@kb.add('escape', '[', 'R')
def _(_event: KeyPressEvent) -> None:
"""Enable/Disable Multiline Mode."""
_logger.debug('Detected alternate F3 key sequence.')
mycli.multi_line = not mycli.multi_line
@kb.add("f4")
def _(event: KeyPressEvent) -> None:
"""Toggle between Vi and Emacs mode."""
_logger.debug("Detected F4 key.")
if mycli.key_bindings == "vi":
event.app.editing_mode = EditingMode.EMACS
mycli.key_bindings = "emacs"
event.app.ttimeoutlen = mycli.emacs_ttimeoutlen
else:
event.app.editing_mode = EditingMode.VI
mycli.key_bindings = "vi"
event.app.ttimeoutlen = mycli.vi_ttimeoutlen
@kb.add('escape', '[', 'S')
def _(event: KeyPressEvent) -> None:
"""Toggle between Vi and Emacs mode."""
_logger.debug('Detected alternate F4 key sequence.')
if mycli.key_bindings == 'vi':
event.app.editing_mode = EditingMode.EMACS
mycli.key_bindings = 'emacs'
event.app.ttimeoutlen = mycli.emacs_ttimeoutlen
else:
event.app.editing_mode = EditingMode.VI
mycli.key_bindings = 'vi'
event.app.ttimeoutlen = mycli.vi_ttimeoutlen
@kb.add("tab")
def _(event: KeyPressEvent) -> None:
"""Complete action at cursor."""
_logger.debug("Detected <Tab> key.")
b = event.app.current_buffer
behaviors = mycli.config['keys'].as_list('tab')
if 'toolkit_default' in behaviors:
if b.complete_state:
b.complete_next()
else:
b.start_completion(select_first=True)
if b.complete_state:
if 'advance' in behaviors:
b.complete_next()
elif 'cancel' in behaviors:
b.cancel_completion()
return
if 'advancing_summon' in behaviors:
b.start_completion(select_first=True)
elif 'prefixing_summon' in behaviors:
b.start_completion(insert_common_part=True)
elif 'summon' in behaviors:
b.start_completion(select_first=False)
@kb.add("escape", eager=True, filter=in_completion)
def _(event: KeyPressEvent) -> None:
"""Cancel completion menu.
There will be a lag when canceling Escape due to the processing of
Alt- keystrokes as Escape- sequences.
There will be no lag when using control-g to cancel."""
event.app.current_buffer.cancel_completion()
@kb.add("c-space")
def _(event: KeyPressEvent) -> None:
"""
Complete action at cursor.
By default, 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
behaviors = mycli.config['keys'].as_list('control_space')
if 'toolkit_default' in behaviors:
if b.text:
b.start_selection(selection_type=SelectionType.CHARACTERS)
return
if b.complete_state:
if 'advance' in behaviors:
b.complete_next()
elif 'cancel' in behaviors:
b.cancel_completion()
return
if 'advancing_summon' in behaviors:
b.start_completion(select_first=True)
elif 'prefixing_summon' in behaviors:
b.start_completion(insert_common_part=True)
elif 'summon' in behaviors:
b.start_completion(select_first=False)
@kb.add("c-x", "p", filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Prettify and indent current statement, usually into multiple lines.
Only accepts buffers containing single SQL statements.
"""
_logger.debug("Detected <C-x p>/> key.")
b = event.app.current_buffer
if b.text:
b.transform_region(0, len(b.text), mycli.handle_prettify_binding)
@kb.add("c-x", "u", filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Unprettify and dedent current statement, usually into one line.
Only accepts buffers containing single SQL statements.
"""
_logger.debug("Detected <C-x u>/< key.")
b = event.app.current_buffer
if b.text:
b.transform_region(0, len(b.text), mycli.handle_unprettify_binding)
@kb.add("c-o", "d", filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Insert the current date.
"""
_logger.debug("Detected <C-o d> key.")
event.app.current_buffer.insert_text(shortcuts.server_date(mycli.sqlexecute))
@kb.add("c-o", "c-d", filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Insert the quoted current date.
"""
_logger.debug("Detected <C-o C-d> key.")
event.app.current_buffer.insert_text(shortcuts.server_date(mycli.sqlexecute, quoted=True))
@kb.add("c-o", "t", filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Insert the current datetime.
"""
_logger.debug("Detected <C-o t> key.")
event.app.current_buffer.insert_text(shortcuts.server_datetime(mycli.sqlexecute))
@kb.add("c-o", "c-t", filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Insert the quoted current datetime.
"""
_logger.debug("Detected <C-o C-t> key.")
event.app.current_buffer.insert_text(shortcuts.server_datetime(mycli.sqlexecute, quoted=True))
@kb.add("c-r", filter=control_is_searchable)
def _(event: KeyPressEvent) -> None:
"""Search history using fzf or reverse incremental search."""
_logger.debug("Detected <C-r> key.")
mode = mycli.config.get('keys', {}).get('control_r', 'auto')
if mode == 'reverse_isearch':
search_history(event, incremental=True)
else:
search_history(
event,
highlight_preview=mycli.highlight_preview,
highlight_style=mycli.syntax_style,
)
@kb.add("escape", "r", filter=control_is_searchable & emacs_mode)
def _(event: KeyPressEvent) -> None:
"""Search history using fzf when available."""
_logger.debug("Detected <alt-r> key.")
search_history(
event,
highlight_preview=mycli.highlight_preview,
highlight_style=mycli.syntax_style,
)
@kb.add('c-d', filter=ctrl_d_condition)
def _(event: KeyPressEvent) -> None:
"""Exit mycli or ignore keypress."""
_logger.debug('Detected <C-d> key on empty line.')
mode = mycli.config.get('keys', {}).get('control_d', 'exit')
if mode == 'exit':
event.app.exit(exception=EOFError, style='class:exiting')
else:
event.app.output.bell()
@kb.add("enter", filter=completion_is_selected)
def _(event: KeyPressEvent) -> None:
"""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.")
event.current_buffer.complete_state = None
b = event.app.current_buffer
b.complete_state = None
@kb.add("escape", "enter")
def _(event: KeyPressEvent) -> None:
"""Introduces a line break in multi-line mode, or dispatches the
command in single-line mode."""
_logger.debug("Detected alt-enter key.")
if mycli.multi_line:
event.app.current_buffer.validate_and_handle()
else:
event.app.current_buffer.insert_text("\n")
return kb
================================================
FILE: mycli/lexer.py
================================================
from pygments.lexer import inherit
from pygments.lexers.sql import MySqlLexer
from pygments.token import Keyword
class MyCliLexer(MySqlLexer):
"""Extends MySQL lexer to add keywords."""
tokens = {
"root": [(r"\brepair\b", Keyword), (r"\boffset\b", Keyword), inherit],
}
================================================
FILE: mycli/magic.py
================================================
import logging
from typing import Any
import sql.connection
import sql.parse
from mycli.main import MyCli, Query
_logger: logging.Logger = logging.getLogger(__name__)
def load_ipython_extension(ipython) -> None:
# This is called via the ipython command '%load_ext mycli.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(mycli_line_magic, "line", "mycli")
def mycli_line_magic(line: str):
_logger.debug("mycli magic called: %r", line)
parsed: dict[str, Any] = 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 mycli object already exists
mycli: MyCli = conn._mycli
_logger.debug("Reusing existing mycli")
except AttributeError:
mycli = MyCli()
u = conn.session.engine.url
_logger.debug("New mycli: %r", str(u))
mycli.connect(host=u.host, port=u.port, passwd=u.password, database=u.database, user=u.username, init_command=None)
conn._mycli = mycli
# For convenience, print the connection alias
print(f'Connected: {conn.name}')
try:
mycli.run_cli()
except SystemExit:
pass
if not mycli.query_history:
return
q: Query = mycli.query_history[-1]
if q.mutating:
_logger.debug("Mutating query detected -- ignoring")
return
if q.successful:
ipython = get_ipython() # type: ignore # noqa: F821
return ipython.run_cell_magic("sql", line, q.query)
================================================
FILE: mycli/main.py
================================================
from __future__ import annotations
from collections import defaultdict, namedtuple
from decimal import Decimal
import functools
from io import TextIOWrapper
import logging
import os
import random
import re
import shutil
import subprocess
import sys
import threading
import traceback
from typing import IO, Any, Callable, Generator, Iterable, Literal
try:
from pwd import getpwuid
except ImportError:
pass
from datetime import datetime
from importlib import resources
import itertools
from random import choice
from textwrap import dedent
from time import sleep, time
from urllib.parse import parse_qs, unquote, urlparse
import warnings
from cli_helpers.tabular_output import TabularOutputFormatter, preprocessors
from cli_helpers.tabular_output.output_formatter import MISSING_VALUE as DEFAULT_MISSING_VALUE
from cli_helpers.utils import strip_ansi
import click
from configobj import ConfigObj
import keyring
from prompt_toolkit import print_formatted_text
from prompt_toolkit.application.current import get_app
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ThreadedAutoSuggest
from prompt_toolkit.completion import Completion, DynamicCompleter
from prompt_toolkit.document import Document
from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
from prompt_toolkit.filters import Condition, HasFocus, IsDone
from prompt_toolkit.formatted_text import (
ANSI,
HTML,
AnyFormattedText,
FormattedText,
to_formatted_text,
to_plain_text,
)
from prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.output import ColorDepth
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
import pymysql
from pymysql.constants.CR import CR_SERVER_LOST
from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR
from pymysql.cursors import Cursor
import sqlparse
with warnings.catch_warnings():
# for sqlglot v29.0.1
warnings.filterwarnings(
'ignore',
message=r'sqlglot\[rs\] is deprecated',
category=UserWarning,
module='sqlglot',
)
import sqlglot
from mycli import __version__
from mycli.clibuffer import cli_is_multiline
from mycli.clistyle import style_factory_helpers, style_factory_toolkit
from mycli.clitoolbar import create_toolbar_tokens_func
from mycli.compat import WIN
from mycli.completion_refresher import CompletionRefresher
from mycli.config import get_mylogin_cnf_path, open_mylogin_cnf, read_config_files, str_to_bool, strip_matching_quotes, write_default_config
from mycli.constants import (
DEFAULT_CHARSET,
DEFAULT_HOST,
DEFAULT_PORT,
HOME_URL,
ISSUES_URL,
REPO_URL,
)
from mycli.key_bindings import mycli_bindings
from mycli.lexer import MyCliLexer
from mycli.packages import special
from mycli.packages.checkup import do_checkup
from mycli.packages.filepaths import dir_path_exists, guess_socket_location
from mycli.packages.hybrid_redirection import get_redirect_components, is_redirect_command
from mycli.packages.parseutils import is_destructive, is_dropping_database, is_valid_connection_scheme
from mycli.packages.prompt_utils import confirm, confirm_destructive_query
from mycli.packages.special.favoritequeries import FavoriteQueries
from mycli.packages.special.main import ArgType
from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count
from mycli.packages.sqlresult import SQLResult
from mycli.packages.string_utils import sanitize_terminal_title
from mycli.packages.tabular_output import sql_format
from mycli.packages.toolkit.history import FileHistoryWithTimestamp
from mycli.sqlcompleter import SQLCompleter
from mycli.sqlexecute import FIELD_TYPES, SQLExecute
try:
import paramiko
except ImportError:
from mycli.packages.paramiko_stub import paramiko # type: ignore[no-redef]
sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
sqlparse.engine.grouping.MAX_GROUPING_TOKENS = None # type: ignore[assignment]
# Query tuples are used for maintaining history
Query = namedtuple("Query", ["query", "successful", "mutating"])
SUPPORT_INFO = f"Home: {HOME_URL}\nBug tracker: {ISSUES_URL}"
DEFAULT_WIDTH = 80
DEFAULT_HEIGHT = 25
MIN_COMPLETION_TRIGGER = 1
MAX_MULTILINE_BATCH_STATEMENT = 5000
EMPTY_PASSWORD_FLAG_SENTINEL = -1
@Condition
def complete_while_typing_filter() -> bool:
"""Whether enough characters have been typed to trigger completion.
Written in a verbose way, with a string slice, for efficiency."""
if MIN_COMPLETION_TRIGGER <= 1:
return True
app = get_app()
text = app.current_buffer.text.lstrip()
text_len = len(text)
if text_len < MIN_COMPLETION_TRIGGER:
return False
last_word = text[-MIN_COMPLETION_TRIGGER:]
if len(last_word) == text_len:
return text_len >= MIN_COMPLETION_TRIGGER
if text[:6].lower() in ['source', r'\.']:
# Different word characters for paths; see comment below.
# In fact, it might be nice if paths had a different threshold.
return not bool(re.search(r'[\s!-,:-@\[-^\{\}-]', last_word))
else:
# This is "whitespace and all punctuation except underscore and backtick"
# acting as word breaks, but it would be neat if we could complete differently
# when inside a backtick, accepting all legal characters towards the trigger
# limit. We would have to parse the statement, or at least go back more
# characters, costing performance. This still works within a backtick! So
# long as there are three trailing non-punctuation characters.
return not bool(re.search(r'[\s!-/:-@\[-^\{-~]', last_word))
class IntOrStringClickParamType(click.ParamType):
name = 'string' # display as STRING in helpdoc
def convert(self, value, param, ctx):
if isinstance(value, int):
return value
elif isinstance(value, str):
return value
elif value is None:
return value
else:
self.fail('Not a valid password string', param, ctx)
INT_OR_STRING_CLICK_TYPE = IntOrStringClickParamType()
class MyCli:
default_prompt = "\\t \\u@\\h:\\d> "
default_prompt_splitln = "\\u@\\h\\n(\\t):\\d>"
max_len_prompt = 45
defaults_suffix = None
# In order of being loaded. Files lower in list override earlier ones.
cnf_files: list[str | IO[str]] = [
"/etc/my.cnf",
"/etc/mysql/my.cnf",
"/usr/local/etc/my.cnf",
os.path.expanduser("~/.my.cnf"),
]
# check XDG_CONFIG_HOME exists and not an empty string
xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config")
system_config_files: list[str | IO[str]] = [
"/etc/myclirc",
os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc"),
]
pwd_config_file = os.path.join(os.getcwd(), ".myclirc")
def __init__(
self,
sqlexecute: SQLExecute | None = None,
prompt: str | None = None,
toolbar_format: str | None = None,
logfile: TextIOWrapper | Literal[False] | None = None,
defaults_suffix: str | None = None,
defaults_file: str | None = None,
login_path: str | None = None,
auto_vertical_output: bool = False,
show_warnings: bool = False,
warn: bool | None = None,
myclirc: str = "~/.myclirc",
) -> None:
global MIN_COMPLETION_TRIGGER
self.sqlexecute = sqlexecute
self.logfile = logfile
self.defaults_suffix = defaults_suffix
self.login_path = login_path
self.toolbar_error_message: str | None = None
self.prompt_app: PromptSession | None = None
self._keepalive_counter = 0
self.keepalive_ticks: int | None = 0
# self.cnf_files is a class variable that stores the list of mysql
# config files to read in at launch.
# If defaults_file is specified then override the class variable with
# defaults_file.
if defaults_file:
self.cnf_files = [defaults_file]
# Load config.
config_files: list[str | IO[str]] = self.system_config_files + [myclirc] + [self.pwd_config_file]
c = self.config = read_config_files(config_files)
# this parallel config exists to
# * compare with my.cnf
# * support the --checkup feature
# todo: after removing my.cnf, create the parallel configs only when --checkup is set
self.config_without_package_defaults = read_config_files(config_files, ignore_package_defaults=True)
# this parallel config exists to compare with my.cnf support the --checkup feature
self.config_without_user_options = read_config_files(config_files, ignore_user_options=True)
self.multi_line = c["main"].as_bool("multi_line")
self.key_bindings = c["main"]["key_bindings"]
self.emacs_ttimeoutlen = c['keys'].as_float('emacs_ttimeoutlen')
self.vi_ttimeoutlen = c['keys'].as_float('vi_ttimeoutlen')
special.set_timing_enabled(c["main"].as_bool("timing"))
special.set_show_favorite_query(c["main"].as_bool("show_favorite_query"))
self.beep_after_seconds = float(c["main"]["beep_after_seconds"] or 0)
self.default_keepalive_ticks = c['connection'].as_int('default_keepalive_ticks')
FavoriteQueries.instance = FavoriteQueries.from_config(self.config)
self.dsn_alias: str | None = None
self.main_formatter = TabularOutputFormatter(format_name=c["main"]["table_format"])
self.redirect_formatter = TabularOutputFormatter(format_name=c["main"].get("redirect_format", "csv"))
sql_format.register_new_formatter(self.main_formatter)
sql_format.register_new_formatter(self.redirect_formatter)
self.main_formatter.mycli = self
self.redirect_formatter.mycli = self
self.syntax_style = c["main"]["syntax_style"]
self.less_chatty = c["main"].as_bool("less_chatty")
self.cli_style = c["colors"]
self.toolkit_style = style_factory_toolkit(self.syntax_style, self.cli_style)
self.helpers_style = style_factory_helpers(self.syntax_style, self.cli_style)
self.helpers_warnings_style = style_factory_helpers(self.syntax_style, self.cli_style, warnings=True)
self.wider_completion_menu = c["main"].as_bool("wider_completion_menu")
c_dest_warning = c["main"].as_bool("destructive_warning")
self.destructive_warning = c_dest_warning if warn is None else warn
self.login_path_as_host = c["main"].as_bool("login_path_as_host")
self.post_redirect_command = c['main'].get('post_redirect_command')
self.null_string = c['main'].get('null_string')
self.numeric_alignment = c['main'].get('numeric_alignment', 'right')
self.binary_display = c['main'].get('binary_display')
if 'llm' in c and re.match(r'^\d+$', c['llm'].get('prompt_field_truncate', '')):
self.llm_prompt_field_truncate = int(c['llm'].get('prompt_field_truncate'))
else:
self.llm_prompt_field_truncate = 0
if 'llm' in c and re.match(r'^\d+$', c['llm'].get('prompt_section_truncate', '')):
self.llm_prompt_section_truncate = int(c['llm'].get('prompt_section_truncate'))
else:
self.llm_prompt_section_truncate = 0
# set ssl_mode if a valid option is provided in a config file, otherwise None
ssl_mode = c["main"].get("ssl_mode", None) or c["connection"].get("default_ssl_mode", None)
if ssl_mode not in ("auto", "on", "off", None):
self.echo(f"Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.", err=True, fg="red")
self.ssl_mode = None
else:
self.ssl_mode = ssl_mode
# read from cli argument or user config file
self.auto_vertical_output = auto_vertical_output or c["main"].as_bool("auto_vertical_output")
self.show_warnings = show_warnings or c["main"].as_bool("show_warnings")
# Write user config if system config wasn't the last config loaded.
if c.filename not in self.system_config_files and not os.path.exists(myclirc):
write_default_config(myclirc)
# audit log
if self.logfile is None and "audit_log" in c["main"]:
try:
self.logfile = open(os.path.expanduser(c["main"]["audit_log"]), "a")
except (IOError, OSError):
self.echo("Error: Unable to open the audit log file. Your queries will not be logged.", err=True, fg="red")
self.logfile = False
self.completion_refresher = CompletionRefresher()
self.logger = logging.getLogger(__name__)
self.initialize_logging()
keyword_casing = c["main"].get("keyword_casing", "auto")
self.highlight_preview = c['search'].as_bool('highlight_preview')
self.query_history: list[Query] = []
# Initialize completer.
self.smart_completion = c["main"].as_bool("smart_completion")
self.completer = SQLCompleter(
self.smart_completion, supported_formats=self.main_formatter.supported_formats, keyword_casing=keyword_casing
)
self._completer_lock = threading.Lock()
self.min_completion_trigger = c["main"].as_int("min_completion_trigger")
MIN_COMPLETION_TRIGGER = self.min_completion_trigger
self.last_prompt_message = ANSI('')
self.last_custom_toolbar_message = ANSI('')
# Register custom special commands.
self.register_special_commands()
# Load .mylogin.cnf if it exists.
mylogin_cnf_path = get_mylogin_cnf_path()
if mylogin_cnf_path:
mylogin_cnf = open_mylogin_cnf(mylogin_cnf_path)
if mylogin_cnf_path and mylogin_cnf:
# .mylogin.cnf gets read last, even if defaults_file is specified.
self.cnf_files.append(mylogin_cnf)
elif mylogin_cnf_path and not mylogin_cnf:
# There was an error reading the login path file.
print("Error: Unable to read login path file.")
self.my_cnf = read_config_files(self.cnf_files, list_values=False)
if not self.my_cnf.get('client'):
self.my_cnf['client'] = {}
if not self.my_cnf.get('mysqld'):
self.my_cnf['mysqld'] = {}
prompt_cnf = self.read_my_cnf(self.my_cnf, ["prompt"])["prompt"]
self.prompt_format = prompt or prompt_cnf or c["main"]["prompt"] or self.default_prompt
self.prompt_lines = 0
self.multiline_continuation_char = c["main"]["prompt_continuation"]
self.toolbar_format = toolbar_format or c['main']['toolbar']
self.terminal_tab_title_format = c['main']['terminal_tab_title']
self.terminal_window_title_format = c['main']['terminal_window_title']
self.multiplex_window_title_format = c['main']['multiplex_window_title']
self.multiplex_pane_title_format = c['main']['multiplex_pane_title']
self.prompt_app = None
self.destructive_keywords = [
keyword for keyword in c["main"].get("destructive_keywords", "DROP SHUTDOWN DELETE TRUNCATE ALTER UPDATE").split(' ') if keyword
]
special.set_destructive_keywords(self.destructive_keywords)
def close(self) -> None:
if self.sqlexecute is not None:
self.sqlexecute.close()
def register_special_commands(self) -> None:
special.register_special_command(self.change_db, "use", "use <database>", "Change to a new database.", aliases=["\\u"])
special.register_special_command(
self.manual_reconnect,
"connect",
"connect [database]",
"Reconnect to the server, optionally switching databases.",
aliases=["\\r"],
case_sensitive=True,
)
special.register_special_command(
self.refresh_completions, "rehash", "rehash", "Refresh auto-completions.", arg_type=ArgType.NO_QUERY, aliases=["\\#"]
)
special.register_special_command(
self.change_table_format,
"tableformat",
"tableformat <format>",
"Change the table format used to output interactive results.",
aliases=["\\T"],
case_sensitive=True,
)
special.register_special_command(
self.change_redirect_format,
"redirectformat",
"redirectformat <format>",
"Change the table format used to output redirected results.",
aliases=["\\Tr"],
case_sensitive=True,
)
special.register_special_command(
self.disable_show_warnings,
"nowarnings",
"nowarnings",
"Disable automatic warnings display.",
aliases=["\\w"],
case_sensitive=True,
)
special.register_special_command(
self.enable_show_warnings,
"warnings",
"warnings",
"Enable automatic warnings display.",
aliases=["\\W"],
case_sensitive=True,
)
special.register_special_command(
self.execute_from_file, "source", "source <filename>", "Execute queries from a file.", aliases=["\\."]
)
special.register_special_command(
self.change_prompt_format, "prompt", "prompt <string>", "Change prompt format.", aliases=["\\R"], case_sensitive=True
)
def manual_reconnect(self, arg: str = "", **_) -> Generator[SQLResult, None, None]:
"""
Interactive method to use for the \r command, so that the utility method
may be cleanly used elsewhere.
"""
if not self.reconnect(database=arg):
yield SQLResult(status="Not connected")
elif not arg or arg == '``':
yield SQLResult()
else:
yield self.change_db(arg).send(None)
def enable_show_warnings(self, **_) -> Generator[SQLResult, None, None]:
self.show_warnings = True
msg = "Show warnings enabled."
yield SQLResult(status=msg)
def disable_show_warnings(self, **_) -> Generator[SQLResult, None, None]:
self.show_warnings = False
msg = "Show warnings disabled."
yield SQLResult(status=msg)
def change_table_format(self, arg: str, **_) -> Generator[SQLResult, None, None]:
try:
self.main_formatter.format_name = arg
yield SQLResult(status=f"Changed table format to {arg}")
except ValueError:
msg = f"Table format {arg} not recognized. Allowed formats:"
for table_type in self.main_formatter.supported_formats:
msg += f"\n\t{table_type}"
yield SQLResult(status=msg)
def change_redirect_format(self, arg: str, **_) -> Generator[SQLResult, None, None]:
try:
self.redirect_formatter.format_name = arg
yield SQLResult(status=f"Changed redirect format to {arg}")
except ValueError:
msg = f"Redirect format {arg} not recognized. Allowed formats:"
for table_type in self.redirect_formatter.supported_formats:
msg += f"\n\t{table_type}"
yield SQLResult(status=msg)
def change_db(self, arg: str, **_) -> Generator[SQLResult, None, None]:
if arg.startswith("`") and arg.endswith("`"):
arg = re.sub(r"^`(.*)`$", r"\1", arg)
arg = re.sub(r"``", r"`", arg)
if not arg:
click.secho("No database selected", err=True, fg="red")
return
assert isinstance(self.sqlexecute, SQLExecute)
if self.sqlexecute.dbname == arg:
msg = f'You are already connected to database "{self.sqlexecute.dbname}" as user "{self.sqlexecute.user}"'
else:
self.sqlexecute.change_db(arg)
msg = f'You are now connected to database "{self.sqlexecute.dbname}" as user "{self.sqlexecute.user}"'
self.set_all_external_titles()
yield SQLResult(status=msg)
def execute_from_file(self, arg: str, **_) -> Iterable[SQLResult]:
if not arg:
message = "Missing required argument: filename."
return [SQLResult(status=message)]
try:
with open(os.path.expanduser(arg)) as f:
query = f.read()
except IOError as e:
return [SQLResult(status=str(e))]
if self.destructive_warning and confirm_destructive_query(self.destructive_keywords, query) is False:
message = "Wise choice. Command execution stopped."
return [SQLResult(status=message)]
assert isinstance(self.sqlexecute, SQLExecute)
return self.sqlexecute.run(query)
def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
"""
Change the prompt format.
"""
if not arg:
message = "Missing required argument, format."
return [SQLResult(status=message)]
self.prompt_format = arg
return [SQLResult(status=f"Changed prompt format to {arg}")]
def initialize_logging(self) -> None:
log_file = os.path.expanduser(self.config["main"]["log_file"])
log_level = self.config["main"]["log_level"]
level_map = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
# 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.Handler = logging.NullHandler()
log_level = "CRITICAL"
elif dir_path_exists(log_file):
handler = logging.FileHandler(log_file)
else:
self.echo(f'Error: Unable to open the log file "{log_file}".', err=True, fg="red")
return
formatter = logging.Formatter("%(asctime)s (%(process)d/%(threadName)s) %(name)s %(levelname)s - %(message)s")
handler.setFormatter(formatter)
root_logger = logging.getLogger("mycli")
root_logger.addHandler(handler)
root_logger.setLevel(level_map[log_level.upper()])
logging.captureWarnings(True)
root_logger.debug("Initializing mycli logging.")
root_logger.debug("Log file %r.", log_file)
def read_my_cnf(self, cnf: ConfigObj, keys: list[str]) -> dict[str, Any]:
"""
Retrieves some keys from a configuration, applies transformations, returns a new configuration.
:param cnf: configuration to read
:param keys: list of keys to retrieve
:returns: tuple, with None for missing keys.
"""
sections = ["client", "mysqld"]
key_transformations = {
"mysqld": {
"socket": "default_socket",
"port": "default_port",
"user": "default_user",
},
}
if self.login_path and self.login_path != "client":
sections.append(self.login_path)
if self.defaults_suffix:
sections.extend([sect + self.defaults_suffix for sect in sections])
configuration: dict[str, Any] = defaultdict(lambda: None)
for key in keys:
for section in cnf:
if section not in sections or key not in cnf[section]:
continue
new_key = key_transformations.get(section, {}).get(key) or key
configuration[new_key] = strip_matching_quotes(cnf[section][key])
return configuration
def merge_ssl_with_cnf(self, ssl: dict[str, Any], cnf: dict[str, Any]) -> dict[str, Any]:
"""Merge SSL configuration dict with cnf dict"""
merged = {}
merged.update(ssl)
prefix = "ssl-"
for k, v in cnf.items():
# skip unrelated options
if not k.startswith(prefix):
continue
if v is None:
continue
# special case because PyMySQL argument is significantly different
# from commandline
if k == "ssl-verify-server-cert":
merged["check_hostname"] = str_to_bool(v)
else:
# use argument name just strip "ssl-" prefix
arg = k[len(prefix) :]
merged[arg] = v
return merged
def connect(
self,
database: str | None = "",
user: str | None = "",
passwd: str | int | None = None,
host: str | None = "",
port: str | int | None = "",
socket: str | None = "",
character_set: str | None = "",
local_infile: bool = False,
ssl: dict[str, Any] | None = None,
ssh_user: str | None = "",
ssh_host: str | None = "",
ssh_port: int = 22,
ssh_password: str | None = "",
ssh_key_filename: str | None = "",
init_command: str | None = "",
unbuffered: bool | None = None,
use_keyring: bool | None = None,
reset_keyring: bool | None = None,
keepalive_ticks: int | None = None,
) -> None:
cnf = {
"database": None,
"user": None,
"password": None,
"host": None,
"port": None,
"socket": None,
"default_socket": None,
"default-character-set": None,
"local-infile": None,
"loose-local-infile": None,
"ssl-ca": None,
"ssl-cert": None,
"ssl-key": None,
"ssl-cipher": None,
"ssl-verify-server-cert": None,
}
cnf = self.read_my_cnf(self.my_cnf, list(cnf.keys()))
# Fall back to config values only if user did not specify a value.
database = database or cnf["database"]
user = user or cnf["user"] or os.getenv("USER")
host = host or cnf["host"]
port = port or cnf["port"]
ssl_config: dict[str, Any] = ssl or {}
user_connection_config = self.config_without_package_defaults.get('connection', {})
self.keepalive_ticks = keepalive_ticks
int_port = port and int(port)
if not int_port:
int_port = DEFAULT_PORT
if not host or host == DEFAULT_HOST:
socket = (
socket
or user_connection_config.get("default_socket")
or cnf["socket"]
or cnf["default_socket"]
or guess_socket_location()
)
passwd = passwd if isinstance(passwd, (str, int)) else cnf["password"]
# default_character_set doesn't check in self.config_without_package_defaults, because the
# option already existed before the my.cnf deprecation. For the same reason,
# default_character_set can be in [connection] or [main].
if not character_set:
if 'default_character_set' in self.config['connection']:
character_set = self.config['connection']['default_character_set']
elif 'default_character_set' in self.config['main']:
character_set = self.config['main']['default_character_set']
elif 'default_character_set' in cnf:
character_set = cnf['default_character_set']
elif 'default-character-set' in cnf:
character_set = cnf['default-character-set']
if not character_set:
character_set = DEFAULT_CHARSET
# Favor whichever local_infile option is set.
use_local_infile = False
for local_infile_option in (
local_infile,
user_connection_config.get('default_local_infile'),
cnf['local_infile'],
cnf['local-infile'],
cnf['loose_local_infile'],
cnf['loose-local-infile'],
False,
):
try:
use_local_infile = str_to_bool(local_infile_option or '')
break
except (TypeError, ValueError):
pass
# temporary my.cnf override mappings
if 'default_ssl_ca' in user_connection_config:
cnf['ssl-ca'] = user_connection_config.get('default_ssl_ca') or None
if 'default_ssl_cert' in user_connection_config:
cnf['ssl-cert'] = user_connection_config.get('default_ssl_cert') or None
if 'default_ssl_key' in user_connection_config:
cnf['ssl-key'] = user_connection_config.get('default_ssl_key') or None
if 'default_ssl_cipher' in user_connection_config:
cnf['ssl-cipher'] = user_connection_config.get('default_ssl_cipher') or None
if 'default_ssl_verify_server_cert' in user_connection_config:
cnf['ssl-verify-server-cert'] = user_connection_config.get('default_ssl_verify_server_cert') or None
# todo: rewrite the merge method using self.config['connection'] instead of cnf, after removing my.cnf support
ssl_config_or_none: dict[str, Any] | None = self.merge_ssl_with_cnf(ssl_config, cnf)
# default_ssl_ca_path is not represented in my.cnf
if 'default_ssl_ca_path' in self.config['connection'] and (not ssl_config_or_none or not ssl_config_or_none.get('capath')):
if ssl_config_or_none is None:
ssl_config_or_none = {}
ssl_config_or_none['capath'] = self.config['connection']['default_ssl_ca_path'] or False
# prune lone check_hostname=False
if not any(v for v in ssl_config.values()):
ssl_config_or_none = None
# password hierarchy
# 1. -p / --pass/--password CLI options
# 2. --password-file CLI option
# 3. envvar (MYSQL_PWD)
# 4. DSN (mysql://user:password)
# 5. cnf (.my.cnf / etc)
# 6. keyring
keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}'
keyring_domain = 'mycli.net'
keyring_retrieved_cleanly = False
if passwd is None and use_keyring and not reset_keyring:
passwd = keyring.get_password(keyring_domain, keyring_identifier)
if passwd is not None:
keyring_retrieved_cleanly = True
# prompt for password if requested by user
if passwd == EMPTY_PASSWORD_FLAG_SENTINEL:
passwd = click.prompt(f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True)
keyring_retrieved_cleanly = False
# should not fail, but will help the typechecker
assert not isinstance(passwd, int)
connection_info: dict[Any, Any] = {
"database": database,
"user": user,
"password": passwd,
"host": host,
"port": int_port,
"socket": socket,
"character_set": character_set,
"local_infile": use_local_infile,
"ssl": ssl_config_or_none,
"ssh_user": ssh_user,
"ssh_host": ssh_host,
"ssh_port": int(ssh_port) if ssh_port else None,
"ssh_password": ssh_password,
"ssh_key_filename": ssh_key_filename,
"init_command": init_command,
"unbuffered": unbuffered,
}
def _update_keyring(password: str | None, keyring_retrieved_cleanly: bool):
if not password:
return
if reset_keyring or (use_keyring and not keyring_retrieved_cleanly):
try:
saved_pw = keyring.get_password(keyring_domain, keyring_identifier)
if password != saved_pw or reset_keyring:
keyring.set_password(keyring_domain, keyring_identifier, password)
click.secho(f'Password saved to the system keyring at {keyring_domain}/{keyring_identifier}', err=True)
except Exception as e:
click.secho(f'Password not saved to the system keyring: {e}', err=True, fg='red')
def _connect(
retry_ssl: bool = False,
retry_password: bool = False,
keyring_save_eligible: bool = True,
keyring_retrieved_cleanly: bool = False,
) -> None:
try:
if keyring_save_eligible:
_update_keyring(connection_info["password"], keyring_retrieved_cleanly=keyring_retrieved_cleanly)
self.sqlexecute = SQLExecute(**connection_info)
except pymysql.OperationalError as e1:
if e1.args[0] == HANDSHAKE_ERROR and ssl is not None and ssl.get("mode", None) == "auto":
# if we already tried and failed to connect without SSL, raise the error
if retry_ssl:
raise e1
# disable SSL and try to connect again
connection_info["ssl"] = None
_connect(
retry_ssl=True, keyring_retrieved_cleanly=keyring_retrieved_cleanly, keyring_save_eligible=keyring_save_eligible
)
elif e1.args[0] == ACCESS_DENIED_ERROR and connection_info["password"] is None:
# if we already tried and failed to connect with a new password, raise the error
if retry_password:
raise e1
# ask the user for a new password and try to connect again
new_password = click.prompt(
f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True
)
connection_info["password"] = new_password
keyring_retrieved_cleanly = False
_connect(
retry_password=True,
keyring_retrieved_cleanly=keyring_retrieved_cleanly,
keyring_save_eligible=keyring_save_eligible,
)
elif e1.args[0] == CR_SERVER_LOST:
self.echo(
(
"Connection to server lost. If this error persists, it may be a mismatch between the server and "
"client SSL configuration. To troubleshoot the issue, try --ssl-mode=off or --ssl-mode=on."
),
err=True,
fg='red',
)
raise e1
else:
raise e1
try:
if not WIN and socket:
try:
socket_owner = getpwuid(os.stat(socket).st_uid).pw_name
except KeyError:
socket_owner = '<unknown>'
self.echo(f"Connecting to socket {socket}, owned by user {socket_owner}", err=True)
try:
_connect(keyring_retrieved_cleanly=keyring_retrieved_cleanly)
except pymysql.OperationalError as e:
# These are "Can't open socket" and 2x "Can't connect"
if [code for code in (2001, 2002, 2003) if code == e.args[0]]:
self.logger.debug("Database connection failed: %r.", e)
self.logger.error("traceback: %r", traceback.format_exc())
self.logger.debug("Retrying over TCP/IP")
self.echo(f"Failed to connect to local MySQL server through socket '{socket}':")
self.echo(str(e), err=True)
self.echo("Retrying over TCP/IP", err=True)
# Else fall back to TCP/IP localhost
socket = ""
host = DEFAULT_HOST
port = DEFAULT_PORT
# todo should reload the keyring identifier here instead of invalidating
_connect(keyring_save_eligible=False)
else:
raise e
else:
host = host or DEFAULT_HOST
port = port or DEFAULT_PORT
# could try loading the keyring again here instead of assuming nothing important changed
# Bad ports give particularly daft error messages
try:
port = int(port)
except ValueError:
self.echo(f"Error: Invalid port number: '{port}'.", err=True, fg="red")
sys.exit(1)
_connect(keyring_retrieved_cleanly=keyring_retrieved_cleanly)
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())
self.echo(str(e), err=True, fg="red")
sys.exit(1)
def handle_editor_command(
self,
text: str,
inputhook: Callable | None,
loaded_message_fn: Callable,
) -> str:
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
"""
while special.editor_command(text):
filename = special.get_filename(text)
query = special.get_editor_query(text) or self.get_last_query()
sql, message = special.open_external_editor(filename=filename, sql=query)
if message:
# Something went wrong. Raise an exception and bail.
raise RuntimeError(message)
while True:
try:
assert isinstance(self.prompt_app, PromptSession)
text = self.prompt_app.prompt(
default=sql,
inputhook=inputhook,
message=loaded_message_fn,
)
break
except KeyboardInterrupt:
sql = ""
continue
return text
def handle_clip_command(self, text: str) -> bool:
r"""A clip command is any query that is prefixed or suffixed by a
'\clip'.
:param text: Document
:return: Boolean
"""
if special.clip_command(text):
query = special.get_clip_query(text) or self.get_last_query()
message = special.copy_query_to_clipboard(sql=query)
if message:
raise RuntimeError(message)
return True
return False
def handle_prettify_binding(self, text: str) -> str:
if not text:
return ''
try:
statements = sqlglot.parse(text, read='mysql')
except Exception:
statements = []
if len(statements) == 1 and statements[0]:
parse_succeeded = True
pretty_text = statements[0].sql(pretty=True, pad=4, dialect='mysql')
else:
parse_succeeded = False
pretty_text = text.rstrip(';')
self.toolbar_error_message = 'Prettify failed to parse single statement'
if pretty_text and parse_succeeded:
pretty_text = pretty_text + ';'
return pretty_text
def handle_unprettify_binding(self, text: str) -> str:
if not text:
return ''
try:
statements = sqlglot.parse(text, read='mysql')
except Exception:
statements = []
if len(statements) == 1 and statements[0]:
parse_succeeded = True
unpretty_text = statements[0].sql(pretty=False, dialect='mysql')
else:
parse_succeeded = False
unpretty_text = text.rstrip(';')
self.toolbar_error_message = 'Unprettify failed to parse single statement'
if unpretty_text and parse_succeeded:
unpretty_text = unpretty_text + ';'
return unpretty_text
def output_timing(self, timing: str, is_warnings_style: bool = False) -> None:
self.log_output(timing)
add_style = 'class:warnings.timing' if is_warnings_style else 'class:output.timing'
formatted_timing = FormattedText([('', timing)])
styled_timing = to_formatted_text(formatted_timing, style=add_style)
print_formatted_text(styled_timing, style=self.toolkit_style)
def run_cli(self) -> None:
iterations = 0
sqlexecute = self.sqlexecute
assert isinstance(sqlexecute, SQLExecute)
logger = self.logger
self.configure_pager()
if self.smart_completion:
self.refresh_completions()
history_file = os.path.expanduser(os.environ.get("MYCLI_HISTFILE", self.config.get("history_file", "~/.mycli-history")))
if dir_path_exists(history_file):
history = FileHistoryWithTimestamp(history_file)
else:
history = None
self.echo(
f'Error: Unable to open the history file "{history_file}". Your query history will not be saved.',
err=True,
fg="red",
)
key_bindings = mycli_bindings(self)
if not self.less_chatty:
print(sqlexecute.server_info)
print("mycli", __version__)
print(SUPPORT_INFO)
if random.random() <= 0.5:
print("Thanks to the contributor —", thanks_picker())
else:
print("Tip —", tips_picker())
def get_prompt_message(app) -> ANSI:
if app.current_buffer.text:
return self.last_prompt_message
prompt = self.get_prompt(self.prompt_format, app.render_counter)
if self.prompt_format == self.default_prompt and len(prompt) > self.max_len_prompt:
prompt = self.get_prompt(self.default_prompt_splitln, app.render_counter)
self.prompt_lines = prompt.count('\n') + 1
prompt = prompt.replace("\\x1b", "\x1b")
if not self.prompt_lines:
self.prompt_lines = prompt.count('\n') + 1
self.last_prompt_message = ANSI(prompt)
return self.last_prompt_message
def get_continuation(width: int, _two: int, _three: int) -> AnyFormattedText:
if self.multiline_continuation_char == "":
continuation = ""
elif self.multiline_continuation_char:
left_padding = width - len(self.multiline_continuation_char)
continuation = " " * max((left_padding - 1), 0) + self.multiline_continuation_char + " "
else:
continuation = " "
return [("class:continuation", continuation)]
def show_initial_toolbar_help() -> bool:
return iterations == 0
# Keep track of whether or not the query is mutating. In case
# of a multi-statement query, the overall query is considered
# mutating if any one of the component statements is mutating
mutating = False
def output_res(results: Generator[SQLResult], start: float) -> None:
nonlocal mutating
result_count = watch_count = 0
for result in results:
logger.debug("preamble: %r", result.preamble)
logger.debug("header: %r", result.header)
logger.debug("rows: %r", result.rows)
logger.debug("status: %r", result.status)
logger.debug("command: %r", result.command)
threshold = 1000
# If this is a watch query, offset the start time on the 2nd+ iteration
# to account for the sleep duration
if result.command is not None and result.command["name"] == "watch":
if watch_count > 0:
try:
watch_seconds = float(result.command["seconds"])
start += watch_seconds
except ValueError as e:
self.echo(f"Invalid watch sleep time provided ({e}).", err=True, fg="red")
sys.exit(1)
else:
watch_count += 1
if is_select(result.status_plain) and isinstance(result.rows, Cursor) and result.rows.rowcount > threshold:
self.echo(
f"The result set has more than {threshold} rows.",
fg="red",
)
if not confirm("Do you want to continue?"):
self.echo("Aborted!", err=True, fg="red")
break
if self.auto_vertical_output:
if self.prompt_app is not None:
max_width = self.prompt_app.output.get_size().columns
else:
max_width = DEFAULT_WIDTH
else:
max_width = None
formatted = self.format_sqlresult(
result,
is_expanded=special.is_expanded_output(),
is_redirected=special.is_redirected(),
null_string=self.null_string,
numeric_alignment=self.numeric_alignment,
binary_display=self.binary_display,
max_width=max_width,
)
t = time() - start
try:
if result_count > 0:
self.echo("")
try:
self.output(formatted, result)
except KeyboardInterrupt:
pass
if self.beep_after_seconds > 0 and t >= self.beep_after_seconds:
assert self.prompt_app is not None
self.prompt_app.output.bell()
if special.is_timing_enabled():
self.output_timing(f"Time: {t:0.03f}s")
except KeyboardInterrupt:
pass
start = time()
result_count += 1
mutating = mutating or is_mutating(result.status_plain)
# get and display warnings if enabled
if self.show_warnings and isinstance(result.rows, Cursor) and result.rows.warning_count > 0:
warnings = sqlexecute.run("SHOW WARNINGS")
t = time() - start
saw_warning = False
for warning in warnings:
saw_warning = True
formatted = self.format_sqlresult(
warning,
is_expanded=special.is_expanded_output(),
is_redirected=special.is_redirected(),
null_string=self.null_string,
numeric_alignment=self.numeric_alignment,
binary_display=self.binary_display,
max_width=max_width,
is_warnings_style=True,
)
self.echo("")
self.output(formatted, warning, is_warnings_style=True)
if saw_warning and special.is_timing_enabled():
self.output_timing(f"Time: {t:0.03f}s", is_warnings_style=True)
def keepalive_hook(_context):
"""
prompt_toolkit shares the event loop with this hook, which seems
to get called a bit faster than once/second on one machine.
It would be nice to reset the counter whenever user input is made,
but was not clear how to do that with context.input_is_ready().
Example at https://github.com/prompt-toolkit/python-prompt-toolkit/blob/main/examples/prompts/inputhook.py
"""
if self.keepalive_ticks is None:
return
if self.keepalive_ticks < 1:
return
self._keepalive_counter += 1
if self._keepalive_counter > self.keepalive_ticks:
self._keepalive_counter = 0
self.logger.debug('keepalive ping')
try:
assert self.sqlexecute is not None
assert self.sqlexecute.conn is not None
self.sqlexecute.conn.ping(reconnect=False)
except Exception as e:
self.logger.debug('keepalive ping error %r', e)
def one_iteration(text: str | None = None) -> None:
inputhook = keepalive_hook if self.keepalive_ticks and self.keepalive_ticks >= 1 else None
if text is None:
try:
assert self.prompt_app is not None
loaded_message_fn = functools.partial(get_prompt_message, self.prompt_app.app)
text = self.prompt_app.prompt(
inputhook=inputhook,
message=loaded_message_fn,
)
except KeyboardInterrupt:
return
special.set_expanded_output(False)
special.set_forced_horizontal_output(False)
try:
text = self.handle_editor_command(
text,
inputhook,
loaded_message_fn,
)
except RuntimeError as e:
logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc())
self.echo(str(e), err=True, fg="red")
return
try:
if self.handle_clip_command(text):
return
except RuntimeError as e:
logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc())
self.echo(str(e), err=True, fg="red")
return
# LLM command support
while special.is_llm_command(text):
start = time()
try:
assert isinstance(self.sqlexecute, SQLExecute)
assert sqlexecute.conn is not None
cur = sqlexecute.conn.cursor()
context, sql, duration = special.handle_llm(
text,
cur,
sqlexecute.dbname or '',
self.llm_prompt_field_truncate,
self.llm_prompt_section_truncate,
)
if context:
click.echo("LLM Response:")
click.echo(context)
click.echo("---")
if special.is_timing_enabled():
self.output_timing(f"Time: {duration:.2f} seconds")
text = self.prompt_app.prompt(
default=sql or '',
inputhook=inputhook,
message=loaded_message_fn,
)
except KeyboardInterrupt:
return
except special.FinishIteration as e:
if e.results:
return output_res(e.results, start)
else:
return None
except RuntimeError as e:
logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc())
self.echo(str(e), err=True, fg="red")
return
text = text.strip()
if not text:
return
if is_redirect_command(text):
sql_part, command_part, file_operator_part, file_part = get_redirect_components(text)
text = sql_part or ''
try:
special.set_redirect(command_part, file_operator_part, file_part)
except (FileNotFoundError, OSError, RuntimeError) as e:
logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc())
self.echo(str(e), err=True, fg="red")
return
if self.destructive_warning:
destroy = confirm_destructive_query(self.destructive_keywords, text)
if destroy is None:
pass # Query was not destructive. Nothing to do here.
elif destroy is True:
self.echo("Your call!")
else:
self.echo("Wise choice!")
return
else:
destroy = True
try:
logger.debug("sql: %r", text)
special.write_tee(self.last_prompt_message, nl=False)
special.write_tee(text)
self.log_query(text)
successful = False
start = time()
res = sqlexecute.run(text)
self.main_formatter.query = text
self.redirect_formatter.query = text
successful = True
output_res(res, start)
special.unset_once_if_written(self.post_redirect_command)
special.flush_pipe_once_if_written(self.post_redirect_command)
except pymysql.err.InterfaceError:
# attempt to reconnect
if not self.reconnect():
return
one_iteration(text)
return # OK to just return, cuz the recursion call runs to the end.
except EOFError as e:
raise e
except KeyboardInterrupt:
# get last connection id
connection_id_to_kill = sqlexecute.connection_id or 0
# some mysql-compatible databases may not implement connection_id()
if connection_id_to_kill > 0:
logger.debug("connection id to kill: %r", connection_id_to_kill)
try:
sqlexecute.connect()
for kill_result in sqlexecute.run(f"kill {connection_id_to_kill}"):
status_str = str(kill_result.status_plain).lower()
if status_str.find("ok") > -1:
logger.debug("cancelled query, connection id: %r, sql: %r", connection_id_to_kill, text)
self.echo(f"Cancelled query id: {connection_id_to_kill}", err=True, fg="blue")
else:
logger.debug(
"Failed to confirm query cancellation, connection id: %r, sql: %r",
connection_id_to_kill,
text,
)
self.echo(f"Failed to confirm query cancellation, id: {connection_id_to_kill}", err=True, fg="red")
except Exception as e2:
self.echo(f"Encountered error while cancelling query: {e2}", err=True, fg="red")
else:
logger.debug("Did not get a connection id, skip cancelling query")
self.echo("Did not get a connection id, skip cancelling query", err=True, fg="red")
except NotImplementedError:
self.echo("Not Yet Implemented.", fg="yellow")
except pymysql.OperationalError as e1:
logger.debug("Exception: %r", e1)
if e1.args[0] in (2003, 2006, 2013):
# attempt to reconnect
if not self.reconnect():
return
one_iteration(text)
return # OK to just return, cuz the recursion call runs to the end.
else:
logger.error("sql: %r, error: %r", text, e1)
logger.error("traceback: %r", traceback.format_exc())
self.echo(str(e1), err=True, fg="red")
except Exception as e:
logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc())
self.echo(str(e), err=True, fg="red")
else:
if is_dropping_database(text, sqlexecute.dbname):
sqlexecute.dbname = None
sqlexecute.connect()
# Refresh the table names and column names if necessary.
if need_completion_refresh(text):
self.refresh_completions(reset=need_completion_reset(text))
finally:
if self.logfile is False:
self.echo("Warning: This query was not logged.", err=True, fg="red")
query = Query(text, successful, mutating)
self.query_history.append(query)
if self.toolbar_format.lower() == 'none':
get_toolbar_tokens = None
else:
get_toolbar_tokens = create_toolbar_tokens_func(
self,
show_initial_toolbar_help,
self.toolbar_format,
)
if self.wider_completion_menu:
complete_style = CompleteStyle.MULTI_COLUMN
else:
complete_style = CompleteStyle.COLUMN
with self._completer_lock:
if self.key_bindings == "vi":
editing_mode = EditingMode.VI
else:
editing_mode = EditingMode.EMACS
self.prompt_app = PromptSession(
color_depth=ColorDepth.DEPTH_24_BIT if 'truecolor' in os.getenv('COLORTERM', '').lower() else None,
lexer=PygmentsLexer(MyCliLexer),
reserve_space_for_menu=self.get_reserved_space(),
prompt_continuation=get_continuation,
bottom_toolbar=get_toolbar_tokens,
complete_style=complete_style,
input_processors=[
ConditionalProcessor(
processor=HighlightMatchingBracketProcessor(chars="[](){}"), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()
)
],
tempfile_suffix=".sql",
completer=DynamicCompleter(lambda: self.completer),
complete_in_thread=True,
history=history,
auto_suggest=ThreadedAutoSuggest(AutoSuggestFromHistory()),
complete_while_typing=complete_while_typing_filter,
multiline=cli_is_multiline(self),
# why not self.toolkit_style here?
style=style_factory_toolkit(self.syntax_style, self.cli_style),
include_default_pygments_style=False,
key_bindings=key_bindings,
enable_op
gitextract_8u893jm8/ ├── .coveragerc ├── .git-blame-ignore-revs ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── codex-review.yml │ ├── lint.yml │ ├── publish.yml │ └── typecheck.yml ├── .gitignore ├── AUTHORS.rst ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── SPONSORS.rst ├── changelog.md ├── doc/ │ ├── key_bindings.rst │ └── llm.md ├── mycli/ │ ├── AUTHORS │ ├── SPONSORS │ ├── TIPS │ ├── __init__.py │ ├── clibuffer.py │ ├── clistyle.py │ ├── clitoolbar.py │ ├── compat.py │ ├── completion_refresher.py │ ├── config.py │ ├── constants.py │ ├── key_bindings.py │ ├── lexer.py │ ├── magic.py │ ├── main.py │ ├── myclirc │ ├── packages/ │ │ ├── __init__.py │ │ ├── checkup.py │ │ ├── completion_engine.py │ │ ├── filepaths.py │ │ ├── hybrid_redirection.py │ │ ├── paramiko_stub/ │ │ │ └── __init__.py │ │ ├── parseutils.py │ │ ├── prompt_utils.py │ │ ├── shortcuts.py │ │ ├── special/ │ │ │ ├── __init__.py │ │ │ ├── dbcommands.py │ │ │ ├── delimitercommand.py │ │ │ ├── favoritequeries.py │ │ │ ├── iocommands.py │ │ │ ├── llm.py │ │ │ ├── main.py │ │ │ └── utils.py │ │ ├── sqlresult.py │ │ ├── string_utils.py │ │ ├── tabular_output/ │ │ │ ├── __init__.py │ │ │ └── sql_format.py │ │ └── toolkit/ │ │ ├── __init__.py │ │ ├── fzf.py │ │ ├── history.py │ │ └── utils.py │ ├── sqlcompleter.py │ └── sqlexecute.py ├── pyproject.toml ├── pytest.ini ├── test/ │ ├── __init__.py │ ├── conftest.py │ ├── features/ │ │ ├── __init__.py │ │ ├── auto_vertical.feature │ │ ├── basic_commands.feature │ │ ├── connection.feature │ │ ├── crud_database.feature │ │ ├── crud_table.feature │ │ ├── db_utils.py │ │ ├── environment.py │ │ ├── fixture_data/ │ │ │ ├── help.txt │ │ │ └── help_commands.txt │ │ ├── fixture_utils.py │ │ ├── iocommands.feature │ │ ├── named_queries.feature │ │ ├── specials.feature │ │ ├── steps/ │ │ │ ├── __init__.py │ │ │ ├── auto_vertical.py │ │ │ ├── basic_commands.py │ │ │ ├── connection.py │ │ │ ├── crud_database.py │ │ │ ├── crud_table.py │ │ │ ├── iocommands.py │ │ │ ├── named_queries.py │ │ │ ├── specials.py │ │ │ ├── utils.py │ │ │ └── wrappers.py │ │ └── wrappager.py │ ├── myclirc │ ├── mylogin.cnf │ ├── test.txt │ ├── test_clistyle.py │ ├── test_clitoolbar.py │ ├── test_completion_engine.py │ ├── test_completion_refresher.py │ ├── test_config.py │ ├── test_dbspecial.py │ ├── test_llm_special.py │ ├── test_main.py │ ├── test_naive_completion.py │ ├── test_parseutils.py │ ├── test_plan.wiki │ ├── test_prompt_utils.py │ ├── test_smart_completion_public_schema_only.py │ ├── test_special_iocommands.py │ ├── test_sqlexecute.py │ ├── test_tabular_output.py │ └── utils.py └── tox.ini
SYMBOL INDEX (766 symbols across 63 files)
FILE: mycli/clibuffer.py
function cli_is_multiline (line 9) | def cli_is_multiline(mycli) -> Filter:
function _multiline_exception (line 22) | def _multiline_exception(text: str) -> bool:
FILE: mycli/clistyle.py
function parse_pygments_style (line 90) | def parse_pygments_style(
function is_valid_pygments (line 111) | def is_valid_pygments(name: str) -> bool:
function is_valid_ptoolkit (line 124) | def is_valid_ptoolkit(name: str) -> bool:
function style_factory_toolkit (line 133) | def style_factory_toolkit(name: str, cli_style: dict[str, str]) -> _Merg...
function style_factory_helpers (line 163) | def style_factory_helpers(
FILE: mycli/clitoolbar.py
function create_toolbar_tokens_func (line 11) | def create_toolbar_tokens_func(mycli, show_initial_toolbar_help: Callabl...
function _get_vi_mode (line 85) | def _get_vi_mode() -> str:
FILE: mycli/completion_refresher.py
class CompletionRefresher (line 10) | class CompletionRefresher:
method __init__ (line 13) | def __init__(self) -> None:
method refresh (line 17) | def refresh(
method is_refreshing (line 48) | def is_refreshing(self) -> bool:
method _bg_refresh (line 51) | def _bg_refresh(
function refresher (line 103) | def refresher(name: str, refreshers: dict = CompletionRefresher.refreshe...
function refresh_databases (line 116) | def refresh_databases(completer: SQLCompleter, executor: SQLExecute) -> ...
function refresh_schemata (line 121) | def refresh_schemata(completer: SQLCompleter, executor: SQLExecute) -> N...
function refresh_tables (line 129) | def refresh_tables(completer: SQLCompleter, executor: SQLExecute) -> None:
function refresh_enum_values (line 136) | def refresh_enum_values(completer: SQLCompleter, executor: SQLExecute) -...
function refresh_users (line 141) | def refresh_users(completer: SQLCompleter, executor: SQLExecute) -> None:
function refresh_functions (line 152) | def refresh_functions(completer: SQLCompleter, executor: SQLExecute) -> ...
function refresh_procedures (line 159) | def refresh_procedures(completer: SQLCompleter, executor: SQLExecute) ->...
function refresh_character_sets (line 164) | def refresh_character_sets(completer: SQLCompleter, executor: SQLExecute...
function refresh_collations (line 169) | def refresh_collations(completer: SQLCompleter, executor: SQLExecute) ->...
function refresh_special (line 174) | def refresh_special(completer: SQLCompleter, executor: SQLExecute) -> None:
function refresh_show_commands (line 179) | def refresh_show_commands(completer: SQLCompleter, executor: SQLExecute)...
function refresh_keywords (line 184) | def refresh_keywords(completer: SQLCompleter, executor: SQLExecute) -> N...
FILE: mycli/config.py
function log (line 17) | def log(logger: logging.Logger, level: int, message: str) -> None:
function read_config_file (line 26) | def read_config_file(f: str | IO[str], list_values: bool = True) -> Conf...
function get_included_configs (line 53) | def get_included_configs(config_file: str | IO[str]) -> list[str | IO[st...
function read_config_files (line 81) | def read_config_files(
function create_default_config (line 113) | def create_default_config(list_values: bool = True) -> ConfigObj:
function write_default_config (line 120) | def write_default_config(destination: str, overwrite: bool = False) -> N...
function get_mylogin_cnf_path (line 133) | def get_mylogin_cnf_path() -> str | None:
function open_mylogin_cnf (line 150) | def open_mylogin_cnf(name: str) -> TextIOWrapper | None:
function encrypt_mylogin_cnf (line 174) | def encrypt_mylogin_cnf(plaintext: IO[str]) -> BytesIO:
function read_and_decrypt_mylogin_cnf (line 223) | def read_and_decrypt_mylogin_cnf(f: BinaryIO) -> BytesIO | None:
function str_to_bool (line 290) | def str_to_bool(s: str | bool) -> bool:
function strip_matching_quotes (line 308) | def strip_matching_quotes(s: str) -> str:
function _remove_pad (line 320) | def _remove_pad(line: bytes) -> bytes | Literal[False]:
FILE: mycli/key_bindings.py
function ctrl_d_condition (line 26) | def ctrl_d_condition() -> bool:
function in_completion (line 33) | def in_completion() -> bool:
function print_f1_help (line 38) | def print_f1_help():
function mycli_bindings (line 56) | def mycli_bindings(mycli) -> KeyBindings:
FILE: mycli/lexer.py
class MyCliLexer (line 6) | class MyCliLexer(MySqlLexer):
FILE: mycli/magic.py
function load_ipython_extension (line 12) | def load_ipython_extension(ipython) -> None:
function mycli_line_magic (line 23) | def mycli_line_magic(line: str):
FILE: mycli/main.py
function complete_while_typing_filter (line 127) | def complete_while_typing_filter() -> bool:
class IntOrStringClickParamType (line 155) | class IntOrStringClickParamType(click.ParamType):
method convert (line 158) | def convert(self, value, param, ctx):
class MyCli (line 172) | class MyCli:
method __init__ (line 195) | def __init__(
method close (line 359) | def close(self) -> None:
method register_special_commands (line 363) | def register_special_commands(self) -> None:
method manual_reconnect (line 415) | def manual_reconnect(self, arg: str = "", **_) -> Generator[SQLResult,...
method enable_show_warnings (line 427) | def enable_show_warnings(self, **_) -> Generator[SQLResult, None, None]:
method disable_show_warnings (line 432) | def disable_show_warnings(self, **_) -> Generator[SQLResult, None, None]:
method change_table_format (line 437) | def change_table_format(self, arg: str, **_) -> Generator[SQLResult, N...
method change_redirect_format (line 447) | def change_redirect_format(self, arg: str, **_) -> Generator[SQLResult...
method change_db (line 457) | def change_db(self, arg: str, **_) -> Generator[SQLResult, None, None]:
method execute_from_file (line 478) | def execute_from_file(self, arg: str, **_) -> Iterable[SQLResult]:
method change_prompt_format (line 495) | def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
method initialize_logging (line 506) | def initialize_logging(self) -> None:
method read_my_cnf (line 542) | def read_my_cnf(self, cnf: ConfigObj, keys: list[str]) -> dict[str, Any]:
method merge_ssl_with_cnf (line 575) | def merge_ssl_with_cnf(self, ssl: dict[str, Any], cnf: dict[str, Any])...
method connect (line 598) | def connect(
method handle_editor_command (line 870) | def handle_editor_command(
method handle_clip_command (line 910) | def handle_clip_command(self, text: str) -> bool:
method handle_prettify_binding (line 927) | def handle_prettify_binding(self, text: str) -> str:
method handle_unprettify_binding (line 945) | def handle_unprettify_binding(self, text: str) -> str:
method output_timing (line 963) | def output_timing(self, timing: str, is_warnings_style: bool = False) ...
method run_cli (line 970) | def run_cli(self) -> None:
method reconnect (line 1399) | def reconnect(self, database: str = "") -> bool:
method log_query (line 1454) | def log_query(self, query: str) -> None:
method log_output (line 1460) | def log_output(self, output: str | AnyFormattedText) -> None:
method echo (line 1467) | def echo(self, s: str, **kwargs) -> None:
method get_output_margin (line 1478) | def get_output_margin(self, status: str | None = None) -> int:
method output (line 1495) | def output(
method configure_pager (line 1571) | def configure_pager(self) -> None:
method refresh_completions (line 1592) | def refresh_completions(self, reset: bool = False) -> list[SQLResult]:
method _on_completions_refreshed (line 1609) | def _on_completions_refreshed(self, new_completer: SQLCompleter) -> None:
method get_completions (line 1619) | def get_completions(self, text: str, cursor_position: int) -> Iterable...
method set_all_external_titles (line 1623) | def set_all_external_titles(self) -> None:
method set_external_terminal_tab_title (line 1629) | def set_external_terminal_tab_title(self) -> None:
method set_external_terminal_window_title (line 1640) | def set_external_terminal_window_title(self) -> None:
method set_external_multiplex_window_title (line 1651) | def set_external_multiplex_window_title(self) -> None:
method set_external_multiplex_pane_title (line 1670) | def set_external_multiplex_pane_title(self) -> None:
method get_custom_toolbar (line 1683) | def get_custom_toolbar(self, toolbar_format: str) -> ANSI:
method get_prompt (line 1700) | def get_prompt(self, string: str, _render_counter: int) -> str:
method run_query (line 1772) | def run_query(
method format_sqlresult (line 1816) | def format_sqlresult(
method get_reserved_space (line 1908) | def get_reserved_space(self) -> int:
method get_last_query (line 1915) | def get_last_query(self) -> str | None:
function cli (line 2024) | def cli(
function need_completion_refresh (line 2594) | def need_completion_refresh(queries: str) -> bool:
function need_completion_reset (line 2607) | def need_completion_reset(queries: str) -> bool:
function is_mutating (line 2622) | def is_mutating(status_plain: str | None) -> bool:
function is_select (line 2631) | def is_select(status_plain: str | None) -> bool:
function thanks_picker (line 2638) | def thanks_picker() -> str:
function tips_picker (line 2661) | def tips_picker() -> str:
function edit_and_execute (line 2680) | def edit_and_execute(event: KeyPressEvent) -> None:
function read_ssh_config (line 2687) | def read_ssh_config(ssh_config_path: str):
FILE: mycli/packages/checkup.py
function pypi_api_fetch (line 14) | def pypi_api_fetch(fragment: str) -> dict:
function _dependencies_checkup (line 25) | def _dependencies_checkup() -> None:
function _executables_checkup (line 43) | def _executables_checkup() -> None:
function _environment_checkup (line 56) | def _environment_checkup() -> None:
function _configuration_checkup (line 68) | def _configuration_checkup(mycli) -> None:
function do_checkup (line 152) | def do_checkup(mycli) -> None:
FILE: mycli/packages/completion_engine.py
function _enum_value_suggestion (line 45) | def _enum_value_suggestion(text_before_cursor: str, full_text: str) -> d...
function _charset_suggestion (line 66) | def _charset_suggestion(tokens: list[Token]) -> list[dict[str, str]] | N...
function _is_where_or_having (line 83) | def _is_where_or_having(token: Token | None) -> bool:
function _find_doubled_backticks (line 87) | def _find_doubled_backticks(text: str) -> list[int]:
function is_inside_quotes (line 112) | def is_inside_quotes(text: str, pos: int) -> Literal[False, 'single', 'd...
function suggest_type (line 168) | def suggest_type(full_text: str, text_before_cursor: str) -> list[dict[s...
function suggest_special (line 246) | def suggest_special(text: str) -> list[dict[str, Any]]:
function suggest_based_on_last_token (line 295) | def suggest_based_on_last_token(
function identifies (line 558) | def identifies(
FILE: mycli/packages/filepaths.py
function list_path (line 12) | def list_path(root_dir: str) -> list[str]:
function complete_path (line 34) | def complete_path(curr_dir: str, last_dir: str) -> str:
function parse_path (line 53) | def parse_path(root_dir: str) -> tuple[str, str, int]:
function suggest_path (line 69) | def suggest_path(root_dir: str) -> list[str]:
function dir_path_exists (line 100) | def dir_path_exists(path: str) -> bool:
function guess_socket_location (line 113) | def guess_socket_location() -> str | None:
FILE: mycli/packages/hybrid_redirection.py
function find_token_indices (line 22) | def find_token_indices(tokens: list[sqlglot.Token]) -> dict[str, list[in...
function find_sql_part (line 48) | def find_sql_part(
function find_command_tokens (line 65) | def find_command_tokens(
function find_file_tokens (line 84) | def find_file_tokens(
function assemble_tokens (line 105) | def assemble_tokens(tokens: list[sqlglot.Token]) -> str:
function invalid_shell_part (line 121) | def invalid_shell_part(
function get_redirect_components (line 138) | def get_redirect_components(command: str) -> tuple[str | None, str | Non...
function is_redirect_command (line 205) | def is_redirect_command(command: str) -> bool:
FILE: mycli/packages/paramiko_stub/__init__.py
class Paramiko (line 10) | class Paramiko:
method __getattr__ (line 11) | def __getattr__(self, name: str) -> None:
FILE: mycli/packages/parseutils.py
function is_valid_connection_scheme (line 36) | def is_valid_connection_scheme(text: str) -> tuple[bool, str | None]:
function last_word (line 47) | def last_word(
function is_subselect (line 103) | def is_subselect(parsed: TokenList) -> bool:
function get_last_select (line 112) | def get_last_select(parsed: TokenList) -> TokenList:
function extract_from_part (line 148) | def extract_from_part(parsed: TokenList, stop_at_punctuation: bool = Tru...
function extract_table_identifiers (line 193) | def extract_table_identifiers(token_stream: Generator[Any, None, None]) ...
function extract_tables (line 222) | def extract_tables(sql: str) -> list[tuple[str | None, str, str]]:
function extract_columns_from_select (line 241) | def extract_columns_from_select(sql: str) -> list[str]:
function extract_tables_from_complete_statements (line 291) | def extract_tables_from_complete_statements(sql: str) -> list[tuple[str ...
function find_prev_keyword (line 327) | def find_prev_keyword(sql: str) -> tuple[Token | None, str]:
function query_starts_with (line 361) | def query_starts_with(query: str, prefixes: list[str]) -> bool:
function queries_start_with (line 368) | def queries_start_with(queries: str, prefixes: list[str]) -> bool:
function query_has_where_clause (line 376) | def query_has_where_clause(query: str) -> bool:
function query_is_single_table_update (line 382) | def query_is_single_table_update(query: str) -> bool:
function is_destructive (line 401) | def is_destructive(keywords: list[str], queries: str) -> bool:
function is_dropping_database (line 418) | def is_dropping_database(queries: str, dbname: str | None) -> bool:
FILE: mycli/packages/prompt_utils.py
class ConfirmBoolParamType (line 8) | class ConfirmBoolParamType(click.ParamType):
method convert (line 11) | def convert(self, value: bool | str, param: click.Parameter | None, ct...
method __repr__ (line 21) | def __repr__(self):
function confirm_destructive_query (line 28) | def confirm_destructive_query(keywords: list[str], queries: str) -> bool...
function confirm (line 44) | def confirm(*args, **kwargs) -> bool:
function prompt (line 52) | def prompt(*args, **kwargs) -> bool:
FILE: mycli/packages/shortcuts.py
function server_date (line 4) | def server_date(sqlexecute: SQLExecute, quoted: bool = False) -> str:
function server_datetime (line 12) | def server_datetime(sqlexecute: SQLExecute, quoted: bool = False) -> str:
FILE: mycli/packages/special/dbcommands.py
function list_tables (line 18) | def list_tables(
function list_databases (line 51) | def list_databases(cur: Cursor, **_) -> list[SQLResult]:
function status (line 66) | def status(cur: Cursor, **_) -> list[SQLResult]:
FILE: mycli/packages/special/delimitercommand.py
class DelimiterCommand (line 14) | class DelimiterCommand:
method __init__ (line 15) | def __init__(self) -> None:
method _split (line 18) | def _split(self, sql: str) -> list[str]:
method queries_iter (line 38) | def queries_iter(self, input_str: str) -> Generator[str, None, None]:
method set (line 63) | def set(self, arg: str, **_) -> list[SQLResult]:
method current (line 85) | def current(self) -> str:
FILE: mycli/packages/special/favoritequeries.py
class FavoriteQueries (line 4) | class FavoriteQueries:
method __init__ (line 39) | def __init__(self, config) -> None:
method from_config (line 43) | def from_config(cls, config):
method list (line 46) | def list(self) -> list[str | None]:
method get (line 49) | def get(self, name) -> str | None:
method save (line 52) | def save(self, name: str, query: str) -> None:
method delete (line 59) | def delete(self, name: str) -> str:
FILE: mycli/packages/special/iocommands.py
function set_favorite_queries (line 51) | def set_favorite_queries(config):
function set_timing_enabled (line 56) | def set_timing_enabled(val: bool) -> None:
function set_pager_enabled (line 61) | def set_pager_enabled(val: bool) -> None:
function is_pager_enabled (line 66) | def is_pager_enabled() -> bool:
function set_show_favorite_query (line 70) | def set_show_favorite_query(val: bool) -> None:
function is_show_favorite_query (line 75) | def is_show_favorite_query() -> bool:
function set_destructive_keywords (line 79) | def set_destructive_keywords(val: list[str]) -> None:
function set_pager (line 92) | def set_pager(arg: str, **_) -> list[SQLResult]:
function disable_pager (line 109) | def disable_pager() -> list[SQLResult]:
function toggle_timing (line 115) | def toggle_timing() -> list[SQLResult]:
function is_timing_enabled (line 123) | def is_timing_enabled() -> bool:
function set_expanded_output (line 127) | def set_expanded_output(val: bool) -> None:
function is_expanded_output (line 132) | def is_expanded_output() -> bool:
function set_forced_horizontal_output (line 136) | def set_forced_horizontal_output(val: bool) -> None:
function forced_horizontal (line 141) | def forced_horizontal() -> bool:
function editor_command (line 148) | def editor_command(command: str) -> bool:
function get_filename (line 163) | def get_filename(sql: str) -> str | None:
function get_editor_query (line 171) | def get_editor_query(sql: str) -> str:
function open_external_editor (line 185) | def open_external_editor(filename: str | None = None, sql: str | None = ...
function clip_command (line 219) | def clip_command(command: str) -> bool:
function get_clip_query (line 230) | def get_clip_query(sql: str) -> str:
function copy_query_to_clipboard (line 243) | def copy_query_to_clipboard(sql: str | None = None) -> str | None:
function set_redirect (line 257) | def set_redirect(command_part: str | None, file_operator_part: str | Non...
function execute_favorite_query (line 270) | def execute_favorite_query(cur: Cursor, arg: str, **_) -> Generator[SQLR...
function list_favorite_queries (line 309) | def list_favorite_queries() -> list[SQLResult]:
function subst_favorite_query_args (line 322) | def subst_favorite_query_args(query: str, args: list[str]) -> list[str |...
function save_favorite_query (line 339) | def save_favorite_query(arg: str, **_) -> list[SQLResult]:
function delete_favorite_query (line 357) | def delete_favorite_query(arg: str, **_) -> list[SQLResult]:
function execute_system_command (line 369) | def execute_system_command(arg: str, **_) -> list[SQLResult]:
function parseargfile (line 431) | def parseargfile(arg: str) -> tuple[str, str]:
function set_tee (line 446) | def set_tee(arg: str, **_) -> list[SQLResult]:
function close_tee (line 457) | def close_tee() -> None:
function no_tee (line 465) | def no_tee(arg: str, **_) -> list[SQLResult]:
function write_tee (line 470) | def write_tee(output: str | ANSI | FormattedText, nl: bool = True) -> None:
function set_once (line 481) | def set_once(arg: str, **_) -> list[SQLResult]:
function is_redirected (line 493) | def is_redirected() -> bool:
function write_once (line 497) | def write_once(output: str) -> None:
function unset_once_if_written (line 506) | def unset_once_if_written(post_redirect_command: str) -> None:
function _run_post_redirect_hook (line 516) | def _run_post_redirect_hook(post_redirect_command: str, filename: str) -...
function set_pipe_once (line 534) | def set_pipe_once(arg: str, **_) -> list[SQLResult]:
function write_pipe_once (line 555) | def write_pipe_once(line: str) -> None:
function flush_pipe_once_if_written (line 560) | def flush_pipe_once_if_written(post_redirect_command: str) -> None:
function watch_query (line 593) | def watch_query(arg: str, **kwargs) -> Generator[SQLResult, None, None]:
function set_delimiter (line 660) | def set_delimiter(arg: str, **_) -> list[SQLResult]:
function get_current_delimiter (line 664) | def get_current_delimiter() -> str:
function split_queries (line 668) | def split_queries(input_str: str) -> Generator[str, None, None]:
FILE: mycli/packages/special/llm.py
function run_external_cmd (line 47) | def run_external_cmd(
function _build_command_tree (line 92) | def _build_command_tree(cmd) -> dict[str, Any] | None:
function build_command_tree (line 106) | def build_command_tree(cmd) -> dict[str, Any]:
function get_completions (line 114) | def get_completions(
class FinishIteration (line 129) | class FinishIteration(Exception):
method __init__ (line 130) | def __init__(self, results=None):
function ensure_mycli_template (line 207) | def ensure_mycli_template(replace: bool = False) -> None:
function cli_commands (line 216) | def cli_commands() -> list[str]:
function handle_llm (line 220) | def handle_llm(
function is_llm_command (line 287) | def is_llm_command(command: str) -> bool:
function truncate_list_elements (line 292) | def truncate_list_elements(row: list, prompt_field_truncate: int, prompt...
function truncate_table_lines (line 308) | def truncate_table_lines(table: list[str], prompt_section_truncate: int)...
function get_schema (line 321) | def get_schema(cur: Cursor, dbname: str, prompt_section_truncate: int) -...
function get_sample_data (line 339) | def get_sample_data(
function sql_using_llm (line 368) | def sql_using_llm(
FILE: mycli/packages/special/main.py
class ArgType (line 41) | class ArgType(Enum):
class CommandNotFound (line 47) | class CommandNotFound(Exception):
class Verbosity (line 51) | class Verbosity(Enum):
function parse_special_command (line 57) | def parse_special_command(sql: str) -> tuple[str, Verbosity, str]:
function special_command (line 68) | def special_command(
function register_special_command (line 93) | def register_special_command(
function execute (line 129) | def execute(cur: Cursor, sql: str) -> list[SQLResult]:
function show_help (line 163) | def show_help(*_args) -> list[SQLResult]:
function show_keyword_help (line 173) | def show_keyword_help(cur: Cursor, arg: str) -> list[SQLResult]:
function file_bug (line 197) | def file_bug(*_args) -> list[SQLResult]:
function quit_ (line 204) | def quit_(*_args):
function stub (line 218) | def stub():
function llm_stub (line 232) | def llm_stub():
FILE: mycli/packages/special/utils.py
function handle_cd_command (line 13) | def handle_cd_command(command: list[str]) -> tuple[bool, str | None]:
function format_uptime (line 28) | def format_uptime(uptime_in_seconds: str) -> str:
function get_uptime (line 59) | def get_uptime(cur: Cursor) -> int:
function get_warning_count (line 75) | def get_warning_count(cur: Cursor) -> int:
function get_ssl_version (line 91) | def get_ssl_version(cur: Cursor) -> str | None:
FILE: mycli/packages/sqlresult.py
class SQLResult (line 9) | class SQLResult:
method __iter__ (line 17) | def __iter__(self):
method __str__ (line 20) | def __str__(self):
method status_plain (line 24) | def status_plain(self):
FILE: mycli/packages/string_utils.py
function sanitize_terminal_title (line 6) | def sanitize_terminal_title(title: str) -> str:
FILE: mycli/packages/tabular_output/sql_format.py
function escape_for_sql_statement (line 23) | def escape_for_sql_statement(value: Union[bytes, str]) -> str:
function adapter (line 30) | def adapter(data: list[str], headers: list[str], table_format: Union[str...
function register_new_formatter (line 67) | def register_new_formatter(tof: TabularOutputFormatter):
FILE: mycli/packages/toolkit/fzf.py
class Fzf (line 13) | class Fzf(FzfPrompt):
method __init__ (line 14) | def __init__(self):
method is_available (line 19) | def is_available(self) -> bool:
function search_history (line 23) | def search_history(
FILE: mycli/packages/toolkit/history.py
class FileHistoryWithTimestamp (line 9) | class FileHistoryWithTimestamp(FileHistory):
method __init__ (line 14) | def __init__(self, filename: _StrOrBytesPath) -> None:
method load_history_with_timestamp (line 18) | def load_history_with_timestamp(self) -> list[tuple[str, str]]:
FILE: mycli/packages/toolkit/utils.py
function safe_invalidate_display (line 4) | def safe_invalidate_display(app: Application) -> None:
FILE: mycli/sqlcompleter.py
class Fuzziness (line 24) | class Fuzziness(IntEnum):
class SQLCompleter (line 32) | class SQLCompleter(Completer):
method __init__ (line 934) | def __init__(
method escape_name (line 954) | def escape_name(self, name: str) -> str:
method escaped_names (line 960) | def escaped_names(self, names: Collection[str]) -> list[str]:
method extend_special_commands (line 963) | def extend_special_commands(self, special_commands: list[str]) -> None:
method extend_database_names (line 968) | def extend_database_names(self, databases: list[str]) -> None:
method extend_keywords (line 971) | def extend_keywords(self, keywords: list[str], replace: bool = False) ...
method extend_show_items (line 978) | def extend_show_items(self, show_items: Iterable[tuple]) -> None:
method extend_change_items (line 983) | def extend_change_items(self, change_items: Iterable[tuple]) -> None:
method extend_users (line 988) | def extend_users(self, users: Iterable[tuple]) -> None:
method extend_schemata (line 993) | def extend_schemata(self, schema: str | None) -> None:
method extend_relations (line 1004) | def extend_relations(self, data: list[tuple[str, str]], kind: Literal[...
method extend_columns (line 1023) | def extend_columns(self, column_data: list[tuple[str, str]], kind: Lit...
method extend_enum_values (line 1044) | def extend_enum_values(self, enum_data: Iterable[tuple[str, str, list[...
method extend_functions (line 1055) | def extend_functions(self, func_data: list[str] | Generator[tuple[str,...
method extend_procedures (line 1079) | def extend_procedures(self, procedure_data: Generator[tuple]) -> None:
method extend_character_sets (line 1093) | def extend_character_sets(self, character_set_data: Generator[tuple]) ...
method extend_collations (line 1102) | def extend_collations(self, collation_data: Generator[tuple]) -> None:
method set_dbname (line 1111) | def set_dbname(self, dbname: str | None) -> None:
method reset_completions (line 1114) | def reset_completions(self) -> None:
method find_matches (line 1131) | def find_matches(
method get_completions (line 1243) | def get_completions(
method find_files (line 1534) | def find_files(self, word: str) -> Generator[tuple[str, int], None, No...
method populate_scoped_cols (line 1550) | def populate_scoped_cols(self, scoped_tbls: list[tuple[str | None, str...
method populate_enum_values (line 1596) | def populate_enum_values(
method _escape_identifier (line 1622) | def _escape_identifier(self, name: str) -> str:
method _strip_backticks (line 1626) | def _strip_backticks(name: str | None) -> str:
method _matches_parent (line 1632) | def _matches_parent(parent: str, schema: str | None, relname: str, ali...
method _quote_sql_string (line 1642) | def _quote_sql_string(value: str) -> str:
method populate_schema_objects (line 1645) | def populate_schema_objects(self, schema: str | None, obj_type: str, c...
FILE: mycli/sqlexecute.py
class ServerSpecies (line 36) | class ServerSpecies(enum.Enum):
class ServerInfo (line 44) | class ServerInfo:
method __init__ (line 45) | def __init__(self, species: ServerSpecies | None, version_str: str) ->...
method calc_mysql_version_value (line 51) | def calc_mysql_version_value(version_str: str) -> int:
method from_version_string (line 62) | def from_version_string(cls, version_string: str) -> ServerInfo:
method __str__ (line 84) | def __str__(self) -> str:
class SQLExecute (line 91) | class SQLExecute:
method _parse_enum_values (line 121) | def _parse_enum_values(column_type: str) -> list[str]:
method __init__ (line 156) | def __init__(
method connect (line 196) | def connect(
method run (line 347) | def run(self, statement: str) -> Generator[SQLResult, None, None]:
method get_result (line 392) | def get_result(self, cursor: Cursor) -> SQLResult:
method tables (line 415) | def tables(self) -> Generator[tuple[str], None, None]:
method table_columns (line 424) | def table_columns(self) -> Generator[tuple[str, str], None, None]:
method enum_values (line 432) | def enum_values(self) -> Generator[tuple[str, str, list[str]], None, N...
method databases (line 443) | def databases(self) -> list[str]:
method functions (line 450) | def functions(self) -> Generator[tuple[str, str], None, None]:
method procedures (line 459) | def procedures(self) -> Generator[tuple, None, None]:
method character_sets (line 473) | def character_sets(self) -> Generator[tuple, None, None]:
method collations (line 487) | def collations(self) -> Generator[tuple, None, None]:
method show_candidates (line 501) | def show_candidates(self) -> Generator[tuple, None, None]:
method users (line 514) | def users(self) -> Generator[tuple, None, None]:
method now (line 526) | def now(self) -> datetime.datetime:
method get_connection_id (line 536) | def get_connection_id(self) -> int | None:
method reset_connection_id (line 541) | def reset_connection_id(self) -> None:
method change_db (line 560) | def change_db(self, db: str) -> None:
method _create_ssl_ctx (line 565) | def _create_ssl_ctx(self, sslp: dict) -> ssl.SSLContext:
method close (line 599) | def close(self) -> None:
FILE: test/conftest.py
function connection (line 10) | def connection():
function cursor (line 19) | def cursor(connection):
function executor (line 25) | def executor(connection):
FILE: test/features/db_utils.py
function create_db (line 8) | def create_db(hostname=DEFAULT_HOST, port=DEFAULT_PORT, username=None, p...
function create_cn (line 33) | def create_cn(hostname, port, password, username, dbname):
function drop_db (line 57) | def drop_db(hostname=DEFAULT_HOST, port=DEFAULT_PORT, username=None, pas...
function close_cn (line 83) | def close_cn(cn=None):
FILE: test/features/environment.py
function get_db_name_from_context (line 28) | def get_db_name_from_context(context):
function before_all (line 32) | def before_all(context):
function after_all (line 85) | def after_all(context):
function before_step (line 103) | def before_step(context, _):
function before_scenario (line 107) | def before_scenario(context, arg):
function after_scenario (line 124) | def after_scenario(context, _):
FILE: test/features/fixture_utils.py
function read_fixture_lines (line 6) | def read_fixture_lines(filename):
function read_fixture_files (line 19) | def read_fixture_files():
FILE: test/features/steps/auto_vertical.py
function step_run_cli_with_arg (line 11) | def step_run_cli_with_arg(context, arg):
function step_execute_small_query (line 16) | def step_execute_small_query(context):
function step_execute_large_query (line 21) | def step_execute_large_query(context):
function step_see_small_results (line 26) | def step_see_small_results(context):
function step_see_large_results (line 49) | def step_see_large_results(context):
FILE: test/features/steps/basic_commands.py
function step_run_cli (line 21) | def step_run_cli(context):
function step_wait_prompt (line 26) | def step_wait_prompt(context):
function step_ctrl_d (line 31) | def step_ctrl_d(context):
function step_ctrl_o_ctrl_d (line 38) | def step_ctrl_o_ctrl_d(context):
function step_send_help (line 48) | def step_send_help(context):
function step_send_source_command (line 59) | def step_send_source_command(context):
function step_check_application_name (line 68) | def step_check_application_name(context):
function step_see_found (line 75) | def step_see_found(context):
function step_see_date (line 97) | def step_see_date(context):
function step_confirm_destructive_command (line 123) | def step_confirm_destructive_command(context): # noqa
function step_confirm_destructive_command (line 130) | def step_confirm_destructive_command(context, confirmation): # noqa
function step_confirm_destructive_command (line 137) | def step_confirm_destructive_command(context, confirmation, text): # noqa
FILE: test/features/steps/connection.py
function step_run_cli_without_args (line 19) | def step_run_cli_without_args(context, excluded_args, exact_args=""):
function status_contains (line 24) | def status_contains(context, expression):
function step_create_my_cnf_file (line 35) | def step_create_my_cnf_file(context):
function step_create_mylogin_cnf_file (line 42) | def step_create_mylogin_cnf_file(context):
function we_are_logged_in (line 51) | def we_are_logged_in(context):
FILE: test/features/steps/crud_database.py
function step_db_create (line 18) | def step_db_create(context):
function step_db_drop (line 26) | def step_db_drop(context):
function step_db_connect_test (line 32) | def step_db_connect_test(context):
function step_db_connect_quoted_tmp (line 40) | def step_db_connect_quoted_tmp(context):
function step_db_connect_tmp (line 48) | def step_db_connect_tmp(context):
function step_db_connect_dbserver (line 56) | def step_db_connect_dbserver(context):
function step_wait_exit (line 63) | def step_wait_exit(context):
function step_see_prompt (line 69) | def step_see_prompt(context):
function step_see_help (line 78) | def step_see_help(context):
function step_see_db_created (line 87) | def step_see_db_created(context):
function step_see_db_dropped (line 93) | def step_see_db_dropped(context):
function step_see_db_dropped_no_default (line 99) | def step_see_db_dropped_no_default(context):
function step_see_db_connected (line 111) | def step_see_db_connected(context):
FILE: test/features/steps/crud_table.py
function step_create_table (line 17) | def step_create_table(context):
function step_insert_into_table (line 23) | def step_insert_into_table(context):
function step_update_table (line 29) | def step_update_table(context):
function step_select_from_table (line 35) | def step_select_from_table(context):
function step_delete_from_table (line 41) | def step_delete_from_table(context):
function step_drop_table (line 47) | def step_drop_table(context):
function step_see_table_created (line 53) | def step_see_table_created(context):
function step_see_record_inserted (line 59) | def step_see_record_inserted(context):
function step_see_record_updated (line 65) | def step_see_record_updated(context):
function step_see_data_selected (line 71) | def step_see_data_selected(context):
function step_see_data_deleted (line 95) | def step_see_data_deleted(context):
function step_see_table_dropped (line 101) | def step_see_table_dropped(context):
function step_select_null (line 107) | def step_select_null(context):
function step_see_null_selected (line 113) | def step_see_null_selected(context):
FILE: test/features/steps/iocommands.py
function step_edit_file (line 11) | def step_edit_file(context):
function step_edit_type_sql (line 22) | def step_edit_type_sql(context, query):
function step_edit_quit (line 30) | def step_edit_quit(context):
function step_edit_done_sql (line 36) | def step_edit_done_sql(context, query):
function step_tee_ouptut (line 47) | def step_tee_ouptut(context):
function step_query_select_number (line 55) | def step_query_select_number(context, param):
function step_see_tabular_result (line 79) | def step_see_tabular_result(context, result):
function step_see_csv_result (line 84) | def step_see_csv_result(context, result):
function step_query (line 89) | def step_query(context, query):
function step_notee_output (line 94) | def step_notee_output(context):
function step_see_123456_in_ouput (line 99) | def step_see_123456_in_ouput(context):
function step_see_csv_result_in_redirected_ouput (line 107) | def step_see_csv_result_in_redirected_ouput(context, result):
function step_see_text_result_in_redirected_ouput (line 115) | def step_see_text_result_in_redirected_ouput(context, result):
function step_see_space_12_in_command_ouput (line 123) | def step_see_space_12_in_command_ouput(context):
function step_see_space_6_in_command_ouput (line 128) | def step_see_space_6_in_command_ouput(context):
function delimiter_is_set (line 133) | def delimiter_is_set(context, delimiter):
FILE: test/features/steps/named_queries.py
function step_save_named_query (line 15) | def step_save_named_query(context):
function step_use_named_query (line 21) | def step_use_named_query(context):
function step_delete_named_query (line 27) | def step_delete_named_query(context):
function step_see_named_query_saved (line 33) | def step_see_named_query_saved(context):
function step_see_named_query_executed (line 39) | def step_see_named_query_executed(context):
function step_see_named_query_deleted (line 45) | def step_see_named_query_deleted(context):
function step_save_named_query_with_parameters (line 51) | def step_save_named_query_with_parameters(context):
function step_use_named_query_with_parameters (line 57) | def step_use_named_query_with_parameters(context):
function step_see_named_query_with_parameters_executed (line 63) | def step_see_named_query_with_parameters_executed(context):
function step_use_named_query_with_too_few_parameters (line 69) | def step_use_named_query_with_too_few_parameters(context):
function step_see_named_query_with_parameters_fail_with_missing_parameters (line 75) | def step_see_named_query_with_parameters_fail_with_missing_parameters(co...
function step_use_named_query_with_too_many_parameters (line 81) | def step_use_named_query_with_too_many_parameters(context):
function step_see_named_query_with_parameters_fail_with_extra_parameters (line 87) | def step_see_named_query_with_parameters_fail_with_extra_parameters(cont...
FILE: test/features/steps/specials.py
function step_refresh_completions (line 15) | def step_refresh_completions(context):
function step_see_text (line 21) | def step_see_text(context, text):
function step_see_refresh_started (line 27) | def step_see_refresh_started(context):
FILE: test/features/steps/utils.py
function parse_cli_args_to_dict (line 6) | def parse_cli_args_to_dict(cli_args: str):
FILE: test/features/steps/wrappers.py
function expect_exact (line 11) | def expect_exact(context, expected, timeout):
function expect_pager (line 40) | def expect_pager(context, expected, timeout):
function run_cli (line 44) | def run_cli(context, run_args=None, exclude_args=None):
function wait_prompt (line 95) | def wait_prompt(context, prompt=None):
FILE: test/features/wrappager.py
function wrappager (line 6) | def wrappager(boundary: str) -> None:
FILE: test/test_clistyle.py
function test_style_factory_toolkit (line 13) | def test_style_factory_toolkit():
function test_style_factory_toolkit_unknown_name (line 25) | def test_style_factory_toolkit_unknown_name():
FILE: test/test_clitoolbar.py
function test_create_toolbar_tokens_func_initial (line 7) | def test_create_toolbar_tokens_func_initial():
function test_create_toolbar_tokens_func_short (line 16) | def test_create_toolbar_tokens_func_short():
FILE: test/test_completion_engine.py
function sorted_dicts (line 13) | def sorted_dicts(dicts):
function test_select_suggests_cols_with_visible_table_scope (line 18) | def test_select_suggests_cols_with_visible_table_scope():
function test_select_suggests_cols_with_qualified_table_scope (line 28) | def test_select_suggests_cols_with_qualified_table_scope():
function test_where_suggests_columns_functions (line 52) | def test_where_suggests_columns_functions(expression):
function test_where_equals_suggests_enum_values_first (line 62) | def test_where_equals_suggests_enum_values_first():
function test_where_in_suggests_columns (line 81) | def test_where_in_suggests_columns(expression):
function test_where_equals_any_suggests_columns_or_keywords (line 91) | def test_where_equals_any_suggests_columns_or_keywords():
function test_where_convert_using_suggests_character_set (line 102) | def test_where_convert_using_suggests_character_set():
function test_where_cast_character_set_suggests_character_set (line 108) | def test_where_cast_character_set_suggests_character_set():
function test_lparen_suggests_cols (line 114) | def test_lparen_suggests_cols():
function test_operand_inside_function_suggests_cols1 (line 119) | def test_operand_inside_function_suggests_cols1():
function test_operand_inside_function_suggests_cols2 (line 124) | def test_operand_inside_function_suggests_cols2():
function test_operand_inside_function_suggests_cols3 (line 129) | def test_operand_inside_function_suggests_cols3():
function test_operand_inside_function_suggests_cols4 (line 134) | def test_operand_inside_function_suggests_cols4():
function test_operand_inside_function_suggests_cols5 (line 139) | def test_operand_inside_function_suggests_cols5():
function test_arrow_op_inside_function_suggests_nothing (line 145) | def test_arrow_op_inside_function_suggests_nothing():
function test_select_suggests_cols_and_funcs (line 150) | def test_select_suggests_cols_and_funcs():
function test_expression_suggests_tables_views_and_schemas (line 173) | def test_expression_suggests_tables_views_and_schemas(expression):
function test_expression_suggests_qualified_tables_views_and_schemas (line 195) | def test_expression_suggests_qualified_tables_views_and_schemas(expressi...
function test_truncate_suggests_tables_and_schemas (line 203) | def test_truncate_suggests_tables_and_schemas():
function test_truncate_suggests_qualified_tables (line 211) | def test_truncate_suggests_qualified_tables():
function test_distinct_suggests_cols (line 218) | def test_distinct_suggests_cols():
function test_col_comma_suggests_cols (line 223) | def test_col_comma_suggests_cols():
function test_table_comma_suggests_tables_and_schemas (line 233) | def test_table_comma_suggests_tables_and_schemas():
function test_into_suggests_tables_and_schemas (line 242) | def test_into_suggests_tables_and_schemas():
function test_insert_into_lparen_suggests_cols (line 251) | def test_insert_into_lparen_suggests_cols():
function test_insert_into_lparen_partial_text_suggests_cols (line 256) | def test_insert_into_lparen_partial_text_suggests_cols():
function test_insert_into_lparen_comma_suggests_cols (line 261) | def test_insert_into_lparen_comma_suggests_cols():
function test_partially_typed_col_name_suggests_col_names (line 266) | def test_partially_typed_col_name_suggests_col_names():
function test_dot_suggests_cols_of_a_table_or_schema_qualified_table (line 276) | def test_dot_suggests_cols_of_a_table_or_schema_qualified_table():
function test_dot_suggests_cols_of_an_alias (line 286) | def test_dot_suggests_cols_of_an_alias():
function test_dot_col_comma_suggests_cols_or_schema_qualified_table (line 296) | def test_dot_col_comma_suggests_cols_or_schema_qualified_table():
function test_sub_select_suggests_keyword (line 315) | def test_sub_select_suggests_keyword(expression):
function test_sub_select_partial_text_suggests_keyword (line 328) | def test_sub_select_partial_text_suggests_keyword(expression):
function test_outer_table_reference_in_exists_subquery_suggests_columns (line 333) | def test_outer_table_reference_in_exists_subquery_suggests_columns():
function test_sub_select_table_name_completion (line 352) | def test_sub_select_table_name_completion(expression):
function test_sub_select_col_name_completion (line 361) | def test_sub_select_col_name_completion():
function test_sub_select_multiple_col_name_completion (line 372) | def test_sub_select_multiple_col_name_completion():
function test_sub_select_dot_col_name_completion (line 381) | def test_sub_select_dot_col_name_completion():
function test_join_suggests_tables_and_schemas (line 393) | def test_join_suggests_tables_and_schemas(tbl_alias, join_type):
function test_join_alias_dot_suggests_cols1 (line 410) | def test_join_alias_dot_suggests_cols1(sql):
function test_join_alias_dot_suggests_cols2 (line 427) | def test_join_alias_dot_suggests_cols2(sql):
function test_on_suggests_aliases (line 446) | def test_on_suggests_aliases(sql):
function test_on_suggests_tables (line 458) | def test_on_suggests_tables(sql):
function test_on_suggests_aliases_right_side (line 470) | def test_on_suggests_aliases_right_side(sql):
function test_on_suggests_tables_right_side (line 482) | def test_on_suggests_tables_right_side(sql):
function test_join_using_suggests_common_columns (line 488) | def test_join_using_suggests_common_columns(col_list):
function test_two_join_alias_dot_suggests_cols1 (line 500) | def test_two_join_alias_dot_suggests_cols1(sql):
function test_2_statements_2nd_current (line 510) | def test_2_statements_2nd_current():
function test_2_statements_1st_current (line 535) | def test_2_statements_1st_current():
function test_3_statements_2nd_current (line 552) | def test_3_statements_2nd_current():
function test_create_db_with_template (line 569) | def test_create_db_with_template():
function test_specials_included_for_initial_completion (line 576) | def test_specials_included_for_initial_completion(initial_text):
function test_specials_included_with_caps (line 583) | def test_specials_included_with_caps(initial_text):
function test_specials_not_included_after_initial_token (line 589) | def test_specials_not_included_after_initial_token():
function test_drop_schema_qualified_table_suggests_only_tables (line 595) | def test_drop_schema_qualified_table_suggests_only_tables():
function test_handle_pre_completion_comma_gracefully (line 602) | def test_handle_pre_completion_comma_gracefully(text):
function test_cross_join (line 608) | def test_cross_join():
function test_after_as (line 625) | def test_after_as(expression):
function test_source_is_file (line 643) | def test_source_is_file(expression):
function test_favorite_name_suggestion (line 656) | def test_favorite_name_suggestion(expression):
function test_order_by (line 661) | def test_order_by():
function test_quoted_where (line 667) | def test_quoted_where():
function test_find_doubled_backticks_none (line 673) | def test_find_doubled_backticks_none():
function test_find_doubled_backticks_some (line 678) | def test_find_doubled_backticks_some():
function test_inside_quotes_01 (line 683) | def test_inside_quotes_01():
function test_inside_quotes_02 (line 688) | def test_inside_quotes_02():
function test_inside_quotes_03 (line 693) | def test_inside_quotes_03():
function test_inside_quotes_04 (line 698) | def test_inside_quotes_04():
function test_inside_quotes_05 (line 703) | def test_inside_quotes_05():
function test_inside_quotes_06 (line 708) | def test_inside_quotes_06():
function test_inside_quotes_backtick_01 (line 731) | def test_inside_quotes_backtick_01(text, position, expected):
function test_inside_quotes_backtick_02 (line 735) | def test_inside_quotes_backtick_02():
function test_inside_quotes_backtick_03 (line 743) | def test_inside_quotes_backtick_03():
FILE: test/test_completion_refresher.py
function refresher (line 10) | def refresher():
function test_ctor (line 16) | def test_ctor(refresher):
function test_refresh_called_once (line 42) | def test_refresh_called_once(refresher):
function test_refresh_called_twice (line 61) | def test_refresh_called_twice(refresher):
function test_refresh_with_callbacks (line 92) | def test_refresh_with_callbacks(refresher):
FILE: test/test_config.py
function open_bmylogin_cnf (line 26) | def open_bmylogin_cnf(name):
function test_read_mylogin_cnf (line 34) | def test_read_mylogin_cnf():
function test_decrypt_blank_mylogin_cnf (line 45) | def test_decrypt_blank_mylogin_cnf():
function test_corrupted_login_key (line 51) | def test_corrupted_login_key():
function test_corrupted_pad (line 67) | def test_corrupted_pad():
function test_get_mylogin_cnf_path (line 93) | def test_get_mylogin_cnf_path(monkeypatch):
function test_alternate_get_mylogin_cnf_path (line 110) | def test_alternate_get_mylogin_cnf_path(monkeypatch):
function test_str_to_bool (line 126) | def test_str_to_bool():
function test_read_config_file_list_values_default (line 147) | def test_read_config_file_list_values_default():
function test_read_config_file_list_values_off (line 156) | def test_read_config_file_list_values_off():
function test_strip_quotes_with_matching_quotes (line 165) | def test_strip_quotes_with_matching_quotes():
function test_strip_quotes_with_unmatching_quotes (line 173) | def test_strip_quotes_with_unmatching_quotes():
function test_strip_quotes_with_empty_string (line 181) | def test_strip_quotes_with_empty_string():
function test_strip_quotes_with_none (line 187) | def test_strip_quotes_with_none():
function test_strip_quotes_with_quotes (line 193) | def test_strip_quotes_with_quotes():
FILE: test/test_dbspecial.py
function test_list_tables_verbose_preserves_field_results (line 11) | def test_list_tables_verbose_preserves_field_results():
function test_u_suggests_databases (line 73) | def test_u_suggests_databases():
function test_describe_table (line 78) | def test_describe_table():
function test_list_or_show_create_tables (line 83) | def test_list_or_show_create_tables():
function test_format_uptime (line 88) | def test_format_uptime():
FILE: test/test_llm_special.py
function executor (line 17) | def executor():
function test_llm_command_without_args (line 23) | def test_llm_command_without_args(mock_llm, executor):
function test_llm_command_with_help_subcommand (line 36) | def test_llm_command_with_help_subcommand(mock_llm, executor):
function test_llm_command_with_c_flag (line 50) | def test_llm_command_with_c_flag(mock_run_cmd, mock_llm, executor):
function test_llm_command_with_c_flag_and_fenced_sql (line 63) | def test_llm_command_with_c_flag_and_fenced_sql(mock_run_cmd, mock_llm, ...
function test_llm_command_known_subcommand (line 78) | def test_llm_command_known_subcommand(mock_run_cmd, mock_llm, executor):
function test_llm_command_with_help_flag (line 89) | def test_llm_command_with_help_flag(mock_run_cmd, mock_llm, executor):
function test_llm_command_with_install_flag (line 99) | def test_llm_command_with_install_flag(mock_run_cmd, mock_llm, executor):
function test_llm_command_with_prompt (line 110) | def test_llm_command_with_prompt(mock_sql_using_llm, mock_ensure_templat...
function test_llm_command_question_with_context (line 127) | def test_llm_command_question_with_context(mock_sql_using_llm, mock_ensu...
function test_llm_command_question_verbose (line 144) | def test_llm_command_question_verbose(mock_sql_using_llm, mock_ensure_te...
function test_is_llm_command (line 156) | def test_is_llm_command():
function test_sql_using_llm_no_connection (line 164) | def test_sql_using_llm_no_connection():
function test_sql_using_llm_success (line 173) | def test_sql_using_llm_success(mock_run_cmd):
function test_handle_llm_aliases_without_args (line 206) | def test_handle_llm_aliases_without_args(prefix, executor, monkeypatch):
FILE: test/test_main.py
function test_binary_display_hex (line 55) | def test_binary_display_hex(executor):
function test_binary_display_utf8 (line 94) | def test_binary_display_utf8(executor):
function test_select_from_empty_table (line 133) | def test_select_from_empty_table(executor):
function test_is_valid_connection_scheme_valid (line 146) | def test_is_valid_connection_scheme_valid(executor, capsys):
function test_is_valid_connection_scheme_invalid (line 151) | def test_is_valid_connection_scheme_invalid(executor, capsys):
function test_ssl_mode_on (line 157) | def test_ssl_mode_on(executor, capsys):
function test_ssl_mode_auto (line 168) | def test_ssl_mode_auto(executor, capsys):
function test_ssl_mode_off (line 179) | def test_ssl_mode_off(executor, capsys):
function test_ssl_mode_overrides_ssl (line 190) | def test_ssl_mode_overrides_ssl(executor, capsys):
function test_ssl_mode_overrides_no_ssl (line 201) | def test_ssl_mode_overrides_no_ssl(executor, capsys):
function test_reconnect_database_is_selected (line 212) | def test_reconnect_database_is_selected(executor, capsys):
function test_reconnect_no_database (line 247) | def test_reconnect_no_database(executor, capsys):
function test_reconnect_with_different_database (line 275) | def test_reconnect_with_different_database(executor):
function test_reconnect_with_same_database (line 306) | def test_reconnect_with_same_database(executor):
function test_prompt_no_host_only_socket (line 336) | def test_prompt_no_host_only_socket(executor):
function test_prompt_socket_overrides_port (line 351) | def test_prompt_socket_overrides_port(executor):
function test_prompt_socket_short_host (line 366) | def test_prompt_socket_short_host(executor):
function test_enable_show_warnings (line 381) | def test_enable_show_warnings(executor):
function test_disable_show_warnings (line 390) | def test_disable_show_warnings(executor):
function test_output_ddl_with_warning_and_show_warnings_enabled (line 399) | def test_output_ddl_with_warning_and_show_warnings_enabled(executor):
function test_output_with_warning_and_show_warnings_enabled (line 410) | def test_output_with_warning_and_show_warnings_enabled(executor):
function test_output_with_warning_and_show_warnings_disabled (line 419) | def test_output_with_warning_and_show_warnings_disabled(executor):
function test_output_with_multiple_warnings_in_single_statement (line 428) | def test_output_with_multiple_warnings_in_single_statement(executor):
function test_output_with_multiple_warnings_in_multiple_statements (line 443) | def test_output_with_multiple_warnings_in_multiple_statements(executor):
function test_execute_arg (line 461) | def test_execute_arg(executor):
function test_execute_arg_with_checkpoint (line 483) | def test_execute_arg_with_checkpoint(executor):
function test_execute_arg_with_table (line 519) | def test_execute_arg_with_table(executor):
function test_execute_arg_with_csv (line 533) | def test_execute_arg_with_csv(executor):
function test_batch_mode (line 547) | def test_batch_mode(executor):
function test_batch_mode_multiline_statement (line 561) | def test_batch_mode_multiline_statement(executor):
function test_batch_mode_table (line 575) | def test_batch_mode_table(executor):
function test_batch_mode_csv (line 601) | def test_batch_mode_csv(executor):
function test_thanks_picker_utf8 (line 616) | def test_thanks_picker_utf8():
function test_help_strings_end_with_periods (line 621) | def test_help_strings_end_with_periods():
function test_command_descriptions_end_with_periods (line 629) | def test_command_descriptions_end_with_periods():
function output (line 636) | def output(monkeypatch, terminal_size, testdata, explicit_pager, expect_...
function test_conditional_pager (line 684) | def test_conditional_pager(monkeypatch):
function test_reserved_space_is_integer (line 700) | def test_reserved_space_is_integer(monkeypatch):
function test_list_dsn (line 712) | def test_list_dsn(monkeypatch):
function test_prettify_statement (line 739) | def test_prettify_statement():
function test_unprettify_statement (line 746) | def test_unprettify_statement():
function test_list_ssh_config (line 753) | def test_list_ssh_config():
function test_dsn (line 781) | def test_dsn(monkeypatch):
function test_password_flag_uses_sentinel (line 1042) | def test_password_flag_uses_sentinel(monkeypatch):
function test_ssh_config (line 1100) | def test_ssh_config(monkeypatch):
function test_init_command_arg (line 1202) | def test_init_command_arg(executor):
function test_init_command_multiple_arg (line 1214) | def test_init_command_multiple_arg(executor):
function test_global_init_commands (line 1229) | def test_global_init_commands(executor):
function test_execute_with_logfile (line 1241) | def test_execute_with_logfile(executor):
function test_null_string_config (line 1259) | def test_null_string_config(monkeypatch):
FILE: test/test_naive_completion.py
function completer (line 9) | def completer():
function complete_event (line 16) | def complete_event():
function test_empty_string_completion (line 22) | def test_empty_string_completion(completer, complete_event):
function test_select_keyword_completion (line 29) | def test_select_keyword_completion(completer, complete_event):
function test_function_name_completion (line 36) | def test_function_name_completion(completer, complete_event):
function test_column_name_completion (line 84) | def test_column_name_completion(completer, complete_event):
function test_special_name_completion (line 91) | def test_special_name_completion(completer, complete_event):
FILE: test/test_parseutils.py
function test_extract_columns_from_select (line 17) | def test_extract_columns_from_select():
function test_empty_string (line 25) | def test_empty_string():
function test_simple_select_single_table (line 30) | def test_simple_select_single_table():
function test_simple_select_single_table_schema_qualified (line 35) | def test_simple_select_single_table_schema_qualified():
function test_simple_select_multiple_tables (line 40) | def test_simple_select_multiple_tables():
function test_simple_select_multiple_tables_schema_qualified (line 45) | def test_simple_select_multiple_tables_schema_qualified():
function test_simple_select_with_cols_single_table (line 50) | def test_simple_select_with_cols_single_table():
function test_simple_select_with_cols_single_table_schema_qualified (line 55) | def test_simple_select_with_cols_single_table_schema_qualified():
function test_simple_select_with_cols_multiple_tables (line 60) | def test_simple_select_with_cols_multiple_tables():
function test_simple_select_with_cols_multiple_tables_with_schema (line 65) | def test_simple_select_with_cols_multiple_tables_with_schema():
function test_select_with_hanging_comma_single_table (line 70) | def test_select_with_hanging_comma_single_table():
function test_select_with_hanging_comma_multiple_tables (line 75) | def test_select_with_hanging_comma_multiple_tables():
function test_select_with_hanging_period_multiple_tables (line 80) | def test_select_with_hanging_period_multiple_tables():
function test_simple_insert_single_table (line 85) | def test_simple_insert_single_table():
function test_simple_insert_single_table_schema_qualified (line 93) | def test_simple_insert_single_table_schema_qualified():
function test_simple_update_table (line 98) | def test_simple_update_table():
function test_simple_update_table_with_schema (line 103) | def test_simple_update_table_with_schema():
function test_join_table (line 108) | def test_join_table():
function test_join_table_schema_qualified (line 113) | def test_join_table_schema_qualified():
function test_join_as_table (line 118) | def test_join_as_table():
function test_extract_tables_from_complete_statements (line 123) | def test_extract_tables_from_complete_statements():
function test_extract_tables_from_complete_statements_cte (line 128) | def test_extract_tables_from_complete_statements_cte():
function test_extract_tables_from_multiple_complete_statements (line 134) | def test_extract_tables_from_multiple_complete_statements():
function test_query_starts_with (line 139) | def test_query_starts_with():
function test_query_starts_with_comment (line 147) | def test_query_starts_with_comment():
function test_queries_start_with (line 152) | def test_queries_start_with():
function test_is_destructive (line 159) | def test_is_destructive():
function test_is_destructive_update_with_where_clause (line 164) | def test_is_destructive_update_with_where_clause():
function test_is_destructive_update_with_where_clause_and_comment (line 169) | def test_is_destructive_update_with_where_clause_and_comment():
function test_is_destructive_update_multiple_tables_with_where_clause (line 174) | def test_is_destructive_update_multiple_tables_with_where_clause():
function test_is_destructive_update_without_where_clause (line 179) | def test_is_destructive_update_without_where_clause():
function test_query_has_where_clause (line 191) | def test_query_has_where_clause(sql, has_where_clause):
function test_is_dropping_database (line 211) | def test_is_dropping_database(sql, dbname, is_dropping):
FILE: test/test_prompt_utils.py
function test_confirm_destructive_query_notty (line 8) | def test_confirm_destructive_query_notty() -> None:
FILE: test/test_smart_completion_public_schema_only.py
function completer (line 26) | def completer():
function empty_completer (line 52) | def empty_completer():
function complete_event (line 74) | def complete_event():
function test_use_database_completion (line 80) | def test_use_database_completion(completer, complete_event):
function test_special_name_completion (line 91) | def test_special_name_completion(completer, complete_event):
function test_empty_string_completion (line 98) | def test_empty_string_completion(completer, complete_event):
function test_select_keyword_completion (line 105) | def test_select_keyword_completion(completer, complete_event):
function test_select_star (line 121) | def test_select_star(completer, complete_event):
function test_introducer_completion (line 128) | def test_introducer_completion(completer, complete_event):
function test_collation_completion (line 138) | def test_collation_completion(completer, complete_event):
function test_transcoding_completion_1 (line 148) | def test_transcoding_completion_1(completer, complete_event):
function test_transcoding_completion_2 (line 158) | def test_transcoding_completion_2(completer, complete_event):
function test_transcoding_completion_3 (line 168) | def test_transcoding_completion_3(completer, complete_event):
function test_transcoding_completion_4 (line 178) | def test_transcoding_completion_4(completer, complete_event):
function test_where_transcoding_completion_1 (line 188) | def test_where_transcoding_completion_1(completer, complete_event):
function test_where_transcoding_completion_2 (line 198) | def test_where_transcoding_completion_2(completer, complete_event):
function test_table_completion (line 208) | def test_table_completion(completer, complete_event):
function test_select_filtered_table_completion (line 227) | def test_select_filtered_table_completion(completer, complete_event):
function test_sub_select_filtered_table_completion (line 246) | def test_sub_select_filtered_table_completion(completer, complete_event):
function test_enum_value_completion (line 265) | def test_enum_value_completion(completer, complete_event):
function test_function_name_completion (line 275) | def test_function_name_completion(completer, complete_event):
function test_suggested_column_names (line 290) | def test_suggested_column_names(completer, complete_event):
function test_suggested_column_names_empty_db (line 314) | def test_suggested_column_names_empty_db(empty_completer, complete_event):
function test_suggested_column_names_in_function (line 333) | def test_suggested_column_names_in_function(completer, complete_event):
function test_suggested_column_names_with_table_dot (line 354) | def test_suggested_column_names_with_table_dot(completer, complete_event):
function test_suggested_column_names_with_alias (line 374) | def test_suggested_column_names_with_alias(completer, complete_event):
function test_suggested_multiple_column_names (line 394) | def test_suggested_multiple_column_names(completer, complete_event):
function test_suggested_multiple_column_names_with_alias (line 419) | def test_suggested_multiple_column_names_with_alias(completer, complete_...
function test_suggested_multiple_column_names_with_dot (line 440) | def test_suggested_multiple_column_names_with_dot(completer, complete_ev...
function test_suggested_aliases_after_on (line 461) | def test_suggested_aliases_after_on(completer, complete_event):
function test_suggested_aliases_after_on_right_side (line 471) | def test_suggested_aliases_after_on_right_side(completer, complete_event):
function test_suggested_tables_after_on (line 481) | def test_suggested_tables_after_on(completer, complete_event):
function test_suggested_tables_after_on_right_side (line 491) | def test_suggested_tables_after_on_right_side(completer, complete_event):
function test_table_names_after_from (line 501) | def test_table_names_after_from(completer, complete_event):
function test_table_names_leading_partial (line 520) | def test_table_names_leading_partial(completer, complete_event):
function test_table_names_inter_partial (line 533) | def test_table_names_inter_partial(completer, complete_event):
function test_table_names_fuzzy (line 545) | def test_table_names_fuzzy(completer, complete_event):
function test_auto_escaped_col_names (line 554) | def test_auto_escaped_col_names(completer, complete_event):
function test_un_escaped_table_names (line 571) | def test_un_escaped_table_names(completer, complete_event):
function test_grant_on_suggets_tables_and_schemata (line 584) | def test_grant_on_suggets_tables_and_schemata(completer, complete_event):
function test_deleted_keyword_completion (line 605) | def test_deleted_keyword_completion(completer, complete_event):
function test_numbers_no_completion (line 617) | def test_numbers_no_completion(completer, complete_event):
function dummy_list_path (line 624) | def dummy_list_path(dir_name):
function test_file_name_completion (line 653) | def test_file_name_completion(completer, complete_event, text, expected):
function test_auto_case_heuristic (line 661) | def test_auto_case_heuristic(completer, complete_event):
function test_create_table_like_completion (line 671) | def test_create_table_like_completion(completer, complete_event):
function test_source_eager_completion (line 684) | def test_source_eager_completion(completer, complete_event):
function test_source_leading_dot_suggestions_completion (line 708) | def test_source_leading_dot_suggestions_completion(completer, complete_e...
function test_string_no_completion (line 732) | def test_string_no_completion(completer, complete_event):
function test_string_no_completion_single_quote (line 739) | def test_string_no_completion_single_quote(completer, complete_event):
function test_string_no_completion_spaces (line 746) | def test_string_no_completion_spaces(completer, complete_event):
function test_string_no_completion_spaces_inner_1 (line 753) | def test_string_no_completion_spaces_inner_1(completer, complete_event):
function test_string_no_completion_spaces_inner_2 (line 760) | def test_string_no_completion_spaces_inner_2(completer, complete_event):
function test_backticked_column_completion (line 767) | def test_backticked_column_completion(completer, complete_event):
function test_backticked_column_completion_component (line 794) | def test_backticked_column_completion_component(completer, complete_event):
function test_backticked_column_completion_two_character (line 819) | def test_backticked_column_completion_two_character(completer, complete_...
function test_backticked_column_completion_three_character (line 888) | def test_backticked_column_completion_three_character(completer, complet...
function test_backticked_column_completion_four_character (line 918) | def test_backticked_column_completion_four_character(completer, complete...
function test_backticked_table_completion_required (line 932) | def test_backticked_table_completion_required(completer, complete_event):
function test_backticked_table_completion_not_required (line 941) | def test_backticked_table_completion_not_required(completer, complete_ev...
function test_string_no_completion_backtick (line 956) | def test_string_no_completion_backtick(completer, complete_event):
function test_backticked_no_completion_spaces (line 966) | def test_backticked_no_completion_spaces(completer, complete_event):
FILE: test/test_special_iocommands.py
function test_set_get_pager (line 16) | def test_set_get_pager(monkeypatch):
function test_set_get_timing (line 32) | def test_set_get_timing():
function test_set_get_expanded_output (line 39) | def test_set_get_expanded_output():
function test_editor_command (line 46) | def test_editor_command(monkeypatch):
function test_tee_command (line 67) | def test_tee_command():
function test_tee_command_error (line 103) | def test_tee_command_error():
function test_favorite_query (line 115) | def test_favorite_query():
function test_special_favorite_query (line 124) | def test_special_favorite_query():
function test_once_command (line 133) | def test_once_command():
function test_pipe_once_command (line 166) | def test_pipe_once_command():
function test_parseargfile (line 188) | def test_parseargfile():
function test_parseargfile_no_file (line 204) | def test_parseargfile_no_file():
function test_watch_query_iteration (line 214) | def test_watch_query_iteration():
function test_watch_query_full (line 228) | def test_watch_query_full():
function test_watch_query_clear (line 256) | def test_watch_query_clear(clear_mock):
function test_watch_query_bad_arguments (line 271) | def test_watch_query_bad_arguments():
function test_watch_query_interval_clear (line 288) | def test_watch_query_interval_clear(clear_mock):
function test_split_sql_by_delimiter (line 307) | def test_split_sql_by_delimiter():
function test_switch_delimiter_within_query (line 316) | def test_switch_delimiter_within_query():
function test_set_delimiter (line 324) | def test_set_delimiter():
function teardown_function (line 330) | def teardown_function():
FILE: test/test_sqlexecute.py
function assert_result_equal (line 15) | def assert_result_equal(
function test_timediff_negative_value (line 50) | def test_timediff_negative_value(executor):
function test_timediff_positive_value (line 58) | def test_timediff_positive_value(executor):
function test_get_result_status_without_warning (line 66) | def test_get_result_status_without_warning(executor):
function test_get_result_status_with_warning (line 73) | def test_get_result_status_with_warning(executor):
function test_conn (line 85) | def test_conn(executor):
function test_bools (line 94) | def test_bools(executor):
function test_binary (line 103) | def test_binary(executor):
function test_table_and_columns_query (line 118) | def test_table_and_columns_query(executor):
function test_database_list (line 127) | def test_database_list(executor):
function test_invalid_syntax (line 133) | def test_invalid_syntax(executor):
function test_invalid_column_name (line 140) | def test_invalid_column_name(executor):
function test_unicode_support_in_output (line 147) | def test_unicode_support_in_output(executor):
function test_multiple_queries_same_line (line 157) | def test_multiple_queries_same_line(executor):
function test_multiple_queries_same_line_syntaxerror (line 182) | def test_multiple_queries_same_line_syntaxerror(executor):
function test_favorite_query (line 190) | def test_favorite_query(executor):
function test_favorite_query_multiple_statement (line 208) | def test_favorite_query_multiple_statement(executor):
function test_favorite_query_expanded_output (line 244) | def test_favorite_query_expanded_output(executor):
function test_collapsed_output_special_command (line 263) | def test_collapsed_output_special_command(executor):
function test_special_command (line 270) | def test_special_command(executor):
function test_cd_command_without_a_folder_name (line 276) | def test_cd_command_without_a_folder_name(executor):
function test_cd_command_with_one_nonexistent_folder_name (line 284) | def test_cd_command_with_one_nonexistent_folder_name(executor):
function test_cd_command_with_one_real_folder_name (line 290) | def test_cd_command_with_one_real_folder_name(executor):
function test_cd_command_with_two_folder_names (line 297) | def test_cd_command_with_two_folder_names(executor):
function test_cd_command_unbalanced (line 305) | def test_cd_command_unbalanced(executor):
function test_system_command_not_found (line 315) | def test_system_command_not_found(executor):
function test_system_command_output (line 324) | def test_system_command_output(executor):
function test_cd_command_current_dir (line 333) | def test_cd_command_current_dir(executor):
function test_unicode_support (line 340) | def test_unicode_support(executor):
function test_timestamp_null (line 346) | def test_timestamp_null(executor):
function test_datetime_null (line 354) | def test_datetime_null(executor):
function test_date_null (line 362) | def test_date_null(executor):
function test_time_null (line 370) | def test_time_null(executor):
function test_multiple_results (line 378) | def test_multiple_results(executor):
function test_version_parsing (line 424) | def test_version_parsing(version_string, species, parsed_version_string,...
FILE: test/test_tabular_output.py
function mycli (line 16) | def mycli():
function test_sql_output (line 24) | def test_sql_output(mycli):
function test_postamble_output (line 123) | def test_postamble_output(mycli):
FILE: test/utils.py
function db_connection (line 33) | def db_connection(dbname=None):
function create_db (line 48) | def create_db(dbname):
function run (line 57) | def run(executor, sql, rows_as_list=True):
function set_expanded_output (line 75) | def set_expanded_output(is_expanded):
function is_expanded_output (line 80) | def is_expanded_output():
function send_ctrl_c_to_pid (line 85) | def send_ctrl_c_to_pid(pid, wait_seconds):
function send_ctrl_c (line 96) | def send_ctrl_c(wait_seconds):
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (743K chars).
[
{
"path": ".coveragerc",
"chars": 21,
"preview": "[run]\nsource = mycli\n"
},
{
"path": ".git-blame-ignore-revs",
"chars": 0,
"preview": ""
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 465,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!-- You can del"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 108,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 458,
"preview": "## Description\n<!--- Describe your changes in detail. -->\n\n\n\n## Checklist\n<!--- We appreciate your help and want to give"
},
{
"path": ".github/dependabot.yml",
"chars": 117,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: \"daily\"\n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2395,
"preview": "name: CI\n\non:\n pull_request:\n paths-ignore:\n - '**.md'\n - '**.rst'\n - 'LICENSE.txt'\n - 'doc/**/*"
},
{
"path": ".github/workflows/codex-review.yml",
"chars": 2684,
"preview": "name: Codex Review\n\non:\n pull_request_target:\n types: [opened, labeled, reopened, ready_for_review]\n paths-ignore"
},
{
"path": ".github/workflows/lint.yml",
"chars": 664,
"preview": "name: Lint\n\non:\n pull_request:\n paths-ignore:\n - '**.md'\n - '**.rst'\n - 'LICENSE.txt'\n - 'doc/**"
},
{
"path": ".github/workflows/publish.yml",
"chars": 2816,
"preview": "name: Publish Python Package\n\non:\n release:\n types: [created]\n\npermissions:\n contents: read\n\njobs:\n docs:\n runs"
},
{
"path": ".github/workflows/typecheck.yml",
"chars": 950,
"preview": "name: Typecheck\n\non:\n pull_request:\n paths-ignore:\n - '**.md'\n - '**.rst'\n - 'LICENSE.txt'\n - 'd"
},
{
"path": ".gitignore",
"chars": 151,
"preview": ".idea/\n.vscode/\n/build\n/dist\n/mycli.egg-info\n/src\n/test/behave.ini\n\n.vagrant\n*.pyc\n*.deb\n.cache/\n.coverage\n.coverage.*\n\n"
},
{
"path": "AUTHORS.rst",
"chars": 54,
"preview": "Check out our `AUTHORS`_.\n\n.. _AUTHORS: mycli/AUTHORS\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 3269,
"preview": "# Development Guide\n\nThis is a guide for developers who would like to contribute to this project.\n\nIf you're interested "
},
{
"path": "LICENSE.txt",
"chars": 1485,
"preview": "Copyright (c) 2015-2026, mycli maintainers\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with"
},
{
"path": "MANIFEST.in",
"chars": 201,
"preview": "include LICENSE.txt *.md *.rst screenshots/*\ninclude tasks.py .coveragerc tox.ini\nrecursive-include test *.cnf\nrecursive"
},
{
"path": "README.md",
"chars": 5007,
"preview": "# mycli\n\n[](https://github.com/dbcli/mycli/acti"
},
{
"path": "SPONSORS.rst",
"chars": 57,
"preview": "Check out our `SPONSORS`_.\n\n.. _SPONSORS: mycli/SPONSORS\n"
},
{
"path": "changelog.md",
"chars": 53504,
"preview": "1.65.2 (2026/03/19)\n==============\n\nSecurity\n--------\n* Harden `codex-review` workflow against script injection from unt"
},
{
"path": "doc/key_bindings.rst",
"chars": 1665,
"preview": "*************\nKey Bindings:\n*************\n\nMost key bindings are simply inherited from `prompt-toolkit <https://python-p"
},
{
"path": "doc/llm.md",
"chars": 5423,
"preview": "# Using the \\llm Command (AI-assisted SQL)\n\nThe `\\llm` special command lets you ask natural-language questions and get S"
},
{
"path": "mycli/AUTHORS",
"chars": 1948,
"preview": "Project Lead:\n-------------\n\n * Roland Walker\n\nCore Developers:\n----------------\n\n * Thomas Roten\n * Irina Truong\n *"
},
{
"path": "mycli/SPONSORS",
"chars": 510,
"preview": "Many thanks to the following Kickstarter backers.\n\n* Tech Blue Software\n* jweiland.net\n\n# Silver Sponsors\n\n* Whitane Tec"
},
{
"path": "mycli/TIPS",
"chars": 6946,
"preview": "###\n### CLI arguments\n###\n\ncheck your ~/.myclirc settings using the --checkup flag!\n\nlist your aliased DSNs with the --l"
},
{
"path": "mycli/__init__.py",
"chars": 82,
"preview": "import importlib.metadata\n\n__version__: str = importlib.metadata.version(\"mycli\")\n"
},
{
"path": "mycli/clibuffer.py",
"chars": 1662,
"preview": "from prompt_toolkit.application import get_app\nfrom prompt_toolkit.enums import DEFAULT_BUFFER\nfrom prompt_toolkit.filte"
},
{
"path": "mycli/clistyle.py",
"chars": 8476,
"preview": "import logging\n\nfrom prompt_toolkit.styles import Style, merge_styles\nfrom prompt_toolkit.styles.pygments import style_f"
},
{
"path": "mycli/clitoolbar.py",
"chars": 3670,
"preview": "from typing import Callable\n\nfrom prompt_toolkit.application import get_app\nfrom prompt_toolkit.enums import EditingMode"
},
{
"path": "mycli/compat.py",
"chars": 118,
"preview": "\"\"\"Platform and Python version compatibility support.\"\"\"\n\nimport sys\n\nWIN: bool = sys.platform in (\"win32\", \"cygwin\")\n"
},
{
"path": "mycli/completion_refresher.py",
"chars": 6505,
"preview": "import threading\nfrom typing import Callable\n\nfrom mycli.packages.special.main import COMMANDS\nfrom mycli.packages.sqlre"
},
{
"path": "mycli/config.py",
"chars": 10560,
"preview": "from copy import copy\nfrom importlib import resources\nfrom io import BytesIO, TextIOWrapper\nimport logging\nimport os\nfro"
},
{
"path": "mycli/constants.py",
"chars": 297,
"preview": "HOME_URL = 'https://mycli.net'\nREPO_URL = 'https://github.com/dbcli/mycli'\nDOCS_URL = f'{HOME_URL}/docs'\nISSUES_URL = f'"
},
{
"path": "mycli/key_bindings.py",
"chars": 10749,
"preview": "import logging\nimport webbrowser\n\nimport prompt_toolkit\nfrom prompt_toolkit.application.current import get_app\nfrom prom"
},
{
"path": "mycli/lexer.py",
"chars": 293,
"preview": "from pygments.lexer import inherit\nfrom pygments.lexers.sql import MySqlLexer\nfrom pygments.token import Keyword\n\n\nclass"
},
{
"path": "mycli/magic.py",
"chars": 2161,
"preview": "import logging\nfrom typing import Any\n\nimport sql.connection\nimport sql.parse\n\nfrom mycli.main import MyCli, Query\n\n_log"
},
{
"path": "mycli/main.py",
"chars": 114921,
"preview": "from __future__ import annotations\n\nfrom collections import defaultdict, namedtuple\nfrom decimal import Decimal\nimport f"
},
{
"path": "mycli/myclirc",
"chars": 14022,
"preview": "# vi: ft=dosini\n[main]\n\n# Enable or disable the automatic displaying of warnings (\"SHOW WARNINGS\")\n# after executing a S"
},
{
"path": "mycli/packages/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "mycli/packages/checkup.py",
"chars": 6460,
"preview": "import importlib.metadata\nimport json\nimport os\nimport shutil\nimport sys\nimport urllib.error\nimport urllib.request\n\nfrom"
},
{
"path": "mycli/packages/completion_engine.py",
"chars": 22445,
"preview": "import functools\nimport re\nfrom typing import Any, Literal\n\nimport sqlparse\nfrom sqlparse.sql import Comparison, Identif"
},
{
"path": "mycli/packages/filepaths.py",
"chars": 3487,
"preview": "import os\nimport platform\n\nDEFAULT_SOCKET_DIRS: list[str] = []\nif os.name == \"posix\":\n if platform.system() == \"Darwi"
},
{
"path": "mycli/packages/hybrid_redirection.py",
"chars": 6314,
"preview": "import functools\nimport logging\nimport warnings\n\nwith warnings.catch_warnings():\n # for sqlglot v29.0.1\n warnings."
},
{
"path": "mycli/packages/paramiko_stub/__init__.py",
"chars": 882,
"preview": "\"\"\"A module to import instead of paramiko when it is not available (to avoid\nchecking for paramiko all over the place).\n"
},
{
"path": "mycli/packages/parseutils.py",
"chars": 16046,
"preview": "from __future__ import annotations\n\nimport re\nfrom typing import Any, Generator, Literal\nimport warnings\n\nimport sqlpars"
},
{
"path": "mycli/packages/prompt_utils.py",
"chars": 1692,
"preview": "import sys\n\nimport click\n\nfrom mycli.packages.parseutils import is_destructive\n\n\nclass ConfirmBoolParamType(click.ParamT"
},
{
"path": "mycli/packages/shortcuts.py",
"chars": 517,
"preview": "from mycli.sqlexecute import SQLExecute\n\n\ndef server_date(sqlexecute: SQLExecute, quoted: bool = False) -> str:\n serv"
},
{
"path": "mycli/packages/special/__init__.py",
"chars": 2227,
"preview": "from mycli.packages.special.dbcommands import (\n list_databases,\n list_tables,\n status,\n)\nfrom mycli.packages.s"
},
{
"path": "mycli/packages/special/dbcommands.py",
"chars": 6126,
"preview": "import logging\nimport os\nimport platform\n\nfrom pymysql import ProgrammingError\nfrom pymysql.cursors import Cursor\n\nfrom "
},
{
"path": "mycli/packages/special/delimitercommand.py",
"chars": 2978,
"preview": "from __future__ import annotations\n\nimport re\nfrom typing import Generator\n\nimport sqlparse\n\nfrom mycli.packages.sqlresu"
},
{
"path": "mycli/packages/special/favoritequeries.py",
"chars": 1824,
"preview": "from __future__ import annotations\n\n\nclass FavoriteQueries:\n section_name: str = \"favorite_queries\"\n\n usage = \"\"\"\n"
},
{
"path": "mycli/packages/special/iocommands.py",
"chars": 21496,
"preview": "from __future__ import annotations\n\nimport locale\nimport logging\nimport os\nimport re\nimport shlex\nimport subprocess\nfrom"
},
{
"path": "mycli/packages/special/llm.py",
"chars": 11714,
"preview": "import contextlib\nimport functools\nimport io\nimport logging\nimport os\nimport re\nfrom runpy import run_module\nimport shle"
},
{
"path": "mycli/packages/special/main.py",
"chars": 6673,
"preview": "from collections import namedtuple\nfrom enum import Enum\nimport logging\nimport os\nfrom typing import Callable\nimport web"
},
{
"path": "mycli/packages/special/utils.py",
"chars": 2898,
"preview": "import logging\nimport os\n\nimport click\nimport pymysql\nfrom pymysql.cursors import Cursor\n\nlogger = logging.getLogger(__n"
},
{
"path": "mycli/packages/sqlresult.py",
"chars": 785,
"preview": "from dataclasses import dataclass\nfrom functools import cached_property\n\nfrom prompt_toolkit.formatted_text import Forma"
},
{
"path": "mycli/packages/string_utils.py",
"chars": 259,
"preview": "import re\n\nfrom cli_helpers.utils import strip_ansi\n\n\ndef sanitize_terminal_title(title: str) -> str:\n sanitized = st"
},
{
"path": "mycli/packages/tabular_output/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "mycli/packages/tabular_output/sql_format.py",
"chars": 2269,
"preview": "\"\"\"Format adapter for sql.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Generator, Union\n\nfrom cli_helpers"
},
{
"path": "mycli/packages/toolkit/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "mycli/packages/toolkit/fzf.py",
"chars": 2359,
"preview": "import re\nimport shlex\nfrom shutil import which\n\nfrom prompt_toolkit import search\nfrom prompt_toolkit.key_binding.key_p"
},
{
"path": "mycli/packages/toolkit/history.py",
"chars": 1611,
"preview": "import os\nfrom typing import Union\n\nfrom prompt_toolkit.history import FileHistory\n\n_StrOrBytesPath = Union[str, bytes, "
},
{
"path": "mycli/packages/toolkit/utils.py",
"chars": 686,
"preview": "from prompt_toolkit.application import Application, run_in_terminal\n\n\ndef safe_invalidate_display(app: Application) -> N"
},
{
"path": "mycli/sqlcompleter.py",
"chars": 50708,
"preview": "from __future__ import annotations\n\nfrom collections import Counter\nfrom enum import IntEnum\nimport logging\nimport re\nfr"
},
{
"path": "mycli/sqlexecute.py",
"chars": 22667,
"preview": "from __future__ import annotations\n\nimport datetime\nimport enum\nimport logging\nimport re\nimport ssl\nfrom typing import A"
},
{
"path": "pyproject.toml",
"chars": 2981,
"preview": "[project]\nname = \"mycli\"\ndynamic = [\"version\"]\ndescription = \"CLI for MySQL Database. With auto-completion and syntax hi"
},
{
"path": "pytest.ini",
"chars": 69,
"preview": "[pytest]\naddopts = --ignore=mycli/packages/paramiko_stub/__init__.py\n"
},
{
"path": "test/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "test/conftest.py",
"chars": 892,
"preview": "# type: ignore\n\nimport pytest\n\nimport mycli.sqlexecute\nfrom test.utils import CHARACTER_SET, DATABASE, HOST, PASSWORD, P"
},
{
"path": "test/features/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "test/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": "test/features/basic_commands.feature",
"chars": 591,
"preview": "Feature: run the cli,\n call the help command,\n check our application name,\n insert the date,\n exit the cli\n\n Scenar"
},
{
"path": "test/features/connection.feature",
"chars": 1201,
"preview": "Feature: connect to a database:\n\n @requires_local_db\n Scenario: run mycli on localhost without port\n When we run my"
},
{
"path": "test/features/crud_database.feature",
"chars": 1022,
"preview": "Feature: manipulate databases:\n create, drop, connect, disconnect\n\n Scenario: create and drop temporary database\n "
},
{
"path": "test/features/crud_table.feature",
"chars": 1842,
"preview": "Feature: manipulate tables:\n create, insert, update, select, delete from, drop\n\n Scenario: create, insert, select from"
},
{
"path": "test/features/db_utils.py",
"chars": 1987,
"preview": "# type: ignore\n\nimport pymysql\n\nfrom mycli.constants import DEFAULT_CHARSET, DEFAULT_HOST, DEFAULT_PORT\n\n\ndef create_db("
},
{
"path": "test/features/environment.py",
"chars": 5586,
"preview": "# type: ignore\n\nimport os\nimport shutil\nimport sys\nfrom tempfile import NamedTemporaryFile\n\nimport db_utils as dbutils\ni"
},
{
"path": "test/features/fixture_data/help.txt",
"chars": 1848,
"preview": "+--------------------------+-----------------------------------------------+\n| Command | Description "
},
{
"path": "test/features/fixture_data/help_commands.txt",
"chars": 4699,
"preview": "+----------------+----------+---------------------------------+---------------------------------------------------------"
},
{
"path": "test/features/fixture_utils.py",
"chars": 717,
"preview": "# type: ignore\n\nimport os\n\n\ndef read_fixture_lines(filename):\n \"\"\"Read lines of text from file.\n\n :param filename:"
},
{
"path": "test/features/iocommands.feature",
"chars": 3700,
"preview": "Feature: I/O commands\n\n Scenario: edit sql in file with external editor\n When we start external editor providing a "
},
{
"path": "test/features/named_queries.feature",
"chars": 1007,
"preview": "Feature: named queries:\n save, use and delete named queries\n\n Scenario: save, use and delete named queries\n When w"
},
{
"path": "test/features/specials.feature",
"chars": 174,
"preview": "Feature: Special commands\n\n @wip\n Scenario: run refresh command\n When we refresh completions\n and we wait for"
},
{
"path": "test/features/steps/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "test/features/steps/auto_vertical.py",
"chars": 1412,
"preview": "# type: ignore\n\nfrom textwrap import dedent\n\nfrom behave import then, when\nfrom utils import parse_cli_args_to_dict\nimpo"
},
{
"path": "test/features/steps/basic_commands.py",
"chars": 4069,
"preview": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string deco"
},
{
"path": "test/features/steps/connection.py",
"chars": 1915,
"preview": "# type: ignore\n\nimport io\nimport os\n\nfrom behave import then, when\nimport wrappers\n\nfrom mycli.config import encrypt_myl"
},
{
"path": "test/features/steps/crud_database.py",
"chars": 3383,
"preview": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string deco"
},
{
"path": "test/features/steps/crud_table.py",
"chars": 3178,
"preview": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string deco"
},
{
"path": "test/features/steps/iocommands.py",
"chars": 4056,
"preview": "# type: ignore\n\nimport os\nfrom textwrap import dedent\n\nfrom behave import then, when\nimport wrappers\n\n\n@when(\"we start e"
},
{
"path": "test/features/steps/named_queries.py",
"chars": 2865,
"preview": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string deco"
},
{
"path": "test/features/steps/specials.py",
"chars": 764,
"preview": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string deco"
},
{
"path": "test/features/steps/utils.py",
"chars": 298,
"preview": "# type: ignore\n\nimport shlex\n\n\ndef parse_cli_args_to_dict(cli_args: str):\n args_dict = {}\n for arg in shlex.split("
},
{
"path": "test/features/steps/wrappers.py",
"chars": 3165,
"preview": "# type: ignore\n\nfrom io import StringIO\nimport re\nimport sys\nimport textwrap\n\nimport pexpect\n\n\ndef expect_exact(context,"
},
{
"path": "test/features/wrappager.py",
"chars": 286,
"preview": "#!/usr/bin/env python\n\nimport sys\n\n\ndef wrappager(boundary: str) -> None:\n print(boundary)\n while 1:\n buf ="
},
{
"path": "test/myclirc",
"chars": 13955,
"preview": "# vi: ft=dosini\n[main]\n\n# Enable or disable the automatic displaying of warnings (\"SHOW WARNINGS\")\n# after executing a S"
},
{
"path": "test/test.txt",
"chars": 13,
"preview": "mycli rocks!\n"
},
{
"path": "test/test_clistyle.py",
"chars": 892,
"preview": "# type: ignore\n\n\"\"\"Test the mycli.clistyle module.\"\"\"\n\nfrom pygments.style import Style\nfrom pygments.token import Token"
},
{
"path": "test/test_clitoolbar.py",
"chars": 745,
"preview": "from prompt_toolkit.shortcuts import PromptSession\n\nfrom mycli.clitoolbar import create_toolbar_tokens_func\nfrom mycli.m"
},
{
"path": "test/test_completion_engine.py",
"chars": 24755,
"preview": "# type: ignore\n\nimport pytest\n\nfrom mycli.packages import special\nfrom mycli.packages.completion_engine import (\n _fi"
},
{
"path": "test/test_completion_refresher.py",
"chars": 2792,
"preview": "# type: ignore\n\nimport time\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\n\n@pytest.fixture\ndef refresher():\n "
},
{
"path": "test/test_config.py",
"chars": 5796,
"preview": "# type: ignore\n\n\"\"\"Unit tests for the mycli.config module.\"\"\"\n\nfrom io import BytesIO, StringIO, TextIOWrapper\nimport os"
},
{
"path": "test/test_dbspecial.py",
"chars": 3621,
"preview": "# type: ignore\n\nfrom unittest.mock import MagicMock\n\nfrom mycli.packages.completion_engine import suggest_type\nfrom mycl"
},
{
"path": "test/test_llm_special.py",
"chars": 8126,
"preview": "from unittest.mock import patch\n\nimport pytest\n\nfrom mycli.packages.special.llm import (\n USAGE,\n FinishIteration,"
},
{
"path": "test/test_main.py",
"chars": 40804,
"preview": "# type: ignore\n\nfrom collections import namedtuple\nfrom contextlib import redirect_stdout\nimport csv\nimport io\nimport os"
},
{
"path": "test/test_naive_completion.py",
"chars": 2902,
"preview": "# type: ignore\n\nfrom prompt_toolkit.completion import Completion\nfrom prompt_toolkit.document import Document\nimport pyt"
},
{
"path": "test/test_parseutils.py",
"chars": 7177,
"preview": "# type: ignore\n\nimport pytest\n\nfrom mycli.packages.parseutils import (\n extract_columns_from_select,\n extract_tabl"
},
{
"path": "test/test_plan.wiki",
"chars": 1550,
"preview": "= Gross Checks =\n * [ ] Check connecting to a local database.\n * [ ] Check connecting to a remote database.\n * "
},
{
"path": "test/test_prompt_utils.py",
"chars": 320,
"preview": "# type: ignore\n\nimport click\n\nfrom mycli.packages.prompt_utils import confirm_destructive_query\n\n\ndef test_confirm_destr"
},
{
"path": "test/test_smart_completion_public_schema_only.py",
"chars": 39357,
"preview": "# type: ignore\n\nimport os.path\nfrom unittest.mock import patch\n\nfrom prompt_toolkit.completion import Completion\nfrom pr"
},
{
"path": "test/test_special_iocommands.py",
"chars": 12501,
"preview": "# type: ignore\n\nimport os\nimport stat\nimport tempfile\nfrom time import time\nfrom unittest.mock import patch\n\nfrom pymysq"
},
{
"path": "test/test_sqlexecute.py",
"chars": 14175,
"preview": "# type: ignore\n\nfrom datetime import time\nimport os\n\nfrom prompt_toolkit.formatted_text import FormattedText\nimport pymy"
},
{
"path": "test/test_tabular_output.py",
"chars": 5867,
"preview": "# type: ignore\n\n\"\"\"Test the sql output adapter.\"\"\"\n\nfrom textwrap import dedent\n\nfrom pymysql.constants import FIELD_TYP"
},
{
"path": "test/utils.py",
"chars": 2890,
"preview": "# type: ignore\n\nimport multiprocessing\nimport os\nimport platform\nimport signal\nimport time\n\nimport pymysql\nimport pytest"
},
{
"path": "tox.ini",
"chars": 393,
"preview": "[tox]\nenvlist = py\n\n[testenv]\nskip_install = true\ndeps = uv\npassenv = PYTEST_HOST\n PYTEST_USER\n PYTEST_PASSWORD\n "
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the dbcli/mycli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (685.8 KB), approximately 168.4k tokens, and a symbol index with 766 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.