[
  {
    "path": ".coveragerc",
    "content": "[run]\nsource = mycli\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": ""
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!-- You can delete any parts of this template not applicable to your issue. -->\n\n### Suggested troubleshooting steps for bug reports\n\n * [ ] Upgraded to the latest mycli if possible.\n * [ ] Ran `mycli --checkup`, if supported.\n\n### Expected Behavior\n\n\n### Actual Behavior\n\n\n### Steps to Reproduce\n\n\n### System\n\n * mycli version:\n * OS/version:\n\n### Discussion\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Description\n<!--- Describe your changes in detail. -->\n\n\n\n## Checklist\n<!--- We appreciate your help and want to give you credit. Place an `x` in the boxes below as you complete them. -->\n- [ ] I added this contribution to the `changelog.md` file.\n- [ ] I added my name to the `AUTHORS` file (or it's already there).\n- [ ] To lint and format the code, I ran\n    ```bash\n    uv run ruff check && uv run ruff format && uv run mypy --install-types .\n    ```\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.rst'\n      - 'LICENSE.txt'\n      - 'doc/**/*.txt'\n      - '**/AUTHORS'\n      - '**/SPONSORS'\n      - '**/TIPS'\n\njobs:\n  tests:\n    name: Tests\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0\n        with:\n          version: \"latest\"\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Start MySQL\n        run: |\n          sudo /etc/init.d/mysql start\n\n      - name: Install dependencies\n        run: uv sync --all-extras -p ${{ matrix.python-version }}\n\n      - name: Wait for MySQL connection\n        run: |\n          while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do\n            sleep 5\n          done\n\n      - name: Pytest / behave\n        env:\n          PYTEST_PASSWORD: root\n          PYTEST_HOST: 127.0.0.1\n          TERM: xterm\n        run: |\n          uv run tox -e py${{ matrix.python-version }}\n\n  test-no-extras:\n    name: Tests Without Extras\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0\n        with:\n          version: \"latest\"\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: '3.13'\n\n      - name: Start MySQL\n        run: |\n          sudo /etc/init.d/mysql start\n\n      - name: Install dependencies\n        run: uv sync --extra dev -p python3.13\n\n      - name: Wait for MySQL connection\n        run: |\n          while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do\n            sleep 5\n          done\n\n      - name: Pytest / behave\n        env:\n          PYTEST_PASSWORD: root\n          PYTEST_HOST: 127.0.0.1\n          TERM: xterm\n        run: |\n          uv run tox -e py3.13\n"
  },
  {
    "path": ".github/workflows/codex-review.yml",
    "content": "name: Codex Review\n\non:\n  pull_request_target:\n    types: [opened, labeled, reopened, ready_for_review]\n    paths-ignore:\n      - '**.md'\n      - '**.rst'\n      - 'LICENSE.txt'\n      - 'doc/**/*.txt'\n      - '**/AUTHORS'\n      - '**/SPONSORS'\n      - '**/TIPS'\n\njobs:\n  codex-review:\n    if: github.event.pull_request.draft == false || (github.event.action == 'labeled' && contains(github.event.pull_request.labels.*.name, 'codex'))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    outputs:\n      final_message: ${{ steps.run_codex.outputs.final-message }}\n\n    steps:\n      - name: Check out PR merge commit\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: refs/pull/${{ github.event.pull_request.number }}/merge\n\n      - name: Fetch base and head refs\n        run: |\n          git fetch --no-tags origin \\\n            ${{ github.event.pull_request.base.ref }} \\\n            +refs/pull/${{ github.event.pull_request.number }}/head\n\n      - name: Run Codex review\n        id: run_codex\n        uses: openai/codex-action@v1\n        env:\n          # Use env variables to handle untrusted metadata safely\n          PR_TITLE: ${{ github.event.pull_request.title }}\n          PR_BODY: ${{ github.event.pull_request.body }}\n        with:\n          openai-api-key: ${{ secrets.OPENAI_API_KEY }}\n          prompt: |\n            You are reviewing PR #${{ github.event.pull_request.number }} for ${{ github.repository }}.\n\n            Only review changes introduced by this PR:\n            git log --oneline ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}\n\n            Focus on:\n            - correctness bugs and regressions\n            - security concerns\n            - missing tests or edge cases\n\n            Keep feedback concise and actionable.\n\n            Pull request title and body:\n            ----\n            $PR_TITLE\n            $PR_BODY\n\n  post-feedback:\n    runs-on: ubuntu-latest\n    needs: codex-review\n    if: needs.codex-review.outputs.final_message != ''\n    permissions:\n      issues: write\n      pull-requests: write\n\n    steps:\n      - name: Post Codex review as PR comment\n        uses: actions/github-script@v8\n        env:\n          CODEX_FINAL_MESSAGE: ${{ needs.codex-review.outputs.final_message }}\n        with:\n          github-token: ${{ github.token }}\n          script: |\n            await github.rest.issues.createComment({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.payload.pull_request.number,\n              body: process.env.CODEX_FINAL_MESSAGE,\n            });\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.rst'\n      - 'LICENSE.txt'\n      - 'doc/**/*.txt'\n      - '**/AUTHORS'\n      - '**/SPONSORS'\n      - '**/TIPS'\n\njobs:\n  linters:\n    name: Linters\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Run ruff check\n        uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1\n\n      - name: Run ruff format\n        uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1\n        with:\n          args: 'format --check'\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish Python Package\n\non:\n  release:\n    types: [created]\n\npermissions:\n  contents: read\n\njobs:\n  docs:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n    - name: Require release changelog form\n      run: |\n        if grep -q TBD changelog.md; then false; fi\n\n  test:\n    runs-on: ubuntu-latest\n    needs: [docs]\n    continue-on-error: true\n\n    strategy:\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n    - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0\n      with:\n        version: \"latest\"\n\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Start MySQL\n      run: |\n        sudo /etc/init.d/mysql start\n\n    - name: Install dependencies\n      run: uv sync --all-extras -p ${{ matrix.python-version }}\n\n    - name: Wait for MySQL connection\n      run: |\n        while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do\n          sleep 5\n        done\n\n    - name: Pytest / behave\n      env:\n        PYTEST_PASSWORD: root\n        PYTEST_HOST: 127.0.0.1\n      run: |\n        uv run tox -e py${{ matrix.python-version }}\n\n      # arguably this should be made identical to CI for PRs\n    - name: Run Style Checks\n      run: uv run tox -e style\n\n  build:\n    runs-on: ubuntu-latest\n    needs: [test]\n\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n    - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0\n      with:\n        version: \"latest\"\n\n    - name: Set up Python\n      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n      with:\n        python-version: '3.13'\n\n    - name: Install dependencies\n      run: uv sync --all-extras -p 3.13\n\n    - name: Build\n      run: uv build\n\n    - name: Store the distribution packages\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: python-packages\n        path: dist/\n\n  publish:\n    name: Publish to PyPI\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/')\n    needs: [build]\n    environment: release\n    permissions:\n      id-token: write\n    steps:\n    - name: Download distribution packages\n      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        name: python-packages\n        path: dist/\n    - name: Publish to PyPI\n      uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0\n"
  },
  {
    "path": ".github/workflows/typecheck.yml",
    "content": "name: Typecheck\n\non:\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.rst'\n      - 'LICENSE.txt'\n      - 'doc/**/*.txt'\n      - '**/AUTHORS'\n      - '**/SPONSORS'\n      - '**/TIPS'\n\njobs:\n  typecheck:\n    name: Typecheck\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Python\n        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: '3.13'\n\n      - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0\n        with:\n          version: 'latest'\n\n      - name: Install dependencies\n        run: uv sync --all-extras\n\n      - name: Run mypy\n        run: |\n          uv run --no-sync --frozen -- python -m ensurepip\n          uv run --no-sync --frozen -- python -m mypy --no-pretty --install-types --non-interactive .\n"
  },
  {
    "path": ".gitignore",
    "content": ".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.venv/\nvenv/\n\n.myclirc\nuv.lock\n"
  },
  {
    "path": "AUTHORS.rst",
    "content": "Check out our `AUTHORS`_.\n\n.. _AUTHORS: mycli/AUTHORS\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Development Guide\n\nThis is a guide for developers who would like to contribute to this project.\n\nIf you're interested in contributing to mycli, thank you. We'd love your help!\nYou'll always get credit for your work.\n\n## GitHub Workflow\n\n1. [Fork the repository](https://github.com/dbcli/mycli) on GitHub.\n\n2. Clone your fork locally:\n    ```bash\n    $ git clone <url-for-your-fork>\n    ```\n\n3. Add the official repository (`upstream`) as a remote repository:\n    ```bash\n    $ git remote add upstream git@github.com:dbcli/mycli.git\n    ```\n\n4. Set up [uv](https://docs.astral.sh/uv/getting-started/installation/)\n   for development:\n\n    ```bash\n    $ cd mycli\n    $ uv sync --extra dev\n    ```\n\n    We've just created a virtual environment and installed all the dependencies\n    and tools we need to work on mycli.\n\n5. Create a branch for your bugfix or feature based off the `main` branch:\n\n    ```bash\n    $ git checkout -b <name-of-bugfix-or-feature> main\n    ```\n\n6. 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:\n\n    ```bash\n    $ git pull upstream main\n    ```\n\n7. When your work is ready for the mycli team to review it, push your branch to your fork:\n\n    ```bash\n    $ git push origin <name-of-bugfix-or-feature>\n    ```\n\n8. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)\n   on GitHub.\n\n\n## Running mycli\n\nTo run mycli with your local changes:\n\n```bash\n$ uv run mycli\n```\n\n\n## Running the Tests\n\nWhile you work on mycli, it's important to run the tests to make sure your code\nhasn't broken any existing functionality. To run the tests, just type in:\n\n```bash\n$ uv run tox\n```\n\n### Test Database Credentials\n\nSome tests require a database connection to work. You can tell the tests which\ncredentials to use by setting the applicable environment variables:\n\n```bash\n$ export PYTEST_HOST=localhost\n$ export PYTEST_USER=mycli\n$ export PYTEST_PASSWORD=myclirocks\n$ export PYTEST_PORT=3306\n$ export PYTEST_CHARSET=utf8mb4\n```\n\nThe default values are `localhost`, `root`, no password, `3306`, and `utf8mb4`.\nYou only need to set the values that differ from the defaults.\n\nIf you would like to run the tests as a user with only the necessary privileges,\ncreate a `mycli` user and run the following grant statements.\n\n```sql\nGRANT ALL PRIVILEGES ON `mycli_%`.* TO 'mycli'@'localhost';\nGRANT SELECT ON mysql.* TO 'mycli'@'localhost';\nGRANT SELECT ON performance_schema.* TO 'mycli'@'localhost';\n```\n\n### CLI Tests\n\nSome CLI tests expect the program `ex` to be a symbolic link to `vim`.\n\nIn some systems (e.g. Arch Linux) `ex` is a symbolic link to `vi`, which will\nchange the output and therefore make some tests fail.\n\nYou can check this by running:\n```bash\n$ readlink -f $(which ex)\n```\n\n# Github PR checklist\n- add the contribution to the `changelog.md`\n- add your name to the `AUTHORS` file (or it's already there).\n- run `uv run ruff check && uv run ruff format && uv run mypy --install-types .`\n\n\n## Releasing a new version of mycli\n\nCreate 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.\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Copyright (c) 2015-2026, mycli maintainers\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of mycli nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE.txt *.md *.rst screenshots/*\ninclude tasks.py .coveragerc tox.ini\nrecursive-include test *.cnf\nrecursive-include test *.feature\nrecursive-include test *.py\nrecursive-include test *.txt\n"
  },
  {
    "path": "README.md",
    "content": "# mycli\n\n[![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli)\n\nA command line client for MySQL that can do auto-completion and syntax highlighting.\n\nHomepage: [https://mycli.net](https://mycli.net)\nDocumentation: [https://mycli.net/docs](https://mycli.net/docs)\n\n![Completion](screenshots/tables.png)\n![CompletionGif](screenshots/main.gif)\n\nPostgres Equivalent: [https://pgcli.com](https://pgcli.com)\n\nQuick Start\n-----------\n\nIf you already know how to install Python packages, then you can install it via `pip`:\n\nYou might need sudo on Linux.\n\n```bash\npip install -U 'mycli[all]'\n```\n\nor\n\n```bash\nbrew update && brew install mycli  # Only on macOS\n```\n\nor\n\n```bash\nsudo apt-get install mycli  # Only on Debian or Ubuntu\n```\n\n### Usage\n\nSee\n\n```bash\nmycli --help\n```\n\nFeatures\n--------\n\n`mycli` is written using [prompt_toolkit](https://github.com/jonathanslenders/python-prompt-toolkit/).\n\n* Auto-completion as you type for SQL keywords as well as tables, views and\n  columns in the database.\n* Fuzzy history search using [fzf](https://github.com/junegunn/fzf).\n* Syntax highlighting using Pygments.\n* Smart-completion (enabled by default) will suggest context-sensitive completion.\n    - `SELECT * FROM <tab>` will only show table names.\n    - `SELECT * FROM users WHERE <tab>` will only show column names.\n* Support for multiline queries.\n* Favorite queries with optional positional parameters. Save a query using\n  `\\fs <alias> <query>` and execute it with `\\f <alias>`.\n* Timing of sql statements and table rendering.\n* Log every query and its results to a file (disabled by default).\n* Pretty print tabular data (with colors!).\n* Support for SSL connections\n* Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.\n* Support for querying LLMs with context derived from your schema.\n* Support for storing passwords in the system keyring.\n\nMycli creates a config file `~/.myclirc` on first run; you can use the\noptions in that file to configure the above features, and more.\n\nSome features are only exposed as [key bindings](doc/key_bindings.rst).\n\nContributions:\n--------------\n\nIf you're interested in contributing to this project, first of all I would like\nto extend my heartfelt gratitude. I've written a small doc to describe how to\nget this running in a development setup.\n\nhttps://github.com/dbcli/mycli/blob/main/CONTRIBUTING.md\n\n\n## Additional Install Instructions:\n\nThese 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.\n\n### Arch, Manjaro\n\nYou can install the mycli package available in the AUR:\n\n```\nyay -S mycli\n```\n\n### Debian, Ubuntu\n\nOn Debian, Ubuntu distributions, you can easily install the mycli package using apt:\n\n```\nsudo apt-get install mycli\n```\n\n### Fedora\n\nFedora has a package available for mycli, install it using dnf:\n\n```\nsudo dnf install mycli\n```\n\n### Windows\n\n#### Option 1: Native Windows\n\nInstall the `less` pager, for example by `scoop install less`.\n\nFollow the instructions on this blogpost: https://web.archive.org/web/20221006045208/https://www.codewall.co.uk/installing-using-mycli-on-windows/\n\n**Mycli is not tested on Windows**, but the libraries used in the app are Windows-compatible.\nThis means it should work without any modifications, but isn't supported.\n\nPRs to add native Windows testing to Mycli CI would be welcome!\n\n#### Option 2: WSL\n\nEverything should work as expected in WSL.  This is a good option for using\nMycli on Windows.\n\n\n### Thanks:\n\nThis project was funded through kickstarter. My thanks to the [backers](https://mycli.net/sponsors) who supported the project.\n\nA special thanks to [Jonathan Slenders](https://twitter.com/jonathan_s) for\ncreating [Python Prompt Toolkit](https://github.com/jonathanslenders/python-prompt-toolkit),\nwhich is quite literally the backbone library, that made this app possible.\nJonathan has also provided valuable feedback and support during the development\nof this app.\n\n[Click](https://palletsprojects.com/projects/click) is used for command line option parsing\nand printing error messages.\n\nThanks to [PyMysql](https://github.com/PyMySQL/PyMySQL) for a pure python adapter to MySQL database.\n\n\n### Compatibility\n\nMycli is tested on macOS and Linux, and requires Python 3.10 or better.\n\nTo connect to MySQL versions earlier than 5.5, you may need to set the following in `~/.myclirc`:\n\n```\n# character set for connections without --charset being set at the CLI\ndefault_character_set = utf8\n```\n\nor set `--charset=utf8` when invoking MyCLI.\n\n### Configuration and Usage\n\nFor more information on using and configuring mycli, [check out our documentation](https://mycli.net/docs).\n\nCommon topics include:\n- [Configuring mycli](https://mycli.net/config)\n- [Using/Disabling the pager](https://mycli.net/pager)\n- [Syntax colors](https://mycli.net/syntax)\n"
  },
  {
    "path": "SPONSORS.rst",
    "content": "Check out our `SPONSORS`_.\n\n.. _SPONSORS: mycli/SPONSORS\n"
  },
  {
    "path": "changelog.md",
    "content": "1.65.2 (2026/03/19)\n==============\n\nSecurity\n--------\n* Harden `codex-review` workflow against script injection from untrusted PR metadata.\n\n\n1.65.1 (2026/03/18)\n==============\n\nBug Fixes\n---------\n* Require `sqlglot` 30.x.\n\n\n1.65.0 (2026/03/16)\n==============\n\nFeatures\n---------\n* Add prompt format string for literal backslash.\n* Add collation completions, and complete charsets in more positions.\n\n\nBug Fixes\n---------\n* Suppress warnings when `sqlglotrs` is installed.\n* Improve completions after operators, by recognizing more operators.\n\n\n1.64.0 (2026/03/13)\n==============\n\nFeatures\n---------\n* Add `-r` raw mode to `system` command.\n* Set timeouts, show exit codes, and improve formatting for `system` commands.\n* Add a dependencies section to `--checkup`.\n\n\nBug Fixes\n---------\n* Require `sqlglot` 29.x, suppressing a deprecation warning.\n\n\n1.63.0 (2026/03/12)\n==============\n\nFeatures\n---------\n* Make short toolbar message show after one prompt.\n\n\nInternal\n---------\n* Migrate more repeated values to `constants.py`.\n* Support `sqlglot` 28 and 29.\n\n\n1.62.0 (2026/03/07)\n==============\n\nFeatures\n---------\n* Dynamic terminal titles based on prompt format strings.\n* Ability to turn off the toolbar.\n* Add completions for introducers on literals.\n* Load whole-line autosuggest candidates in a background thread for speed.\n\n\nBug Fixes\n---------\n* Improve query cancellation on control-c.\n* Improve refresh of some format strings in the toolbar.\n* Improve keyring storage, requiring re-entering most keyring passwords.\n* Improve sentinel value for `--password` without argument.\n\n\nInternal\n---------\n* Require a more recent version of the `wcwidth` library.\n* Make `safe_invalidate_display` function safer.\n\n\n1.61.0 (2026/03/07)\n==============\n\nFeatures\n---------\n* Allow shorter timeout lengths after pressing Esc, for vi-mode.\n* Let tab and control-space behaviors be configurable.\n* Add short hostname prompt format string.\n\n\n1.60.0 (2026/03/05)\n==============\n\nFeatures\n---------\n* Prioritize common functions in the \"value\" position.\n* Improve value-position keywords.\n* Allow warning-count in status output to be styled.\n\n\nBug Fixes\n---------\n* Fix crash for completion edge case (#1668).\n* Update to a `cli_helpers` version with a `tabulate` bugfix.\n\n\n1.59.0 (2026/03/03)\n==============\n\nFeatures\n---------\n* Offer filename completions on more special commands, such as `\\edit`.\n* Allow styling of status and timings text.\n* Set up customization of prompt/continuation colors in `~/.myclirc`.\n* Allow customization of the toolbar with prompt format strings.\n* Add warnings-count prompt format strings: `\\w` and `\\W`.\n* Handle/document more attributes in the `[colors]` section of `~/.myclirc`.\n* Enable customization of table border color/attributes in `~/.myclirc`.\n* Complete much more precisely in the \"value\" position.\n\n\nBug Fixes\n---------\n* Make toolbar widths consistent on toggle actions.\n* Don't write ANSI prompt escapes to `tee` output.\n\n\nInternal\n---------\n* Use prompt_toolkit's `bell()`.\n* Refactor `SQLResult` dataclass.\n* Avoid depending on string matches into host info.\n* Add more URL constants.\n* Set `$VISUAL` whenever `$EDITOR` is set.\n* Fix tempfile leak in test suite.\n* Avoid refreshing the prompt unless needed.\n\n\n1.58.0 (2026/02/28)\n==============\n\nFeatures\n---------\n* Add `\\bug` command.\n* Let the `F1` key open a browser to mycli.net/docs and emit help text.\n* Add documentation index URL to inline help.\n* Rewrite bottom toolbar, showing more statuses, but staying compact.\n* Let `help <keyword>` list similar keywords when not found.\n* Optionally highlight fuzzy search previews.\n* Make `\\edit` synonymous with the `\\e` command.\n* Add environment variable section to `--checkup`.\n\n\nBug Fixes\n---------\n* Force a prompt_toolkit refresh after fzf history search to avoid display glitches.\n* Include `status` footer in paged output.\n* Ensure fullscreen in fuzzy history search.\n\n\nDocumentation\n---------\n* Add `help <keyword>` to TIPS.\n* Refine inline help descriptions.\n* Add `$VISUAL` environment variable hint to TIPS.\n\n\nInternal\n---------\n* Better tests for `null_string` configuration option.\n* Better cleanup of resources in the test suite.\n* Simplify prettify/unprettify handlers.\n* Make prettify/unprettify logic more robust.\n\n\n1.57.0 (2026/02/25)\n==============\n\nFeatures\n---------\n* Add extra error output on connection failure for possible SSL mismatch (#1584).\n* Bind alternate terminal sequences for function keys F2 - F4.\n* Add `llm help` subcommand.\n* Rewrite `help` table.\n* Remove \"info\" counter from fzf history-search UI.\n\n\nBug Fixes\n---------\n* Let interactive changes to the prompt format respect dynamically-computed values.\n* Better handle arguments to `system cd`.\n* Fix missing keepalives in `\\e` prompt loop.\n* Always strip trailing newlines with `\\e`.\n* Fix `\\llm` without arguments, and remove debug output.\n\n\nDocumentation\n---------\n* Startup tips: add right-arrow key binding.\n* Startup tips: add control-space and the `min_completion_trigger` setting.\n* Startup tips: add history-search bindings.\n* Prefer `https` protocol over `http` in documentation.\n\n\nInternal\n---------\n* Remove outdated email address in `pyproject.toml`.\n* Set well-known URL values in `pyproject.toml`.\n\n\n1.56.0 (2026/02/23)\n==============\n\nFeatures\n---------\n* Let the `--dsn` argument accept literal DSNs as well as aliases.\n* Accept `--character-set` as an alias for `--charset` at the CLI.\n* Add SSL/TLS version to `status` output.\n* Accept `socket` as a DSN query parameter.\n* Accept new-style `ssl_mode` in DSN URI query parameters, to match CLI argument.\n* Fully deprecate the built-in SSH functionality.\n* Let `--keepalive-ticks` be set per-connection, as a CLI option or DSN parameter.\n* Accept `character_set` as a DSN query parameter.\n* Don't attempt SSL for local socket connections when in \"auto\" SSL mode.\n* Add prompt format string for SSL/TLS version of the connection.\n* Add prompt format strings for displaying uptime.\n* Add batch mode to startup tips.\n* Update startup tips with new options.\n\n\nBug Fixes\n---------\n* Make `--ssl-capath` argument a directory.\n* Allow users to use empty passwords without prompting or any configuration (#1584).\n* Check the existence of a socket more directly in `status`.\n* Allow multi-line SQL statements in batch mode on the standard input.\n* Fix extraneous prompt refresh on every keystroke.\n\n\n1.55.0 (2026/02/20)\n==============\n\nFeatures\n---------\n* `--checkup` now checks for external executables.\n\n\nBug Fixes\n---------\n* Improve completion suggestions within backticks.\n* Watch command now returns correct time when run as part of a multi-part query (#1565).\n* Don't diagnose free-entry sections such as `[favorite_queries]` in `--checkup`.\n* When accepting a filename completion, fill in leading `./` if given.\n\n\nInternal\n--------\n* Bump `cli_helpers` to non-yanked version.\n\n\n1.54.1 (2026/02/17)\n==============\n\nBug Fixes\n--------\n* Don't offer autocomplete suggestions when the cursor is within a string.\n* Catch `getpwuid` error on unknown socket owner.\n\n\nInternal\n--------\n* Tune Codex reviews.\n* Refactor `is_inside_quotes()` detection.\n\n\n1.54.0 (2026/02/16)\n==============\n\nFeatures\n--------\n* Add many CLI flags to startup tips.\n* Accept all special commands without trailing semicolons in multi-line mode.\n* Add prompt format strings for socket connections.\n* Optionally defer auto-completions until a minimum number of characters is typed.\n* Make the completion interface more responsive using a background thread.\n* Option to suppress control-d exit behavior.\n* Better support Truecolor terminals.\n* Ability to send app-layer keepalive pings to the server.\n* Add `WITH`, `EXPLAIN`, and `LEFT JOIN` to favorite keyword suggestions.\n* Let the Escape key cancel completion popups.\n\n\nBug Fixes\n---------\n* Correct parameterization for completion queries.\n* Grammar nits in help display.\n\n\nInternal\n--------\n* Prefer `yield from` over yielding in a loop.\n* Update `ruff` linter and CI.\n* Update `LICENSE.txt` for dates and GitHub detection.\n* Update key feature list in `README.md`, syncing with web.\n* Sync prompt format string commentary with web.\n* Add a GitHub Actions workflow to run Codex review on pull requests.\n* Remove vim-style exit sequence which had no effect.\n* Pin dependencies more tightly in `pyproject.toml`.\n* Exclude more documentation files from CI.\n\n\n1.53.0 (2026/02/12)\n==============\n\nFeatures\n--------\n* Add all `~/.myclirc` entries/sections to startup tips.\n\n\nBug Fixes\n---------\n* Fix `\\dt+ table_name` returning empty results.\n* Further bulletproof generating completions on stored procedures.\n\n\nInternal\n--------\n* Add GitHub Issue templates.\n\n\n1.52.0 (2026/02/11)\n==============\n\nFeatures\n--------\n* Suggest tables/views that contain the given columns first when provided in a SELECT query.\n\n\nBug Fixes\n--------\n* Reduce duplicated `--checkup` output.\n* Handle errors generating completions on stored procedures.\n* Fix whitespace/inline comments breaking destructive `UPDATE … WHERE` statement detection.\n\n\nInternal\n--------\n* Let CI ignore additional documentation files.\n* Upgrade `cli_helpers` library to v2.10.0.\n* Organize startup tips.\n\n\n1.51.1 (2026/02/09)\n==============\n\nFeatures\n--------\n* Options to limit size of LLM prompts; cache LLM prompt data.\n* Add startup usage tips.\n* Move `main.ssl_mode` config option to `connection.default_ssl_mode`.\n* Add \"unsupported\" and \"deprecated\" `--checkup` sections.\n\n\nBug Fixes\n--------\n* Correct mangled schema info sent in LLM prompts.\n* Give destructive warning on multi-table `UPDATE`s.\n\n\n1.50.0 (2026/02/07)\n==============\n\nFeatures\n--------\n* Deprecate reading configuration values from `my.cnf` files.\n* Add `--checkup` mode to show unconfigured new features.\n* Add `binary_display` configuration option.\n\n\nBug Fixes\n--------\n* Link to `--ssl`/`--no-ssl` GitHub issue in deprecation warning.\n* Don't emit keyring-updated message unless needed.\n* Include port and socket in keyring identifier.\n\n\n1.49.0 (2026/02/02)\n==============\n\nFeatures\n--------\n* \"Eager\" completions for the `source` command, limited to `*.sql` files.\n* Suggest column names from all tables in the current database after SELECT (#212).\n* Put fuzzy completions more often to the bottom of the suggestion list.\n* Store and retrieve passwords using the system keyring.\n\n\nBug Fixes\n--------\n* Refactor completions for special commands, with minor casing fixes.\n* Raise `--password-file` higher in the precedence of password specification.\n* Fix regression: show username in password prompt.\n\n\nInternal\n--------\n* Remove `align_decimals` preprocessor, which had no effect.\n* Fix TLS deprecation warning in test suite.\n* Convert importlib read_text and open_text uses to newer files() syntax.\n* Update Pull Request template.\n\n\n1.48.0 (2026/01/27)\n==============\n\nFeatures\n--------\n* Right-align numeric columns, and make the behavior configurable.\n* Add completions for stored procedures.\n* Escape database completions.\n* Offer completions on `CREATE TABLE ... LIKE`.\n* Use 0x-style hex literals for binaries in SQL output formats.\n\n\nBug Fixes\n--------\n* Better respect case when `keyword_casing` is `auto`.\n* Fix error when selecting from an empty table.\n* Let favorite queries contain special commands.\n* Render binary values more consistently as hex literals.\n* Offer format completions on special command `\\Tr`/`redirectformat`.\n\n\n1.47.0 (2026/01/24)\n==============\n\nFeatures\n--------\n* Add a `--checkpoint=` argument to log successful queries in batch mode.\n* Add `--throttle` option for batch mode.\n\n\nBug Fixes\n--------\n* Fix timediff output when the result is a negative value (#1113).\n* Don't offer completions for numeric text.\n\n\n1.46.0 (2026/01/22)\n==============\n\nFeatures\n--------\n* Add `--unbuffered` mode which fetches rows as needed, to save memory.\n* Default to standards-compliant `utf8mb4` character set.\n* Stream input from STDIN to consume less memory, adding `--noninteractive` and `--format=` CLI arguments.\n* Remove suggested quoting on completions for identifiers with uppercase.\n* Allow table names to be completed with leading schema names.\n* Soft deprecate the built-in SSH features.\n* Add true fuzzy-match completions with rapidfuzz.\n\n\nBug Fixes\n--------\n* Fix CamelCase fuzzy matching.\n* Place special commands first in the list of completion candidates, and remove duplicates.\n\n\n1.45.0 (2026/01/20)\n==============\n\nFeatures\n--------\n* Make password options also function as flags. Reworked password logic to prompt user as early as possible (#341).\n* More complete and up-to-date set of MySQL reserved words for completions.\n* Place exact-leading completions first.\n* Allow history file location to be configured.\n* Make destructive-warning keywords configurable.\n* Smarter fuzzy completion matches.\n\n\nBug Fixes\n--------\n* Respect `--logfile` when using `--execute` or standard input at the shell CLI.\n* Gracefully catch Paramiko parsing errors on `--list-ssh-config`.\n* Downgrade to Paramiko 3.5.1 to avoid crashing on DSA SSH keys.\n* Offer schema name completions in `GRANT ... ON` forms.\n\n\n1.44.2 (2026/01/13)\n==============\n\nBug Fixes\n--------\n* Update watch query output to display the correct execution time on all iterations (#763).\n* Use correct database (if applicable) when reconnecting after a connection loss (#1437).\n\nInternal\n--------\n* Create new data class to handle SQL/command results to make further code improvements easier.\n\n\n1.44.1 (2026/01/10)\n==============\n\nBug Fixes\n--------\n* Let `sqlparse` accept arbitrarily-large queries.\n\n\n1.44.0 (2026/01/08)\n==============\n\nFeatures\n--------\n* Add enum value completions for WHERE/HAVING clauses. (#790)\n* Add `show_favorite_query` config option to control query printing when running favorite queries. (#1118)\n\n\n1.43.1 (2026/01/03)\n==============\n\nBug Fixes\n--------\n* Prompt for password within SSL-auto retry flow.\n\n\n1.43.0 (2026/01/02)\n==============\n\nFeatures\n--------\n* Update query processing functions to allow automatic show_warnings to work for more code paths like DDL.\n* Add new ssl_mode config / --ssl-mode CLI option to control SSL connection behavior. This setting will supercede the\n  existing --ssl/--no-ssl CLI options, which are deprecated and will be removed in a future release.\n* Rework reconnect logic to actually reconnect or create a new connection instead of simply changing the database (#746).\n* Configurable string for missing values (NULLs) in outputs.\n\n\nBug Fixes\n--------\n* Update the prompt display logic to handle an edge case where a socket is used without\n  a host being parsed from any other method (#707).\n\n\nInternal\n--------\n* Refine documentation for Windows.\n* Target Python 3.10 for linting.\n* Use fully-qualified pymysql exception classes.\n\n\n1.42.0 (2025/12/20)\n==============\n\nFeatures\n--------\n* Add support for the automatic displaying of warnings after a SQL statement is executed.\n  May be set with the commands \\W and \\w, in the config file with show_warnings, or\n  with --show-warnings/--no-show-warnings on the command line.\n\n\nInternal\n--------\n* Improve robustness for flaky tests when publishing.\n* Improve type annotations for latest mypy/type stubs.\n* Set mypy version more strictly.\n\n\n1.41.2 (2025/11/24)\n==============\n\nBug Fixes\n--------\n* Close connection to server properly to avoid \"Aborted connection\" warnings in server logs.\n\nInternal\n--------\n* Add ruff to developement dependencies.\n* Update contributing guidelines to match GitHub pull request checklist.\n\n\n1.41.1 (2025/11/15)\n==============\n\nBug Fixes\n--------\n* Upgrade `click` to v8.3.1, resolving a longstanding pager bug.\n\n\nInternal\n--------\n* Include LLM dependencies in tox configuration.\n\n\n1.41.0 (2025/11/01)\n==============\n\nFeatures\n--------\n* Make LLM dependencies an optional extra.\n\n\nBug Fixes\n--------\n* Let LLM commands respect show-timing configuration.\n\n\nInternal\n--------\n* Add mypy to Pull Request template.\n* Enable flake8-bugbear lint rules.\n* Fix flaky editor-command tests in CI.\n* Require release format of `changelog.md` when making a release.\n* Improve type annotations on LLM driver.\n\n\n1.40.0 (2025/10/14)\n==============\n\nFeatures\n--------\n* Support reconnecting to mysql server when the server restarts.\n\n\nInternal\n--------\n* Test on Python 3.14.\n* Switch from pyaes to pycryptodomex as it seems to be more actively maintained.\n\n\n1.39.1 (2025/10/06)\n==============\n\nBug Fixes\n--------\n* Don't require `--ssl` argument when other SSL arguments are given.\n\n\n1.39.0 (2025/09/30)\n==============\n\nFeatures\n--------\n* Support only Python 3.10+.\n\n\nBug Fixes\n--------\n* Fixes use of incorrect ssl config after retrying connection with prompted password.\n* Fix ssl_context always created.\n\n\nInternal\n--------\nTyping fix for `pymysql.connect()`.\n\n\n1.38.4 (2025/09/06)\n==============\n\nBug Fixes\n--------\n* Limit Alt-R bindings to Emacs mode.\n* Fix timing being printed twice.\n\n\nInternal\n--------\n* Only read \"my\" configuration files once, rather than once per call to read_my_cnf_files.\n\n\n1.38.3 (2025/08/21)\n==============\n\nBug Fixes\n--------\n* Fix the infinite looping when `\\llm` is called without args.\n\n\n1.38.2 (2025/08/19)\n======================\n\nBug Fixes\n--------\n* Fix failure to save Favorite Queries.\n\n\n1.38.1 (2025/08/19)\n======================\n\nBug Fixes\n--------\n* Partially fix Favorite Query completion crash.\n\n\nInternal\n--------\n* Improve CI workflow naming.\n\n\n1.38.0 (2025/08/16)\n======================\n\nFeatures\n--------\n* Add LLM support.\n\n\nBug Fixes\n--------\n* Improve missing ssh-extras message.\n* Fix repeated control-r in traditional reverse isearch.\n* Fix spelling of `ssl-verify-server-cert` option.\n* Improve handling of `ssl-verify-server-cert` False values.\n* Guard against missing contributors file on startup.\n* Friendlier errors on password-file failures.\n* Better handle empty-string passwords.\n* Permit empty-string passwords at the interactive prompt.\n\n\nInternal\n--------\n* Improve pull request template lint commands.\n* Complete typehinting the non-test codebase.\n* Modernization: conversion to f-strings.\n* Modernization: remove more Python 2 compatibility logic.\n\n\n1.37.1 (2025/07/28)\n======================\n\nInternal\n--------\n\n* Align LICENSE with SPDX format.\n* Fix deprecated `license` specification format in `pyproject.toml`.\n\n\n1.37.0 (2025/07/28)\n======================\n\nFeatures\n--------\n* Show username in password prompt.\n* Add `mysql` and `mysql_unicode` table formats.\n\n\nBug Fixes\n--------\n* Help Windows installations find a working default pager.\n\n\nInternal\n--------\n\n* Support only Python 3.9+ in `pyproject.toml`.\n* Add linting suggestion to pull request template.\n* Make CI names and properties more consistent.\n* Enable typechecking for most of the non-test codebase.\n* CI: turn off fail-fast matrix strategy.\n* Remove unused Python 2 compatibility code.\n* Also run CI tests without installing SSH extra dependencies.\n* Update `cli_helpers` dependency, and list of table formats.\n\n\n1.36.0 (2025/07/19)\n======================\n\nFeatures\n--------\n* Make control-r reverse search style configurable.\n* Make fzf search key bindings more compatible with traditional isearch.\n\n\nBug Fixes\n--------\n\n* Better reset after pipe command failures.\n\n\nInternal\n--------\n\n* Add limited typechecking to CI.\n\n\n1.35.0 (2025/07/18)\n======================\n\nFeatures\n--------\n\n* Support chained pipe operators such as `select first_name from users $| grep '^J' $| head -10`.\n* Support trailing file redirects after pipe operators, such as `select 10 $| tail -1 $> ten.txt`.\n\n\n1.34.4 (2025/07/15)\n======================\n\nBug Fixes\n--------\n\n* Fix old-style `\\pipe_once`.\n\n\n1.34.3 (2025/07/14)\n======================\n\nBug Fixes\n--------\n\n* Use only `communicate()` to communicate with subprocess.\n\n\n1.34.2 (2025/07/12)\n======================\n\nBug Fixes\n--------\n\n* Use plain `print()` to communicate with subprocess.\n\n\n1.34.1 (2025/07/12)\n======================\n\nInternal\n--------\n\n* Bump cli_helpers dependency for corrected output formats.\n\n\n1.34.0 (2025/07/11)\n======================\n\nFeatures\n--------\n\n* Post-save command hook for redirected output.\n\nInternal\n--------\n\n* Documentation cleanup.\n* Bump cli_helpers dependency for more output formats.\n\n\n1.33.0 (2025/07/07)\n======================\n\nFeatures\n--------\n\n* Keybindings to insert current date/datetime.\n* Improve feedback when running external commands.\n* Independent format for redirected output.\n* Trailing shell-style redirect syntax.\n\n\nInternal\n--------\n\n* Remove `requirements-dev.txt` in favor of uv/`pyproject.toml`.\n\n\n1.32.0 (2025/07/04)\n======================\n\nFeatures\n--------\n\n* Support SSL query parameters on DSNs.\n* More information and care on KeyboardInterrupt.\n\nInternal\n--------\n\n* Work on passing `ruff check` linting.\n* Relax expectation for unreliable test.\n* Bump sqlglot version to v26 and add rs extras.\n\n\n1.31.2 (2025/05/01)\n===================\n\nBug Fixes\n---------\n\n* Let table-name extraction work on multi-statement inputs.\n\n\nInternal\n--------\n\n* Work on passing `ruff check` linting.\n* Remove backward-compatibility hacks.\n* Pin more GitHub Actions and add Dependabot support.\n* Enable xpassing test.\n\n\n1.31.1 (2025/04/25)\n===================\n\nInternal\n--------\n\n* skip style checks on Publish action\n\n\n1.31.0 (NEVER RELEASED)\n===================\n\nFeatures\n--------\n* Added explicit error handle to get_password_from_file with EAFP.\n* Use the \"history\" scheme for fzf searches.\n* Deduplicate history in fzf searches.\n* Add a preview window to fzf history searches.\n\nInternal\n--------\n\n* New Project Lead: [Roland Walker](https://github.com/rolandwalker)\n* Update sqlparse to <=0.6.0\n* Typing/lint fixes.\n\n\n1.30.0 (2025/04/19)\n===================\n\nFeatures\n--------\n\n* DSN specific init-command in myclirc. Fixes (#1195)\n* Add `\\\\g` to force the horizontal output.\n\n\n1.29.2 (2024/12/11)\n===================\n\nInternal\n--------\n\n* Exclude tests from the python package.\n\n1.29.1 (2024/12/11)\n===================\n\nInternal\n--------\n\n* Fix the GH actions to publish a new version.\n\n1.29.0 (NEVER RELEASED)\n=======================\n\nBug Fixes\n----------\n\n* fix SSL through SSH jump host by using a true python socket for a tunnel\n* Fix mycli crash when connecting to Vitess\n\nInternal\n---------\n\n* Modernize to use PEP-621. Use `uv` instead of `pip` in GH actions.\n* Remove Python 3.8 and add Python 3.13 in test matrix.\n\n1.28.0 (2024/11/10)\n======================\n\nFeatures\n---------\n\n* 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.\n\nBug Fixes\n----------\n\n* Fixes `Database connection failed: error('unpack requires a buffer of 4 bytes')`\n* Only show keyword completions after *\n* Enable fuzzy matching for keywords\n\n1.27.2 (2024/04/03)\n===================\n\nBug Fixes\n----------\n\n* Don't use default prompt when one is not supplied to the --prompt option.\n\n1.27.1 (2024/03/28)\n===================\n\nBug Fixes\n----------\n\n* Don't install tests.\n* Do not ignore the socket passed with the -S option, even when no port is passed\n* Fix unexpected exception when using dsn without username & password (Thanks: [Will Wang])\n* Let the `--prompt` option act normally with its predefined default value\n\nInternal\n---------\n\n* paramiko is newer than 2.11.0 now, remove version pinning `cryptography`.\n* Drop support for Python 3.7\n\n1.27.0 (2023/08/11)\n===================\n\nFeatures\n---------\n\n* Detect TiDB instance, show in the prompt, and use additional keywords.\n* Fix the completion order to show more commonly-used keywords at the top.\n\nBug Fixes\n----------\n\n* Better handle empty statements in un/prettify\n* Remove vi-mode bindings for prettify/unprettify.\n* Honor `\\G` when executing from commandline with `-e`.\n* Correctly report the version of TiDB.\n* Revised `botton` spelling mistakes with `bottom` in `mycli/clitoolbar.py`\n\n1.26.1 (2022/09/01)\n===================\n\nBug Fixes\n----------\n\n* Require Python 3.7 in `setup.py`\n\n1.26.0 (2022/09/01)\n===================\n\nFeatures\n---------\n\n* Add `--ssl` flag to enable ssl/tls.\n* Add `pager` option to `~/.myclirc`, for instance `pager = 'pspg --csv'` (Thanks: [BuonOmo])\n* Add prettify/unprettify keybindings to format the current statement using `sqlglot`.\n\nFeatures\n---------\n\n* Add `--tls-version` option to control the tls version used.\n\nInternal\n---------\n\n* Pin `cryptography` to suppress `paramiko` warning, helping CI complete and presumably affecting some users.\n* Upgrade some dev requirements\n* Change tests to always use databases prefixed with 'mycli_' for better security\n\nBug Fixes\n----------\n\n* Support for some MySQL compatible databases, which may not implement connection_id().\n* Fix the status command to work with missing 'Flush_commands' (mariadb)\n* Ignore the user of the system [myslqd] config.\n\n1.25.0 (2022/04/02)\n===================\n\nFeatures\n---------\n\n* Add `beep_after_seconds` option to `~/.myclirc`, to ring the terminal bell after long queries.\n\n1.24.4 (2022/03/30)\n===================\n\nInternal\n---------\n\n* Upgrade Ubuntu VM for runners as Github has deprecated it\n\nBug Fixes\n----------\n\n* Change in main.py - Replace the `click.get_terminal_size()` with `shutil.get_terminal_size()`\n\n1.24.3 (2022/01/20)\n===================\n\nBug Fixes\n----------\n\n* Upgrade cli_helpers to workaround Pygments regression.\n\n1.24.2 (2022/01/11)\n===================\n\nBug Fixes\n----------\n\n* Fix autocompletion for more than one JOIN\n* Fix the status command when connected to TiDB or other servers that don't implement 'Threads\\_connected'\n* Pin pygments version to avoid a breaking change\n\n1.24.1\n=======\n\nBug Fixes\n---------\n\n* Restore dependency on cryptography for the interactive password prompt\n\nInternal\n---------\n\n* Deprecate Python mock\n\n1.24.0\n======\n\nBug Fixes\n----------\n\n* Allow `FileNotFound` exception for SSH config files.\n* Fix startup error on MySQL < 5.0.22\n* Check error code rather than message for Access Denied error\n* Fix login with ~/.my.cnf files\n\nFeatures\n---------\n\n* Add `-g` shortcut to option `--login-path`.\n* Alt-Enter dispatches the command in multi-line mode.\n* 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>)\n\nInternal\n---------\n\n* Remove unused function is_open_quote()\n* Use importlib, instead of file links, to locate resources\n* Test various host-port combinations in command line arguments\n* Switched from Cryptography to pyaes for decrypting mylogin.cnf\n\n1.23.2\n======\n\nBug Fixes\n----------\n\n* Ensure `--port` is always an int.\n\n1.23.1\n======\n\nBug Fixes\n----------\n\n* Allow `--host` without `--port` to make a TCP connection.\n\n1.23.0\n======\n\nBug Fixes\n----------\n\n* Fix config file include logic\n\nFeatures\n---------\n\n* Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]).\n* Use InputMode.REPLACE_SINGLE\n* Add support for ANSI escape sequences for coloring the prompt.\n* Allow customization of Pygments SQL syntax-highlighting styles.\n* Add a `\\clip` special command to copy queries to the system clipboard.\n* Add a special command `\\pipe_once` to pipe output to a subprocess.\n* Add an option `--charset` to set the default charset when connect database.\n\nBug Fixes\n----------\n\n* Fixed compatibility with sqlparse 0.4 (Thanks: [mtorromeo]).\n* Fixed iPython magic (Thanks: [mwcm]).\n* Send \"Connecting to socket\" message to the standard error.\n* Respect empty string for prompt_continuation via `prompt_continuation = ''` in `.myclirc`\n* Fix \\once -o to overwrite output whole, instead of line-by-line.\n* Dispatch lines ending with `\\e` or `\\clip` on return, even in multiline mode.\n* Restore working local `--socket=<UDS>` (Thanks: [xeron]).\n* Allow backtick quoting around the database argument to the `use` command.\n* Avoid opening `/dev/tty` when `--no-warn` is given.\n* Fixed some typo errors in `README.md`.\n\n1.22.2\n======\n\nBug Fixes\n----------\n\n* Make the `pwd` module optional.\n\n1.22.1\n======\n\nBug Fixes\n----------\n\n* Fix the breaking change introduced in PyMySQL 0.10.0. (Thanks: [Amjith]).\n\nFeatures\n---------\n\n* Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file.\n* Add an option `--list-ssh-config` to list ssh configurations.\n* Add an option `--ssh-config-path` to choose ssh configuration path.\n\nBug Fixes\n----------\n\n* Fix specifying empty password with `--password=''` when config file has a password set (Thanks: [Zach DeCook]).\n\n1.21.1\n======\n\nBug Fixes\n----------\n\n* Fix broken auto-completion for favorite queries (Thanks: [Amjith]).\n* Fix undefined variable exception when running with --no-warn (Thanks: [Georgy Frolov])\n* Support setting color for null value (Thanks: [laixintao])\n\n1.21.0\n======\n\nFeatures\n---------\n\n* Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]).\n* Mark `update` without `where`-clause as destructive query (Thanks: [Klaus Wünschel]).\n* Added DELIMITER command (Thanks: [Georgy Frolov])\n* Added clearer error message when failing to connect to the default socket.\n* Extend main.is_dropping_database check with create after delete statement.\n* Search `${XDG_CONFIG_HOME}/mycli/myclirc` after `${HOME}/.myclirc` and before `/etc/myclirc` (Thanks: [Takeshi D. Itoh])\n\nBug Fixes\n----------\n\n* Allow \\o command more than once per session (Thanks: [Georgy Frolov])\n* Fixed crash when the query dropping the current database starts with a comment (Thanks: [Georgy Frolov])\n\nInternal\n---------\n\n* deprecate python versions 2.7, 3.4, 3.5; support python 3.8\n\n1.20.1\n======\n\nBug Fixes\n----------\n\n* Fix an error when using login paths with an explicit database name (Thanks: [Thomas Roten]).\n\n1.20.0\n======\n\nFeatures\n----------\n\n* Auto find alias dsn when `://` not in `database` (Thanks: [QiaoHou Peng]).\n* Mention URL encoding as escaping technique for special characters in connection DSN (Thanks: [Aljosha Papsch]).\n* 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]).\n* Use a generator to stream the output to the pager (Thanks: [Dick Marinus]).\n\nBug Fixes\n----------\n\n* Fix the missing completion for special commands (Thanks: [Amjith Ramanujam]).\n* Fix favorites queries being loaded/stored only from/in default config file and not --myclirc (Thanks: [Matheus Rosa])\n* Fix automatic vertical output with native syntax style (Thanks: [Thomas Roten]).\n* Update `cli_helpers` version, this will remove quotes from batch output like the official client (Thanks: [Dick Marinus])\n* 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])\n* workaround for ConfigObj parsing strings containing \",\" as lists (Thanks: [Mike Palandra])\n\nInternal\n---------\n\n* fix unhashable FormattedText from prompt toolkit in unit tests (Thanks: [Dick Marinus]).\n\n1.19.0\n======\n\nInternal\n---------\n\n* Add Python 3.7 trove classifier (Thanks: [Thomas Roten]).\n* Fix pytest in Fedora mock (Thanks: [Dick Marinus]).\n* Require `prompt_toolkit>=2.0.6` (Thanks: [Dick Marinus]).\n\nFeatures\n---------\n\n* Add Token.Prompt/Continuation (Thanks: [Dick Marinus]).\n* Don't reconnect when switching databases using use (Thanks: [Angelo Lupo]).\n* Handle MemoryErrors while trying to pipe in large files and exit gracefully with an error (Thanks: [Amjith Ramanujam])\n\nBug Fixes\n----------\n\n* Enable Ctrl-Z to suspend the app (Thanks: [Amjith Ramanujam]).\n\n1.18.2\n======\n\nBug Fixes\n----------\n\n* Fixes database reconnecting feature (Thanks: [Yang Zou]).\n\nInternal\n---------\n\n* Update Twine version to 1.12.1 (Thanks: [Thomas Roten]).\n* Fix warnings for running tests on Python 3.7 (Thanks: [Dick Marinus]).\n* Clean up and add behave logging (Thanks: [Dick Marinus]).\n\n1.18.1\n======\n\nFeatures\n---------\n\n* Add Keywords: TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT (Thanks: [QiaoHou Peng]).\n\nInternal\n---------\n\n* Update prompt toolkit (Thanks: [Jonathan Slenders], [Irina Truong], [Dick Marinus]).\n\n1.18.0\n======\n\nFeatures\n---------\n\n* Display server version in welcome message (Thanks: [Irina Truong]).\n* Set `program_name` connection attribute (Thanks: [Dick Marinus]).\n* Use `return` to terminate a generator for better Python 3.7 support (Thanks: [Zhongyang Guan]).\n* Add `SAVEPOINT` to SQLCompleter (Thanks: [Huachao Mao]).\n* Connect using a SSH transport (Thanks: [Dick Marinus]).\n* Add `FROM_UNIXTIME` and `UNIX_TIMESTAMP` to SQLCompleter (Thanks: [QiaoHou Peng])\n* Search `${PWD}/.myclirc`, then `${HOME}/.myclirc`, lastly `/etc/myclirc` (Thanks: [QiaoHao Peng])\n\nBug Fixes\n----------\n\n* When DSN is used, allow overrides from mycli arguments (Thanks: [Dick Marinus]).\n* A DSN without password should be allowed (Thanks: [Dick Marinus])\n\nBug Fixes\n----------\n\n* Convert `sql_format` to unicode strings for py27 compatibility (Thanks: [Dick Marinus]).\n* Fixes mycli compatibility with pbr (Thanks: [Thomas Roten]).\n* Don't align decimals for `sql_format` (Thanks: [Dick Marinus]).\n\nInternal\n---------\n\n* Use fileinput (Thanks: [Dick Marinus]).\n* Enable tests for Python 3.7 (Thanks: [Thomas Roten]).\n* Remove `*.swp` from gitignore (Thanks: [Dick Marinus]).\n\n1.17.0\n=======\n\nFeatures\n----------\n\n* Add `CONCAT` to SQLCompleter and remove unused code (Thanks: [caitinggui])\n* Do not quit when aborting a confirmation prompt (Thanks: [Thomas Roten]).\n* Add option list-dsn (Thanks: [Frederic Aoustin]).\n* Add verbose option for list-dsn, add tests and clean up code (Thanks: [Dick Marinus]).\n\nBug Fixes\n----------\n\n* Add enable_pager to the config file (Thanks: [Frederic Aoustin]).\n* Mark `test_sql_output` as a dbtest (Thanks: [Dick Marinus]).\n* Don't crash if the log/history file directories don't exist (Thanks: [Thomas Roten]).\n* Unquote dsn username and password (Thanks: [Dick Marinus]).\n* Output `Password:` prompt to stderr (Thanks: [ushuz]).\n* Mark `alter` as a destructive query (Thanks: [Dick Marinus]).\n* Quote CSV fields (Thanks: [Thomas Roten]).\n* Fix `thanks_picker` (Thanks: [Dick Marinus]).\n\nInternal\n---------\n\n* Refactor Destructive Warning behave tests (Thanks: [Dick Marinus]).\n\n1.16.0\n=======\n\nFeatures\n---------\n\n* Add DSN aliases to the config file (Thanks: [Frederic Aoustin]).\n\nBug Fixes\n----------\n\n* Do not try to connect to a unix socket on Windows (Thanks: [Thomas Roten]).\n\n1.15.0\n=======\n\nFeatures\n---------\n\n* Add sql-update/insert output format. (Thanks: [Dick Marinus]).\n* Also complete aliases in WHERE. (Thanks: [Dick Marinus]).\n\n1.14.0\n=======\n\nFeatures\n---------\n\n* Add `watch [seconds] query` command to repeat a query every [seconds] seconds (by default 5). (Thanks: [David Caro](https://github.com/Terseus))\n* Default to unix socket connection if host and port are unspecified. This simplifies authentication on some systems and matches mysql behaviour.\n* Add support for positional parameters to favorite queries. (Thanks: [Scrappy Soft](https://github.com/scrappysoft))\n\nBug Fixes\n----------\n\n* Fix source command for script in current working directory. (Thanks: [Dick Marinus]).\n* Fix issue where the `tee` command did not work on Python 2.7 (Thanks: [Thomas Roten]).\n\nInternal Changes\n-----------------\n\n* Drop support for Python 3.3 (Thanks: [Thomas Roten]).\n\n* Make tests more compatible between different build environments. (Thanks: [David Caro])\n* Merge `_on_completions_refreshed` and `_swap_completer_objects` functions (Thanks: [Dick Marinus]).\n\n1.13.1\n=======\n\nBug Fixes\n----------\n\n* Fix keyword completion suggestion for `SHOW` (Thanks: [Thomas Roten]).\n* Prevent mycli from crashing when failing to read login path file (Thanks: [Thomas Roten]).\n\nInternal Changes\n-----------------\n\n* Make tests ignore user config files (Thanks: [Thomas Roten]).\n\n1.13.0\n=======\n\nFeatures\n---------\n\n* Add file name completion for source command (issue #500). (Thanks: [Irina Truong]).\n\nBug Fixes\n----------\n\n* Fix UnicodeEncodeError when editing sql command in external editor (Thanks: Klaus Wünschel).\n* Fix MySQL4 version comment retrieval (Thanks: [François Pietka])\n* Fix error that occurred when outputting JSON and NULL data (Thanks: [Thomas Roten]).\n\n1.12.1\n=======\n\nBug Fixes\n----------\n\n* Prevent missing MySQL help database from causing errors in completions (Thanks: [Thomas Roten]).\n* Fix mycli from crashing with small terminal windows under Python 2 (Thanks: [Thomas Roten]).\n* Prevent an error from displaying when you drop the current database (Thanks: [Thomas Roten]).\n\nInternal Changes\n-----------------\n\n* Use less memory when formatting results for display (Thanks: [Dick Marinus]).\n* Preliminary work for a future change in outputting results that uses less memory (Thanks: [Dick Marinus]).\n\n1.12.0\n=======\n\nFeatures\n---------\n\n* Add fish-style auto-suggestion from history. (Thanks: [Amjith Ramanujam])\n\n1.11.0\n=======\n\nFeatures\n---------\n\n* Handle reserved space for completion menu better in small windows. (Thanks: [Thomas Roten]).\n* Display current vi mode in toolbar. (Thanks: [Thomas Roten]).\n* Opening an external editor will edit the last-run query. (Thanks: [Thomas Roten]).\n* Output once special command. (Thanks: [Dick Marinus]).\n* Add special command to show create table statement. (Thanks: [Ryan Smith])\n* Display all result sets returned by stored procedures (Thanks: [Thomas Roten]).\n* Add current time to prompt options (Thanks: [Thomas Roten]).\n* Output status text in a more intuitive way (Thanks: [Thomas Roten]).\n* Add colored/styled headers and odd/even rows (Thanks: [Thomas Roten]).\n* Keyword completion casing (upper/lower/auto) (Thanks: [Irina Truong]).\n\nBug Fixes\n----------\n\n* Fixed incorrect timekeeping when running queries from a file. (Thanks: [Thomas Roten]).\n* Do not display time and empty line for blank queries (Thanks: [Thomas Roten]).\n* Fixed issue where quit command would sometimes not work (Thanks: [Thomas Roten]).\n* Remove shebang from main.py (Thanks: [Dick Marinus]).\n* Only use pager if output doesn't fit. (Thanks: [Dick Marinus]).\n* Support tilde user directory for output file names (Thanks: [Thomas Roten]).\n* Auto vertical output is a little bit better at its calculations (Thanks: [Thomas Roten]).\n\nInternal Changes\n-----------------\n\n* Rename tests/ to test/. (Thanks: [Dick Marinus]).\n* Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []).\n* Switch from pycryptodome to cryptography (Thanks: [Thomas Roten]).\n* Add pager wrapper for behave tests (Thanks: [Dick Marinus]).\n* Behave test source command (Thanks: [Dick Marinus]).\n* Test using behave the tee command (Thanks: [Dick Marinus]).\n* Behave fix clean up. (Thanks: [Dick Marinus]).\n* Remove output formatter code in favor of CLI Helpers dependency (Thanks: [Thomas Roten]).\n* Better handle common before/after scenarios in behave. (Thanks: [Dick Marinus])\n* Added a regression test for sqlparse >= 0.2.3 (Thanks: [Dick Marinus]).\n* Reverted removal of temporary hack for sqlparse (Thanks: [Dick Marinus]).\n* Add setup.py commands to simplify development tasks (Thanks: [Thomas Roten]).\n* Add behave tests to tox (Thanks: [Dick Marinus]).\n* Add missing @dbtest to tests (Thanks: [Dick Marinus]).\n* Standardizes punctuation/grammar for help strings (Thanks: [Thomas Roten]).\n\n1.10.0\n=======\n\nFeatures\n---------\n\n* Add ability to specify alternative myclirc file. (Thanks: [Dick Marinus]).\n* Add new display formats for pretty printing query results. (Thanks: [Amjith\n  Ramanujam], [Dick Marinus], [Thomas Roten]).\n* Add logic to shorten the default prompt if it becomes too long once generated. (Thanks: [John Sterling]).\n\nBug Fixes\n----------\n\n* Fix external editor bug (issue #377). (Thanks: [Irina Truong]).\n* Fixed bug so that favorite queries can include unicode characters. (Thanks:\n  [Thomas Roten]).\n* Fix requirements and remove old compatibility code (Thanks: [Dick Marinus])\n* Fix bug where mycli would not start due to the thanks/credit intro text.\n  (Thanks: [Thomas Roten]).\n* Use pymysql default conversions (issue #375). (Thanks: [Dick Marinus]).\n\nInternal Changes\n-----------------\n\n* Upload mycli distributions in a safer manner (using twine). (Thanks: [Thomas\n  Roten]).\n* Test mycli using pexpect/python-behave (Thanks: [Dick Marinus]).\n* Run pep8 checks in travis (Thanks: [Irina Truong]).\n* Remove temporary hack for sqlparse (Thanks: [Dick Marinus]).\n\n1.9.0\n======\n\nFeatures\n---------\n\n* Add tee/notee commands for outputing results to a file. (Thanks: [Dick Marinus]).\n* Add date, port, and whitespace options to prompt configuration. (Thanks: [Matheus Rosa]).\n* Allow user to specify LESS pager flags. (Thanks: [John Sterling]).\n* Add support for auto-reconnect. (Thanks: [Jialong Liu]).\n* Add CSV batch output. (Thanks: [Matheus Rosa]).\n* Add `auto_vertical_output` config to myclirc. (Thanks: [Matheus Rosa]).\n* Improve Fedora install instructions. (Thanks: [Dick Marinus]).\n\nBug Fixes\n----------\n\n* Fix crashes occuring from commands starting with #. (Thanks: [Zhidong]).\n* Fix broken PyMySQL link in README. (Thanks: [Daniël van Eeden]).\n* Add various missing keywords for highlighting and autocompletion. (Thanks: [zer09]).\n* Add the missing REGEXP keyword for highlighting and autocompletion. (Thanks: [cxbig]).\n* Fix duplicate username entries in completion list. (Thanks: [John Sterling]).\n* Remove extra spaces in TSV table format output. (Thanks: [Dick Marinus]).\n* Kill running query when interrupted via Ctrl-C. (Thanks: [chainkite]).\n* Read the `smart_completion` config from myclirc. (Thanks: [Thomas Roten]).\n\nInternal Changes\n-----------------\n\n* Improve handling of test database credentials. (Thanks: [Dick Marinus]).\n* Add Python 3.6 to test environments and PyPI metadata. (Thanks: [Thomas Roten]).\n* Drop Python 2.6 support. (Thanks: [Thomas Roten]).\n* Swap pycrypto dependency for pycryptodome. (Thanks: [Michał Górny]).\n* Bump sqlparse version so pgcli and mycli can be installed together. (Thanks: [darikg]).\n\n1.8.1\n======\n\nBug Fixes\n----------\n\n* Remove duplicate listing of DISTINCT keyword. (Thanks: [Amjith Ramanujam]).\n* Add an try/except for AS keyword crash. (Thanks: [Amjith Ramanujam]).\n* Support python-sqlparse 0.2. (Thanks: [Dick Marinus]).\n* Fallback to the raw object for invalid time values. (Thanks: [Amjith Ramanujam]).\n* Reset the show items when completion is refreshed. (Thanks: [Amjith Ramanujam]).\n\nInternal Changes\n-----------------\n\n* Make the dependency of sqlparse slightly more liberal. (Thanks: [Amjith Ramanujam]).\n\n1.8.0\n======\n\nFeatures\n---------\n\n* Add support for --execute/-e commandline arg. (Thanks: [Matheus Rosa]).\n* Add `less_chatty` config option to skip the intro messages. (Thanks: [Scrappy Soft]).\n* Support `MYCLI_HISTFILE` environment variable to specify where to write the history file. (Thanks: [Scrappy Soft]).\n* Add `prompt_continuation` config option to allow configuring the continuation prompt for multi-line queries. (Thanks: [Scrappy Soft]).\n* Display login-path instead of host in prompt. (Thanks: [Irina Truong]).\n\nBug Fixes\n----------\n\n* Pin sqlparse to version 0.1.19 since the new version is breaking completion. (Thanks: [Amjith Ramanujam]).\n* Remove unsupported keywords. (Thanks: [Matheus Rosa]).\n* Fix completion suggestion inside functions with operands. (Thanks: [Irina Truong]).\n\n1.7.0\n======\n\nFeatures\n---------\n\n* Add stdin batch mode. (Thanks: [Thomas Roten]).\n* Add warn/no-warn command-line options. (Thanks: [Thomas Roten]).\n* Upgrade sqlparse dependency to 0.1.19. (Thanks: [Amjith Ramanujam]).\n* Update features list in README.md. (Thanks: [Matheus Rosa]).\n* Remove extra \\n in features list in README.md. (Thanks: [Matheus Rosa]).\n\nBug Fixes\n----------\n\n* Enable history search via <C-r>. (Thanks: [Amjith Ramanujam]).\n\nInternal Changes\n-----------------\n\n* Upgrade `prompt_toolkit` to 1.0.0. (Thanks: [Jonathan Slenders])\n\n1.6.0\n======\n\nFeatures\n---------\n\n* Change continuation prompt for multi-line mode to match default mysql.\n* Add `status` command to match mysql's `status` command. (Thanks: [Thomas Roten]).\n* Add SSL support for `mycli`. (Thanks: [Artem Bezsmertnyi]).\n* Add auto-completion and highlight support for OFFSET keyword. (Thanks: [Matheus Rosa]).\n* Add support for `MYSQL_TEST_LOGIN_FILE` env variable to specify alternate login file. (Thanks: [Thomas Roten]).\n* Add support for `--auto-vertical-output` to automatically switch to vertical output if the output doesn't fit in the table format.\n* Add support for system-wide config. Now /etc/myclirc will be honored. (Thanks: [Thomas Roten]).\n* Add support for `nopager` and `\\n` to turn off the pager. (Thanks: [Thomas Roten]).\n* Add support for `--local-infile` command-line option. (Thanks: [Thomas Roten]).\n\nBug Fixes\n----------\n\n* Remove -S from `less` option which was clobbering the scroll back in history. (Thanks: [Thomas Roten]).\n* Make system command work with Python 3. (Thanks: [Thomas Roten]).\n* Support \\G terminator for \\f queries. (Thanks: [Terseus]).\n\nInternal Changes\n-----------------\n\n* Upgrade `prompt_toolkit` to 0.60.\n* Add Python 3.5 to test environments. (Thanks: [Thomas Roten]).\n* Remove license meta-data. (Thanks: [Thomas Roten]).\n* Skip binary tests if PyMySQL version does not support it. (Thanks: [Thomas Roten]).\n* Refactor pager handling. (Thanks: [Thomas Roten])\n* Capture warnings to log file. (Thanks: [Mikhail Borisov]).\n* Make `syntax_style` a tiny bit more intuitive. (Thanks: [Phil Cohen]).\n\n1.5.2\n======\n\nBug Fixes\n----------\n\n* Protect against port number being None when no port is specified in command line.\n\n1.5.1\n======\n\nBug Fixes\n----------\n\n* Cast the value of port read from my.cnf to int.\n\n1.5.0\n======\n\nFeatures\n---------\n\n* Make a config option to enable `audit_log`. (Thanks: [Matheus Rosa]).\n* Add support for reading .mylogin.cnf to get user credentials. (Thanks: [Thomas Roten]).\n  This feature is only available when `pycrypto` package is installed.\n* Register the special command `prompt` with the `\\R` as alias. (Thanks: [Matheus Rosa]).\n  Users can now change the mysql prompt at runtime using `prompt` command.\n  eg:\n\n  ```\n  mycli> prompt \\u@\\h>\n  Changed prompt format to \\u@\\h>\n  Time: 0.001s\n  amjith@localhost>\n  ```\n\n* Perform completion refresh in a background thread. Now mycli can handle\n  databases with thousands of tables without blocking.\n* Add support for `system` command. (Thanks: [Matheus Rosa]).\n  Users can now run a system command from within mycli as follows:\n\n  ```\n  amjith@localhost:(none)>system cat tmp.sql\n  select 1;\n  select * from django_migrations;\n  ```\n\n* Caught and hexed binary fields in MySQL. (Thanks: [Daniel West]).\n  Geometric fields stored in a database will be displayed as hexed strings.\n* Treat enter key as tab when the suggestion menu is open. (Thanks: [Matheus Rosa])\n* Add \"delete\" and \"truncate\" as destructive commands. (Thanks: [Martijn Engler]).\n* Change \\dt syntax to add an optional table name. (Thanks: [Shoma Suzuki]).\n  `\\dt [tablename]` will describe the columns in a table.\n* Add TRANSACTION related keywords.\n* Treat DESC and EXPLAIN as DESCRIBE. (Thanks: [spacewander]).\n\nBug Fixes\n----------\n\n* Fix the removal of whitespace from table output.\n* Add ability to make suggestions for compound join clauses. (Thanks: [Matheus Rosa]).\n* Fix the incorrect reporting of command time.\n* Add type validation for port argument. (Thanks [Matheus Rosa])\n\nInternal Changes\n-----------------\n\n* Make pycrypto optional and only install it in \\*nix systems. (Thanks: [Irina Truong]).\n* Add badge for PyPI version to README. (Thanks: [Shoma Suzuki]).\n* Updated release script with a --dry-run and --confirm-steps option. (Thanks: [Irina Truong]).\n* Adds support for PyMySQL 0.6.2 and above. This is useful for debian package builders. (Thanks: [Thomas Roten]).\n* Disable click warning.\n\n1.4.0\n======\n\nFeatures\n---------\n\n* Add `source` command. This allows running sql statement from a file.\n\n  eg:\n\n  ```\n  mycli> source filename.sql\n  ```\n\n* Added a config option to make the warning before destructive commands optional. (Thanks: [Daniel West](https://github.com/danieljwest))\n\n  In the config file ~/.myclirc set `destructive_warning = False` which will\n  disable the warning before running `DROP` commands.\n\n* Add completion support for CHANGE TO and other master/slave commands. This is\n  still preliminary and it will be enhanced in the future.\n\n* Add custom styles to color the menus and toolbars.\n\n* Upgrade `prompt_toolkit` to 0.46. (Thanks: [Jonathan Slenders])\n\n  Multi-line queries are automatically indented.\n\nBug Fixes\n----------\n\n* Fix keyword completion after the `WHERE` clause.\n* Add `\\g` and `\\G` as valid query terminators. Previously in multi-line mode\n  ending a query with a `\\G` wouldn't run the query. This is now fixed.\n\n1.3.0\n======\n\nFeatures\n---------\n\n* Add a new special command (\\T) to change the table format on the fly. (Thanks: [Jonathan Bruno](https://github.com/brewneaux))\n  eg:\n\n  ```\n  mycli> \\T tsv\n  ```\n\n* Add `--defaults-group-suffix` to the command line. This lets the user specify\n  a group to use in the my.cnf files. (Thanks: [Irina Truong](https://github.com/j-bennet))\n\n  In the my.cnf file a user can specify credentials for different databases and\n  invoke mycli with the group name to use the appropriate credentials.\n  eg:\n\n  ```\n  # my.cnf\n  [client]\n  user   = 'root'\n  socket = '/tmp/mysql.sock'\n  pager = 'less -RXSF'\n  database = 'account'\n\n  [clientamjith]\n  user     = 'amjith'\n  database  = 'user_management'\n\n  $ mycli --defaults-group-suffix=amjith   # uses the [clientamjith] section in my.cnf\n  ```\n\n* Add `--defaults-file` option to the command line. This allows specifying a\n  `my.cnf` to use at launch. This also makes it play nice with mysql sandbox.\n\n* Make `-p` and `--password` take the password in commandline. This makes mycli\n  a drop in replacement for mysql.\n\n1.2.0\n======\n\nFeatures\n---------\n\n* Add support for wider completion menus in the config file.\n\n  Add `wider_completion_menu = True` in the config file (~/.myclirc) to enable this feature.\n\nBug Fixes\n---------\n\n* Prevent Ctrl-C from quitting mycli while the pager is active.\n* Refresh auto-completions after the database is changed via a CONNECT command.\n\nInternal Changes\n-----------------\n\n* Upgrade `prompt_toolkit` dependency version to 0.45.\n* Added Travis CI to run the tests automatically.\n\n1.1.1\n======\n\nBug Fixes\n----------\n\n* Change dictonary comprehension used in mycnf reader to list comprehension to make it compatible with Python 2.6.\n\n1.1.0\n======\n\nFeatures\n---------\n\n* Fuzzy completion is now case-insensitive. (Thanks: [bjarnagin](https://github.com/bjarnagin))\n* Added new-line (`\\n`) to the list of special characters to use in prompt. (Thanks: [brewneaux](https://github.com/brewneaux))\n* Honor the `pager` setting in my.cnf files. (Thanks: [Irina Truong](https://github.com/j-bennet))\n\nBug Fixes\n----------\n\n* Fix a crashing bug in completion engine for cross joins.\n* Make `<null>` value consistent between tabular and vertical output.\n\nInternal Changes\n-----------------\n\n* Changed pymysql version to be greater than 0.6.6.\n* Upgrade `prompt_toolkit` version to 0.42. (Thanks: [Yasuhiro Matsumoto](https://github.com/mattn))\n* Removed the explicit dependency on six.\n\n2015/06/10\n===========\n\nFeatures\n---------\n\n* Customizable prompt. (Thanks [Steve Robbins](https://github.com/steverobbins))\n* Make `\\G` formatting to behave more like mysql.\n\nBug Fixes\n----------\n\n* Formatting issue in \\G for really long column values.\n\n2015/06/07\n===========\n\nFeatures\n---------\n\n* Upgrade `prompt_toolkit` to 0.38. This improves the performance of pasting long queries.\n* Add support for reading my.cnf files.\n* Add editor command \\e.\n* Replace ConfigParser with ConfigObj.\n* Add \\dt to show all tables.\n* Add fuzzy completion for table names and column names.\n* Automatically reconnect when connection is lost to the database.\n\nBug Fixes\n----------\n\n* Fix a bug with reconnect failure.\n* Fix the issue with `use` command not changing the prompt.\n* Fix the issue where `\\\\r` shortcut was not recognized.\n\n2015/05/24\n==========\n\nFeatures\n---------\n\n* Add support for connecting via socket.\n* Add completion for SQL functions.\n* Add completion support for SHOW statements.\n* Made the timing of sql statements human friendly.\n* Automatically prompt for a password if needed.\n\nBug Fixes\n----------\n\n* Fixed the installation issues with PyMySQL dependency on case-sensitive file systems.\n\n[Amjith Ramanujam]: https://blog.amjith.com\n[Artem Bezsmertnyi]: https://github.com/mrdeathless\n[BuonOmo]: https://github.com/BuonOmo\n[Daniel West]: https://github.com/danieljwest\n[Dick Marinus]: https://github.com/meeuw\n[François Pietka]: https://github.com/fpietka\n[Frederic Aoustin]: https://github.com/fraoustin\n[Georgy Frolov]: https://github.com/pasenor\n[Irina Truong]: https://github.com/j-bennet\n[Jonathan Slenders]: https://github.com/jonathanslenders\n[laixintao]: https://github.com/laixintao\n[Martijn Engler]: https://github.com/martijnengler\n[Matheus Rosa]:  https://github.com/mdsrosa\n[Mikhail Borisov]: https://github.com/borman\n[mtorromeo]: https://github.com/mtorromeo\n[mwcm]: https://github.com/mwcm\n[Phil Cohen]: https://github.com/phlipper\n[Scrappy Soft]: https://github.com/scrappysoft\n[Shoma Suzuki]: https://github.com/shoma\n[spacewander]: https://github.com/spacewander\n[Terseus]: https://github.com/Terseus\n[Thomas Roten]: https://github.com/tsroten\n[xeron]: https://github.com/xeron\n[Zach DeCook]: https://zachdecook.com\n[Will Wang]: https://github.com/willww64\n"
  },
  {
    "path": "doc/key_bindings.rst",
    "content": "*************\nKey Bindings:\n*************\n\nMost key bindings are simply inherited from `prompt-toolkit <https://python-prompt-toolkit.readthedocs.io/en/master/index.html>`_ .\n\nThe following key bindings are special to mycli:\n\n###\nF1\n###\n\nOpen documentation index in a browser tab.\n\n###\nF2\n###\n\nEnable/Disable SmartCompletion Mode.\n\n###\nF3\n###\n\nEnable/Disable Multiline Mode.\n\n###\nF4\n###\n\nToggle between Vi and Emacs mode.\n\n###\nTab\n###\n\nForce autocompletion at cursor.\n\n#######\nC-space\n#######\n\nInitialize autocompletion at cursor.\n\nIf the autocompletion menu is not showing, display it with the appropriate completions for the context.\n\nIf the menu is showing, select the next completion.\n\n#########\nESC Enter\n#########\n\nIntroduce a line break in multi-line mode, or dispatch the command in single-line mode.\n\nThe sequence ESC-Enter is often sent by Alt-Enter.\n\n##################\nC-x p (Emacs-mode)\n##################\n\nPrettify and indent current statement, usually into multiple lines.\n\nOnly accepts buffers containing single SQL statements.\n\n##################\nC-x u (Emacs-mode)\n##################\n\nUnprettify and dedent current statement, usually into one line.\n\nOnly accepts buffers containing single SQL statements.\n\n##################\nC-o d (Emacs-mode)\n##################\n\nInsert the current date at cursor, defined by NOW() on the server.\n\n####################\nC-o C-d (Emacs-mode)\n####################\n\nInsert the quoted current date at cursor.\n\n##################\nC-o t (Emacs-mode)\n##################\n\nInsert the current datetime at cursor.\n\n####################\nC-o C-t (Emacs-mode)\n####################\n\nInsert the quoted current datetime at cursor.\n"
  },
  {
    "path": "doc/llm.md",
    "content": "# Using the \\llm Command (AI-assisted SQL)\n\nThe `\\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.\n\nAlias: `\\ai` works the same as `\\llm`.\n\n---\n\n## Quick Start\n\n1) Make sure mycli is installed with the `[llm]` extras, like\n```bash\npip install 'mycli[llm]'\n```\nor that the `llm` dependency is installed separately:\n```bash\npip install llm\n```\n\n2) From the mycli prompt, configure your API key (only needed for remote providers like OpenAI):\n\n```text\n\\llm keys set openai\n```\n\n3) Ask a question. The response’s SQL (inside a ```sql fenced block) is extracted and pre-filled at the prompt:\n\n```text\nWorld> \\llm \"Capital of India?\"\n-- Answer text from the model...\n-- ```sql\n-- SELECT ...;\n-- ```\n-- Your prompt is prefilled with the SQL above.\n```\n\nYou can now hit Enter to run, or edit the query first.\n\n---\n\n## What Context Is Sent\n\nWhen you ask a plain question via `\\llm \"...\"`, mycli:\n- Sends your question.\n- Adds your current database schema: table names with column types.\n- Adds one sample row (if available) from each table.\n\nThis 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)”).\n\nNote: Context is gathered from the current connection. If you are not connected, using contextual mode will fail — connect first.\n\n---\n\n## Using `llm` Subcommands from mycli\n\nYou can run any `llm` CLI subcommand by prefixing it with `\\llm` inside mycli. Examples:\n\n- List models:\n  ```text\n  \\llm models\n  ```\n- Set the default model:\n  ```text\n  \\llm models default gpt-5\n  ```\n- Set provider API key:\n  ```text\n  \\llm keys set openai\n  ```\n- Install a plugin (e.g., local models via Ollama):\n  ```text\n  \\llm install llm-ollama\n  ```\n  After installing or uninstalling plugins, mycli will restart to pick up new commands.\n\nTab completion works for `\\llm` subcommands, and even for model IDs under `models default`.\n\nAside: <https://ollama.com/> for using local models.\n\n---\n\n## Ask Questions With DB Context (default)\n\nAsk your question in quotes. mycli sends database context and extracts a SQL block if present.\n\n```text\nWorld> \\llm \"Most visited urls?\"\n```\n\nBehavior:\n- Response is printed in the output pane.\n- If the response contains a ```sql fenced block, mycli extracts the SQL and pre-fills it at your prompt.\n\n---\n\n## Continue Conversation (-c)\n\nUse `-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.\n\n```text\nWorld> \\llm \"Top 10 customers by spend\"\n-- model returns analysis and a ```sql block; SQL is prefilled\nWorld> \\llm -c \"Now include each customer's email and order count\"\n```\n\nBehavior:\n- Continues the last conversation in the `llm` history.\n- Database context is not re-sent on follow‑ups.\n- If the response includes a ```sql block, the SQL is pre-filled at your prompt.\n\n\n---\n\n## Examples\n\n- List available models:\n  ```text\n  World> \\llm models\n  ```\n\n- Change default model:\n  ```text\n  World> \\llm models default llama3\n  ```\n\n- Set API key (for providers that require it):\n  ```text\n  World> \\llm keys set openai\n  ```\n\n- Ask a question with context:\n  ```text\n  World> \\llm \"Capital of India?\"\n  ```\n\n- Use a local model (after installing a plugin such as `llm-ollama`):\n  ```text\n  World> \\llm install llm-ollama\n  World> \\llm models default llama3\n  World> \\llm \"Top 10 customers by spend\"\n  ```\n\nSee: <https://ollama.com/> for details.\n\n---\n\n## Customize the Prompt Template\n\nmycli uses a saved `llm` template named `mycli-llm-template` for contextual questions. You can view or edit it:\n\n```text\nWorld> \\llm templates edit mycli-llm-template\n```\n\nTip: After first use, mycli ensures this template exists. To just view it without editing, use:\n\n```text\nWorld> \\llm templates show mycli-llm-template\n```\n\n---\n\n## Troubleshooting\n\n- 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.\n- Not connected to a database: Contextual questions require a live connection. Connect first. Follow‑ups with `-c` only help after a successful contextual call.\n- Plugin changes not recognized: After `\\llm install` or `\\llm uninstall`, mycli restarts automatically to load new commands.\n- Provider/API issues: Use `\\llm keys list` and `\\llm keys set <provider>` to check credentials. Use `\\llm models` to confirm available models.\n\n---\n\n## Notes and Safety\n\n- 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.\n- Help: Running `\\llm` with no arguments shows a short usage message.\n\n## Turning Off LLM Support\n\nTo turn off LLM support even when the `llm` dependency is installed, set the `MYCLI_LLM_OFF` environment variable:\n```bash\nexport MYCLI_LLM_OFF=1\n```\n\nThis may be desirable for faster startup times.\n\n\n---\n\n## Learn More\n\n- `llm` project docs: https://llm.datasette.io/\n- `llm` plugin directory: https://llm.datasette.io/en/stable/plugins/directory.html\n"
  },
  {
    "path": "mycli/AUTHORS",
    "content": "Project Lead:\n-------------\n\n  * Roland Walker\n\nCore Developers:\n----------------\n\n  * Thomas Roten\n  * Irina Truong\n  * Matheus Rosa\n  * Darik Gamble\n  * Dick Marinus\n  * Amjith Ramanujam\n\nContributors:\n-------------\n\n  * 0xflotus\n  * Abirami P\n  * Adam Chainz\n  * Aljosha Papsch\n  * Allrob\n  * Andy Teijelo Pérez\n  * Angelo Lupo\n  * Artem Bezsmertnyi\n  * bitkeen\n  * bjarnagin\n  * BuonOmo\n  * caitinggui\n  * Carlos Afonso\n  * Casper Langemeijer\n  * chainkite\n  * Claude Becker\n  * Colin Caine\n  * cxbig\n  * Daniel Black\n  * Daniel West\n  * Daniël van Eeden\n  * Fabrizio Gennari\n  * FatBoyXPC\n  * François Pietka\n  * Frederic Aoustin\n  * Georgy Frolov\n  * Heath Naylor\n  * Huachao Mao\n  * Ishaan Bhimwal\n  * Jakub Boukal\n  * jbruno\n  * Jerome Provensal\n  * Jialong Liu\n  * Johannes Hoff\n  * John Sterling\n  * Jonathan Bruno\n  * Jonathan Lloyd\n  * Jonathan Slenders\n  * Kacper Kwapisz\n  * Karthikeyan Singaravelan\n  * kevinhwang91\n  * KITAGAWA Yasutaka\n  * Klaus Wünschel\n  * laixintao\n  * Lennart Weller\n  * Martijn Engler\n  * Massimiliano Torromeo\n  * Michał Górny\n  * Mike Palandra\n  * Mikhail Borisov\n  * Miodrag Tokić\n  * Morgan Mitchell\n  * mrdeathless\n  * Nathan Huang\n  * Nicolas Palumbo\n  * Phil Cohen\n  * QiaoHou Peng\n  * Roland Walker\n  * Ryan Smith\n  * Scrappy Soft\n  * Seamile\n  * Shoma Suzuki\n  * spacewander\n  * Steve Robbins\n  * Takeshi D. Itoh\n  * Terje Røsten\n  * Terseus\n  * Tyler Kuipers\n  * ushuz\n  * William GARCIA\n  * xeron\n  * Yang Zou\n  * Yasuhiro Matsumoto\n  * Yuanchun Shang\n  * Zach DeCook\n  * Zane C. Bowers-Hadley\n  * zer09\n  * Zhaolong Zhu\n  * Zhidong\n  * Zhongyang Guan\n  * Arvind Mishra\n  * Kevin Schmeichel\n  * Mel Dafert\n  * Thomas Copper\n  * Will Wang\n  * Alfred Wingate\n  * Zhanze Wang\n  * Houston Wong\n  * Mohamed Rezk\n  * Ryosuke Kazami\n  * Cornel Cruceru\n  * Sherlock Holo\n  * keltaklo\n  * 924060929\n  * tmijieux\n  * Scott Nemes\n  * Angelino Storm\n  * Abhay Kumar\n\n\nCreated by:\n-----------\n\nAmjith Ramanujam\n"
  },
  {
    "path": "mycli/SPONSORS",
    "content": "Many thanks to the following Kickstarter backers.\n\n* Tech Blue Software\n* jweiland.net\n\n# Silver Sponsors\n\n* Whitane Tech\n* Open Query Pty Ltd\n* Prathap Ramamurthy\n* Lincoln Loop\n\n# Sponsors\n\n* Nathan Taggart\n* Iryna Cherniavska\n* Sudaraka Wijesinghe\n* www.mysqlfanboy.com\n* Steve Robbins\n* Norbert Spichtig\n* orpharion bestheneme\n* Daniel Black\n* Anonymous\n* Magnus udd\n* Anonymous\n* Lewis Peckover\n* Cyrille Tabary\n* Heath Naylor\n* Ted Pennings\n* Chris Anderton\n* Jonathan Slenders\n\n# Other Donors\n\n* OpenAI\n"
  },
  {
    "path": "mycli/TIPS",
    "content": "###\n### CLI arguments\n###\n\ncheck your ~/.myclirc settings using the --checkup flag!\n\nlist your aliased DSNs with the --list-dsn flag!\n\nlog every query and result with the --logfile option!\n\nthe --checkpoint option helps track successful queries in batch mode!\n\nthe --format option helps set the output format in batch mode!\n\nthe --throttle option helps slow down queries in batch mode!\n\nthe --password-file option can be used with a FIFO to avoid saving creds to a file!\n\nthe --character-set option sets the character set for a single session!\n\nthe --unbuffered flag can save memory when in batch mode!\n\n--use-keyring=true lets you access the system keyring for passwords!\n\n--use-keyring=reset resets a password saved to the system keyring!\n\nthe --myclirc option can change the config file location for a single session!\n\nthe --execute option lets you execute a single line of SQL!\n\nthe --auto-vertical-output flag lets you automatically switch to vertical output!\n\nthe --show-warnings flag turns on warnings from the MySQL server!\n\nthe --no-warn flag turns off warnings befor running a destructive query!\n\nthe --init-command option lets you execute initialization SQL before a session!\n\nthe --login-path option lets you work with login-path files!\n\n--keepalive-ticks=<num> sets keepalive pings for a single session!\n\n###\n### commands\n###\n\ninteract with an LLM using the \\llm command!\n\ncopy a query to the clipboard using \\clip at the end of the query!\n\n\\dt lists tables; \\dt <table> describes <table>!\n\nedit a query in an external editor using <query>\\edit!\n\nedit a query in an external editor using \\edit <filename>!\n\nset \"export VISUAL='code --wait'\" in your shell to `\\edit` queries using VS Code!\n\n\\f lists favorite queries; \\f <name> executes a favorite!\n\n\\fs <name> <query> saves a favorite query!\n\n\\fd <name> deletes a saved favorite query!\n\n\\l lists databases!\n\n\\once <filename> appends the next result to <filename>!\n\n\\| <command> sends the next result to a subprocess!\n\n\\t toggles timing of commands!\n\n\\r or \"connect\" reconnects to the server!\n\n\\delimiter changes the SQL delimiter!\n\n\\q, \"quit\", or \"exit\" exits from the prompt!\n\n\\? or \"help\" for help!\n\n\"help <keyword>\" for help on SQL keywords!\n\n\\n or \"nopager\" to disable the pager!\n\nuse \"tee\"/\"notee\" to write/stop-writing results to a output file!\n\n\\W or \"warnings\" enables automatic warnings display!\n\n\\w or \"nowarnings\" disables automatic warnings display!\n\n\\P or \"pager\" sets the pager.  Try \"pager less\"!\n\n\\R or \"prompt\" changes the prompt format!\n\n\\Tr or \"redirectformat\" changes the table format for redirects!\n\n\\# or \"rehash\" refreshes autocompletions!\n\n\\. or \"source\" executes queries from a file!\n\n\\s or \"status\" requests status information from the server!\n\nuse \"system <command>\" to execute a shell command!\n\n\\T or \"tableformat\" changes the interactive table format!\n\n\\u or \"use\" changes to a new database!\n\nthe \"watch\" command executes a query every N seconds!\n\nuse \\bug to file a bug on GitHub!\n\n###\n### general\n###\n\ndisplay query output vertically using \\G at the end of a query!\n\nrun SQL scripts in batch mode using the standard input!\n\n###\n### keystrokes\n###\n\nedit a query in an external editor using keystrokes control-x + control-e!\n\nopen a documentation browser using keystroke F1!\n\ntoggle smart completion using keystroke F2!\n\ntoggle multi-line mode using keystroke F3!\n\ntoggle vi mode using keystroke F4!\n\ncomplete at cursor using the tab key!\n\nsummon completion candidates using control-space!\n\ncontrol-space works well with \"min_completion_trigger\" in ~/.myclirc!\n\nprettify a query using keystrokes control-x + p!\n\nun-prettify a query using keystrokes control-x + u!\n\ninsert the current date using keystrokes control-o + d!\n\ninsert the quoted current date using keystrokes control-o + control-d!\n\ninsert the current datetime using keystrokes control-o + t!\n\ninsert the quoted current date using keystrokes control-o + control-t!\n\nsearch query history using keystroke control-r!\n\nuse keystroke control-g to cancel completion popups!\n\nuse keystroke right-arrow to accept a full-line suggestion from your history!\n\ncancel history search using keystrokes Escape or control-g!\n\n###\n### myclirc options\n###\n\nset \"less_chatty = True\" in ~/.myclirc to turn off these tips!\n\nset a fancy table format like \"table_format = psql_unicode\" in ~/.myclirc!\n\nchange the string for NULLs with \"null_string = <null>\" in ~/.myclirc!\n\nchoose a color theme with \"syntax_style\" in ~/.myclirc!\n\ndesign a prompt with the \"prompt\" option in ~/.myclirc!\n\nturn off multi-line prompt indentation with \"prompt_continuation = ''\" in ~/.myclirc!\n\nsave passwords in the system keyring with \"use_keyring\" in ~/.myclirc!\n\nenable SHOW WARNINGS with \"show warnings\" in ~/.myclirc!\n\nturn off smart completions with \"smart_completion\" in ~/.myclirc!\n\nturn on multi-line mode with \"multi_line\" in ~/.myclirc!\n\nturn off destructive warnings with \"destructive_warning\" in ~/.myclirc!\n\ncontrol destructive warnings with \"destructive_keywords\" in ~/.myclirc!\n\nmove the history file locattion with \"history_file\" in ~/.myclirc!\n\nenable an audit log with \"audit_log\" in ~/.myclirc!\n\ndisable timing of SQL statements with \"timiing\" in ~/.myclirc!\n\ndisable display of SQL when running a favorite with \"show_favorite_query\" in ~/.myclirc!\n\nnotify after a long query by setting \"beep_after_seconds\" in ~/.myclirc!\n\ncontrol alignment with \"numeric_alignment\" in ~/.myclirc!\n\ncontrol binary value display with \"binary_display\" in ~/.myclirc!\n\nset vi key bindings with \"key_bindings\" in ~/.myclirc!\n\nshow more suggestions with \"wider_completion_menu\" in ~/.myclirc!\n\nuse the host alias in the prompt with \"login_path_as_host\" in ~/.myclirc!\n\nauto-display wide results vertically with \"auto_vertical_output\" in ~/.myclirc!\n\ncontrol keyword casing in completions using \"keyword_casing\" in ~/.myclirc!\n\ndisable pager on startup using \"enable_pager\" in ~/.myclirc!\n\nchoose a pager command with \"pager\" in ~/.myclirc!\n\ncustomize colors using the \"[colors]\" section in ~/.myclirc!\n\ncustomize LLM commands using the \"[llm]\" section in ~/.myclirc!\n\ncustomize history search using \"control_r\" in ~/.myclirc!\n\nedit favorite queries directly using the \"[favorite_queries]\" section in ~/.myclirc!\n\nset up initial commands using the \"[init-commands]\" section in ~/.myclirc!\n\ncreate DSN shortcuts using the \"[alias_dsn]\" section in ~/.myclirc!\n\nset up per-DSN initial commands using the \"[alias_dsn.init-commands]\" section in ~/.myclirc!\n\nset up connection defaults using the \"[connection]\" section in ~/.myclirc!\n\nuse \"min_completion_trigger\" in ~/.myclirc to defer completions!\n\ncolorize search previews with \"highlight_preview\" in ~/.myclirc!\n\n###\n### redirection\n###\n\nredirect query output to a shell command with \"$| <command>\"!\n\nredirect query output to a file with \"$> <filename>\"!\n\nappend query output to a file with \"$>> <filename>\"!\n\nrun a command after shell redirects with \"post_redirect_command\" in ~/.myclirc!\n"
  },
  {
    "path": "mycli/__init__.py",
    "content": "import importlib.metadata\n\n__version__: str = importlib.metadata.version(\"mycli\")\n"
  },
  {
    "path": "mycli/clibuffer.py",
    "content": "from prompt_toolkit.application import get_app\nfrom prompt_toolkit.enums import DEFAULT_BUFFER\nfrom prompt_toolkit.filters import Condition, Filter\n\nfrom mycli.packages.special import iocommands\nfrom mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS\n\n\ndef cli_is_multiline(mycli) -> Filter:\n    @Condition\n    def cond():\n        doc = get_app().layout.get_buffer_by_name(DEFAULT_BUFFER).document\n\n        if not mycli.multi_line:\n            return False\n        else:\n            return not _multiline_exception(doc.text)\n\n    return cond\n\n\ndef _multiline_exception(text: str) -> bool:\n    orig = text\n    text = text.strip()\n    first_word = text.split(' ')[0]\n\n    # Multi-statement favorite query is a special case. Because there will\n    # be a semicolon separating statements, we can't consider semicolon an\n    # EOL. Let's consider an empty line an EOL instead.\n    if first_word.startswith(\"\\\\fs\"):\n        return orig.endswith(\"\\n\")\n\n    return (\n        # Special Command\n        first_word.startswith(\"\\\\\")\n        or text.endswith((\n            # Ended with the current delimiter (usually a semi-column)\n            iocommands.get_current_delimiter(),\n            # or ended with certain commands\n            \"\\\\g\",\n            \"\\\\G\",\n            r\"\\e\",\n            r\"\\edit\",\n            r\"\\clip\",\n        ))\n        or\n        # non-backslashed special commands such as \"exit\" or \"help\" don't need semicolon\n        first_word in SPECIAL_COMMANDS\n        or\n        # uppercase variants accepted\n        first_word.lower() in SPECIAL_COMMANDS\n        or\n        # just a plain enter without any text\n        (first_word == \"\")\n    )\n"
  },
  {
    "path": "mycli/clistyle.py",
    "content": "import logging\n\nfrom prompt_toolkit.styles import Style, merge_styles\nfrom prompt_toolkit.styles.pygments import style_from_pygments_cls\nfrom prompt_toolkit.styles.style import _MergedStyle\nfrom pygments.style import Style as PygmentsStyle\nimport pygments.styles\nfrom pygments.token import Token, string_to_tokentype\nfrom pygments.util import ClassNotFound\n\nlogger = logging.getLogger(__name__)\n\n# map Pygments tokens (ptk 1.0) to class names (ptk 2.0).\nTOKEN_TO_PROMPT_STYLE: dict[Token, str] = {\n    Token.Menu.Completions.Completion.Current: \"completion-menu.completion.current\",\n    Token.Menu.Completions.Completion: \"completion-menu.completion\",\n    Token.Menu.Completions.Meta.Current: \"completion-menu.meta.completion.current\",\n    Token.Menu.Completions.Meta: \"completion-menu.meta.completion\",\n    Token.Menu.Completions.MultiColumnMeta: \"completion-menu.multi-column-meta\",\n    Token.Menu.Completions.ProgressButton: \"scrollbar.arrow\",  # best guess\n    Token.Menu.Completions.ProgressBar: \"scrollbar\",  # best guess\n    Token.SelectedText: \"selected\",\n    Token.SearchMatch: \"search\",\n    Token.SearchMatch.Current: \"search.current\",\n    Token.Toolbar: \"bottom-toolbar\",\n    Token.Toolbar.Off: \"bottom-toolbar.off\",\n    Token.Toolbar.On: \"bottom-toolbar.on\",\n    Token.Toolbar.Search: \"search-toolbar\",\n    Token.Toolbar.Search.Text: \"search-toolbar.text\",\n    Token.Toolbar.System: \"system-toolbar\",\n    Token.Toolbar.Arg: \"arg-toolbar\",\n    Token.Toolbar.Arg.Text: \"arg-toolbar.text\",\n    Token.Toolbar.Transaction.Valid: \"bottom-toolbar.transaction.valid\",\n    Token.Toolbar.Transaction.Failed: \"bottom-toolbar.transaction.failed\",\n    Token.Output.TableSeparator: \"output.table-separator\",\n    Token.Output.Header: \"output.header\",\n    Token.Output.OddRow: \"output.odd-row\",\n    Token.Output.EvenRow: \"output.even-row\",\n    Token.Output.Null: \"output.null\",\n    Token.Output.Status: \"output.status\",\n    Token.Output.Status.WarningCount: \"output.status.warning-count\",\n    Token.Output.Timing: \"output.timing\",\n    Token.Warnings.TableSeparator: \"warnings.table-separator\",\n    Token.Warnings.Header: \"warnings.header\",\n    Token.Warnings.OddRow: \"warnings.odd-row\",\n    Token.Warnings.EvenRow: \"warnings.even-row\",\n    Token.Warnings.Null: \"warnings.null\",\n    Token.Warnings.Status: \"warnings.status\",\n    Token.Warnings.Status.WarningCount: \"warnings.status.warning-count\",\n    Token.Warnings.Timing: \"warnings.timing\",\n    Token.Prompt: \"prompt\",\n    Token.Continuation: \"continuation\",\n}\n\n# reverse dict for cli_helpers, because they still expect Pygments tokens.\nPROMPT_STYLE_TO_TOKEN: dict[str, Token] = {v: k for k, v in TOKEN_TO_PROMPT_STYLE.items()}\n\n# all tokens that the Pygments MySQL lexer can produce\nOVERRIDE_STYLE_TO_TOKEN: dict[str, Token] = {\n    \"sql.comment\": Token.Comment,\n    \"sql.comment.multi-line\": Token.Comment.Multiline,\n    \"sql.comment.single-line\": Token.Comment.Single,\n    \"sql.comment.optimizer-hint\": Token.Comment.Special,\n    \"sql.escape\": Token.Error,\n    \"sql.keyword\": Token.Keyword,\n    \"sql.datatype\": Token.Keyword.Type,\n    \"sql.literal\": Token.Literal,\n    \"sql.literal.date\": Token.Literal.Date,\n    \"sql.symbol\": Token.Name,\n    \"sql.quoted-schema-object\": Token.Name.Quoted,\n    \"sql.quoted-schema-object.escape\": Token.Name.Quoted.Escape,\n    \"sql.constant\": Token.Name.Constant,\n    \"sql.function\": Token.Name.Function,\n    \"sql.variable\": Token.Name.Variable,\n    \"sql.number\": Token.Number,\n    \"sql.number.binary\": Token.Number.Bin,\n    \"sql.number.float\": Token.Number.Float,\n    \"sql.number.hex\": Token.Number.Hex,\n    \"sql.number.integer\": Token.Number.Integer,\n    \"sql.operator\": Token.Operator,\n    \"sql.punctuation\": Token.Punctuation,\n    \"sql.string\": Token.String,\n    \"sql.string.double-quouted\": Token.String.Double,\n    \"sql.string.escape\": Token.String.Escape,\n    \"sql.string.single-quoted\": Token.String.Single,\n    \"sql.whitespace\": Token.Text,\n}\n\n\ndef parse_pygments_style(\n    token_name: str,\n    style_object: PygmentsStyle | str,\n    style_dict: dict[str, str],\n) -> tuple[Token, str]:\n    \"\"\"Parse token type and style string.\n\n    :param token_name: str name of Pygments token. Example: \"Token.String\"\n    :param style_object: pygments.style.Style instance to use as base\n    :param style_dict: dict of token names and their styles, customized to this cli\n\n    \"\"\"\n    token_type = string_to_tokentype(token_name)\n    if isinstance(style_object, PygmentsStyle):\n        # When a Pygments Style class is passed, use its \"styles\" mapping.\n        other_token_type = string_to_tokentype(style_dict[token_name])\n        return token_type, style_object.styles[other_token_type]\n    else:\n        return token_type, style_dict[token_name]\n\n\ndef is_valid_pygments(name: str) -> bool:\n    try:\n\n        class TestStyle(PygmentsStyle):\n            default_style = ''\n            styles = {Token.Default: name}\n\n        return True\n    except AssertionError:\n        # can't emit error because some styles are valid pygments and not valid ptoolkit\n        return False\n\n\ndef is_valid_ptoolkit(name: str) -> bool:\n    try:\n        _s = Style([(\"default\", name)])\n        return True\n    except ValueError:\n        # can't emit error because some styles are valid pygments and not valid ptoolkit\n        return False\n\n\ndef style_factory_toolkit(name: str, cli_style: dict[str, str]) -> _MergedStyle:\n    try:\n        style: PygmentsStyle = pygments.styles.get_style_by_name(name)\n    except ClassNotFound:\n        style = pygments.styles.get_style_by_name(\"native\")\n\n    prompt_styles: list[tuple[str, str]] = []\n    # prompt-toolkit used pygments tokens for styling before, switched to style\n    # names in 2.0. Convert old token types to new style names, for backwards compatibility.\n    for token in cli_style:\n        if token.startswith(\"Token.\"):\n            # treat as pygments token (1.0)\n            token_type, style_value = parse_pygments_style(token, style, cli_style)\n            if token_type in TOKEN_TO_PROMPT_STYLE:\n                prompt_style = TOKEN_TO_PROMPT_STYLE[token_type]\n                if is_valid_ptoolkit(style_value):\n                    prompt_styles.append((prompt_style, style_value))\n            else:\n                # we don't want to support tokens anymore\n                logger.error(\"Unhandled style / class name: %s\", token)\n        else:\n            # treat as prompt style name (2.0). See default style names here:\n            # https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/styles/defaults.py\n            if is_valid_ptoolkit(cli_style[token]):\n                prompt_styles.append((token, cli_style[token]))\n\n    override_style: Style = Style([(\"bottom-toolbar\", \"noreverse\")])\n    return merge_styles([style_from_pygments_cls(style), override_style, Style(prompt_styles)])\n\n\ndef style_factory_helpers(\n    name: str,\n    cli_style: dict[str, str],\n    warnings: bool = False,\n) -> PygmentsStyle:\n    try:\n        style: dict[PygmentsStyle | str, str] = pygments.styles.get_style_by_name(name).styles\n    except ClassNotFound:\n        style = pygments.styles.get_style_by_name(\"native\").styles\n\n    for token in cli_style:\n        if token.startswith(\"Token.\"):\n            token_type, style_value = parse_pygments_style(token, style, cli_style)\n            if is_valid_pygments(style_value):\n                style.update({token_type: style_value})\n        elif token in PROMPT_STYLE_TO_TOKEN:\n            token_type = PROMPT_STYLE_TO_TOKEN[token]\n            if is_valid_pygments(cli_style[token]):\n                style.update({token_type: cli_style[token]})\n        elif token in OVERRIDE_STYLE_TO_TOKEN:\n            token_type = OVERRIDE_STYLE_TO_TOKEN[token]\n            if is_valid_pygments(cli_style[token]):\n                style.update({token_type: cli_style[token]})\n        else:\n            # TODO: cli helpers will have to switch to ptk.Style\n            logger.error(\"Unhandled style / class name: %s\", token)\n\n    if warnings:\n        for warning_token in list(style.keys()):\n            if 'Warnings' not in str(warning_token):\n                continue\n            warning_str = str(warning_token)\n            output_str = warning_str.replace('Warnings', 'Output')\n            output_token = string_to_tokentype(output_str)\n            style[output_token] = style[warning_token]\n\n    class OutputStyle(PygmentsStyle):\n        default_style = \"\"\n        styles = style\n\n    return OutputStyle\n"
  },
  {
    "path": "mycli/clitoolbar.py",
    "content": "from typing import Callable\n\nfrom prompt_toolkit.application import get_app\nfrom prompt_toolkit.enums import EditingMode\nfrom prompt_toolkit.formatted_text import to_formatted_text\nfrom prompt_toolkit.key_binding.vi_state import InputMode\n\nfrom mycli.packages import special\n\n\ndef create_toolbar_tokens_func(mycli, show_initial_toolbar_help: Callable, format_string: str | None) -> Callable:\n    \"\"\"Return a function that generates the toolbar tokens.\"\"\"\n\n    def get_toolbar_tokens() -> list[tuple[str, str]]:\n        divider = ('class:bottom-toolbar', ' │ ')\n\n        result = [(\"class:bottom-toolbar\", \"[Tab] Complete\")]\n        dynamic = []\n\n        result.append(divider)\n        result.append((\"class:bottom-toolbar\", \"[F1] Help\"))\n\n        if mycli.completer.smart_completion:\n            result.append(divider)\n            result.append((\"class:bottom-toolbar\", \"[F2] Smart-complete:\"))\n            result.append((\"class:bottom-toolbar.on\", \"ON \"))\n        else:\n            result.append(divider)\n            result.append((\"class:bottom-toolbar\", \"[F2] Smart-complete:\"))\n            result.append((\"class:bottom-toolbar.off\", \"OFF\"))\n\n        if mycli.multi_line:\n            result.append(divider)\n            result.append((\"class:bottom-toolbar\", \"[F3] Multiline:\"))\n            result.append((\"class:bottom-toolbar.on\", \"ON \"))\n        else:\n            result.append(divider)\n            result.append((\"class:bottom-toolbar\", \"[F3] Multiline:\"))\n            result.append((\"class:bottom-toolbar.off\", \"OFF\"))\n\n        if mycli.prompt_app.editing_mode == EditingMode.VI:\n            result.append(divider)\n            result.append((\"class:bottom-toolbar\", \"Vi:\"))\n            result.append((\"class:bottom-toolbar.on\", _get_vi_mode()))\n\n        if mycli.toolbar_error_message:\n            dynamic.append(divider)\n            dynamic.append((\"class:bottom-toolbar.transaction.failed\", mycli.toolbar_error_message))\n            mycli.toolbar_error_message = None\n\n        if mycli.multi_line:\n            delimiter = special.get_current_delimiter()\n            if delimiter != ';' or show_initial_toolbar_help():\n                dynamic.append(divider)\n                dynamic.append(('class:bottom-toolbar', '\"'))\n                dynamic.append(('class:bottom-toolbar.on', delimiter))\n                dynamic.append(('class:bottom-toolbar', '\" ends a statement'))\n\n        if show_initial_toolbar_help():\n            dynamic.append(divider)\n            dynamic.append((\"class:bottom-toolbar\", \"right-arrow accepts full-line suggestion\"))\n\n        if mycli.completion_refresher.is_refreshing():\n            dynamic.append(divider)\n            dynamic.append((\"class:bottom-toolbar\", \"Refreshing completions…\"))\n\n        if format_string and format_string != r'\\B':\n            if format_string.startswith(r'\\B'):\n                amended_format = format_string[2:]\n                result.extend(dynamic)\n                dynamic = []\n                result.append(('class:bottom-toolbar', '\\n'))\n            else:\n                amended_format = format_string\n                result = []\n            formatted = to_formatted_text(mycli.get_custom_toolbar(amended_format), style='class:bottom-toolbar')\n            result.extend([*formatted])  # coerce to list for mypy\n\n        result.extend(dynamic)\n        return result\n\n    return get_toolbar_tokens\n\n\ndef _get_vi_mode() -> str:\n    \"\"\"Get the current vi mode for display.\"\"\"\n    return {\n        InputMode.INSERT: \"I\",\n        InputMode.NAVIGATION: \"N\",\n        InputMode.REPLACE: \"R\",\n        InputMode.REPLACE_SINGLE: \"R\",\n        InputMode.INSERT_MULTIPLE: \"M\",\n    }[get_app().vi_state.input_mode]\n"
  },
  {
    "path": "mycli/compat.py",
    "content": "\"\"\"Platform and Python version compatibility support.\"\"\"\n\nimport sys\n\nWIN: bool = sys.platform in (\"win32\", \"cygwin\")\n"
  },
  {
    "path": "mycli/completion_refresher.py",
    "content": "import threading\nfrom typing import Callable\n\nfrom mycli.packages.special.main import COMMANDS\nfrom mycli.packages.sqlresult import SQLResult\nfrom mycli.sqlcompleter import SQLCompleter\nfrom mycli.sqlexecute import ServerSpecies, SQLExecute\n\n\nclass CompletionRefresher:\n    refreshers: dict = {}\n\n    def __init__(self) -> None:\n        self._completer_thread: threading.Thread | None = None\n        self._restart_refresh = threading.Event()\n\n    def refresh(\n        self,\n        executor: SQLExecute,\n        callbacks: Callable | list[Callable],\n        completer_options: dict | None = None,\n    ) -> list[SQLResult]:\n        \"\"\"Creates a SQLCompleter object and populates it with the relevant\n        completion suggestions in a background thread.\n\n        executor - SQLExecute object, used to extract the credentials to connect\n                   to the database.\n        callbacks - A function or a list of functions to call after the thread\n                    has completed the refresh. The newly created completion\n                    object will be passed in as an argument to each callback.\n        completer_options - dict of options to pass to SQLCompleter.\n\n        \"\"\"\n        if completer_options is None:\n            completer_options = {}\n\n        if self.is_refreshing():\n            self._restart_refresh.set()\n            return [SQLResult(status=\"Auto-completion refresh restarted.\")]\n        else:\n            self._completer_thread = threading.Thread(\n                target=self._bg_refresh, args=(executor, callbacks, completer_options), name=\"completion_refresh\"\n            )\n            self._completer_thread.daemon = True\n            self._completer_thread.start()\n            return [SQLResult(status=\"Auto-completion refresh started in the background.\")]\n\n    def is_refreshing(self) -> bool:\n        return bool(self._completer_thread and self._completer_thread.is_alive())\n\n    def _bg_refresh(\n        self,\n        sqlexecute: SQLExecute,\n        callbacks: Callable | list[Callable],\n        completer_options: dict,\n    ) -> None:\n        completer = SQLCompleter(**completer_options)\n\n        # Create a new sqlexecute method to populate the completions.\n        e = sqlexecute\n        executor = SQLExecute(\n            e.dbname,\n            e.user,\n            e.password,\n            e.host,\n            e.port,\n            e.socket,\n            e.character_set,\n            e.local_infile,\n            e.ssl,\n            e.ssh_user,\n            e.ssh_host,\n            e.ssh_port,\n            e.ssh_password,\n            e.ssh_key_filename,\n        )\n\n        # If callbacks is a single function then push it into a list.\n        if callable(callbacks):\n            callbacks = [callbacks]\n\n        while 1:\n            for refresher in self.refreshers.values():\n                refresher(completer, executor)\n                if self._restart_refresh.is_set():\n                    self._restart_refresh.clear()\n                    break\n            else:\n                # Break out of while loop if the for loop finishes natually\n                # without hitting the break statement.\n                break\n\n            # Start over the refresh from the beginning if the for loop hit the\n            # break statement.\n            continue\n\n        for callback in callbacks:\n            callback(completer)\n\n        executor.close()\n\n\ndef refresher(name: str, refreshers: dict = CompletionRefresher.refreshers) -> Callable:\n    \"\"\"Decorator to add the decorated function to the dictionary of\n    refreshers. Any function decorated with a @refresher will be executed as\n    part of the completion refresh routine.\"\"\"\n\n    def wrapper(wrapped):\n        refreshers[name] = wrapped\n        return wrapped\n\n    return wrapper\n\n\n@refresher(\"databases\")\ndef refresh_databases(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_database_names(executor.databases())\n\n\n@refresher(\"schemata\")\ndef refresh_schemata(completer: SQLCompleter, executor: SQLExecute) -> None:\n    # schemata - In MySQL Schema is the same as database. But for mycli\n    # schemata will be the name of the current database.\n    completer.extend_schemata(executor.dbname)\n    completer.set_dbname(executor.dbname)\n\n\n@refresher(\"tables\")\ndef refresh_tables(completer: SQLCompleter, executor: SQLExecute) -> None:\n    table_columns_dbresult = list(executor.table_columns())\n    completer.extend_relations(table_columns_dbresult, kind=\"tables\")\n    completer.extend_columns(table_columns_dbresult, kind=\"tables\")\n\n\n@refresher(\"enum_values\")\ndef refresh_enum_values(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_enum_values(executor.enum_values())\n\n\n@refresher(\"users\")\ndef refresh_users(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_users(executor.users())\n\n\n# @refresher('views')\n# def refresh_views(completer: SQLCompleter, executor: SQLExecute) -> None:\n#     completer.extend_relations(executor.views(), kind='views')\n#     completer.extend_columns(executor.view_columns(), kind='views')\n\n\n@refresher(\"functions\")\ndef refresh_functions(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_functions(executor.functions())\n    if executor.server_info and executor.server_info.species == ServerSpecies.TiDB:\n        completer.extend_functions(completer.tidb_functions, builtin=True)\n\n\n@refresher(\"procedures\")\ndef refresh_procedures(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_procedures(executor.procedures())\n\n\n@refresher(\"character_sets\")\ndef refresh_character_sets(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_character_sets(executor.character_sets())\n\n\n@refresher(\"collations\")\ndef refresh_collations(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_collations(executor.collations())\n\n\n@refresher(\"special_commands\")\ndef refresh_special(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_special_commands(list(COMMANDS.keys()))\n\n\n@refresher(\"show_commands\")\ndef refresh_show_commands(completer: SQLCompleter, executor: SQLExecute) -> None:\n    completer.extend_show_items(executor.show_candidates())\n\n\n@refresher(\"keywords\")\ndef refresh_keywords(completer: SQLCompleter, executor: SQLExecute) -> None:\n    if executor.server_info and executor.server_info.species == ServerSpecies.TiDB:\n        completer.extend_keywords(completer.tidb_keywords, replace=True)\n"
  },
  {
    "path": "mycli/config.py",
    "content": "from copy import copy\nfrom importlib import resources\nfrom io import BytesIO, TextIOWrapper\nimport logging\nimport os\nfrom os.path import exists\nimport struct\nimport sys\nfrom typing import IO, BinaryIO, Literal\n\nfrom configobj import ConfigObj, ConfigObjError\nfrom Cryptodome.Cipher import AES\n\nlogger = logging.getLogger(__name__)\n\n\ndef log(logger: logging.Logger, level: int, message: str) -> None:\n    \"\"\"Logs message to stderr if logging isn't initialized.\"\"\"\n\n    if logger.parent and logger.parent.name == \"root\":\n        print(message, file=sys.stderr)\n\n    logger.log(level, message)\n\n\ndef read_config_file(f: str | IO[str], list_values: bool = True) -> ConfigObj | None:\n    \"\"\"Read a config file.\n\n    *list_values* set to `True` is the default behavior of ConfigObj.\n    Disabling it causes values to not be parsed for lists,\n    (e.g. 'a,b,c' -> ['a', 'b', 'c']. Additionally, the config values are\n    not unquoted. We are disabling list_values when reading MySQL config files\n    so we can correctly interpret commas in passwords.\n\n    \"\"\"\n\n    if isinstance(f, str):\n        f = os.path.expanduser(f)\n\n    try:\n        config = ConfigObj(f, interpolation=False, encoding=\"utf8\", list_values=list_values)\n    except ConfigObjError as e:\n        log(logger, logging.WARNING, \"Unable to parse line {0} of config file '{1}'.\".format(e.line_number, f))\n        log(logger, logging.WARNING, \"Using successfully parsed config values.\")\n        return e.config\n    except (IOError, OSError) as e:\n        log(logger, logging.WARNING, \"You don't have permission to read config file '{0}'.\".format(e.filename))\n        return None\n\n    return config\n\n\ndef get_included_configs(config_file: str | IO[str]) -> list[str | IO[str]]:\n    \"\"\"Get a list of configuration files that are included into config_path\n    with !includedir directive.\n\n    \"Normal\" configs should be passed as file paths. The only exception\n    is .mylogin which is decoded into a stream. However, it never\n    contains include directives and so will be ignored by this\n    function.\n\n    \"\"\"\n    if not isinstance(config_file, str) or not os.path.isfile(config_file):\n        return []\n    included_configs: list[str | IO[str]] = []\n\n    try:\n        with open(config_file) as f:\n            include_directives = filter(lambda s: s.startswith(\"!includedir\"), f)\n            dirs_split = (s.strip().split()[-1] for s in include_directives)\n            dirs = filter(os.path.isdir, dirs_split)\n            for dir_ in dirs:\n                for filename in os.listdir(dir_):\n                    if filename.endswith(\".cnf\"):\n                        included_configs.append(os.path.join(dir_, filename))\n    except (PermissionError, UnicodeDecodeError):\n        pass\n    return included_configs\n\n\ndef read_config_files(\n    files: list[str | IO[str]],\n    list_values: bool = True,\n    ignore_package_defaults: bool = False,\n    ignore_user_options: bool = False,\n) -> ConfigObj:\n    \"\"\"Read and merge a list of config files.\"\"\"\n\n    if ignore_package_defaults:\n        config = ConfigObj()\n    else:\n        config = create_default_config(list_values=list_values)\n\n    if ignore_user_options:\n        return config\n\n    _files = copy(files)\n    while _files:\n        _file = _files.pop(0)\n        _config = read_config_file(_file, list_values=list_values)\n\n        # expand includes only if we were able to parse config\n        # (otherwise we'll just encounter the same errors again)\n        if config is not None:\n            _files = get_included_configs(_file) + _files\n        if _config is not None:\n            config.merge(_config)\n            config.filename = _config.filename\n\n    return config\n\n\ndef create_default_config(list_values: bool = True) -> ConfigObj:\n    import mycli\n\n    default_config_file = resources.files(mycli).joinpath(\"myclirc\").open('r')\n    return read_config_file(default_config_file, list_values=list_values)\n\n\ndef write_default_config(destination: str, overwrite: bool = False) -> None:\n    import mycli\n\n    with resources.files(mycli).joinpath(\"myclirc\").open('r') as f:\n        default_config = f.read()\n    destination = os.path.expanduser(destination)\n    if not overwrite and exists(destination):\n        return\n\n    with open(destination, \"w\") as f:\n        f.write(default_config)\n\n\ndef get_mylogin_cnf_path() -> str | None:\n    \"\"\"Return the path to the login path file or None if it doesn't exist.\"\"\"\n    mylogin_cnf_path = os.getenv(\"MYSQL_TEST_LOGIN_FILE\")\n\n    if mylogin_cnf_path is None:\n        app_data = os.getenv(\"APPDATA\")\n        default_dir = os.path.join(app_data, \"MySQL\") if app_data else \"~\"\n        mylogin_cnf_path = os.path.join(default_dir, \".mylogin.cnf\")\n\n    mylogin_cnf_path = os.path.expanduser(mylogin_cnf_path)\n\n    if exists(mylogin_cnf_path):\n        logger.debug(\"Found login path file at '{0}'\".format(mylogin_cnf_path))\n        return mylogin_cnf_path\n    return None\n\n\ndef open_mylogin_cnf(name: str) -> TextIOWrapper | None:\n    \"\"\"Open a readable version of .mylogin.cnf.\n\n    Returns the file contents as a TextIOWrapper object.\n\n    :param str name: The pathname of the file to be opened.\n    :return: the login path file or None\n    \"\"\"\n\n    try:\n        with open(name, \"rb\") as f:\n            plaintext = read_and_decrypt_mylogin_cnf(f)\n    except (OSError, IOError, ValueError):\n        logger.error(\"Unable to open login path file.\")\n        return None\n\n    if not isinstance(plaintext, BytesIO):\n        logger.error(\"Unable to read login path file.\")\n        return None\n\n    return TextIOWrapper(plaintext)\n\n\n# TODO reuse code between encryption an decryption\ndef encrypt_mylogin_cnf(plaintext: IO[str]) -> BytesIO:\n    \"\"\"Encryption of .mylogin.cnf file, analogous to calling\n    mysql_config_editor.\n\n    Code is based on the python implementation by Kristian Koehntopp\n    https://github.com/isotopp/mysql-config-coder\n\n    \"\"\"\n\n    def realkey(key: bytes) -> bytes:\n        \"\"\"Create the AES key from the login key.\"\"\"\n        rkey = bytearray(16)\n        for i in range(len(key)):\n            rkey[i % 16] ^= key[i]\n        return bytes(rkey)\n\n    def encode_line(plaintext: str, real_key: bytes, buf_len: int) -> bytes:\n        aes = AES.new(real_key, AES.MODE_ECB)\n        text_len = len(plaintext)\n        pad_len = buf_len - text_len\n        pad_chr = bytes(chr(pad_len), \"utf8\")\n        plaintext_b = plaintext.encode() + pad_chr * pad_len\n        encrypted_text = b\"\".join([aes.encrypt(plaintext_b[i : i + 16]) for i in range(0, len(plaintext_b), 16)])\n        return encrypted_text\n\n    LOGIN_KEY_LENGTH = 20\n    key = os.urandom(LOGIN_KEY_LENGTH)\n    real_key = realkey(key)\n\n    outfile = BytesIO()\n\n    outfile.write(struct.pack(\"i\", 0))\n    outfile.write(key)\n\n    while True:\n        line = plaintext.readline()\n        if not line:\n            break\n        real_len = len(line)\n        pad_len = (int(real_len / 16) + 1) * 16\n\n        outfile.write(struct.pack(\"i\", pad_len))\n        x = encode_line(line, real_key, pad_len)\n        outfile.write(x)\n\n    outfile.seek(0)\n    return outfile\n\n\ndef read_and_decrypt_mylogin_cnf(f: BinaryIO) -> BytesIO | None:\n    \"\"\"Read and decrypt the contents of .mylogin.cnf.\n\n    This decryption algorithm mimics the code in MySQL's\n    mysql_config_editor.cc.\n\n    The login key is 20-bytes of random non-printable ASCII.\n    It is written to the actual login path file. It is used\n    to generate the real key used in the AES cipher.\n\n    :param f: an I/O object opened in binary mode\n    :return: the decrypted login path file\n    :rtype: io.BytesIO or None\n    \"\"\"\n\n    # Number of bytes used to store the length of ciphertext.\n    MAX_CIPHER_STORE_LEN = 4\n\n    LOGIN_KEY_LEN = 20\n\n    # Move past the unused buffer.\n    buf = f.read(4)\n\n    if not buf or len(buf) != 4:\n        logger.error(\"Login path file is blank or incomplete.\")\n        return None\n\n    # Read the login key.\n    key = f.read(LOGIN_KEY_LEN)\n\n    # Generate the real key.\n    rkey = [0] * 16\n    for i in range(LOGIN_KEY_LEN):\n        try:\n            rkey[i % 16] ^= ord(key[i : i + 1])\n        except TypeError:\n            # ord() was unable to get the value of the byte.\n            logger.error(\"Unable to generate login path AES key.\")\n            return None\n    rkey_b = struct.pack(\"16B\", *rkey)\n\n    # Create a bytes buffer to hold the plaintext.\n    plaintext = BytesIO()\n    aes = AES.new(rkey_b, AES.MODE_ECB)\n\n    while True:\n        # Read the length of the ciphertext.\n        len_buf = f.read(MAX_CIPHER_STORE_LEN)\n        if len(len_buf) < MAX_CIPHER_STORE_LEN:\n            break\n        (cipher_len,) = struct.unpack(\"<i\", len_buf)\n\n        # Read cipher_len bytes from the file and decrypt.\n        cipher = f.read(cipher_len)\n        plain = _remove_pad(b\"\".join([aes.decrypt(cipher[i : i + 16]) for i in range(0, cipher_len, 16)]))\n        if plain is False:\n            continue\n        plaintext.write(plain)\n\n    if plaintext.tell() == 0:\n        logger.error(\"No data successfully decrypted from login path file.\")\n        return None\n\n    plaintext.seek(0)\n    return plaintext\n\n\ndef str_to_bool(s: str | bool) -> bool:\n    \"\"\"Convert a string value to its corresponding boolean value.\"\"\"\n    if isinstance(s, bool):\n        return s\n    elif not isinstance(s, str):\n        raise TypeError(\"argument must be a string\")\n\n    true_values = (\"true\", \"on\", \"1\")\n    false_values = (\"false\", \"off\", \"0\")\n\n    if s.lower() in true_values:\n        return True\n    elif s.lower() in false_values:\n        return False\n    else:\n        raise ValueError(f'not a recognized boolean value: {s}')\n\n\ndef strip_matching_quotes(s: str) -> str:\n    \"\"\"Remove matching, surrounding quotes from a string.\n\n    This is the same logic that ConfigObj uses when parsing config\n    values.\n\n    \"\"\"\n    if isinstance(s, str) and len(s) >= 2 and s[0] == s[-1] and s[0] in ('\"', \"'\"):\n        s = s[1:-1]\n    return s\n\n\ndef _remove_pad(line: bytes) -> bytes | Literal[False]:\n    \"\"\"Remove the pad from the *line*.\"\"\"\n    try:\n        # Determine pad length.\n        pad_length = ord(line[-1:])\n    except TypeError:\n        # ord() was unable to get the value of the byte.\n        logger.warning(\"Unable to remove pad.\")\n        return False\n\n    if pad_length > len(line) or len(set(line[-pad_length:])) != 1:\n        # Pad length should be less than or equal to the length of the\n        # plaintext. The pad should have a single unique byte.\n        logger.warning(\"Invalid pad found in login path file.\")\n        return False\n\n    return line[:-pad_length]\n"
  },
  {
    "path": "mycli/constants.py",
    "content": "HOME_URL = 'https://mycli.net'\nREPO_URL = 'https://github.com/dbcli/mycli'\nDOCS_URL = f'{HOME_URL}/docs'\nISSUES_URL = f'{REPO_URL}/issues'\n\nDEFAULT_CHARSET = 'utf8mb4'\nDEFAULT_DATABASE = 'mysql'\nDEFAULT_HOST = 'localhost'\nDEFAULT_PORT = 3306\nDEFAULT_USER = 'root'\n\nTEST_DATABASE = 'mycli_test_db'\n"
  },
  {
    "path": "mycli/key_bindings.py",
    "content": "import logging\nimport webbrowser\n\nimport prompt_toolkit\nfrom prompt_toolkit.application.current import get_app\nfrom prompt_toolkit.enums import EditingMode\nfrom prompt_toolkit.filters import (\n    Condition,\n    completion_is_selected,\n    control_is_searchable,\n    emacs_mode,\n)\nfrom prompt_toolkit.key_binding import KeyBindings\nfrom prompt_toolkit.key_binding.key_processor import KeyPressEvent\nfrom prompt_toolkit.selection import SelectionType\n\nfrom mycli.constants import DOCS_URL\nfrom mycli.packages import shortcuts\nfrom mycli.packages.toolkit.fzf import search_history\nfrom mycli.packages.toolkit.utils import safe_invalidate_display\n\n_logger = logging.getLogger(__name__)\n\n\n@Condition\ndef ctrl_d_condition() -> bool:\n    \"\"\"Ctrl-D exit binding is only active when the buffer is empty.\"\"\"\n    app = get_app()\n    return not app.current_buffer.text\n\n\n@Condition\ndef in_completion() -> bool:\n    app = get_app()\n    return bool(app.current_buffer.complete_state)\n\n\ndef print_f1_help():\n    app = get_app()\n    app.print_text('\\n')\n    app.print_text([\n        ('', 'Inline help — type \"'),\n        ('bold', 'help'),\n        ('', '\" or \"'),\n        ('bold', r'\\?'),\n        ('', '\"\\n'),\n    ])\n    app.print_text([\n        ('', 'Docs index — '),\n        ('bold', DOCS_URL),\n        ('', '\\n'),\n    ])\n    app.print_text('\\n')\n\n\ndef mycli_bindings(mycli) -> KeyBindings:\n    \"\"\"Custom key bindings for mycli.\"\"\"\n    kb = KeyBindings()\n\n    @kb.add('f1')\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Open browser to documentation index.\"\"\"\n        _logger.debug('Detected F1 key.')\n        webbrowser.open_new_tab(DOCS_URL)\n        prompt_toolkit.application.run_in_terminal(print_f1_help)\n        safe_invalidate_display(event.app)\n\n    @kb.add('escape', '[', 'P')\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Open browser to documentation index.\"\"\"\n        _logger.debug(\"Detected alternate F1 key sequence.\")\n        webbrowser.open_new_tab(DOCS_URL)\n        prompt_toolkit.application.run_in_terminal(print_f1_help)\n        safe_invalidate_display(event.app)\n\n    @kb.add(\"f2\")\n    def _(_event: KeyPressEvent) -> None:\n        \"\"\"Enable/Disable SmartCompletion Mode.\"\"\"\n        _logger.debug(\"Detected F2 key.\")\n        mycli.completer.smart_completion = not mycli.completer.smart_completion\n\n    @kb.add('escape', '[', 'Q')\n    def _(_event: KeyPressEvent) -> None:\n        \"\"\"Enable/Disable SmartCompletion Mode.\"\"\"\n        _logger.debug(\"Detected alternate F2 key sequence.\")\n        mycli.completer.smart_completion = not mycli.completer.smart_completion\n\n    @kb.add(\"f3\")\n    def _(_event: KeyPressEvent) -> None:\n        \"\"\"Enable/Disable Multiline Mode.\"\"\"\n        _logger.debug(\"Detected F3 key.\")\n        mycli.multi_line = not mycli.multi_line\n\n    @kb.add('escape', '[', 'R')\n    def _(_event: KeyPressEvent) -> None:\n        \"\"\"Enable/Disable Multiline Mode.\"\"\"\n        _logger.debug('Detected alternate F3 key sequence.')\n        mycli.multi_line = not mycli.multi_line\n\n    @kb.add(\"f4\")\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Toggle between Vi and Emacs mode.\"\"\"\n        _logger.debug(\"Detected F4 key.\")\n        if mycli.key_bindings == \"vi\":\n            event.app.editing_mode = EditingMode.EMACS\n            mycli.key_bindings = \"emacs\"\n            event.app.ttimeoutlen = mycli.emacs_ttimeoutlen\n        else:\n            event.app.editing_mode = EditingMode.VI\n            mycli.key_bindings = \"vi\"\n            event.app.ttimeoutlen = mycli.vi_ttimeoutlen\n\n    @kb.add('escape', '[', 'S')\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Toggle between Vi and Emacs mode.\"\"\"\n        _logger.debug('Detected alternate F4 key sequence.')\n        if mycli.key_bindings == 'vi':\n            event.app.editing_mode = EditingMode.EMACS\n            mycli.key_bindings = 'emacs'\n            event.app.ttimeoutlen = mycli.emacs_ttimeoutlen\n        else:\n            event.app.editing_mode = EditingMode.VI\n            mycli.key_bindings = 'vi'\n            event.app.ttimeoutlen = mycli.vi_ttimeoutlen\n\n    @kb.add(\"tab\")\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Complete action at cursor.\"\"\"\n        _logger.debug(\"Detected <Tab> key.\")\n        b = event.app.current_buffer\n\n        behaviors = mycli.config['keys'].as_list('tab')\n\n        if 'toolkit_default' in behaviors:\n            if b.complete_state:\n                b.complete_next()\n            else:\n                b.start_completion(select_first=True)\n\n        if b.complete_state:\n            if 'advance' in behaviors:\n                b.complete_next()\n            elif 'cancel' in behaviors:\n                b.cancel_completion()\n            return\n\n        if 'advancing_summon' in behaviors:\n            b.start_completion(select_first=True)\n        elif 'prefixing_summon' in behaviors:\n            b.start_completion(insert_common_part=True)\n        elif 'summon' in behaviors:\n            b.start_completion(select_first=False)\n\n    @kb.add(\"escape\", eager=True, filter=in_completion)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Cancel completion menu.\n\n        There will be a lag when canceling Escape due to the processing of\n        Alt- keystrokes as Escape- sequences.\n\n        There will be no lag when using control-g to cancel.\"\"\"\n        event.app.current_buffer.cancel_completion()\n\n    @kb.add(\"c-space\")\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"\n        Complete action at cursor.\n\n        By default, if the autocompletion menu is not showing, display it with the\n        appropriate completions for the context.\n\n        If the menu is showing, select the next completion.\n        \"\"\"\n        _logger.debug(\"Detected <C-Space> key.\")\n\n        b = event.app.current_buffer\n\n        behaviors = mycli.config['keys'].as_list('control_space')\n\n        if 'toolkit_default' in behaviors:\n            if b.text:\n                b.start_selection(selection_type=SelectionType.CHARACTERS)\n            return\n\n        if b.complete_state:\n            if 'advance' in behaviors:\n                b.complete_next()\n            elif 'cancel' in behaviors:\n                b.cancel_completion()\n            return\n\n        if 'advancing_summon' in behaviors:\n            b.start_completion(select_first=True)\n        elif 'prefixing_summon' in behaviors:\n            b.start_completion(insert_common_part=True)\n        elif 'summon' in behaviors:\n            b.start_completion(select_first=False)\n\n    @kb.add(\"c-x\", \"p\", filter=emacs_mode)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"\n        Prettify and indent current statement, usually into multiple lines.\n\n        Only accepts buffers containing single SQL statements.\n        \"\"\"\n        _logger.debug(\"Detected <C-x p>/> key.\")\n\n        b = event.app.current_buffer\n        if b.text:\n            b.transform_region(0, len(b.text), mycli.handle_prettify_binding)\n\n    @kb.add(\"c-x\", \"u\", filter=emacs_mode)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"\n        Unprettify and dedent current statement, usually into one line.\n\n        Only accepts buffers containing single SQL statements.\n        \"\"\"\n        _logger.debug(\"Detected <C-x u>/< key.\")\n\n        b = event.app.current_buffer\n        if b.text:\n            b.transform_region(0, len(b.text), mycli.handle_unprettify_binding)\n\n    @kb.add(\"c-o\", \"d\", filter=emacs_mode)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"\n        Insert the current date.\n        \"\"\"\n        _logger.debug(\"Detected <C-o d> key.\")\n\n        event.app.current_buffer.insert_text(shortcuts.server_date(mycli.sqlexecute))\n\n    @kb.add(\"c-o\", \"c-d\", filter=emacs_mode)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"\n        Insert the quoted current date.\n        \"\"\"\n        _logger.debug(\"Detected <C-o C-d> key.\")\n\n        event.app.current_buffer.insert_text(shortcuts.server_date(mycli.sqlexecute, quoted=True))\n\n    @kb.add(\"c-o\", \"t\", filter=emacs_mode)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"\n        Insert the current datetime.\n        \"\"\"\n        _logger.debug(\"Detected <C-o t> key.\")\n\n        event.app.current_buffer.insert_text(shortcuts.server_datetime(mycli.sqlexecute))\n\n    @kb.add(\"c-o\", \"c-t\", filter=emacs_mode)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"\n        Insert the quoted current datetime.\n        \"\"\"\n        _logger.debug(\"Detected <C-o C-t> key.\")\n\n        event.app.current_buffer.insert_text(shortcuts.server_datetime(mycli.sqlexecute, quoted=True))\n\n    @kb.add(\"c-r\", filter=control_is_searchable)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Search history using fzf or reverse incremental search.\"\"\"\n        _logger.debug(\"Detected <C-r> key.\")\n        mode = mycli.config.get('keys', {}).get('control_r', 'auto')\n        if mode == 'reverse_isearch':\n            search_history(event, incremental=True)\n        else:\n            search_history(\n                event,\n                highlight_preview=mycli.highlight_preview,\n                highlight_style=mycli.syntax_style,\n            )\n\n    @kb.add(\"escape\", \"r\", filter=control_is_searchable & emacs_mode)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Search history using fzf when available.\"\"\"\n        _logger.debug(\"Detected <alt-r> key.\")\n        search_history(\n            event,\n            highlight_preview=mycli.highlight_preview,\n            highlight_style=mycli.syntax_style,\n        )\n\n    @kb.add('c-d', filter=ctrl_d_condition)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Exit mycli or ignore keypress.\"\"\"\n        _logger.debug('Detected <C-d> key on empty line.')\n        mode = mycli.config.get('keys', {}).get('control_d', 'exit')\n        if mode == 'exit':\n            event.app.exit(exception=EOFError, style='class:exiting')\n        else:\n            event.app.output.bell()\n\n    @kb.add(\"enter\", filter=completion_is_selected)\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Makes the enter key work as the tab key only when showing the menu.\n\n        In other words, don't execute query when enter is pressed in\n        the completion dropdown menu, instead close the dropdown menu\n        (accept current selection).\n\n        \"\"\"\n        _logger.debug(\"Detected enter key.\")\n\n        event.current_buffer.complete_state = None\n        b = event.app.current_buffer\n        b.complete_state = None\n\n    @kb.add(\"escape\", \"enter\")\n    def _(event: KeyPressEvent) -> None:\n        \"\"\"Introduces a line break in multi-line mode, or dispatches the\n        command in single-line mode.\"\"\"\n        _logger.debug(\"Detected alt-enter key.\")\n        if mycli.multi_line:\n            event.app.current_buffer.validate_and_handle()\n        else:\n            event.app.current_buffer.insert_text(\"\\n\")\n\n    return kb\n"
  },
  {
    "path": "mycli/lexer.py",
    "content": "from pygments.lexer import inherit\nfrom pygments.lexers.sql import MySqlLexer\nfrom pygments.token import Keyword\n\n\nclass MyCliLexer(MySqlLexer):\n    \"\"\"Extends MySQL lexer to add keywords.\"\"\"\n\n    tokens = {\n        \"root\": [(r\"\\brepair\\b\", Keyword), (r\"\\boffset\\b\", Keyword), inherit],\n    }\n"
  },
  {
    "path": "mycli/magic.py",
    "content": "import logging\nfrom typing import Any\n\nimport sql.connection\nimport sql.parse\n\nfrom mycli.main import MyCli, Query\n\n_logger: logging.Logger = logging.getLogger(__name__)\n\n\ndef load_ipython_extension(ipython) -> None:\n    # This is called via the ipython command '%load_ext mycli.magic'.\n\n    # First, load the sql magic if it isn't already loaded.\n    if not ipython.find_line_magic(\"sql\"):\n        ipython.run_line_magic(\"load_ext\", \"sql\")\n\n    # Register our own magic.\n    ipython.register_magic_function(mycli_line_magic, \"line\", \"mycli\")\n\n\ndef mycli_line_magic(line: str):\n    _logger.debug(\"mycli magic called: %r\", line)\n    parsed: dict[str, Any] = sql.parse.parse(line, {})\n    # \"get\" was renamed to \"set\" in ipython-sql:\n    # https://github.com/catherinedevlin/ipython-sql/commit/f4283c65aaf68f961e84019e8b939e4a3c501d43\n    if hasattr(sql.connection.Connection, \"get\"):\n        conn = sql.connection.Connection.get(parsed[\"connection\"])\n    else:\n        try:\n            conn = sql.connection.Connection.set(parsed[\"connection\"])\n        # a new positional argument was added to Connection.set in version 0.4.0 of ipython-sql\n        except TypeError:\n            conn = sql.connection.Connection.set(parsed[\"connection\"], False)\n    try:\n        # A corresponding mycli object already exists\n        mycli: MyCli = conn._mycli\n        _logger.debug(\"Reusing existing mycli\")\n    except AttributeError:\n        mycli = MyCli()\n        u = conn.session.engine.url\n        _logger.debug(\"New mycli: %r\", str(u))\n\n        mycli.connect(host=u.host, port=u.port, passwd=u.password, database=u.database, user=u.username, init_command=None)\n        conn._mycli = mycli\n\n    # For convenience, print the connection alias\n    print(f'Connected: {conn.name}')\n\n    try:\n        mycli.run_cli()\n    except SystemExit:\n        pass\n\n    if not mycli.query_history:\n        return\n\n    q: Query = mycli.query_history[-1]\n    if q.mutating:\n        _logger.debug(\"Mutating query detected -- ignoring\")\n        return\n\n    if q.successful:\n        ipython = get_ipython()  # type: ignore # noqa: F821\n        return ipython.run_cell_magic(\"sql\", line, q.query)\n"
  },
  {
    "path": "mycli/main.py",
    "content": "from __future__ import annotations\n\nfrom collections import defaultdict, namedtuple\nfrom decimal import Decimal\nimport functools\nfrom io import TextIOWrapper\nimport logging\nimport os\nimport random\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport threading\nimport traceback\nfrom typing import IO, Any, Callable, Generator, Iterable, Literal\n\ntry:\n    from pwd import getpwuid\nexcept ImportError:\n    pass\nfrom datetime import datetime\nfrom importlib import resources\nimport itertools\nfrom random import choice\nfrom textwrap import dedent\nfrom time import sleep, time\nfrom urllib.parse import parse_qs, unquote, urlparse\nimport warnings\n\nfrom cli_helpers.tabular_output import TabularOutputFormatter, preprocessors\nfrom cli_helpers.tabular_output.output_formatter import MISSING_VALUE as DEFAULT_MISSING_VALUE\nfrom cli_helpers.utils import strip_ansi\nimport click\nfrom configobj import ConfigObj\nimport keyring\nfrom prompt_toolkit import print_formatted_text\nfrom prompt_toolkit.application.current import get_app\nfrom prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ThreadedAutoSuggest\nfrom prompt_toolkit.completion import Completion, DynamicCompleter\nfrom prompt_toolkit.document import Document\nfrom prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode\nfrom prompt_toolkit.filters import Condition, HasFocus, IsDone\nfrom prompt_toolkit.formatted_text import (\n    ANSI,\n    HTML,\n    AnyFormattedText,\n    FormattedText,\n    to_formatted_text,\n    to_plain_text,\n)\nfrom prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register\nfrom prompt_toolkit.key_binding.key_processor import KeyPressEvent\nfrom prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor\nfrom prompt_toolkit.lexers import PygmentsLexer\nfrom prompt_toolkit.output import ColorDepth\nfrom prompt_toolkit.shortcuts import CompleteStyle, PromptSession\nimport pymysql\nfrom pymysql.constants.CR import CR_SERVER_LOST\nfrom pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR\nfrom pymysql.cursors import Cursor\nimport sqlparse\n\nwith warnings.catch_warnings():\n    # for sqlglot v29.0.1\n    warnings.filterwarnings(\n        'ignore',\n        message=r'sqlglot\\[rs\\] is deprecated',\n        category=UserWarning,\n        module='sqlglot',\n    )\n    import sqlglot\n\nfrom mycli import __version__\nfrom mycli.clibuffer import cli_is_multiline\nfrom mycli.clistyle import style_factory_helpers, style_factory_toolkit\nfrom mycli.clitoolbar import create_toolbar_tokens_func\nfrom mycli.compat import WIN\nfrom mycli.completion_refresher import CompletionRefresher\nfrom mycli.config import get_mylogin_cnf_path, open_mylogin_cnf, read_config_files, str_to_bool, strip_matching_quotes, write_default_config\nfrom mycli.constants import (\n    DEFAULT_CHARSET,\n    DEFAULT_HOST,\n    DEFAULT_PORT,\n    HOME_URL,\n    ISSUES_URL,\n    REPO_URL,\n)\nfrom mycli.key_bindings import mycli_bindings\nfrom mycli.lexer import MyCliLexer\nfrom mycli.packages import special\nfrom mycli.packages.checkup import do_checkup\nfrom mycli.packages.filepaths import dir_path_exists, guess_socket_location\nfrom mycli.packages.hybrid_redirection import get_redirect_components, is_redirect_command\nfrom mycli.packages.parseutils import is_destructive, is_dropping_database, is_valid_connection_scheme\nfrom mycli.packages.prompt_utils import confirm, confirm_destructive_query\nfrom mycli.packages.special.favoritequeries import FavoriteQueries\nfrom mycli.packages.special.main import ArgType\nfrom mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count\nfrom mycli.packages.sqlresult import SQLResult\nfrom mycli.packages.string_utils import sanitize_terminal_title\nfrom mycli.packages.tabular_output import sql_format\nfrom mycli.packages.toolkit.history import FileHistoryWithTimestamp\nfrom mycli.sqlcompleter import SQLCompleter\nfrom mycli.sqlexecute import FIELD_TYPES, SQLExecute\n\ntry:\n    import paramiko\nexcept ImportError:\n    from mycli.packages.paramiko_stub import paramiko  # type: ignore[no-redef]\n\nsqlparse.engine.grouping.MAX_GROUPING_DEPTH = None  # type: ignore[assignment]\nsqlparse.engine.grouping.MAX_GROUPING_TOKENS = None  # type: ignore[assignment]\n\n# Query tuples are used for maintaining history\nQuery = namedtuple(\"Query\", [\"query\", \"successful\", \"mutating\"])\n\nSUPPORT_INFO = f\"Home: {HOME_URL}\\nBug tracker: {ISSUES_URL}\"\nDEFAULT_WIDTH = 80\nDEFAULT_HEIGHT = 25\nMIN_COMPLETION_TRIGGER = 1\nMAX_MULTILINE_BATCH_STATEMENT = 5000\nEMPTY_PASSWORD_FLAG_SENTINEL = -1\n\n\n@Condition\ndef complete_while_typing_filter() -> bool:\n    \"\"\"Whether enough characters have been typed to trigger completion.\n\n    Written in a verbose way, with a string slice, for efficiency.\"\"\"\n    if MIN_COMPLETION_TRIGGER <= 1:\n        return True\n    app = get_app()\n    text = app.current_buffer.text.lstrip()\n    text_len = len(text)\n    if text_len < MIN_COMPLETION_TRIGGER:\n        return False\n    last_word = text[-MIN_COMPLETION_TRIGGER:]\n    if len(last_word) == text_len:\n        return text_len >= MIN_COMPLETION_TRIGGER\n    if text[:6].lower() in ['source', r'\\.']:\n        # Different word characters for paths; see comment below.\n        # In fact, it might be nice if paths had a different threshold.\n        return not bool(re.search(r'[\\s!-,:-@\\[-^\\{\\}-]', last_word))\n    else:\n        # This is \"whitespace and all punctuation except underscore and backtick\"\n        # acting as word breaks, but it would be neat if we could complete differently\n        # when inside a backtick, accepting all legal characters towards the trigger\n        # limit.  We would have to parse the statement, or at least go back more\n        # characters, costing performance.  This still works within a backtick!  So\n        # long as there are three trailing non-punctuation characters.\n        return not bool(re.search(r'[\\s!-/:-@\\[-^\\{-~]', last_word))\n\n\nclass IntOrStringClickParamType(click.ParamType):\n    name = 'string'  # display as STRING in helpdoc\n\n    def convert(self, value, param, ctx):\n        if isinstance(value, int):\n            return value\n        elif isinstance(value, str):\n            return value\n        elif value is None:\n            return value\n        else:\n            self.fail('Not a valid password string', param, ctx)\n\n\nINT_OR_STRING_CLICK_TYPE = IntOrStringClickParamType()\n\n\nclass MyCli:\n    default_prompt = \"\\\\t \\\\u@\\\\h:\\\\d> \"\n    default_prompt_splitln = \"\\\\u@\\\\h\\\\n(\\\\t):\\\\d>\"\n    max_len_prompt = 45\n    defaults_suffix = None\n\n    # In order of being loaded. Files lower in list override earlier ones.\n    cnf_files: list[str | IO[str]] = [\n        \"/etc/my.cnf\",\n        \"/etc/mysql/my.cnf\",\n        \"/usr/local/etc/my.cnf\",\n        os.path.expanduser(\"~/.my.cnf\"),\n    ]\n\n    # check XDG_CONFIG_HOME exists and not an empty string\n    xdg_config_home = os.environ.get(\"XDG_CONFIG_HOME\", \"~/.config\")\n    system_config_files: list[str | IO[str]] = [\n        \"/etc/myclirc\",\n        os.path.join(os.path.expanduser(xdg_config_home), \"mycli\", \"myclirc\"),\n    ]\n\n    pwd_config_file = os.path.join(os.getcwd(), \".myclirc\")\n\n    def __init__(\n        self,\n        sqlexecute: SQLExecute | None = None,\n        prompt: str | None = None,\n        toolbar_format: str | None = None,\n        logfile: TextIOWrapper | Literal[False] | None = None,\n        defaults_suffix: str | None = None,\n        defaults_file: str | None = None,\n        login_path: str | None = None,\n        auto_vertical_output: bool = False,\n        show_warnings: bool = False,\n        warn: bool | None = None,\n        myclirc: str = \"~/.myclirc\",\n    ) -> None:\n        global MIN_COMPLETION_TRIGGER\n\n        self.sqlexecute = sqlexecute\n        self.logfile = logfile\n        self.defaults_suffix = defaults_suffix\n        self.login_path = login_path\n        self.toolbar_error_message: str | None = None\n        self.prompt_app: PromptSession | None = None\n        self._keepalive_counter = 0\n        self.keepalive_ticks: int | None = 0\n\n        # self.cnf_files is a class variable that stores the list of mysql\n        # config files to read in at launch.\n        # If defaults_file is specified then override the class variable with\n        # defaults_file.\n        if defaults_file:\n            self.cnf_files = [defaults_file]\n\n        # Load config.\n        config_files: list[str | IO[str]] = self.system_config_files + [myclirc] + [self.pwd_config_file]\n        c = self.config = read_config_files(config_files)\n        # this parallel config exists to\n        #  * compare with my.cnf\n        #  * support the --checkup feature\n        # todo: after removing my.cnf, create the parallel configs only when --checkup is set\n        self.config_without_package_defaults = read_config_files(config_files, ignore_package_defaults=True)\n        # this parallel config exists to compare with my.cnf support the --checkup feature\n        self.config_without_user_options = read_config_files(config_files, ignore_user_options=True)\n        self.multi_line = c[\"main\"].as_bool(\"multi_line\")\n        self.key_bindings = c[\"main\"][\"key_bindings\"]\n        self.emacs_ttimeoutlen = c['keys'].as_float('emacs_ttimeoutlen')\n        self.vi_ttimeoutlen = c['keys'].as_float('vi_ttimeoutlen')\n        special.set_timing_enabled(c[\"main\"].as_bool(\"timing\"))\n        special.set_show_favorite_query(c[\"main\"].as_bool(\"show_favorite_query\"))\n        self.beep_after_seconds = float(c[\"main\"][\"beep_after_seconds\"] or 0)\n        self.default_keepalive_ticks = c['connection'].as_int('default_keepalive_ticks')\n\n        FavoriteQueries.instance = FavoriteQueries.from_config(self.config)\n\n        self.dsn_alias: str | None = None\n        self.main_formatter = TabularOutputFormatter(format_name=c[\"main\"][\"table_format\"])\n        self.redirect_formatter = TabularOutputFormatter(format_name=c[\"main\"].get(\"redirect_format\", \"csv\"))\n        sql_format.register_new_formatter(self.main_formatter)\n        sql_format.register_new_formatter(self.redirect_formatter)\n        self.main_formatter.mycli = self\n        self.redirect_formatter.mycli = self\n        self.syntax_style = c[\"main\"][\"syntax_style\"]\n        self.less_chatty = c[\"main\"].as_bool(\"less_chatty\")\n        self.cli_style = c[\"colors\"]\n        self.toolkit_style = style_factory_toolkit(self.syntax_style, self.cli_style)\n        self.helpers_style = style_factory_helpers(self.syntax_style, self.cli_style)\n        self.helpers_warnings_style = style_factory_helpers(self.syntax_style, self.cli_style, warnings=True)\n        self.wider_completion_menu = c[\"main\"].as_bool(\"wider_completion_menu\")\n        c_dest_warning = c[\"main\"].as_bool(\"destructive_warning\")\n        self.destructive_warning = c_dest_warning if warn is None else warn\n        self.login_path_as_host = c[\"main\"].as_bool(\"login_path_as_host\")\n        self.post_redirect_command = c['main'].get('post_redirect_command')\n        self.null_string = c['main'].get('null_string')\n        self.numeric_alignment = c['main'].get('numeric_alignment', 'right')\n        self.binary_display = c['main'].get('binary_display')\n        if 'llm' in c and re.match(r'^\\d+$', c['llm'].get('prompt_field_truncate', '')):\n            self.llm_prompt_field_truncate = int(c['llm'].get('prompt_field_truncate'))\n        else:\n            self.llm_prompt_field_truncate = 0\n        if 'llm' in c and re.match(r'^\\d+$', c['llm'].get('prompt_section_truncate', '')):\n            self.llm_prompt_section_truncate = int(c['llm'].get('prompt_section_truncate'))\n        else:\n            self.llm_prompt_section_truncate = 0\n\n        # set ssl_mode if a valid option is provided in a config file, otherwise None\n        ssl_mode = c[\"main\"].get(\"ssl_mode\", None) or c[\"connection\"].get(\"default_ssl_mode\", None)\n        if ssl_mode not in (\"auto\", \"on\", \"off\", None):\n            self.echo(f\"Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.\", err=True, fg=\"red\")\n            self.ssl_mode = None\n        else:\n            self.ssl_mode = ssl_mode\n\n        # read from cli argument or user config file\n        self.auto_vertical_output = auto_vertical_output or c[\"main\"].as_bool(\"auto_vertical_output\")\n        self.show_warnings = show_warnings or c[\"main\"].as_bool(\"show_warnings\")\n\n        # Write user config if system config wasn't the last config loaded.\n        if c.filename not in self.system_config_files and not os.path.exists(myclirc):\n            write_default_config(myclirc)\n\n        # audit log\n        if self.logfile is None and \"audit_log\" in c[\"main\"]:\n            try:\n                self.logfile = open(os.path.expanduser(c[\"main\"][\"audit_log\"]), \"a\")\n            except (IOError, OSError):\n                self.echo(\"Error: Unable to open the audit log file. Your queries will not be logged.\", err=True, fg=\"red\")\n                self.logfile = False\n\n        self.completion_refresher = CompletionRefresher()\n\n        self.logger = logging.getLogger(__name__)\n        self.initialize_logging()\n\n        keyword_casing = c[\"main\"].get(\"keyword_casing\", \"auto\")\n\n        self.highlight_preview = c['search'].as_bool('highlight_preview')\n\n        self.query_history: list[Query] = []\n\n        # Initialize completer.\n        self.smart_completion = c[\"main\"].as_bool(\"smart_completion\")\n        self.completer = SQLCompleter(\n            self.smart_completion, supported_formats=self.main_formatter.supported_formats, keyword_casing=keyword_casing\n        )\n        self._completer_lock = threading.Lock()\n\n        self.min_completion_trigger = c[\"main\"].as_int(\"min_completion_trigger\")\n        MIN_COMPLETION_TRIGGER = self.min_completion_trigger\n        self.last_prompt_message = ANSI('')\n        self.last_custom_toolbar_message = ANSI('')\n\n        # Register custom special commands.\n        self.register_special_commands()\n\n        # Load .mylogin.cnf if it exists.\n        mylogin_cnf_path = get_mylogin_cnf_path()\n        if mylogin_cnf_path:\n            mylogin_cnf = open_mylogin_cnf(mylogin_cnf_path)\n            if mylogin_cnf_path and mylogin_cnf:\n                # .mylogin.cnf gets read last, even if defaults_file is specified.\n                self.cnf_files.append(mylogin_cnf)\n            elif mylogin_cnf_path and not mylogin_cnf:\n                # There was an error reading the login path file.\n                print(\"Error: Unable to read login path file.\")\n\n        self.my_cnf = read_config_files(self.cnf_files, list_values=False)\n        if not self.my_cnf.get('client'):\n            self.my_cnf['client'] = {}\n        if not self.my_cnf.get('mysqld'):\n            self.my_cnf['mysqld'] = {}\n        prompt_cnf = self.read_my_cnf(self.my_cnf, [\"prompt\"])[\"prompt\"]\n        self.prompt_format = prompt or prompt_cnf or c[\"main\"][\"prompt\"] or self.default_prompt\n        self.prompt_lines = 0\n        self.multiline_continuation_char = c[\"main\"][\"prompt_continuation\"]\n        self.toolbar_format = toolbar_format or c['main']['toolbar']\n        self.terminal_tab_title_format = c['main']['terminal_tab_title']\n        self.terminal_window_title_format = c['main']['terminal_window_title']\n        self.multiplex_window_title_format = c['main']['multiplex_window_title']\n        self.multiplex_pane_title_format = c['main']['multiplex_pane_title']\n        self.prompt_app = None\n        self.destructive_keywords = [\n            keyword for keyword in c[\"main\"].get(\"destructive_keywords\", \"DROP SHUTDOWN DELETE TRUNCATE ALTER UPDATE\").split(' ') if keyword\n        ]\n        special.set_destructive_keywords(self.destructive_keywords)\n\n    def close(self) -> None:\n        if self.sqlexecute is not None:\n            self.sqlexecute.close()\n\n    def register_special_commands(self) -> None:\n        special.register_special_command(self.change_db, \"use\", \"use <database>\", \"Change to a new database.\", aliases=[\"\\\\u\"])\n        special.register_special_command(\n            self.manual_reconnect,\n            \"connect\",\n            \"connect [database]\",\n            \"Reconnect to the server, optionally switching databases.\",\n            aliases=[\"\\\\r\"],\n            case_sensitive=True,\n        )\n        special.register_special_command(\n            self.refresh_completions, \"rehash\", \"rehash\", \"Refresh auto-completions.\", arg_type=ArgType.NO_QUERY, aliases=[\"\\\\#\"]\n        )\n        special.register_special_command(\n            self.change_table_format,\n            \"tableformat\",\n            \"tableformat <format>\",\n            \"Change the table format used to output interactive results.\",\n            aliases=[\"\\\\T\"],\n            case_sensitive=True,\n        )\n        special.register_special_command(\n            self.change_redirect_format,\n            \"redirectformat\",\n            \"redirectformat <format>\",\n            \"Change the table format used to output redirected results.\",\n            aliases=[\"\\\\Tr\"],\n            case_sensitive=True,\n        )\n        special.register_special_command(\n            self.disable_show_warnings,\n            \"nowarnings\",\n            \"nowarnings\",\n            \"Disable automatic warnings display.\",\n            aliases=[\"\\\\w\"],\n            case_sensitive=True,\n        )\n        special.register_special_command(\n            self.enable_show_warnings,\n            \"warnings\",\n            \"warnings\",\n            \"Enable automatic warnings display.\",\n            aliases=[\"\\\\W\"],\n            case_sensitive=True,\n        )\n        special.register_special_command(\n            self.execute_from_file, \"source\", \"source <filename>\", \"Execute queries from a file.\", aliases=[\"\\\\.\"]\n        )\n        special.register_special_command(\n            self.change_prompt_format, \"prompt\", \"prompt <string>\", \"Change prompt format.\", aliases=[\"\\\\R\"], case_sensitive=True\n        )\n\n    def manual_reconnect(self, arg: str = \"\", **_) -> Generator[SQLResult, None, None]:\n        \"\"\"\n        Interactive method to use for the \\r command, so that the utility method\n        may be cleanly used elsewhere.\n        \"\"\"\n        if not self.reconnect(database=arg):\n            yield SQLResult(status=\"Not connected\")\n        elif not arg or arg == '``':\n            yield SQLResult()\n        else:\n            yield self.change_db(arg).send(None)\n\n    def enable_show_warnings(self, **_) -> Generator[SQLResult, None, None]:\n        self.show_warnings = True\n        msg = \"Show warnings enabled.\"\n        yield SQLResult(status=msg)\n\n    def disable_show_warnings(self, **_) -> Generator[SQLResult, None, None]:\n        self.show_warnings = False\n        msg = \"Show warnings disabled.\"\n        yield SQLResult(status=msg)\n\n    def change_table_format(self, arg: str, **_) -> Generator[SQLResult, None, None]:\n        try:\n            self.main_formatter.format_name = arg\n            yield SQLResult(status=f\"Changed table format to {arg}\")\n        except ValueError:\n            msg = f\"Table format {arg} not recognized. Allowed formats:\"\n            for table_type in self.main_formatter.supported_formats:\n                msg += f\"\\n\\t{table_type}\"\n            yield SQLResult(status=msg)\n\n    def change_redirect_format(self, arg: str, **_) -> Generator[SQLResult, None, None]:\n        try:\n            self.redirect_formatter.format_name = arg\n            yield SQLResult(status=f\"Changed redirect format to {arg}\")\n        except ValueError:\n            msg = f\"Redirect format {arg} not recognized. Allowed formats:\"\n            for table_type in self.redirect_formatter.supported_formats:\n                msg += f\"\\n\\t{table_type}\"\n            yield SQLResult(status=msg)\n\n    def change_db(self, arg: str, **_) -> Generator[SQLResult, None, None]:\n        if arg.startswith(\"`\") and arg.endswith(\"`\"):\n            arg = re.sub(r\"^`(.*)`$\", r\"\\1\", arg)\n            arg = re.sub(r\"``\", r\"`\", arg)\n\n        if not arg:\n            click.secho(\"No database selected\", err=True, fg=\"red\")\n            return\n\n        assert isinstance(self.sqlexecute, SQLExecute)\n\n        if self.sqlexecute.dbname == arg:\n            msg = f'You are already connected to database \"{self.sqlexecute.dbname}\" as user \"{self.sqlexecute.user}\"'\n        else:\n            self.sqlexecute.change_db(arg)\n            msg = f'You are now connected to database \"{self.sqlexecute.dbname}\" as user \"{self.sqlexecute.user}\"'\n\n        self.set_all_external_titles()\n\n        yield SQLResult(status=msg)\n\n    def execute_from_file(self, arg: str, **_) -> Iterable[SQLResult]:\n        if not arg:\n            message = \"Missing required argument: filename.\"\n            return [SQLResult(status=message)]\n        try:\n            with open(os.path.expanduser(arg)) as f:\n                query = f.read()\n        except IOError as e:\n            return [SQLResult(status=str(e))]\n\n        if self.destructive_warning and confirm_destructive_query(self.destructive_keywords, query) is False:\n            message = \"Wise choice. Command execution stopped.\"\n            return [SQLResult(status=message)]\n\n        assert isinstance(self.sqlexecute, SQLExecute)\n        return self.sqlexecute.run(query)\n\n    def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:\n        \"\"\"\n        Change the prompt format.\n        \"\"\"\n        if not arg:\n            message = \"Missing required argument, format.\"\n            return [SQLResult(status=message)]\n\n        self.prompt_format = arg\n        return [SQLResult(status=f\"Changed prompt format to {arg}\")]\n\n    def initialize_logging(self) -> None:\n        log_file = os.path.expanduser(self.config[\"main\"][\"log_file\"])\n        log_level = self.config[\"main\"][\"log_level\"]\n\n        level_map = {\n            \"CRITICAL\": logging.CRITICAL,\n            \"ERROR\": logging.ERROR,\n            \"WARNING\": logging.WARNING,\n            \"INFO\": logging.INFO,\n            \"DEBUG\": logging.DEBUG,\n        }\n\n        # Disable logging if value is NONE by switching to a no-op handler\n        # Set log level to a high value so it doesn't even waste cycles getting called.\n        if log_level.upper() == \"NONE\":\n            handler: logging.Handler = logging.NullHandler()\n            log_level = \"CRITICAL\"\n        elif dir_path_exists(log_file):\n            handler = logging.FileHandler(log_file)\n        else:\n            self.echo(f'Error: Unable to open the log file \"{log_file}\".', err=True, fg=\"red\")\n            return\n\n        formatter = logging.Formatter(\"%(asctime)s (%(process)d/%(threadName)s) %(name)s %(levelname)s - %(message)s\")\n\n        handler.setFormatter(formatter)\n\n        root_logger = logging.getLogger(\"mycli\")\n        root_logger.addHandler(handler)\n        root_logger.setLevel(level_map[log_level.upper()])\n\n        logging.captureWarnings(True)\n\n        root_logger.debug(\"Initializing mycli logging.\")\n        root_logger.debug(\"Log file %r.\", log_file)\n\n    def read_my_cnf(self, cnf: ConfigObj, keys: list[str]) -> dict[str, Any]:\n        \"\"\"\n        Retrieves some keys from a configuration, applies transformations, returns a new configuration.\n        :param cnf: configuration to read\n        :param keys: list of keys to retrieve\n        :returns: tuple, with None for missing keys.\n        \"\"\"\n\n        sections = [\"client\", \"mysqld\"]\n        key_transformations = {\n            \"mysqld\": {\n                \"socket\": \"default_socket\",\n                \"port\": \"default_port\",\n                \"user\": \"default_user\",\n            },\n        }\n\n        if self.login_path and self.login_path != \"client\":\n            sections.append(self.login_path)\n\n        if self.defaults_suffix:\n            sections.extend([sect + self.defaults_suffix for sect in sections])\n\n        configuration: dict[str, Any] = defaultdict(lambda: None)\n        for key in keys:\n            for section in cnf:\n                if section not in sections or key not in cnf[section]:\n                    continue\n                new_key = key_transformations.get(section, {}).get(key) or key\n                configuration[new_key] = strip_matching_quotes(cnf[section][key])\n\n        return configuration\n\n    def merge_ssl_with_cnf(self, ssl: dict[str, Any], cnf: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Merge SSL configuration dict with cnf dict\"\"\"\n\n        merged = {}\n        merged.update(ssl)\n        prefix = \"ssl-\"\n        for k, v in cnf.items():\n            # skip unrelated options\n            if not k.startswith(prefix):\n                continue\n            if v is None:\n                continue\n            # special case because PyMySQL argument is significantly different\n            # from commandline\n            if k == \"ssl-verify-server-cert\":\n                merged[\"check_hostname\"] = str_to_bool(v)\n            else:\n                # use argument name just strip \"ssl-\" prefix\n                arg = k[len(prefix) :]\n                merged[arg] = v\n\n        return merged\n\n    def connect(\n        self,\n        database: str | None = \"\",\n        user: str | None = \"\",\n        passwd: str | int | None = None,\n        host: str | None = \"\",\n        port: str | int | None = \"\",\n        socket: str | None = \"\",\n        character_set: str | None = \"\",\n        local_infile: bool = False,\n        ssl: dict[str, Any] | None = None,\n        ssh_user: str | None = \"\",\n        ssh_host: str | None = \"\",\n        ssh_port: int = 22,\n        ssh_password: str | None = \"\",\n        ssh_key_filename: str | None = \"\",\n        init_command: str | None = \"\",\n        unbuffered: bool | None = None,\n        use_keyring: bool | None = None,\n        reset_keyring: bool | None = None,\n        keepalive_ticks: int | None = None,\n    ) -> None:\n        cnf = {\n            \"database\": None,\n            \"user\": None,\n            \"password\": None,\n            \"host\": None,\n            \"port\": None,\n            \"socket\": None,\n            \"default_socket\": None,\n            \"default-character-set\": None,\n            \"local-infile\": None,\n            \"loose-local-infile\": None,\n            \"ssl-ca\": None,\n            \"ssl-cert\": None,\n            \"ssl-key\": None,\n            \"ssl-cipher\": None,\n            \"ssl-verify-server-cert\": None,\n        }\n\n        cnf = self.read_my_cnf(self.my_cnf, list(cnf.keys()))\n\n        # Fall back to config values only if user did not specify a value.\n        database = database or cnf[\"database\"]\n        user = user or cnf[\"user\"] or os.getenv(\"USER\")\n        host = host or cnf[\"host\"]\n        port = port or cnf[\"port\"]\n        ssl_config: dict[str, Any] = ssl or {}\n        user_connection_config = self.config_without_package_defaults.get('connection', {})\n        self.keepalive_ticks = keepalive_ticks\n\n        int_port = port and int(port)\n        if not int_port:\n            int_port = DEFAULT_PORT\n            if not host or host == DEFAULT_HOST:\n                socket = (\n                    socket\n                    or user_connection_config.get(\"default_socket\")\n                    or cnf[\"socket\"]\n                    or cnf[\"default_socket\"]\n                    or guess_socket_location()\n                )\n\n        passwd = passwd if isinstance(passwd, (str, int)) else cnf[\"password\"]\n\n        # default_character_set doesn't check in self.config_without_package_defaults, because the\n        # option already existed before the my.cnf deprecation.  For the same reason,\n        # default_character_set can be in [connection] or [main].\n        if not character_set:\n            if 'default_character_set' in self.config['connection']:\n                character_set = self.config['connection']['default_character_set']\n            elif 'default_character_set' in self.config['main']:\n                character_set = self.config['main']['default_character_set']\n            elif 'default_character_set' in cnf:\n                character_set = cnf['default_character_set']\n            elif 'default-character-set' in cnf:\n                character_set = cnf['default-character-set']\n        if not character_set:\n            character_set = DEFAULT_CHARSET\n\n        # Favor whichever local_infile option is set.\n        use_local_infile = False\n        for local_infile_option in (\n            local_infile,\n            user_connection_config.get('default_local_infile'),\n            cnf['local_infile'],\n            cnf['local-infile'],\n            cnf['loose_local_infile'],\n            cnf['loose-local-infile'],\n            False,\n        ):\n            try:\n                use_local_infile = str_to_bool(local_infile_option or '')\n                break\n            except (TypeError, ValueError):\n                pass\n\n        # temporary my.cnf override mappings\n        if 'default_ssl_ca' in user_connection_config:\n            cnf['ssl-ca'] = user_connection_config.get('default_ssl_ca') or None\n        if 'default_ssl_cert' in user_connection_config:\n            cnf['ssl-cert'] = user_connection_config.get('default_ssl_cert') or None\n        if 'default_ssl_key' in user_connection_config:\n            cnf['ssl-key'] = user_connection_config.get('default_ssl_key') or None\n        if 'default_ssl_cipher' in user_connection_config:\n            cnf['ssl-cipher'] = user_connection_config.get('default_ssl_cipher') or None\n        if 'default_ssl_verify_server_cert' in user_connection_config:\n            cnf['ssl-verify-server-cert'] = user_connection_config.get('default_ssl_verify_server_cert') or None\n\n        # todo: rewrite the merge method using self.config['connection'] instead of cnf, after removing my.cnf support\n        ssl_config_or_none: dict[str, Any] | None = self.merge_ssl_with_cnf(ssl_config, cnf)\n\n        # default_ssl_ca_path is not represented in my.cnf\n        if 'default_ssl_ca_path' in self.config['connection'] and (not ssl_config_or_none or not ssl_config_or_none.get('capath')):\n            if ssl_config_or_none is None:\n                ssl_config_or_none = {}\n            ssl_config_or_none['capath'] = self.config['connection']['default_ssl_ca_path'] or False\n\n        # prune lone check_hostname=False\n        if not any(v for v in ssl_config.values()):\n            ssl_config_or_none = None\n\n        # password hierarchy\n        # 1. -p / --pass/--password CLI options\n        # 2. --password-file CLI option\n        # 3. envvar (MYSQL_PWD)\n        # 4. DSN (mysql://user:password)\n        # 5. cnf (.my.cnf / etc)\n        # 6. keyring\n\n        keyring_identifier = f'{user}@{host}:{\"\" if socket else int_port}:{socket or \"\"}'\n        keyring_domain = 'mycli.net'\n        keyring_retrieved_cleanly = False\n\n        if passwd is None and use_keyring and not reset_keyring:\n            passwd = keyring.get_password(keyring_domain, keyring_identifier)\n            if passwd is not None:\n                keyring_retrieved_cleanly = True\n\n        # prompt for password if requested by user\n        if passwd == EMPTY_PASSWORD_FLAG_SENTINEL:\n            passwd = click.prompt(f\"Enter password for {user}\", hide_input=True, show_default=False, default='', type=str, err=True)\n            keyring_retrieved_cleanly = False\n\n        # should not fail, but will help the typechecker\n        assert not isinstance(passwd, int)\n\n        connection_info: dict[Any, Any] = {\n            \"database\": database,\n            \"user\": user,\n            \"password\": passwd,\n            \"host\": host,\n            \"port\": int_port,\n            \"socket\": socket,\n            \"character_set\": character_set,\n            \"local_infile\": use_local_infile,\n            \"ssl\": ssl_config_or_none,\n            \"ssh_user\": ssh_user,\n            \"ssh_host\": ssh_host,\n            \"ssh_port\": int(ssh_port) if ssh_port else None,\n            \"ssh_password\": ssh_password,\n            \"ssh_key_filename\": ssh_key_filename,\n            \"init_command\": init_command,\n            \"unbuffered\": unbuffered,\n        }\n\n        def _update_keyring(password: str | None, keyring_retrieved_cleanly: bool):\n            if not password:\n                return\n            if reset_keyring or (use_keyring and not keyring_retrieved_cleanly):\n                try:\n                    saved_pw = keyring.get_password(keyring_domain, keyring_identifier)\n                    if password != saved_pw or reset_keyring:\n                        keyring.set_password(keyring_domain, keyring_identifier, password)\n                        click.secho(f'Password saved to the system keyring at {keyring_domain}/{keyring_identifier}', err=True)\n                except Exception as e:\n                    click.secho(f'Password not saved to the system keyring: {e}', err=True, fg='red')\n\n        def _connect(\n            retry_ssl: bool = False,\n            retry_password: bool = False,\n            keyring_save_eligible: bool = True,\n            keyring_retrieved_cleanly: bool = False,\n        ) -> None:\n            try:\n                if keyring_save_eligible:\n                    _update_keyring(connection_info[\"password\"], keyring_retrieved_cleanly=keyring_retrieved_cleanly)\n                self.sqlexecute = SQLExecute(**connection_info)\n            except pymysql.OperationalError as e1:\n                if e1.args[0] == HANDSHAKE_ERROR and ssl is not None and ssl.get(\"mode\", None) == \"auto\":\n                    # if we already tried and failed to connect without SSL, raise the error\n                    if retry_ssl:\n                        raise e1\n                    # disable SSL and try to connect again\n                    connection_info[\"ssl\"] = None\n                    _connect(\n                        retry_ssl=True, keyring_retrieved_cleanly=keyring_retrieved_cleanly, keyring_save_eligible=keyring_save_eligible\n                    )\n                elif e1.args[0] == ACCESS_DENIED_ERROR and connection_info[\"password\"] is None:\n                    # if we already tried and failed to connect with a new password, raise the error\n                    if retry_password:\n                        raise e1\n                    # ask the user for a new password and try to connect again\n                    new_password = click.prompt(\n                        f\"Enter password for {user}\", hide_input=True, show_default=False, default='', type=str, err=True\n                    )\n                    connection_info[\"password\"] = new_password\n                    keyring_retrieved_cleanly = False\n                    _connect(\n                        retry_password=True,\n                        keyring_retrieved_cleanly=keyring_retrieved_cleanly,\n                        keyring_save_eligible=keyring_save_eligible,\n                    )\n                elif e1.args[0] == CR_SERVER_LOST:\n                    self.echo(\n                        (\n                            \"Connection to server lost. If this error persists, it may be a mismatch between the server and \"\n                            \"client SSL configuration. To troubleshoot the issue, try --ssl-mode=off or --ssl-mode=on.\"\n                        ),\n                        err=True,\n                        fg='red',\n                    )\n                    raise e1\n                else:\n                    raise e1\n\n        try:\n            if not WIN and socket:\n                try:\n                    socket_owner = getpwuid(os.stat(socket).st_uid).pw_name\n                except KeyError:\n                    socket_owner = '<unknown>'\n                self.echo(f\"Connecting to socket {socket}, owned by user {socket_owner}\", err=True)\n                try:\n                    _connect(keyring_retrieved_cleanly=keyring_retrieved_cleanly)\n                except pymysql.OperationalError as e:\n                    # These are \"Can't open socket\" and 2x \"Can't connect\"\n                    if [code for code in (2001, 2002, 2003) if code == e.args[0]]:\n                        self.logger.debug(\"Database connection failed: %r.\", e)\n                        self.logger.error(\"traceback: %r\", traceback.format_exc())\n                        self.logger.debug(\"Retrying over TCP/IP\")\n                        self.echo(f\"Failed to connect to local MySQL server through socket '{socket}':\")\n                        self.echo(str(e), err=True)\n                        self.echo(\"Retrying over TCP/IP\", err=True)\n\n                        # Else fall back to TCP/IP localhost\n                        socket = \"\"\n                        host = DEFAULT_HOST\n                        port = DEFAULT_PORT\n                        # todo should reload the keyring identifier here instead of invalidating\n                        _connect(keyring_save_eligible=False)\n                    else:\n                        raise e\n            else:\n                host = host or DEFAULT_HOST\n                port = port or DEFAULT_PORT\n                # could try loading the keyring again here instead of assuming nothing important changed\n\n                # Bad ports give particularly daft error messages\n                try:\n                    port = int(port)\n                except ValueError:\n                    self.echo(f\"Error: Invalid port number: '{port}'.\", err=True, fg=\"red\")\n                    sys.exit(1)\n\n                _connect(keyring_retrieved_cleanly=keyring_retrieved_cleanly)\n        except Exception as e:  # Connecting to a database could fail.\n            self.logger.debug(\"Database connection failed: %r.\", e)\n            self.logger.error(\"traceback: %r\", traceback.format_exc())\n            self.echo(str(e), err=True, fg=\"red\")\n            sys.exit(1)\n\n    def handle_editor_command(\n        self,\n        text: str,\n        inputhook: Callable | None,\n        loaded_message_fn: Callable,\n    ) -> str:\n        r\"\"\"Editor command is any query that is prefixed or suffixed by a '\\e'.\n        The reason for a while loop is because a user might edit a query\n        multiple times. For eg:\n\n        \"select * from \\e\"<enter> to edit it in vim, then come\n        back to the prompt with the edited query \"select * from\n        blah where q = 'abc'\\e\" to edit it again.\n        :param text: Document\n        :return: Document\n\n        \"\"\"\n\n        while special.editor_command(text):\n            filename = special.get_filename(text)\n            query = special.get_editor_query(text) or self.get_last_query()\n            sql, message = special.open_external_editor(filename=filename, sql=query)\n            if message:\n                # Something went wrong. Raise an exception and bail.\n                raise RuntimeError(message)\n            while True:\n                try:\n                    assert isinstance(self.prompt_app, PromptSession)\n                    text = self.prompt_app.prompt(\n                        default=sql,\n                        inputhook=inputhook,\n                        message=loaded_message_fn,\n                    )\n                    break\n                except KeyboardInterrupt:\n                    sql = \"\"\n\n            continue\n        return text\n\n    def handle_clip_command(self, text: str) -> bool:\n        r\"\"\"A clip command is any query that is prefixed or suffixed by a\n        '\\clip'.\n\n        :param text: Document\n        :return: Boolean\n\n        \"\"\"\n\n        if special.clip_command(text):\n            query = special.get_clip_query(text) or self.get_last_query()\n            message = special.copy_query_to_clipboard(sql=query)\n            if message:\n                raise RuntimeError(message)\n            return True\n        return False\n\n    def handle_prettify_binding(self, text: str) -> str:\n        if not text:\n            return ''\n        try:\n            statements = sqlglot.parse(text, read='mysql')\n        except Exception:\n            statements = []\n        if len(statements) == 1 and statements[0]:\n            parse_succeeded = True\n            pretty_text = statements[0].sql(pretty=True, pad=4, dialect='mysql')\n        else:\n            parse_succeeded = False\n            pretty_text = text.rstrip(';')\n            self.toolbar_error_message = 'Prettify failed to parse single statement'\n        if pretty_text and parse_succeeded:\n            pretty_text = pretty_text + ';'\n        return pretty_text\n\n    def handle_unprettify_binding(self, text: str) -> str:\n        if not text:\n            return ''\n        try:\n            statements = sqlglot.parse(text, read='mysql')\n        except Exception:\n            statements = []\n        if len(statements) == 1 and statements[0]:\n            parse_succeeded = True\n            unpretty_text = statements[0].sql(pretty=False, dialect='mysql')\n        else:\n            parse_succeeded = False\n            unpretty_text = text.rstrip(';')\n            self.toolbar_error_message = 'Unprettify failed to parse single statement'\n        if unpretty_text and parse_succeeded:\n            unpretty_text = unpretty_text + ';'\n        return unpretty_text\n\n    def output_timing(self, timing: str, is_warnings_style: bool = False) -> None:\n        self.log_output(timing)\n        add_style = 'class:warnings.timing' if is_warnings_style else 'class:output.timing'\n        formatted_timing = FormattedText([('', timing)])\n        styled_timing = to_formatted_text(formatted_timing, style=add_style)\n        print_formatted_text(styled_timing, style=self.toolkit_style)\n\n    def run_cli(self) -> None:\n        iterations = 0\n        sqlexecute = self.sqlexecute\n        assert isinstance(sqlexecute, SQLExecute)\n        logger = self.logger\n        self.configure_pager()\n\n        if self.smart_completion:\n            self.refresh_completions()\n\n        history_file = os.path.expanduser(os.environ.get(\"MYCLI_HISTFILE\", self.config.get(\"history_file\", \"~/.mycli-history\")))\n        if dir_path_exists(history_file):\n            history = FileHistoryWithTimestamp(history_file)\n        else:\n            history = None\n            self.echo(\n                f'Error: Unable to open the history file \"{history_file}\". Your query history will not be saved.',\n                err=True,\n                fg=\"red\",\n            )\n\n        key_bindings = mycli_bindings(self)\n\n        if not self.less_chatty:\n            print(sqlexecute.server_info)\n            print(\"mycli\", __version__)\n            print(SUPPORT_INFO)\n            if random.random() <= 0.5:\n                print(\"Thanks to the contributor —\", thanks_picker())\n            else:\n                print(\"Tip —\", tips_picker())\n\n        def get_prompt_message(app) -> ANSI:\n            if app.current_buffer.text:\n                return self.last_prompt_message\n            prompt = self.get_prompt(self.prompt_format, app.render_counter)\n            if self.prompt_format == self.default_prompt and len(prompt) > self.max_len_prompt:\n                prompt = self.get_prompt(self.default_prompt_splitln, app.render_counter)\n                self.prompt_lines = prompt.count('\\n') + 1\n            prompt = prompt.replace(\"\\\\x1b\", \"\\x1b\")\n            if not self.prompt_lines:\n                self.prompt_lines = prompt.count('\\n') + 1\n            self.last_prompt_message = ANSI(prompt)\n            return self.last_prompt_message\n\n        def get_continuation(width: int, _two: int, _three: int) -> AnyFormattedText:\n            if self.multiline_continuation_char == \"\":\n                continuation = \"\"\n            elif self.multiline_continuation_char:\n                left_padding = width - len(self.multiline_continuation_char)\n                continuation = \" \" * max((left_padding - 1), 0) + self.multiline_continuation_char + \" \"\n            else:\n                continuation = \" \"\n            return [(\"class:continuation\", continuation)]\n\n        def show_initial_toolbar_help() -> bool:\n            return iterations == 0\n\n        # Keep track of whether or not the query is mutating. In case\n        # of a multi-statement query, the overall query is considered\n        # mutating if any one of the component statements is mutating\n        mutating = False\n\n        def output_res(results: Generator[SQLResult], start: float) -> None:\n            nonlocal mutating\n            result_count = watch_count = 0\n            for result in results:\n                logger.debug(\"preamble: %r\", result.preamble)\n                logger.debug(\"header: %r\", result.header)\n                logger.debug(\"rows: %r\", result.rows)\n                logger.debug(\"status: %r\", result.status)\n                logger.debug(\"command: %r\", result.command)\n                threshold = 1000\n                # If this is a watch query, offset the start time on the 2nd+ iteration\n                # to account for the sleep duration\n                if result.command is not None and result.command[\"name\"] == \"watch\":\n                    if watch_count > 0:\n                        try:\n                            watch_seconds = float(result.command[\"seconds\"])\n                            start += watch_seconds\n                        except ValueError as e:\n                            self.echo(f\"Invalid watch sleep time provided ({e}).\", err=True, fg=\"red\")\n                            sys.exit(1)\n                    else:\n                        watch_count += 1\n                if is_select(result.status_plain) and isinstance(result.rows, Cursor) and result.rows.rowcount > threshold:\n                    self.echo(\n                        f\"The result set has more than {threshold} rows.\",\n                        fg=\"red\",\n                    )\n                    if not confirm(\"Do you want to continue?\"):\n                        self.echo(\"Aborted!\", err=True, fg=\"red\")\n                        break\n\n                if self.auto_vertical_output:\n                    if self.prompt_app is not None:\n                        max_width = self.prompt_app.output.get_size().columns\n                    else:\n                        max_width = DEFAULT_WIDTH\n                else:\n                    max_width = None\n\n                formatted = self.format_sqlresult(\n                    result,\n                    is_expanded=special.is_expanded_output(),\n                    is_redirected=special.is_redirected(),\n                    null_string=self.null_string,\n                    numeric_alignment=self.numeric_alignment,\n                    binary_display=self.binary_display,\n                    max_width=max_width,\n                )\n\n                t = time() - start\n                try:\n                    if result_count > 0:\n                        self.echo(\"\")\n                    try:\n                        self.output(formatted, result)\n                    except KeyboardInterrupt:\n                        pass\n                    if self.beep_after_seconds > 0 and t >= self.beep_after_seconds:\n                        assert self.prompt_app is not None\n                        self.prompt_app.output.bell()\n                    if special.is_timing_enabled():\n                        self.output_timing(f\"Time: {t:0.03f}s\")\n                except KeyboardInterrupt:\n                    pass\n\n                start = time()\n                result_count += 1\n                mutating = mutating or is_mutating(result.status_plain)\n\n                # get and display warnings if enabled\n                if self.show_warnings and isinstance(result.rows, Cursor) and result.rows.warning_count > 0:\n                    warnings = sqlexecute.run(\"SHOW WARNINGS\")\n                    t = time() - start\n                    saw_warning = False\n                    for warning in warnings:\n                        saw_warning = True\n                        formatted = self.format_sqlresult(\n                            warning,\n                            is_expanded=special.is_expanded_output(),\n                            is_redirected=special.is_redirected(),\n                            null_string=self.null_string,\n                            numeric_alignment=self.numeric_alignment,\n                            binary_display=self.binary_display,\n                            max_width=max_width,\n                            is_warnings_style=True,\n                        )\n                        self.echo(\"\")\n                        self.output(formatted, warning, is_warnings_style=True)\n\n                    if saw_warning and special.is_timing_enabled():\n                        self.output_timing(f\"Time: {t:0.03f}s\", is_warnings_style=True)\n\n        def keepalive_hook(_context):\n            \"\"\"\n            prompt_toolkit shares the event loop with this hook, which seems\n            to get called a bit faster than once/second on one machine.\n\n            It would be nice to reset the counter whenever user input is made,\n            but was not clear how to do that with context.input_is_ready().\n\n            Example at https://github.com/prompt-toolkit/python-prompt-toolkit/blob/main/examples/prompts/inputhook.py\n            \"\"\"\n            if self.keepalive_ticks is None:\n                return\n            if self.keepalive_ticks < 1:\n                return\n            self._keepalive_counter += 1\n            if self._keepalive_counter > self.keepalive_ticks:\n                self._keepalive_counter = 0\n                self.logger.debug('keepalive ping')\n                try:\n                    assert self.sqlexecute is not None\n                    assert self.sqlexecute.conn is not None\n                    self.sqlexecute.conn.ping(reconnect=False)\n                except Exception as e:\n                    self.logger.debug('keepalive ping error %r', e)\n\n        def one_iteration(text: str | None = None) -> None:\n            inputhook = keepalive_hook if self.keepalive_ticks and self.keepalive_ticks >= 1 else None\n            if text is None:\n                try:\n                    assert self.prompt_app is not None\n                    loaded_message_fn = functools.partial(get_prompt_message, self.prompt_app.app)\n                    text = self.prompt_app.prompt(\n                        inputhook=inputhook,\n                        message=loaded_message_fn,\n                    )\n                except KeyboardInterrupt:\n                    return\n\n                special.set_expanded_output(False)\n                special.set_forced_horizontal_output(False)\n\n                try:\n                    text = self.handle_editor_command(\n                        text,\n                        inputhook,\n                        loaded_message_fn,\n                    )\n                except RuntimeError as e:\n                    logger.error(\"sql: %r, error: %r\", text, e)\n                    logger.error(\"traceback: %r\", traceback.format_exc())\n                    self.echo(str(e), err=True, fg=\"red\")\n                    return\n\n                try:\n                    if self.handle_clip_command(text):\n                        return\n                except RuntimeError as e:\n                    logger.error(\"sql: %r, error: %r\", text, e)\n                    logger.error(\"traceback: %r\", traceback.format_exc())\n                    self.echo(str(e), err=True, fg=\"red\")\n                    return\n                # LLM command support\n                while special.is_llm_command(text):\n                    start = time()\n                    try:\n                        assert isinstance(self.sqlexecute, SQLExecute)\n                        assert sqlexecute.conn is not None\n                        cur = sqlexecute.conn.cursor()\n                        context, sql, duration = special.handle_llm(\n                            text,\n                            cur,\n                            sqlexecute.dbname or '',\n                            self.llm_prompt_field_truncate,\n                            self.llm_prompt_section_truncate,\n                        )\n                        if context:\n                            click.echo(\"LLM Response:\")\n                            click.echo(context)\n                            click.echo(\"---\")\n                        if special.is_timing_enabled():\n                            self.output_timing(f\"Time: {duration:.2f} seconds\")\n                        text = self.prompt_app.prompt(\n                            default=sql or '',\n                            inputhook=inputhook,\n                            message=loaded_message_fn,\n                        )\n                    except KeyboardInterrupt:\n                        return\n                    except special.FinishIteration as e:\n                        if e.results:\n                            return output_res(e.results, start)\n                        else:\n                            return None\n                    except RuntimeError as e:\n                        logger.error(\"sql: %r, error: %r\", text, e)\n                        logger.error(\"traceback: %r\", traceback.format_exc())\n                        self.echo(str(e), err=True, fg=\"red\")\n                        return\n\n            text = text.strip()\n\n            if not text:\n                return\n\n            if is_redirect_command(text):\n                sql_part, command_part, file_operator_part, file_part = get_redirect_components(text)\n                text = sql_part or ''\n                try:\n                    special.set_redirect(command_part, file_operator_part, file_part)\n                except (FileNotFoundError, OSError, RuntimeError) as e:\n                    logger.error(\"sql: %r, error: %r\", text, e)\n                    logger.error(\"traceback: %r\", traceback.format_exc())\n                    self.echo(str(e), err=True, fg=\"red\")\n                    return\n\n            if self.destructive_warning:\n                destroy = confirm_destructive_query(self.destructive_keywords, text)\n                if destroy is None:\n                    pass  # Query was not destructive. Nothing to do here.\n                elif destroy is True:\n                    self.echo(\"Your call!\")\n                else:\n                    self.echo(\"Wise choice!\")\n                    return\n            else:\n                destroy = True\n\n            try:\n                logger.debug(\"sql: %r\", text)\n\n                special.write_tee(self.last_prompt_message, nl=False)\n                special.write_tee(text)\n                self.log_query(text)\n\n                successful = False\n                start = time()\n                res = sqlexecute.run(text)\n                self.main_formatter.query = text\n                self.redirect_formatter.query = text\n                successful = True\n                output_res(res, start)\n                special.unset_once_if_written(self.post_redirect_command)\n                special.flush_pipe_once_if_written(self.post_redirect_command)\n            except pymysql.err.InterfaceError:\n                # attempt to reconnect\n                if not self.reconnect():\n                    return\n                one_iteration(text)\n                return  # OK to just return, cuz the recursion call runs to the end.\n            except EOFError as e:\n                raise e\n            except KeyboardInterrupt:\n                # get last connection id\n                connection_id_to_kill = sqlexecute.connection_id or 0\n                # some mysql-compatible databases may not implement connection_id()\n                if connection_id_to_kill > 0:\n                    logger.debug(\"connection id to kill: %r\", connection_id_to_kill)\n                    try:\n                        sqlexecute.connect()\n                        for kill_result in sqlexecute.run(f\"kill {connection_id_to_kill}\"):\n                            status_str = str(kill_result.status_plain).lower()\n                            if status_str.find(\"ok\") > -1:\n                                logger.debug(\"cancelled query, connection id: %r, sql: %r\", connection_id_to_kill, text)\n                                self.echo(f\"Cancelled query id: {connection_id_to_kill}\", err=True, fg=\"blue\")\n                            else:\n                                logger.debug(\n                                    \"Failed to confirm query cancellation, connection id: %r, sql: %r\",\n                                    connection_id_to_kill,\n                                    text,\n                                )\n                                self.echo(f\"Failed to confirm query cancellation, id: {connection_id_to_kill}\", err=True, fg=\"red\")\n                    except Exception as e2:\n                        self.echo(f\"Encountered error while cancelling query: {e2}\", err=True, fg=\"red\")\n                else:\n                    logger.debug(\"Did not get a connection id, skip cancelling query\")\n                    self.echo(\"Did not get a connection id, skip cancelling query\", err=True, fg=\"red\")\n            except NotImplementedError:\n                self.echo(\"Not Yet Implemented.\", fg=\"yellow\")\n            except pymysql.OperationalError as e1:\n                logger.debug(\"Exception: %r\", e1)\n                if e1.args[0] in (2003, 2006, 2013):\n                    # attempt to reconnect\n                    if not self.reconnect():\n                        return\n                    one_iteration(text)\n                    return  # OK to just return, cuz the recursion call runs to the end.\n                else:\n                    logger.error(\"sql: %r, error: %r\", text, e1)\n                    logger.error(\"traceback: %r\", traceback.format_exc())\n                    self.echo(str(e1), err=True, fg=\"red\")\n            except Exception as e:\n                logger.error(\"sql: %r, error: %r\", text, e)\n                logger.error(\"traceback: %r\", traceback.format_exc())\n                self.echo(str(e), err=True, fg=\"red\")\n            else:\n                if is_dropping_database(text, sqlexecute.dbname):\n                    sqlexecute.dbname = None\n                    sqlexecute.connect()\n\n                # Refresh the table names and column names if necessary.\n                if need_completion_refresh(text):\n                    self.refresh_completions(reset=need_completion_reset(text))\n            finally:\n                if self.logfile is False:\n                    self.echo(\"Warning: This query was not logged.\", err=True, fg=\"red\")\n            query = Query(text, successful, mutating)\n            self.query_history.append(query)\n\n        if self.toolbar_format.lower() == 'none':\n            get_toolbar_tokens = None\n        else:\n            get_toolbar_tokens = create_toolbar_tokens_func(\n                self,\n                show_initial_toolbar_help,\n                self.toolbar_format,\n            )\n\n        if self.wider_completion_menu:\n            complete_style = CompleteStyle.MULTI_COLUMN\n        else:\n            complete_style = CompleteStyle.COLUMN\n\n        with self._completer_lock:\n            if self.key_bindings == \"vi\":\n                editing_mode = EditingMode.VI\n            else:\n                editing_mode = EditingMode.EMACS\n\n            self.prompt_app = PromptSession(\n                color_depth=ColorDepth.DEPTH_24_BIT if 'truecolor' in os.getenv('COLORTERM', '').lower() else None,\n                lexer=PygmentsLexer(MyCliLexer),\n                reserve_space_for_menu=self.get_reserved_space(),\n                prompt_continuation=get_continuation,\n                bottom_toolbar=get_toolbar_tokens,\n                complete_style=complete_style,\n                input_processors=[\n                    ConditionalProcessor(\n                        processor=HighlightMatchingBracketProcessor(chars=\"[](){}\"), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()\n                    )\n                ],\n                tempfile_suffix=\".sql\",\n                completer=DynamicCompleter(lambda: self.completer),\n                complete_in_thread=True,\n                history=history,\n                auto_suggest=ThreadedAutoSuggest(AutoSuggestFromHistory()),\n                complete_while_typing=complete_while_typing_filter,\n                multiline=cli_is_multiline(self),\n                # why not self.toolkit_style here?\n                style=style_factory_toolkit(self.syntax_style, self.cli_style),\n                include_default_pygments_style=False,\n                key_bindings=key_bindings,\n                enable_open_in_editor=True,\n                enable_system_prompt=True,\n                enable_suspend=True,\n                editing_mode=editing_mode,\n                search_ignore_case=True,\n            )\n\n            if self.key_bindings == 'vi':\n                self.prompt_app.app.ttimeoutlen = self.vi_ttimeoutlen\n            else:\n                self.prompt_app.app.ttimeoutlen = self.emacs_ttimeoutlen\n\n        self.set_all_external_titles()\n\n        try:\n            while True:\n                one_iteration()\n                iterations += 1\n        except EOFError:\n            special.close_tee()\n            if not self.less_chatty:\n                self.echo(\"Goodbye!\")\n\n    def reconnect(self, database: str = \"\") -> bool:\n        \"\"\"\n        Attempt to reconnect to the server. Return True if successful,\n        False if unsuccessful.\n\n        The \"database\" argument is used only to improve messages.\n        \"\"\"\n        assert self.sqlexecute is not None\n        assert self.sqlexecute.conn is not None\n\n        # First pass with ping(reconnect=False) and minimal feedback levels.  This definitely\n        # works as expected, and is a good idea especially when \"connect\" was used as a\n        # synonym for \"use\".\n        try:\n            self.sqlexecute.conn.ping(reconnect=False)\n            if not database:\n                self.echo(\"Already connected.\", fg=\"yellow\")\n            return True\n        except pymysql.err.Error:\n            pass\n\n        # Second pass with ping(reconnect=True).  It is not demonstrated that this pass ever\n        # gives the benefit it is looking for, _ie_ preserves session state.  We need to test\n        # this with connection pooling.\n        try:\n            old_connection_id = self.sqlexecute.connection_id\n            self.logger.debug(\"Attempting to reconnect.\")\n            self.echo(\"Reconnecting...\", fg=\"yellow\")\n            self.sqlexecute.conn.ping(reconnect=True)\n            # if a database is currently selected, set it on the conn again\n            if self.sqlexecute.dbname:\n                self.sqlexecute.conn.select_db(self.sqlexecute.dbname)\n            self.logger.debug(\"Reconnected successfully.\")\n            self.echo(\"Reconnected successfully.\", fg=\"yellow\")\n            self.sqlexecute.reset_connection_id()\n            if old_connection_id != self.sqlexecute.connection_id:\n                self.echo(\"Any session state was reset.\", fg=\"red\")\n            return True\n        except pymysql.err.Error:\n            pass\n\n        # Third pass with sqlexecute.connect() should always work, but always resets session state.\n        try:\n            self.logger.debug(\"Creating new connection\")\n            self.echo(\"Creating new connection...\", fg=\"yellow\")\n            self.sqlexecute.connect()\n            self.logger.debug(\"New connection created successfully.\")\n            self.echo(\"New connection created successfully.\", fg=\"yellow\")\n            self.echo(\"Any session state was reset.\", fg=\"red\")\n            return True\n        except pymysql.OperationalError as e:\n            self.logger.debug(\"Reconnect failed. e: %r\", e)\n            self.echo(str(e), err=True, fg=\"red\")\n            return False\n\n    def log_query(self, query: str) -> None:\n        if isinstance(self.logfile, TextIOWrapper):\n            self.logfile.write(f\"\\n# {datetime.now()}\\n\")\n            self.logfile.write(query)\n            self.logfile.write(\"\\n\")\n\n    def log_output(self, output: str | AnyFormattedText) -> None:\n        \"\"\"Log the output in the audit log, if it's enabled.\"\"\"\n        if isinstance(output, (ANSI, HTML, FormattedText)):\n            output = to_plain_text(output)\n        if isinstance(self.logfile, TextIOWrapper):\n            click.echo(output, file=self.logfile)\n\n    def echo(self, s: str, **kwargs) -> None:\n        \"\"\"Print a message to stdout.\n\n        The message will be logged in the audit log, if enabled.\n\n        All keyword arguments are passed to click.echo().\n\n        \"\"\"\n        self.log_output(s)\n        click.secho(s, **kwargs)\n\n    def get_output_margin(self, status: str | None = None) -> int:\n        \"\"\"Get the output margin (number of rows for the prompt, footer and\n        timing message.\"\"\"\n        if not self.prompt_lines:\n            if self.prompt_app and self.prompt_app.app:\n                render_counter = self.prompt_app.app.render_counter\n            else:\n                render_counter = 0\n            self.prompt_lines = self.get_prompt(self.prompt_format, render_counter).count('\\n') + 1\n        margin = self.get_reserved_space() + self.prompt_lines\n        if special.is_timing_enabled():\n            margin += 1\n        if status:\n            margin += 1 + status.count(\"\\n\")\n\n        return margin\n\n    def output(\n        self,\n        output: itertools.chain[str],\n        result: SQLResult,\n        is_warnings_style: bool = False,\n    ) -> None:\n        \"\"\"Output text to stdout or a pager command.\n\n        The status text is not outputted to pager or files.\n\n        The message will be logged in the audit log, if enabled. The\n        message will be written to the tee file, if enabled. The\n        message will be written to the output file, if enabled.\n\n        \"\"\"\n        if output:\n            if self.prompt_app is not None:\n                size = self.prompt_app.output.get_size()\n                size_columns = size.columns\n                size_rows = size.rows\n            else:\n                size_columns = DEFAULT_WIDTH\n                size_rows = DEFAULT_HEIGHT\n\n            margin = self.get_output_margin(result.status_plain)\n\n            fits = True\n            buf = []\n            output_via_pager = self.explicit_pager and special.is_pager_enabled()\n            for i, line in enumerate(output, 1):\n                self.log_output(line)\n                special.write_tee(line)\n                special.write_once(line)\n                special.write_pipe_once(line)\n\n                if special.is_redirected():\n                    pass\n                elif fits or output_via_pager:\n                    # buffering\n                    buf.append(line)\n                    if len(line) > size_columns or i > (size_rows - margin):\n                        fits = False\n                        if not self.explicit_pager and special.is_pager_enabled():\n                            # doesn't fit, use pager\n                            output_via_pager = True\n\n                        if not output_via_pager:\n                            # doesn't fit, flush buffer\n                            for buf_line in buf:\n                                click.secho(buf_line)\n                            buf = []\n                else:\n                    click.secho(line)\n\n            if buf:\n                if output_via_pager:\n\n                    def newlinewrapper(text: list[str]) -> Generator[str, None, None]:\n                        for line in text:\n                            yield line + \"\\n\"\n\n                    click.echo_via_pager(newlinewrapper(buf))\n                else:\n                    for line in buf:\n                        click.secho(line)\n\n        if result.status:\n            self.log_output(result.status_plain)\n            add_style = 'class:warnings.status' if is_warnings_style else 'class:output.status'\n            if isinstance(result.status, FormattedText):\n                status = result.status\n            else:\n                status = FormattedText([('', result.status_plain)])\n            styled_status = to_formatted_text(status, style=add_style)\n            print_formatted_text(styled_status, style=self.toolkit_style)\n\n    def configure_pager(self) -> None:\n        # Provide sane defaults for less if they are empty.\n        if not os.environ.get(\"LESS\"):\n            os.environ[\"LESS\"] = \"-RXF\"\n\n        cnf = self.read_my_cnf(self.my_cnf, [\"pager\", \"skip-pager\"])\n        cnf_pager = cnf[\"pager\"] or self.config[\"main\"][\"pager\"]\n\n        # help Windows users who haven't edited the default myclirc\n        if WIN and cnf_pager == 'less' and not shutil.which(cnf_pager):\n            cnf_pager = 'more'\n\n        if cnf_pager:\n            special.set_pager(cnf_pager)\n            self.explicit_pager = True\n        else:\n            self.explicit_pager = False\n\n        if cnf[\"skip-pager\"] or not self.config[\"main\"].as_bool(\"enable_pager\"):\n            special.disable_pager()\n\n    def refresh_completions(self, reset: bool = False) -> list[SQLResult]:\n        if reset:\n            with self._completer_lock:\n                self.completer.reset_completions()\n        assert self.sqlexecute is not None\n        self.completion_refresher.refresh(\n            self.sqlexecute,\n            self._on_completions_refreshed,\n            {\n                \"smart_completion\": self.smart_completion,\n                \"supported_formats\": self.main_formatter.supported_formats,\n                \"keyword_casing\": self.completer.keyword_casing,\n            },\n        )\n\n        return [SQLResult(status=\"Auto-completion refresh started in the background.\")]\n\n    def _on_completions_refreshed(self, new_completer: SQLCompleter) -> None:\n        \"\"\"Swap the completer object in cli with the newly created completer.\"\"\"\n        with self._completer_lock:\n            self.completer = new_completer\n\n        if self.prompt_app:\n            # After refreshing, redraw the CLI to clear the statusbar\n            # \"Refreshing completions...\" indicator\n            self.prompt_app.app.invalidate()\n\n    def get_completions(self, text: str, cursor_position: int) -> Iterable[Completion]:\n        with self._completer_lock:\n            return self.completer.get_completions(Document(text=text, cursor_position=cursor_position), None)\n\n    def set_all_external_titles(self) -> None:\n        self.set_external_terminal_tab_title()\n        self.set_external_terminal_window_title()\n        self.set_external_multiplex_window_title()\n        self.set_external_multiplex_pane_title()\n\n    def set_external_terminal_tab_title(self) -> None:\n        if not self.terminal_tab_title_format:\n            return\n        if not self.prompt_app:\n            return\n        if not sys.stderr.isatty():\n            return\n        title = sanitize_terminal_title(self.get_prompt(self.terminal_tab_title_format, self.prompt_app.app.render_counter))\n        print(f'\\x1b]1;{title}\\a', file=sys.stderr, end='')\n        sys.stderr.flush()\n\n    def set_external_terminal_window_title(self) -> None:\n        if not self.terminal_window_title_format:\n            return\n        if not self.prompt_app:\n            return\n        if not sys.stderr.isatty():\n            return\n        title = sanitize_terminal_title(self.get_prompt(self.terminal_window_title_format, self.prompt_app.app.render_counter))\n        print(f'\\x1b]2;{title}\\a', file=sys.stderr, end='')\n        sys.stderr.flush()\n\n    def set_external_multiplex_window_title(self) -> None:\n        if not self.multiplex_window_title_format:\n            return\n        if not os.getenv('TMUX'):\n            return\n        if not self.prompt_app:\n            return\n        title = sanitize_terminal_title(self.get_prompt(self.multiplex_window_title_format, self.prompt_app.app.render_counter))\n        try:\n            subprocess.run(\n                ['tmux', 'rename-window', title],\n                check=False,\n                stdin=subprocess.DEVNULL,\n                stdout=subprocess.DEVNULL,\n                stderr=subprocess.DEVNULL,\n            )\n        except FileNotFoundError:\n            pass\n\n    def set_external_multiplex_pane_title(self) -> None:\n        if not self.multiplex_pane_title_format:\n            return\n        if not os.getenv('TMUX'):\n            return\n        if not self.prompt_app:\n            return\n        if not sys.stderr.isatty():\n            return\n        title = sanitize_terminal_title(self.get_prompt(self.multiplex_pane_title_format, self.prompt_app.app.render_counter))\n        print(f'\\x1b]2;{title}\\x1b\\\\', file=sys.stderr, end='')\n        sys.stderr.flush()\n\n    def get_custom_toolbar(self, toolbar_format: str) -> ANSI:\n        if not self.prompt_app:\n            return ANSI('')\n        if not self.prompt_app.app:\n            return ANSI('')\n        if self.prompt_app.app.current_buffer.text:\n            return self.last_custom_toolbar_message\n        toolbar = self.get_prompt(toolbar_format, self.prompt_app.app.render_counter)\n        toolbar = toolbar.replace(\"\\\\x1b\", \"\\x1b\")\n        self.last_custom_toolbar_message = ANSI(toolbar)\n        return self.last_custom_toolbar_message\n\n    # Memoizing a method leaks the instance, but we only expect one MyCli instance.\n    # Before memoizing, get_prompt() was called dozens of times per prompt.\n    # Even after memoizing, get_prompt's logic gets called twice per prompt, which\n    # should be addressed, because some format strings take a trip to the server.\n    @functools.lru_cache(maxsize=256)  # noqa: B019\n    def get_prompt(self, string: str, _render_counter: int) -> str:\n        sqlexecute = self.sqlexecute\n        assert sqlexecute is not None\n        assert sqlexecute.server_info is not None\n        assert sqlexecute.server_info.species is not None\n        if self.login_path and self.login_path_as_host:\n            prompt_host = self.login_path\n        elif sqlexecute.host is not None:\n            prompt_host = sqlexecute.host\n        else:\n            prompt_host = DEFAULT_HOST\n        short_prompt_host, _, _ = prompt_host.partition('.')\n        if re.match(r'^[\\d\\.]+$', short_prompt_host):\n            short_prompt_host = prompt_host\n        now = datetime.now()\n        backslash_placeholder = '\\ufffc_backslash'\n        string = string.replace('\\\\\\\\', backslash_placeholder)\n        string = string.replace(\"\\\\u\", sqlexecute.user or \"(none)\")\n        string = string.replace(\"\\\\h\", prompt_host or \"(none)\")\n        string = string.replace(\"\\\\H\", short_prompt_host or \"(none)\")\n        string = string.replace(\"\\\\d\", sqlexecute.dbname or \"(none)\")\n        string = string.replace(\"\\\\t\", sqlexecute.server_info.species.name)\n        string = string.replace(\"\\\\n\", \"\\n\")\n        string = string.replace(\"\\\\D\", now.strftime(\"%a %b %d %H:%M:%S %Y\"))\n        string = string.replace(\"\\\\m\", now.strftime(\"%M\"))\n        string = string.replace(\"\\\\P\", now.strftime(\"%p\"))\n        string = string.replace(\"\\\\R\", now.strftime(\"%H\"))\n        string = string.replace(\"\\\\r\", now.strftime(\"%I\"))\n        string = string.replace(\"\\\\s\", now.strftime(\"%S\"))\n        string = string.replace(\"\\\\p\", str(sqlexecute.port))\n        string = string.replace(\"\\\\j\", os.path.basename(sqlexecute.socket or '(none)'))\n        string = string.replace(\"\\\\J\", sqlexecute.socket or '(none)')\n        string = string.replace(\"\\\\k\", os.path.basename(sqlexecute.socket or str(sqlexecute.port)))\n        string = string.replace(\"\\\\K\", sqlexecute.socket or str(sqlexecute.port))\n        string = string.replace(\"\\\\A\", self.dsn_alias or \"(none)\")\n        string = string.replace(\"\\\\_\", \" \")\n        string = string.replace(backslash_placeholder, '\\\\')\n\n        # jump through hoops for the test environment, and for efficiency\n        if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None:\n            if '\\\\y' in string:\n                with sqlexecute.conn.cursor() as cur:\n                    string = string.replace('\\\\y', str(get_uptime(cur)) or '(none)')\n            if '\\\\Y' in string:\n                with sqlexecute.conn.cursor() as cur:\n                    string = string.replace('\\\\Y', format_uptime(str(get_uptime(cur))) or '(none)')\n        else:\n            string = string.replace('\\\\y', '(none)')\n            string = string.replace('\\\\Y', '(none)')\n\n        if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None:\n            if '\\\\T' in string:\n                with sqlexecute.conn.cursor() as cur:\n                    string = string.replace('\\\\T', get_ssl_version(cur) or '(none)')\n        else:\n            string = string.replace('\\\\T', '(none)')\n\n        if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None:\n            if '\\\\w' in string:\n                with sqlexecute.conn.cursor() as cur:\n                    string = string.replace('\\\\w', str(get_warning_count(cur) or '(none)'))\n        else:\n            string = string.replace('\\\\w', '(none)')\n        if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None:\n            if '\\\\W' in string:\n                with sqlexecute.conn.cursor() as cur:\n                    string = string.replace('\\\\W', str(get_warning_count(cur) or ''))\n        else:\n            string = string.replace('\\\\W', '')\n\n        return string\n\n    def run_query(\n        self,\n        query: str,\n        checkpoint: TextIOWrapper | None = None,\n        new_line: bool = True,\n    ) -> None:\n        \"\"\"Runs *query*.\"\"\"\n        assert self.sqlexecute is not None\n        self.log_query(query)\n        results = self.sqlexecute.run(query)\n        for result in results:\n            self.main_formatter.query = query\n            self.redirect_formatter.query = query\n            output = self.format_sqlresult(\n                result,\n                is_expanded=special.is_expanded_output(),\n                is_redirected=special.is_redirected(),\n                null_string=self.null_string,\n                numeric_alignment=self.numeric_alignment,\n                binary_display=self.binary_display,\n            )\n            for line in output:\n                self.log_output(line)\n                click.echo(line, nl=new_line)\n\n            # get and display warnings if enabled\n            if self.show_warnings and isinstance(result.rows, Cursor) and result.rows.warning_count > 0:\n                warnings = self.sqlexecute.run(\"SHOW WARNINGS\")\n                for warning in warnings:\n                    output = self.format_sqlresult(\n                        warning,\n                        is_expanded=special.is_expanded_output(),\n                        is_redirected=special.is_redirected(),\n                        null_string=self.null_string,\n                        numeric_alignment=self.numeric_alignment,\n                        binary_display=self.binary_display,\n                        is_warnings_style=True,\n                    )\n                    for line in output:\n                        click.echo(line, nl=new_line)\n        if checkpoint:\n            checkpoint.write(query.rstrip('\\n') + '\\n')\n            checkpoint.flush()\n\n    def format_sqlresult(\n        self,\n        result,\n        is_expanded: bool = False,\n        is_redirected: bool = False,\n        null_string: str | None = None,\n        numeric_alignment: str = 'right',\n        binary_display: str | None = None,\n        max_width: int | None = None,\n        is_warnings_style: bool = False,\n    ) -> itertools.chain[str]:\n        if is_redirected:\n            use_formatter = self.redirect_formatter\n        else:\n            use_formatter = self.main_formatter\n\n        is_expanded = is_expanded or use_formatter.format_name == \"vertical\"\n        output: itertools.chain[str] = itertools.chain()\n\n        output_kwargs = {\n            \"dialect\": \"unix\",\n            \"disable_numparse\": True,\n            \"preserve_whitespace\": True,\n            \"style\": self.helpers_warnings_style if is_warnings_style else self.helpers_style,\n        }\n        default_kwargs = use_formatter._output_formats[use_formatter.format_name].formatter_args\n\n        if null_string is not None and default_kwargs.get('missing_value') == DEFAULT_MISSING_VALUE:\n            output_kwargs['missing_value'] = null_string\n\n        if use_formatter.format_name not in sql_format.supported_formats and binary_display != 'utf8':\n            # will run before preprocessors defined as part of the format in cli_helpers\n            output_kwargs[\"preprocessors\"] = (preprocessors.convert_to_undecoded_string,)\n\n        if result.preamble:\n            output = itertools.chain(output, [result.preamble])\n\n        if result.header or (result.rows and result.preamble):\n            column_types = None\n            colalign = None\n            if isinstance(result.rows, Cursor):\n\n                def get_col_type(col) -> type:\n                    col_type = FIELD_TYPES.get(col[1], str)\n                    return col_type if type(col_type) is type else str\n\n                if result.rows.rowcount > 0:\n                    column_types = [get_col_type(tup) for tup in result.rows.description]\n                    colalign = [numeric_alignment if x in (int, float, Decimal) else 'left' for x in column_types]\n                else:\n                    column_types, colalign = [], []\n\n            if max_width is not None and isinstance(result.rows, Cursor):\n                result_rows = list(result.rows)\n            else:\n                result_rows = result.rows\n\n            formatted = use_formatter.format_output(\n                result_rows,\n                result.header or [],\n                format_name=\"vertical\" if is_expanded else None,\n                column_types=column_types,\n                colalign=colalign,\n                **output_kwargs,\n            )\n\n            if isinstance(formatted, str):\n                formatted = formatted.splitlines()\n            formatted = iter(formatted)\n\n            if not is_expanded and max_width and result.header and result_rows:\n                first_line = next(formatted)\n                if len(strip_ansi(first_line)) > max_width:\n                    formatted = use_formatter.format_output(\n                        result_rows,\n                        result.header,\n                        format_name=\"vertical\",\n                        column_types=column_types,\n                        **output_kwargs,\n                    )\n                    if isinstance(formatted, str):\n                        formatted = iter(formatted.splitlines())\n                else:\n                    formatted = itertools.chain([first_line], formatted)\n\n            output = itertools.chain(output, formatted)\n\n        if result.postamble:\n            output = itertools.chain(output, [result.postamble])\n\n        return output\n\n    def get_reserved_space(self) -> int:\n        \"\"\"Get the number of lines to reserve for the completion menu.\"\"\"\n        reserved_space_ratio = 0.45\n        max_reserved_space = 8\n        _, height = shutil.get_terminal_size()\n        return min(int(round(height * reserved_space_ratio)), max_reserved_space)\n\n    def get_last_query(self) -> str | None:\n        \"\"\"Get the last query executed or None.\"\"\"\n        return self.query_history[-1][0] if self.query_history else None\n\n\n@click.command()\n@click.option(\"-h\", \"--host\", envvar=\"MYSQL_HOST\", help=\"Host address of the database.\")\n@click.option(\"-P\", \"--port\", envvar=\"MYSQL_TCP_PORT\", type=int, help=\"Port number to use for connection. Honors $MYSQL_TCP_PORT.\")\n@click.option(\"-u\", \"--user\", help=\"User name to connect to the database.\")\n@click.option(\"-S\", \"--socket\", envvar=\"MYSQL_UNIX_PORT\", help=\"The socket file to use for connection.\")\n@click.option(\n    \"-p\",\n    \"--pass\",\n    \"--password\",\n    \"password\",\n    is_flag=False,\n    flag_value=EMPTY_PASSWORD_FLAG_SENTINEL,\n    type=INT_OR_STRING_CLICK_TYPE,\n    help=\"Prompt for (or pass in cleartext) the password to connect to the database.\",\n)\n@click.option(\"--ssh-user\", help=\"User name to connect to ssh server.\")\n@click.option(\"--ssh-host\", help=\"Host name to connect to ssh server.\")\n@click.option(\"--ssh-port\", default=22, help=\"Port to connect to ssh server.\")\n@click.option(\"--ssh-password\", help=\"Password to connect to ssh server.\")\n@click.option(\"--ssh-key-filename\", help=\"Private key filename (identify file) for the ssh connection.\")\n@click.option(\"--ssh-config-path\", help=\"Path to ssh configuration.\", default=os.path.expanduser(\"~\") + \"/.ssh/config\")\n@click.option(\"--ssh-config-host\", help=\"Host to connect to ssh server reading from ssh configuration.\")\n@click.option(\n    \"--ssl-mode\",\n    \"ssl_mode\",\n    help=\"Set desired SSL behavior. auto=preferred if TCP/IP, on=required, off=off.\",\n    type=click.Choice([\"auto\", \"on\", \"off\"]),\n)\n@click.option(\"--ssl/--no-ssl\", \"ssl_enable\", default=None, help=\"Enable SSL for connection (automatically enabled with other flags).\")\n@click.option(\"--ssl-ca\", help=\"CA file in PEM format.\", type=click.Path(exists=True))\n@click.option(\"--ssl-capath\", help=\"CA directory.\", type=click.Path(exists=True, file_okay=False, dir_okay=True))\n@click.option(\"--ssl-cert\", help=\"X509 cert in PEM format.\", type=click.Path(exists=True))\n@click.option(\"--ssl-key\", help=\"X509 key in PEM format.\", type=click.Path(exists=True))\n@click.option(\"--ssl-cipher\", help=\"SSL cipher to use.\")\n@click.option(\n    \"--tls-version\",\n    type=click.Choice([\"TLSv1\", \"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\"], case_sensitive=False),\n    help=\"TLS protocol version for secure connection.\",\n)\n@click.option(\n    \"--ssl-verify-server-cert\",\n    is_flag=True,\n    help=(\"\"\"Verify server's \"Common Name\" in its cert against hostname used when connecting. This option is disabled by default.\"\"\"),\n)\n@click.version_option(__version__, \"-V\", \"--version\", help=\"Output mycli's version.\")\n@click.option(\"-v\", \"--verbose\", is_flag=True, help=\"Verbose output.\")\n@click.option(\"-D\", \"--database\", \"dbname\", help=\"Database or DSN to use for the connection.\")\n@click.option(\"-d\", \"--dsn\", 'dsn_alias', default=\"\", envvar=\"DSN\", help=\"DSN alias configured in the ~/.myclirc file, or a full DSN.\")\n@click.option(\n    \"--list-dsn\", \"list_dsn\", is_flag=True, help=\"list of DSN aliases configured in the [alias_dsn] section of the ~/.myclirc file.\"\n)\n@click.option(\"--list-ssh-config\", \"list_ssh_config\", is_flag=True, help=\"list ssh configurations in the ssh config (requires paramiko).\")\n@click.option(\"--ssh-warning-off\", is_flag=True, help=\"Suppress the SSH deprecation notice.\")\n@click.option(\"-R\", \"--prompt\", \"prompt\", help=f'Prompt format (Default: \"{MyCli.default_prompt}\").')\n@click.option('--toolbar', 'toolbar_format', help='Toolbar format.')\n@click.option(\"-l\", \"--logfile\", type=click.File(mode=\"a\", encoding=\"utf-8\"), help=\"Log every query and its results to a file.\")\n@click.option(\n    \"--checkpoint\", type=click.File(mode=\"a\", encoding=\"utf-8\"), help=\"In batch or --execute mode, log successful queries to a file.\"\n)\n@click.option(\"--defaults-group-suffix\", type=str, help=\"Read MySQL config groups with the specified suffix.\")\n@click.option(\"--defaults-file\", type=click.Path(), help=\"Only read MySQL options from the given file.\")\n@click.option(\"--myclirc\", type=click.Path(), default=\"~/.myclirc\", help=\"Location of myclirc file.\")\n@click.option(\n    \"--auto-vertical-output\",\n    is_flag=True,\n    help=\"Automatically switch to vertical output mode if the result is wider than the terminal width.\",\n)\n@click.option(\n    \"--show-warnings/--no-show-warnings\", \"show_warnings\", is_flag=True, help=\"Automatically show warnings after executing a SQL statement.\"\n)\n@click.option(\"-t\", \"--table\", is_flag=True, help=\"Shorthand for --format=table.\")\n@click.option(\"--csv\", is_flag=True, help=\"Shorthand for --format=csv.\")\n@click.option(\"--warn/--no-warn\", default=None, help=\"Warn before running a destructive query.\")\n@click.option(\"--local-infile\", type=bool, help=\"Enable/disable LOAD DATA LOCAL INFILE.\")\n@click.option(\"-g\", \"--login-path\", type=str, help=\"Read this path from the login file.\")\n@click.option(\"-e\", \"--execute\", type=str, help=\"Execute command and quit.\")\n@click.option(\"--init-command\", type=str, help=\"SQL statement to execute after connecting.\")\n@click.option(\n    \"--unbuffered\", is_flag=True, help=\"Instead of copying every row of data into a buffer, fetch rows as needed, to save memory.\"\n)\n@click.option(\"--character-set\", \"--charset\", type=str, help=\"Character set for MySQL session.\")\n@click.option(\n    \"--password-file\", type=click.Path(), help=\"File or FIFO path containing the password to connect to the db if not specified otherwise.\"\n)\n@click.argument(\"database\", default=None, nargs=1)\n@click.option(\"--noninteractive\", is_flag=True, help=\"Don't prompt during batch input.  Recommended.\")\n@click.option(\n    '--format', 'batch_format', type=click.Choice(['default', 'csv', 'tsv', 'table']), help='Format for batch or --execute output.'\n)\n@click.option('--throttle', type=float, default=0.0, help='Pause in seconds between queries in batch mode.')\n@click.option(\n    '--use-keyring',\n    'use_keyring_cli_opt',\n    type=click.Choice(['true', 'false', 'reset']),\n    default=None,\n    help='Store and retrieve passwords from the system keyring: true/false/reset.',\n)\n@click.option(\n    '--keepalive-ticks',\n    type=int,\n    help='Send regular keepalive pings to the connection, roughly every <int> seconds.',\n)\n@click.option(\"--checkup\", is_flag=True, help=\"Run a checkup on your config file.\")\n@click.pass_context\ndef cli(\n    ctx: click.Context,\n    database: str | None,\n    user: str | None,\n    host: str | None,\n    port: int | None,\n    socket: str | None,\n    password: str | int | None,\n    dbname: str | None,\n    verbose: bool,\n    prompt: str | None,\n    toolbar_format: str | None,\n    logfile: TextIOWrapper | None,\n    checkpoint: TextIOWrapper | None,\n    defaults_group_suffix: str | None,\n    defaults_file: str | None,\n    login_path: str | None,\n    auto_vertical_output: bool,\n    show_warnings: bool,\n    local_infile: bool,\n    ssl_mode: str | None,\n    ssl_enable: bool,\n    ssl_ca: str | None,\n    ssl_capath: str | None,\n    ssl_cert: str | None,\n    ssl_key: str | None,\n    ssl_cipher: str | None,\n    tls_version: str | None,\n    ssl_verify_server_cert: bool,\n    table: bool,\n    csv: bool,\n    warn: bool | None,\n    execute: str | None,\n    myclirc: str,\n    dsn_alias: str,\n    list_dsn: str | None,\n    ssh_user: str | None,\n    ssh_host: str | None,\n    ssh_port: int,\n    ssh_password: str | None,\n    ssh_key_filename: str | None,\n    list_ssh_config: bool,\n    ssh_config_path: str,\n    ssh_config_host: str | None,\n    ssh_warning_off: bool | None,\n    init_command: str | None,\n    unbuffered: bool | None,\n    character_set: str | None,\n    password_file: str | None,\n    noninteractive: bool,\n    batch_format: str | None,\n    throttle: float,\n    use_keyring_cli_opt: str | None,\n    checkup: bool,\n    keepalive_ticks: int | None,\n) -> None:\n    \"\"\"A MySQL terminal client with auto-completion and syntax highlighting.\n\n    \\b\n    Examples:\n      - mycli my_database\n      - mycli -u my_user -h my_host.com my_database\n      - mycli mysql://my_user@my_host.com:3306/my_database\n\n    \"\"\"\n\n    def get_password_from_file(password_file: str | None) -> str | None:\n        if not password_file:\n            return None\n        try:\n            with open(password_file) as fp:\n                password = fp.readline().strip()\n                return password\n        except FileNotFoundError:\n            click.secho(f\"Password file '{password_file}' not found\", err=True, fg=\"red\")\n            sys.exit(1)\n        except PermissionError:\n            click.secho(f\"Permission denied reading password file '{password_file}'\", err=True, fg=\"red\")\n            sys.exit(1)\n        except IsADirectoryError:\n            click.secho(f\"Path '{password_file}' is a directory, not a file\", err=True, fg=\"red\")\n            sys.exit(1)\n        except Exception as e:\n            click.secho(f\"Error reading password file '{password_file}': {str(e)}\", err=True, fg=\"red\")\n            sys.exit(1)\n\n    # if the password value looks like a DSN, treat it as such and\n    # prompt for password\n    if database is None and isinstance(password, str) and \"://\" in password:\n        # check if the scheme is valid. We do not actually have any logic for these, but\n        # it will most usefully catch the case where we erroneously catch someone's\n        # password, and give them an easy error message to follow / report\n        is_valid_scheme, scheme = is_valid_connection_scheme(password)\n        if not is_valid_scheme:\n            click.secho(f\"Error: Unknown connection scheme provided for DSN URI ({scheme}://)\", err=True, fg=\"red\")\n            sys.exit(1)\n        database = password\n        password = EMPTY_PASSWORD_FLAG_SENTINEL\n\n    # if the password is not specified try to set it using the password_file option\n    if password is None and password_file:\n        password_from_file = get_password_from_file(password_file)\n        if password_from_file is not None:\n            password = password_from_file\n\n    # getting the envvar ourselves because the envvar from a click\n    # option cannot be an empty string, but a password can be\n    if password is None and os.environ.get(\"MYSQL_PWD\") is not None:\n        password = os.environ.get(\"MYSQL_PWD\")\n\n    mycli = MyCli(\n        prompt=prompt,\n        toolbar_format=toolbar_format,\n        logfile=logfile,\n        defaults_suffix=defaults_group_suffix,\n        defaults_file=defaults_file,\n        login_path=login_path,\n        auto_vertical_output=auto_vertical_output,\n        warn=warn,\n        myclirc=myclirc,\n    )\n\n    if checkup:\n        do_checkup(mycli)\n        sys.exit(0)\n\n    if csv and batch_format not in [None, 'csv']:\n        click.secho(\"Conflicting --csv and --format arguments.\", err=True, fg=\"red\")\n        sys.exit(1)\n\n    if table and batch_format not in [None, 'table']:\n        click.secho(\"Conflicting --table and --format arguments.\", err=True, fg=\"red\")\n        sys.exit(1)\n\n    if not batch_format:\n        batch_format = 'default'\n\n    if csv:\n        batch_format = 'csv'\n\n    if table:\n        batch_format = 'table'\n\n    if ssl_enable is not None:\n        click.secho(\n            \"Warning: The --ssl/--no-ssl CLI options are deprecated and will be removed in a future release. \"\n            \"Please use the \\\"default_ssl_mode\\\" config option or --ssl-mode CLI flag instead. \"\n            f\"See issue {ISSUES_URL}/1507\",\n            err=True,\n            fg=\"yellow\",\n        )\n\n    # ssh_port and ssh_config_path have truthy defaults and are not included\n    if any([ssh_user, ssh_host, ssh_password, ssh_key_filename, list_ssh_config, ssh_config_host]) and not ssh_warning_off:\n        click.secho(\n            f\"Warning: The built-in SSH functionality is deprecated and will be removed in a future release. See issue {ISSUES_URL}/1464\",\n            err=True,\n            fg=\"red\",\n        )\n\n    if list_dsn:\n        try:\n            alias_dsn = mycli.config[\"alias_dsn\"]\n        except KeyError:\n            click.secho(\"Invalid DSNs found in the config file. Please check the \\\"[alias_dsn]\\\" section in myclirc.\", err=True, fg=\"red\")\n            sys.exit(1)\n        except Exception as e:\n            click.secho(str(e), err=True, fg=\"red\")\n            sys.exit(1)\n        for alias, value in alias_dsn.items():\n            if verbose:\n                click.secho(f\"{alias} : {value}\")\n            else:\n                click.secho(alias)\n        sys.exit(0)\n    if list_ssh_config:\n        ssh_config = read_ssh_config(ssh_config_path)\n        try:\n            host_entries = ssh_config.get_hostnames()\n        except KeyError:\n            click.secho('Error reading ssh config', err=True, fg=\"red\")\n            sys.exit(1)\n        for host_entry in host_entries:\n            if verbose:\n                host_config = ssh_config.lookup(host_entry)\n                click.secho(f\"{host_entry} : {host_config.get('hostname')}\")\n            else:\n                click.secho(host_entry)\n        sys.exit(0)\n    # Choose which ever one has a valid value.\n    database = dbname or database\n\n    dsn_uri = None\n\n    # Treat the database argument as a DSN alias only if it matches a configured alias\n    # todo why is port tested but not socket?\n    truthy_password = password not in (None, EMPTY_PASSWORD_FLAG_SENTINEL)\n    if (\n        database\n        and \"://\" not in database\n        and not any([user, truthy_password, host, port, login_path])\n        and database in mycli.config.get(\"alias_dsn\", {})\n    ):\n        dsn_alias, database = database, \"\"\n\n    if database and \"://\" in database:\n        dsn_uri, database = database, \"\"\n\n    if dsn_alias:\n        try:\n            dsn_uri = mycli.config[\"alias_dsn\"][dsn_alias]\n        except KeyError:\n            is_valid_scheme, scheme = is_valid_connection_scheme(dsn_alias)\n            if is_valid_scheme:\n                dsn_uri = dsn_alias\n            else:\n                click.secho(\n                    \"Could not find the specified DSN in the config file. Please check the \\\"[alias_dsn]\\\" section in your myclirc.\",\n                    err=True,\n                    fg=\"red\",\n                )\n                sys.exit(1)\n        else:\n            mycli.dsn_alias = dsn_alias\n\n    if dsn_uri:\n        uri = urlparse(dsn_uri)\n        if not database:\n            database = uri.path[1:]  # ignore the leading fwd slash\n        if not user and uri.username is not None:\n            user = unquote(uri.username)\n        # todo: rationalize the behavior of empty-string passwords here\n        if not password and uri.password is not None:\n            password = unquote(uri.password)\n        if not host:\n            host = uri.hostname\n        if not port:\n            port = uri.port\n\n        if uri.query:\n            dsn_params = parse_qs(uri.query)\n        else:\n            dsn_params = {}\n\n        if params := dsn_params.get('ssl'):\n            click.secho(\n                'Warning: The \"ssl\" DSN URI parameter is deprecated and will be removed in a future release. '\n                'Please use the \"ssl_mode\" parameter instead. '\n                f'See issue {ISSUES_URL}/1507',\n                err=True,\n                fg='yellow',\n            )\n            if params[0].lower() == 'true':\n                ssl_mode = 'on'\n        if params := dsn_params.get('ssl_mode'):\n            ssl_mode = ssl_mode or params[0]\n        if params := dsn_params.get('ssl_ca'):\n            ssl_ca = ssl_ca or params[0]\n            ssl_mode = ssl_mode or 'on'\n        if params := dsn_params.get('ssl_capath'):\n            ssl_capath = ssl_capath or params[0]\n            ssl_mode = ssl_mode or 'on'\n        if params := dsn_params.get('ssl_cert'):\n            ssl_cert = ssl_cert or params[0]\n            ssl_mode = ssl_mode or 'on'\n        if params := dsn_params.get('ssl_key'):\n            ssl_key = ssl_key or params[0]\n            ssl_mode = ssl_mode or 'on'\n        if params := dsn_params.get('ssl_cipher'):\n            ssl_cipher = ssl_cipher or params[0]\n            ssl_mode = ssl_mode or 'on'\n        if params := dsn_params.get('tls_version'):\n            tls_version = tls_version or params[0]\n            ssl_mode = ssl_mode or 'on'\n        if params := dsn_params.get('ssl_verify_server_cert'):\n            ssl_verify_server_cert = ssl_verify_server_cert or (params[0].lower() == 'true')\n            ssl_mode = ssl_mode or 'on'\n        if params := dsn_params.get('socket'):\n            socket = socket or params[0]\n        if params := dsn_params.get('keepalive_ticks'):\n            if keepalive_ticks is None:\n                keepalive_ticks = int(params[0])\n        if params := dsn_params.get('character_set'):\n            character_set = character_set or params[0]\n\n    keepalive_ticks = keepalive_ticks if keepalive_ticks is not None else mycli.default_keepalive_ticks\n    ssl_mode = ssl_mode or mycli.ssl_mode  # cli option or config option\n\n    # if there is a mismatch between the ssl_mode value and other sources of ssl config, show a warning\n    # specifically using \"is False\" to not pickup the case where ssl_enable is None (not set by the user)\n    if ssl_enable and ssl_mode == \"off\" or ssl_enable is False and ssl_mode in (\"auto\", \"on\"):\n        click.secho(\n            f\"Warning: The current ssl_mode value of '{ssl_mode}' is overriding the value provided by \"\n            f\"either the --ssl/--no-ssl CLI options or a DSN URI parameter (ssl={ssl_enable}).\",\n            err=True,\n            fg=\"yellow\",\n        )\n\n    # configure SSL if ssl_mode is auto/on or if\n    # ssl_enable = True (from --ssl or a DSN URI) and ssl_mode is None\n    if ssl_mode in (\"auto\", \"on\") or (ssl_enable and ssl_mode is None):\n        if socket and ssl_mode == 'auto':\n            ssl = None\n        else:\n            ssl = {\n                \"mode\": ssl_mode,\n                \"enable\": ssl_enable,\n                \"ca\": ssl_ca and os.path.expanduser(ssl_ca),\n                \"cert\": ssl_cert and os.path.expanduser(ssl_cert),\n                \"key\": ssl_key and os.path.expanduser(ssl_key),\n                \"capath\": ssl_capath,\n                \"cipher\": ssl_cipher,\n                \"tls_version\": tls_version,\n                \"check_hostname\": ssl_verify_server_cert,\n            }\n            # remove empty ssl options\n            ssl = {k: v for k, v in ssl.items() if v is not None}\n    else:\n        ssl = None\n\n    if ssh_config_host:\n        ssh_config = read_ssh_config(ssh_config_path).lookup(ssh_config_host)\n        ssh_host = ssh_host if ssh_host else ssh_config.get(\"hostname\")\n        ssh_user = ssh_user if ssh_user else ssh_config.get(\"user\")\n        if ssh_config.get(\"port\") and ssh_port == 22:\n            # port has a default value, overwrite it if it's in the config\n            ssh_port = int(ssh_config.get(\"port\"))\n        ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get(\"identityfile\", [None])[0]\n\n    ssh_key_filename = ssh_key_filename and os.path.expanduser(ssh_key_filename)\n    # Merge init-commands: global, DSN-specific, then CLI\n    init_cmds: list[str] = []\n    # 1) Global init-commands\n    global_section = mycli.config.get(\"init-commands\", {})\n    for _, val in global_section.items():\n        if isinstance(val, (list, tuple)):\n            init_cmds.extend(val)\n        elif val:\n            init_cmds.append(val)\n    # 2) DSN-specific init-commands\n    if dsn_alias:\n        alias_section = mycli.config.get(\"alias_dsn.init-commands\", {})\n        if dsn_alias in alias_section:\n            val = alias_section.get(dsn_alias)\n            if isinstance(val, (list, tuple)):\n                init_cmds.extend(val)\n            elif val:\n                init_cmds.append(val)\n    # 3) CLI-provided init_command\n    if init_command:\n        init_cmds.append(init_command)\n\n    combined_init_cmd = \"; \".join(cmd.strip() for cmd in init_cmds if cmd)\n\n    # --show-warnings / --no-show-warnings\n    if show_warnings:\n        mycli.show_warnings = show_warnings\n\n    if use_keyring_cli_opt is not None and use_keyring_cli_opt.lower() == 'reset':\n        use_keyring = True\n        reset_keyring = True\n    elif use_keyring_cli_opt is None:\n        use_keyring = str_to_bool(mycli.config['main'].get('use_keyring', 'False'))\n        reset_keyring = False\n    else:\n        use_keyring = str_to_bool(use_keyring_cli_opt)\n        reset_keyring = False\n\n    # todo: removeme after a period of transition\n    for tup in [\n        ('client', 'prompt', 'prompt', 'main', 'prompt'),\n        ('client', 'pager', 'pager', 'main', 'pager'),\n        ('client', 'skip-pager', 'skip-pager', 'main', 'enable_pager'),\n        # this is a white lie, because default_character_set can actually be read from the package config\n        ('client', 'default-character-set', 'default-character-set', 'connection', 'default_character_set'),\n        # local-infile can be read from both sections\n        ('mysqld', 'local-infile', 'local-infile', 'connection', 'default_local_infile'),\n        ('client', 'local-infile', 'local-infile', 'connection', 'default_local_infile'),\n        ('mysqld', 'loose-local-infile', 'loose-local-infile', 'connection', 'default_local_infile'),\n        ('client', 'loose-local-infile', 'loose-local-infile', 'connection', 'default_local_infile'),\n        # todo: in the future we should add default_port, etc, but only in .myclirc\n        # they are currently ignored in my.cnf\n        ('mysqld', 'default_socket', 'socket', 'connection', 'default_socket'),\n        ('client', 'ssl-ca', 'ssl-ca', 'connection', 'default_ssl_ca'),\n        ('client', 'ssl-cert', 'ssl-cert', 'connection', 'default_ssl_cert'),\n        ('client', 'ssl-key', 'ssl-key', 'connection', 'default_ssl_key'),\n        ('client', 'ssl-cipher', 'ssl-cipher', 'connection', 'default_ssl_cipher'),\n        ('client', 'ssl-verify-server-cert', 'ssl-verify-server-cert', 'connection', 'default_ssl_verify_server_cert'),\n    ]:\n        (\n            mycnf_section_name,\n            mycnf_item_name,\n            printable_mycnf_item_name,\n            myclirc_section_name,\n            myclirc_item_name,\n        ) = tup\n        if str_to_bool(mycli.config['main'].get('my_cnf_transition_done', 'False')):\n            break\n        if (\n            mycli.my_cnf[mycnf_section_name].get(mycnf_item_name) is None\n            and mycli.my_cnf[mycnf_section_name].get(mycnf_item_name.replace('-', '_')) is None\n        ):\n            continue\n        user_section = mycli.config_without_package_defaults.get(myclirc_section_name, {})\n        if user_section.get(myclirc_item_name) is None:\n            cnf_value = mycli.my_cnf[mycnf_section_name].get(mycnf_item_name)\n            if cnf_value is None:\n                cnf_value = mycli.my_cnf[mycnf_section_name].get(mycnf_item_name.replace('-', '_'))\n            click.secho(\n                dedent(\n                    f\"\"\"\n                    Reading configuration from my.cnf files is deprecated.\n                    See {ISSUES_URL}/1490 .\n                    The cause of this message is the following in a my.cnf file without a corresponding\n                    ~/.myclirc entry:\n\n                        [{mycnf_section_name}]\n                        {printable_mycnf_item_name} = {cnf_value}\n\n                    To suppress this message, remove the my.cnf item add or the following to ~/.myclirc:\n\n                        [{myclirc_section_name}]\n                        {myclirc_item_name} = <value>\n\n                    The ~/.myclirc setting will take precedence.  In the future, the my.cnf will be ignored.\n\n                    Values are documented at {REPO_URL}/blob/main/mycli/myclirc .  An\n                    empty <value> is generally accepted.\n\n                    To ignore all of this, set\n\n                        [main]\n                        my_cnf_transition_done = True\n\n                    in ~/.myclirc.\n\n                    --------\n\n                    \"\"\"\n                ),\n                err=True,\n                fg='yellow',\n            )\n\n    mycli.connect(\n        database=database,\n        user=user,\n        passwd=password,\n        host=host,\n        port=port,\n        socket=socket,\n        local_infile=local_infile,\n        ssl=ssl,\n        ssh_user=ssh_user,\n        ssh_host=ssh_host,\n        ssh_port=ssh_port,\n        ssh_password=ssh_password,\n        ssh_key_filename=ssh_key_filename,\n        init_command=combined_init_cmd,\n        unbuffered=unbuffered,\n        character_set=character_set,\n        use_keyring=use_keyring,\n        reset_keyring=reset_keyring,\n        keepalive_ticks=keepalive_ticks,\n    )\n\n    if combined_init_cmd:\n        click.echo(f\"Executing init-command: {combined_init_cmd}\", err=True)\n\n    mycli.logger.debug(\"Launch Params: \\n\\tdatabase: %r\\tuser: %r\\thost: %r\\tport: %r\", database, user, host, port)\n\n    #  --execute argument\n    if execute:\n        try:\n            if batch_format == 'csv':\n                mycli.main_formatter.format_name = 'csv'\n                if execute.endswith(r'\\G'):\n                    execute = execute[:-2]\n            elif batch_format == 'tsv':\n                mycli.main_formatter.format_name = 'tsv'\n                if execute.endswith(r'\\G'):\n                    execute = execute[:-2]\n            elif batch_format == 'table':\n                mycli.main_formatter.format_name = 'ascii'\n                if execute.endswith(r'\\G'):\n                    execute = execute[:-2]\n            else:\n                mycli.main_formatter.format_name = 'tsv'\n\n            mycli.run_query(execute, checkpoint=checkpoint)\n            sys.exit(0)\n        except Exception as e:\n            click.secho(str(e), err=True, fg=\"red\")\n            sys.exit(1)\n\n    def dispatch_batch_statements(statements: str, batch_counter: int) -> None:\n        if batch_counter:\n            # this is imperfect if the first line of input has multiple statements\n            if batch_format == 'csv':\n                mycli.main_formatter.format_name = 'csv-noheader'\n            elif batch_format == 'tsv':\n                mycli.main_formatter.format_name = 'tsv_noheader'\n            elif batch_format == 'table':\n                mycli.main_formatter.format_name = 'ascii'\n            else:\n                mycli.main_formatter.format_name = 'tsv'\n        else:\n            if batch_format == 'csv':\n                mycli.main_formatter.format_name = 'csv'\n            elif batch_format == 'tsv':\n                mycli.main_formatter.format_name = 'tsv'\n            elif batch_format == 'table':\n                mycli.main_formatter.format_name = 'ascii'\n            else:\n                mycli.main_formatter.format_name = 'tsv'\n\n        warn_confirmed: bool | None = True\n        if not noninteractive and mycli.destructive_warning and is_destructive(mycli.destructive_keywords, statements):\n            try:\n                # this seems to work, even though we are reading from stdin above\n                sys.stdin = open(\"/dev/tty\")\n                # bug: the prompt will not be visible if stdout is redirected\n                warn_confirmed = confirm_destructive_query(mycli.destructive_keywords, statements)\n            except (IOError, OSError):\n                mycli.logger.warning(\"Unable to open TTY as stdin.\")\n                sys.exit(1)\n        try:\n            if warn_confirmed:\n                if throttle and batch_counter >= 1:\n                    sleep(throttle)\n                mycli.run_query(statements, checkpoint=checkpoint, new_line=True)\n        except Exception as e:\n            click.secho(str(e), err=True, fg=\"red\")\n            sys.exit(1)\n\n    if sys.stdin.isatty():\n        mycli.run_cli()\n    else:\n        stdin = click.get_text_stream(\"stdin\")\n        statements = ''\n        line_counter = 0\n        batch_counter = 0\n        for stdin_text in stdin:\n            line_counter += 1\n            if line_counter > MAX_MULTILINE_BATCH_STATEMENT:\n                click.secho(\n                    f'Saw single input statement greater than {MAX_MULTILINE_BATCH_STATEMENT} lines; assuming a parsing error.',\n                    err=True,\n                    fg=\"red\",\n                )\n                sys.exit(1)\n            statements += stdin_text\n            try:\n                tokens = sqlglot.tokenize(statements, read='mysql')\n                if not tokens:\n                    continue\n                # we don't handle changing the delimiter within the batch input\n                if tokens[-1].text == ';':\n                    dispatch_batch_statements(statements, batch_counter)\n                    batch_counter += 1\n                    statements = ''\n                    line_counter = 0\n            except sqlglot.errors.TokenError:\n                continue\n        if statements:\n            dispatch_batch_statements(statements, batch_counter)\n        sys.exit(0)\n    mycli.close()\n\n\ndef need_completion_refresh(queries: str) -> bool:\n    \"\"\"Determines if the completion needs a refresh by checking if the sql\n    statement is an alter, create, drop or change db.\"\"\"\n    for query in sqlparse.split(queries):\n        try:\n            first_token = query.split()[0]\n            if first_token.lower() in (\"alter\", \"create\", \"use\", \"\\\\r\", \"\\\\u\", \"connect\", \"drop\", \"rename\"):\n                return True\n        except Exception:\n            return False\n    return False\n\n\ndef need_completion_reset(queries: str) -> bool:\n    \"\"\"Determines if the statement is a database switch such as 'use' or '\\\\u'.\n    When a database is changed the existing completions must be reset before we\n    start the completion refresh for the new database.\n    \"\"\"\n    for query in sqlparse.split(queries):\n        try:\n            first_token = query.split()[0]\n            if first_token.lower() in (\"use\", \"\\\\u\"):\n                return True\n        except Exception:\n            return False\n    return False\n\n\ndef is_mutating(status_plain: str | None) -> bool:\n    \"\"\"Determines if the statement is mutating based on the status.\"\"\"\n    if not status_plain:\n        return False\n\n    mutating = {\"insert\", \"update\", \"delete\", \"alter\", \"create\", \"drop\", \"replace\", \"truncate\", \"load\", \"rename\"}\n    return status_plain.split(None, 1)[0].lower() in mutating\n\n\ndef is_select(status_plain: str | None) -> bool:\n    \"\"\"Returns true if the first word in status is 'select'.\"\"\"\n    if not status_plain:\n        return False\n    return status_plain.split(None, 1)[0].lower() == \"select\"\n\n\ndef thanks_picker() -> str:\n    import mycli\n\n    lines: str = \"\"\n    try:\n        with resources.files(mycli).joinpath(\"AUTHORS\").open('r') as f:\n            lines += f.read()\n    except FileNotFoundError:\n        pass\n\n    try:\n        with resources.files(mycli).joinpath(\"SPONSORS\").open('r') as f:\n            lines += f.read()\n    except FileNotFoundError:\n        pass\n\n    contents = []\n    for line in lines.split(\"\\n\"):\n        if m := re.match(r\"^ *\\* (.*)\", line):\n            contents.append(m.group(1))\n    return choice(contents) if contents else 'our sponsors'\n\n\ndef tips_picker() -> str:\n    import mycli\n\n    tips = []\n\n    try:\n        with resources.files(mycli).joinpath('TIPS').open('r') as f:\n            for line in f:\n                if line.startswith(\"#\"):\n                    continue\n                if tip := line.strip():\n                    tips.append(tip)\n    except FileNotFoundError:\n        pass\n\n    return choice(tips) if tips else r'\\? or \"help\" for help!'\n\n\n@prompt_register(\"edit-and-execute-command\")\ndef edit_and_execute(event: KeyPressEvent) -> None:\n    \"\"\"Different from the prompt-toolkit default, we want to have a choice not\n    to execute a query after editing, hence validate_and_handle=False.\"\"\"\n    buff = event.current_buffer\n    buff.open_in_editor(validate_and_handle=False)\n\n\ndef read_ssh_config(ssh_config_path: str):\n    ssh_config = paramiko.config.SSHConfig()\n    try:\n        with open(ssh_config_path) as f:\n            ssh_config.parse(f)\n    except FileNotFoundError as e:\n        click.secho(str(e), err=True, fg=\"red\")\n        sys.exit(1)\n    # Paramiko prior to version 2.7 raises Exception on parse errors.\n    # In 2.7 it has become paramiko.ssh_exception.SSHException,\n    # but let's catch everything for compatibility\n    except Exception as err:\n        click.secho(f\"Could not parse SSH configuration file {ssh_config_path}:\\n{err} \", err=True, fg=\"red\")\n        sys.exit(1)\n    else:\n        return ssh_config\n\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "mycli/myclirc",
    "content": "# vi: ft=dosini\n[main]\n\n# Enable or disable the automatic displaying of warnings (\"SHOW WARNINGS\")\n# after executing a SQL statement when applicable.\nshow_warnings = False\n\n# Enables context sensitive auto-completion. If this is disabled the all\n# possible completions will be listed.\nsmart_completion = True\n\n# Minimum characters typed before offering completion suggestions.\n# Suggestion: 3.\nmin_completion_trigger = 1\n\n# Multi-line mode allows breaking up the sql statements into multiple lines. If\n# this is set to True, then the end of the statements must have a semi-colon.\n# If this is set to False then sql statements can't be split into multiple\n# lines. End of line (return) is considered as the end of the statement.\nmulti_line = False\n\n# Destructive warning mode will alert you before executing a sql statement\n# that may cause harm to the database such as \"drop table\", \"drop database\"\n# or \"shutdown\".\ndestructive_warning = True\n\n# Queries starting with these keywords will activate the destructive warning.\n# UPDATE will not activate the warning if the statement includes a WHERE\n# clause.\ndestructive_keywords = DROP SHUTDOWN DELETE TRUNCATE ALTER UPDATE\n\n# interactive query history location.\nhistory_file = ~/.mycli-history\n\n# log_file location.\nlog_file = ~/.mycli.log\n\n# Default log level. Possible values: \"CRITICAL\", \"ERROR\", \"WARNING\", \"INFO\"\n# and \"DEBUG\". \"NONE\" disables logging.\nlog_level = INFO\n\n# Log every query and its results to a file. Enable this by uncommenting the\n# line below.\n# audit_log = ~/.mycli-audit.log\n\n# Timing of SQL statements and table rendering, or LLM commands.\ntiming = True\n\n# Show the full SQL when running a favorite query. Set to False to hide.\nshow_favorite_query = True\n\n# Beep after long-running queries are completed; 0 to disable.\nbeep_after_seconds = 0\n\n# Table format. Possible values: ascii, ascii_escaped, csv, csv-noheader,\n# csv-tab, csv-tab-noheader, double, fancy_grid, github, grid, html, jira,\n# jsonl, jsonl_escaped, latex, latex_booktabs, mediawiki, minimal, moinmoin,\n# mysql, mysql_unicode, orgtbl, pipe, plain, psql, psql_unicode, rst, simple,\n# sql-insert, sql-update, sql-update-1, sql-update-2, textile, tsv,\n# tsv_noheader, vertical.\n# Recommended: ascii.\ntable_format = ascii\n\n# Redirected otuput format\n# Recommended: csv.\nredirect_format = csv\n\n# How to display the missing value (ie NULL).  Only certain table formats\n# support configuring the missing value.  CSV for example always uses the\n# empty string, and JSON formats use native nulls.\nnull_string = <null>\n\n# How to align numeric data in tabular output: right or left.\nnumeric_alignment = right\n\n# How to display binary values in tabular output: \"hex\", or \"utf8\".  \"utf8\"\n# means attempt to render valid UTF-8 sequences as strings, then fall back\n# to hex rendering if not possible.\nbinary_display = hex\n\n# A command to run after a successful output redirect, with {} to be replaced\n# with the escaped filename.  Mac example: echo {} | pbcopy.  Escaping is not\n# reliable/safe on Windows.\npost_redirect_command =\n\n# Syntax coloring style. Possible values (many support the \"-dark\" suffix):\n# manni, igor, xcode, vim, autumn, vs, rrt, native, perldoc, borland, tango, emacs,\n# friendly, monokai, paraiso, colorful, murphy, bw, pastie, paraiso, trac, default,\n# fruity.\n# Screenshots at https://mycli.net/syntax\n# Can be further modified in [colors]\nsyntax_style = default\n\n# Keybindings: Possible values: emacs, vi.\n# Emacs mode: Ctrl-A is home, Ctrl-E is end. All emacs keybindings are available in the REPL.\n# When Vi mode is enabled you can use modal editing features offered by Vi in the REPL.\nkey_bindings = emacs\n\n# Enabling this option will show the suggestions in a wider menu. Thus more items are suggested.\nwider_completion_menu = False\n\n# MySQL prompt\n# * \\D - full current date, e.g. Sat Feb 14 15:55:48 2026\n# * \\R - current hour in 24-hour time (00–23)\n# * \\r - current hour in 12-hour time (01–12)\n# * \\m - minutes of the current time\n# * \\s - seconds of the current time\n# * \\P - AM/PM\n# * \\d - selected database/schema\n# * \\h - hostname of the server\n# * \\H - shortened hostname of the server\n# * \\p - connection port\n# * \\j - connection socket basename\n# * \\J - full connection socket path\n# * \\k - connection socket basename OR the port\n# * \\K - full connection socket path OR the port\n# * \\T - connection SSL/TLS version\n# * \\t - database vendor (Percona, MySQL, MariaDB, TiDB)\n# * \\u - username\n# * \\w - number of warnings, or \"(none)\" (requires frequent trips to the server)\n# * \\W - number of warnings, or the empty string (requires frequent trips to the server)\n# * \\y - uptime in seconds (requires frequent trips to the server)\n# * \\Y - uptime in words (requires frequent trips to the server)\n# * \\A - DSN alias\n# * \\n - a newline\n# * \\_ - a space\n# * \\\\ - a literal backslash\n# * \\x1b[...m - an ANSI escape sequence (can style with color)\nprompt = '\\t \\u@\\h:\\d> '\nprompt_continuation = '->'\n\n# Use the same prompt format strings to construct a status line in the toolbar,\n# where \\B in the first position refers to the default toolbar showing keystrokes\n# and state.  Example:\n#\n#     toolbar = '\\B\\d \\D'\n#\n# If \\B is included, the additional content will begin on the next line.  More\n# lines can be added with \\n.  If \\B is not included, the customized toolbar\n# can be a single line.  An empty value is the same as the default \"\\B\".  The\n# special literal value \"None\" will suppress the toolbar from appearing.\ntoolbar = ''\n\n# Use the same prompt format strings to construct a terminal tab title.\n# The original XTerm docs call this title the \"window title\", but it now\n# probably refers to a terminal tab.  This title is only updated as frequently\n# as the database is changed.\nterminal_tab_title = ''\n\n# Use the same prompt format strings to construct a terminal window title.\n# The original XTerm docs call this title the \"icon title\", but it now\n# probably refers to a terminal window which contains tabs.  This title is\n# only updated as frequently as the database is changed.\nterminal_window_title = ''\n\n# Use the same prompt format strings to construct a window title in a terminal\n# multiplexer.  Currently only tmux is supported.  This title is only updated\n# as frequently as the database is changed.\nmultiplex_window_title = ''\n\n# Use the same prompt format strings to construct a pane title in a terminal\n# multiplexer.  Currently only tmux is supported.  This title is only updated\n# as frequently as the database is changed.\nmultiplex_pane_title = ''\n\n# Skip intro info on startup and outro info on exit\nless_chatty = False\n\n# Use alias from --login-path instead of host name in prompt\nlogin_path_as_host = False\n\n# Cause result sets to be displayed vertically if they are too wide for the current window,\n# and using normal tabular format otherwise. (This applies to statements terminated by ; or \\G.)\nauto_vertical_output = False\n\n# keyword casing preference. Possible values \"lower\", \"upper\", \"auto\"\nkeyword_casing = auto\n\n# disabled pager on startup\nenable_pager = True\n\n# Choose a specific pager\npager = 'less'\n\n# whether to show verbose warnings about the transition away from reading my.cnf\nmy_cnf_transition_done = False\n\n# Whether to store and retrieve passwords from the system keyring.\n# See the documentation for https://pypi.org/project/keyring/ for your OS.\n# Note that the hostname is considered to be different if short or qualified.\n# This can be overridden with --use-keyring= at the CLI.\n# A password can be reset with --use-keyring=reset at the CLI.\nuse_keyring = False\n\n[search]\n\n# Whether to apply syntax highlighting to the preview window in fuzzy history\n# search.  There is a small performance penalty to enabling this.  The \"pygmentize\"\n# CLI tool must also be available.  The syntax style from the \"syntax_style\"\n# option will be respected, though additional customizations from [colors] will\n# not be applied.\nhighlight_preview = False\n\n[connection]\n\n# character set for connections without --character-set being set\ndefault_character_set = utf8mb4\n\n# whether to enable LOAD DATA LOCAL INFILE for connections without --local-infile being set\ndefault_local_infile = False\n\n# How often to send periodic background pings to the server when input is idle.  Ticks are\n# roughly in seconds, but may be faster.  Set to zero to disable.  Suggestion: 300.\ndefault_keepalive_ticks = 0\n\n# Sets the desired behavior for handling secure connections to the database server.\n# Possible values:\n# auto = SSL is preferred for TCP/IP connections. Will attempt to connect via SSL, but will fall\n#        back to cleartext as needed.  Will not attempt to connect with SSL over local sockets.\n# on   = SSL is required. Will attempt to connect via SSL even on a local socket, and will fail if\n#        a secure connection is not established.\n# off  = do not use SSL. Will fail if the server requires a secure connection.\ndefault_ssl_mode = auto\n\n# SSL CA file for connections without --ssl-ca being set\ndefault_ssl_ca =\n\n# SSL CA directory for connections without --ssl-capath being set\ndefault_ssl_capath =\n\n# SSL X509 cert path for connections without --ssl-cert being set\ndefault_ssl_cert =\n\n# SSL X509 key for connections without --ssl-key being set\ndefault_ssl_key =\n\n# SSL cipher to use for connections without --ssl-cipher being set\ndefault_ssl_cipher =\n\n# whether to verify server's \"Common Name\" in its cert, for connections without\n# --ssl-verify-server-cert being set\ndefault_ssl_verify_server_cert = False\n\n[llm]\n\n# If set to a positive integer, truncate text/binary fields to that width\n# in bytes when sending sample data, to conserve tokens.  Suggestion: 1024.\nprompt_field_truncate = None\n\n# If set to a positive integer, attempt to truncate various sections of LLM\n# prompt input to that number in bytes, to conserve tokens.  Suggestion:\n# 1000000.\nprompt_section_truncate = None\n\n[keys]\n\n# possible values: exit, none\ncontrol_d = exit\n\n# possible values: auto, fzf, reverse_isearch\ncontrol_r = auto\n\n# comma-separated list: toolkit_default, summon, advancing_summon, prefixing_summon, advance, cancel\n#\n# * toolkit_default - ignore other behaviors and use prompt_toolkit's default bindings\n# * summon - when completions are not visible, summon them\n# * advancing_summon - when completions are not visible, summon them _and_ advance in the list\n# * prefixing_summon - when completions are not visible, summon them _and_ insert the common prefix\n# * advance - when completions are visible, advance in the list\n# * cancel - when completions are visible, toggle the list off\ncontrol_space = summon, advance\n\n# comma-separated list: toolkit_default, summon, advancing_summon, prefixing_summon, advance, cancel\ntab = advancing_summon, advance\n\n# How long to wait for an Escape key sequence in vi mode.\n# 0.5 seconds is the prompt_toolkit default, but vi users may find that too long.\n# Shorter values mean that \"Escape\" alone is recognized more quickly.\nvi_ttimeoutlen = 0.1\n\n# How long to wait for an Escape key sequence in Emacs mode.\nemacs_ttimeoutlen = 0.5\n\n# Custom colors for the completion menu, toolbar, etc, with actual support\n# depending on the terminal, and the property being set.\n# Colors: #ffffff, bg:#ffffff, border:#ffffff.\n# Attributes: (no)blink, bold, dim, hidden, inherit, italic, reverse, strike, underline.\n[colors]\ncompletion-menu.completion.current = 'bg:#ffffff #000000'\ncompletion-menu.completion = 'bg:#008888 #ffffff'\ncompletion-menu.meta.completion.current = 'bg:#44aaaa #000000'\ncompletion-menu.meta.completion = 'bg:#448888 #ffffff'\ncompletion-menu.multi-column-meta = 'bg:#aaffff #000000'\nscrollbar.arrow = 'bg:#003333'\nscrollbar = 'bg:#00aaaa'\nselected = '#ffffff bg:#6666aa'\nsearch = '#ffffff bg:#4444aa'\nsearch.current = '#ffffff bg:#44aa44'\nbottom-toolbar = 'bg:#222222 #aaaaaa'\nbottom-toolbar.off = 'bg:#222222 #888888'\nbottom-toolbar.on = 'bg:#222222 #ffffff'\nsearch-toolbar = 'noinherit bold'\nsearch-toolbar.text = 'nobold'\nsystem-toolbar = 'noinherit bold'\narg-toolbar = 'noinherit bold'\narg-toolbar.text = 'nobold'\nbottom-toolbar.transaction.valid = 'bg:#222222 #00ff5f bold'\nbottom-toolbar.transaction.failed = 'bg:#222222 #ff005f bold'\nprompt = ''\ncontinuation = ''\n\n# style classes for colored table output\noutput.table-separator = \"\"\noutput.header = \"#00ff5f bold\"\noutput.odd-row = \"\"\noutput.even-row = \"\"\noutput.null = \"#808080\"\noutput.status = \"\"\noutput.status.warning-count = \"\"\noutput.timing = \"\"\n\n# SQL syntax highlighting overrides\n# sql.comment = 'italic #408080'\n# sql.comment.multi-line = ''\n# sql.comment.single-line = ''\n# sql.comment.optimizer-hint = ''\n# sql.escape = 'border:#FF0000'\n# sql.keyword = 'bold #008000'\n# sql.datatype = 'nobold #B00040'\n# sql.literal = ''\n# sql.literal.date = ''\n# sql.symbol = ''\n# sql.quoted-schema-object = ''\n# sql.quoted-schema-object.escape = ''\n# sql.constant = '#880000'\n# sql.function = '#0000FF'\n# sql.variable = '#19177C'\n# sql.number = '#666666'\n# sql.number.binary = ''\n# sql.number.float = ''\n# sql.number.hex = ''\n# sql.number.integer = ''\n# sql.operator = '#666666'\n# sql.punctuation = ''\n# sql.string = '#BA2121'\n# sql.string.double-quouted = ''\n# sql.string.escape = 'bold #BB6622'\n# sql.string.single-quoted = ''\n# sql.whitespace = ''\n\n# Favorite queries.\n# You can add your favorite queries here. They will be available in the\n# REPL when you type `\\f` or `\\f <query_name>`.\n[favorite_queries]\n# example = \"SELECT * FROM example_table WHERE id = 1\"\n\n# Initial commands to execute when connecting to any database.\n[init-commands]\n# read_only = \"SET SESSION TRANSACTION READ ONLY\"\n\n\n# Use the -d option to reference a DSN.\n# Special characters in passwords and other strings can be escaped with URL encoding.\n[alias_dsn]\n# example_dsn = mysql://[user[:password]@][host][:port][/dbname]\n\n# Initial commands to execute when connecting to a DSN alias.\n[alias_dsn.init-commands]\n# Define one or more SQL statements per alias (semicolon-separated).\n# example_dsn = \"SET sql_select_limit=1000; SET time_zone='+00:00'\"\n"
  },
  {
    "path": "mycli/packages/__init__.py",
    "content": ""
  },
  {
    "path": "mycli/packages/checkup.py",
    "content": "import importlib.metadata\nimport json\nimport os\nimport shutil\nimport sys\nimport urllib.error\nimport urllib.request\n\nfrom mycli.constants import REPO_URL\n\nPYPI_API_BASE = 'https://pypi.org/pypi'\n\n\ndef pypi_api_fetch(fragment: str) -> dict:\n    fragment = fragment.lstrip('/')\n    url = f'{PYPI_API_BASE}/{fragment}'\n    try:\n        with urllib.request.urlopen(url, timeout=5) as response:\n            return json.loads(response.read().decode('utf8'))\n    except urllib.error.URLError:\n        print(f'Failed to connect to PyPi on {url}', file=sys.stderr)\n        return {}\n\n\ndef _dependencies_checkup() -> None:\n    print('\\n### Key Python dependencies:\\n')\n    for dependency in [\n        'cli_helpers',\n        'click',\n        'prompt_toolkit',\n        'pymysql',\n        'tabulate',\n    ]:\n        try:\n            installed_version = importlib.metadata.version(dependency)\n        except importlib.metadata.PackageNotFoundError:\n            installed_version = None\n        pypi_profile = pypi_api_fetch(f'/{dependency}/json')\n        latest_version = pypi_profile.get('info', {}).get('version', None)\n        print(f'{dependency} version {installed_version} (latest {latest_version})')\n\n\ndef _executables_checkup() -> None:\n    print('\\n### External executables:\\n')\n    for executable in [\n        'less',\n        'fzf',\n        'pygmentize',\n    ]:\n        if shutil.which(executable):\n            print(f'The \"{executable}\" executable was found — good!')\n        else:\n            print(f'The recommended \"{executable}\" executable was not found — some functionality will suffer.')\n\n\ndef _environment_checkup() -> None:\n    print('\\n### Environment variables:\\n')\n    for variable in [\n        'EDITOR',\n        'VISUAL',\n    ]:\n        if value := os.environ.get(variable):\n            print(f'The ${variable} environment variable was set to \"{value}\" — good!')\n        else:\n            print(f'The ${variable} environment variable was not set — some functionality will suffer.')\n\n\ndef _configuration_checkup(mycli) -> None:\n    did_output_missing = False\n    did_output_unsupported = False\n    did_output_deprecated = False\n\n    indent = '    '\n    transitions = {\n        f'{indent}[main]\\n{indent}default_character_set': f'{indent}[connection]\\n{indent}default_character_set',\n        f'{indent}[main]\\n{indent}ssl_mode': f'{indent}[connection]\\n{indent}default_ssl_mode',\n    }\n    reverse_transitions = {v: k for k, v in transitions.items()}\n\n    if not list(mycli.config.keys()):\n        print('\\n### Missing file:\\n')\n        print('The local ~/,myclirc is missing or empty.\\n')\n        did_output_missing = True\n    else:\n        for section_name in mycli.config:\n            if section_name not in mycli.config_without_package_defaults:\n                if not did_output_missing:\n                    print('\\n### Missing in user ~/.myclirc:\\n')\n                print(f'The entire section:\\n\\n{indent}[{section_name}]\\n')\n                did_output_missing = True\n                continue\n            for item_name in mycli.config[section_name]:\n                transition_key = f'{indent}[{section_name}]\\n{indent}{item_name}'\n                if transition_key in reverse_transitions:\n                    continue\n                if item_name not in mycli.config_without_package_defaults[section_name]:\n                    if not did_output_missing:\n                        print('\\n### Missing in user ~/.myclirc:\\n')\n                    print(f'The item:\\n\\n{indent}[{section_name}]\\n{indent}{item_name} =\\n')\n                    did_output_missing = True\n\n        for section_name in mycli.config_without_package_defaults:\n            if section_name not in mycli.config_without_user_options:\n                if not did_output_unsupported:\n                    print('\\n### Unsupported in user ~/.myclirc:\\n')\n                did_output_unsupported = True\n                print(f'The entire section:\\n\\n{indent}[{section_name}]\\n')\n                continue\n            for item_name in mycli.config_without_package_defaults[section_name]:\n                if section_name == 'colors' and item_name.startswith('sql.'):\n                    # these are commented out in the package myclirc\n                    continue\n                if section_name in [\n                    'favorite_queries',\n                    'init-commands',\n                    'alias_dsn',\n                    'alias_dsn.init-commands',\n                ]:\n                    # these are free-entry sections, so a comparison per item is not meaningful\n                    continue\n                transition_key = f'{indent}[{section_name}]\\n{indent}{item_name}'\n                if transition_key in transitions:\n                    continue\n                if item_name not in mycli.config_without_user_options[section_name]:\n                    if not did_output_unsupported:\n                        print('\\n### Unsupported in user ~/.myclirc:\\n')\n                    print(f'The item:\\n\\n{indent}[{section_name}]\\n{indent}{item_name} =\\n')\n                    did_output_unsupported = True\n\n        for section_name in mycli.config_without_package_defaults:\n            if section_name not in mycli.config_without_user_options:\n                continue\n            for item_name in mycli.config_without_package_defaults[section_name]:\n                if section_name == 'colors' and item_name.startswith('sql.'):\n                    # these are commented out in the package myclirc\n                    continue\n                transition_key = f'{indent}[{section_name}]\\n{indent}{item_name}'\n                if transition_key in transitions:\n                    if not did_output_deprecated:\n                        print('\\n### Deprecated in user ~/.myclirc:\\n')\n                    transition_value = transitions[transition_key]\n                    print(f'It is recommended to transition:\\n\\n{transition_key}\\n\\nto\\n\\n{transition_value}\\n')\n                    did_output_deprecated = True\n\n    if did_output_missing or did_output_unsupported or did_output_deprecated:\n        print(f'For more info on supported features, see the commentary and defaults at:\\n\\n    * {REPO_URL}/blob/main/mycli/myclirc\\n')\n    else:\n        print('\\n### Configuration:\\n')\n        print('User configuration all up to date!\\n')\n\n\ndef do_checkup(mycli) -> None:\n    _dependencies_checkup()\n    _executables_checkup()\n    _environment_checkup()\n    _configuration_checkup(mycli)\n"
  },
  {
    "path": "mycli/packages/completion_engine.py",
    "content": "import functools\nimport re\nfrom typing import Any, Literal\n\nimport sqlparse\nfrom sqlparse.sql import Comparison, Identifier, Token, Where\n\nfrom mycli.packages.parseutils import extract_tables, find_prev_keyword, last_word\nfrom mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS\nfrom mycli.packages.special.main import parse_special_command\n\nsqlparse.engine.grouping.MAX_GROUPING_DEPTH = None  # type: ignore[assignment]\nsqlparse.engine.grouping.MAX_GROUPING_TOKENS = None  # type: ignore[assignment]\n\n_ENUM_VALUE_RE = re.compile(\n    r\"(?P<lhs>(?:`[^`]+`|[\\w$]+)(?:\\.(?:`[^`]+`|[\\w$]+))?)\\s*=\\s*$\",\n    re.IGNORECASE,\n)\n\n# missing because not binary\n#   BETWEEN\n#   CASE\n# missing because parens are used\n#   IN(), and others\n# unary operands might need to have another set\n#   not, !, ~\n# arrow operators only take a literal on the right\n#   and so might need different treatment\n# := might also need a different context\n# sqlparse would call these identifiers, so they are excluded\n#   xor\n# these are hitting the recursion guard, and so not completing after\n# so we might as well leave them out:\n#   is, 'is not', mod\n# sqlparse might also parse \"not null\" together\n# should also verify how sqlparse parses every space-containing case\nBINARY_OPERANDS = {\n    '&', '>', '>>', '>=', '<', '<>', '!=', '<<', '<=', '<=>', '%',\n    '*', '+', '-', '->', '->>', '/', ':=', '=', '^', 'and', '&&', 'div',\n    'like', 'not like', 'not regexp', 'or', '||', 'regexp', 'rlike',\n    'sounds like', '|',\n}  # fmt: skip\n\n\ndef _enum_value_suggestion(text_before_cursor: str, full_text: str) -> dict[str, Any] | None:\n    match = _ENUM_VALUE_RE.search(text_before_cursor)\n    if not match:\n        return None\n    if is_inside_quotes(text_before_cursor, match.start(\"lhs\")):\n        return None\n\n    lhs = match.group(\"lhs\")\n    if \".\" in lhs:\n        parent, column = lhs.split(\".\", 1)\n    else:\n        parent, column = None, lhs\n\n    return {\n        \"type\": \"enum_value\",\n        \"tables\": extract_tables(full_text),\n        \"column\": column,\n        \"parent\": parent,\n    }\n\n\ndef _charset_suggestion(tokens: list[Token]) -> list[dict[str, str]] | None:\n    token_values = [token.value.lower() for token in tokens if token.value]\n\n    if len(token_values) >= 2 and token_values[-1] == 'set' and token_values[-2] == 'character':\n        return [{'type': 'character_set'}]\n    if len(token_values) >= 3 and token_values[-2] == 'set' and token_values[-3] == 'character':\n        return [{'type': 'character_set'}]\n    if len(token_values) >= 5 and token_values[-1] == 'using' and token_values[-4] == 'convert':\n        return [{'type': 'character_set'}]\n    if len(token_values) >= 6 and token_values[-2] == 'using' and token_values[-5] == 'convert':\n        return [{'type': 'character_set'}]\n    if len(token_values) >= 1 and token_values[-1] == 'collate':\n        return [{'type': 'collation'}]\n\n    return None\n\n\ndef _is_where_or_having(token: Token | None) -> bool:\n    return bool(token and token.value and token.value.lower() in (\"where\", \"having\"))\n\n\ndef _find_doubled_backticks(text: str) -> list[int]:\n    length = len(text)\n    doubled_backtick_positions: list[int] = []\n    backtick = '`'\n    two_backticks = backtick + backtick\n\n    if two_backticks not in text:\n        return doubled_backtick_positions\n\n    for index in range(0, length):\n        ch = text[index]\n        if ch != backtick:\n            index += 1\n            continue\n        if index + 1 < length and text[index + 1] == backtick:\n            doubled_backtick_positions.append(index)\n            doubled_backtick_positions.append(index + 1)\n            index += 2\n            continue\n        index += 1\n\n    return doubled_backtick_positions\n\n\n@functools.lru_cache(maxsize=128)\ndef is_inside_quotes(text: str, pos: int) -> Literal[False, 'single', 'double', 'backtick']:\n    in_single = False\n    in_double = False\n    in_backticks = False\n    escaped = False\n    doubled_backtick_positions = []\n    single_quote = \"'\"\n    double_quote = '\"'\n    backtick = '`'\n    backslash = '\\\\'\n\n    # scanning the string twice seems to be needed to handle doubled backticks\n    doubled_backtick_positions = _find_doubled_backticks(text)\n\n    length = len(text)\n    if pos < 0:\n        pos = length + pos\n        pos = max(pos, 0)\n    pos = min(length, pos)\n\n    # optimization\n    up_to_pos = text[:pos]\n    if backtick not in up_to_pos and single_quote not in up_to_pos and double_quote not in up_to_pos:\n        return False\n\n    for index in range(0, pos):\n        ch = text[index]\n        if index in doubled_backtick_positions:\n            index += 1\n            continue\n        if escaped and (in_double or in_single):\n            escaped = False\n            index += 1\n            continue\n        if ch == backslash and (in_double or in_single):\n            escaped = True\n            index += 1\n            continue\n        if ch == backtick and not in_double and not in_single:\n            in_backticks = not in_backticks\n        elif ch == single_quote and not in_double and not in_backticks:\n            in_single = not in_single\n        elif ch == double_quote and not in_single and not in_backticks:\n            in_double = not in_double\n        index += 1\n\n    if in_single:\n        return 'single'\n    elif in_double:\n        return 'double'\n    elif in_backticks:\n        return 'backtick'\n    else:\n        return False\n\n\ndef suggest_type(full_text: str, text_before_cursor: str) -> list[dict[str, Any]]:\n    \"\"\"Takes the full_text that is typed so far and also the text before the\n    cursor to suggest completion type and scope.\n\n    Returns a tuple with a type of entity ('table', 'column' etc) and a scope.\n    A scope for a column category will be a list of tables.\n    \"\"\"\n\n    word_before_cursor = last_word(text_before_cursor, include=\"many_punctuations\")\n\n    identifier: Identifier | None = None\n\n    # here should be removed once sqlparse has been fixed\n    try:\n        # If we've partially typed a word then word_before_cursor won't be an empty\n        # string. In that case we want to remove the partially typed string before\n        # sending it to the sqlparser. Otherwise the last token will always be the\n        # partially typed string which renders the smart completion useless because\n        # it will always return the list of keywords as completion.\n        if word_before_cursor:\n            if word_before_cursor.endswith(\"(\") or word_before_cursor.startswith(\"\\\\\"):\n                parsed = sqlparse.parse(text_before_cursor)\n            else:\n                parsed = sqlparse.parse(text_before_cursor[: -len(word_before_cursor)])\n\n                # word_before_cursor may include a schema qualification, like\n                # \"schema_name.partial_name\" or \"schema_name.\", so parse it\n                # separately\n                p = sqlparse.parse(word_before_cursor)[0]\n\n                if p.tokens and isinstance(p.tokens[0], Identifier):\n                    identifier = p.tokens[0]\n        else:\n            parsed = sqlparse.parse(text_before_cursor)\n    except (TypeError, AttributeError):\n        return [{\"type\": \"keyword\"}]\n\n    if len(parsed) > 1:\n        # Multiple statements being edited -- isolate the current one by\n        # cumulatively summing statement lengths to find the one that bounds the\n        # current position\n        current_pos = len(text_before_cursor)\n        stmt_start, stmt_end = 0, 0\n\n        for statement in parsed:\n            stmt_len = len(str(statement))\n            stmt_start, stmt_end = stmt_end, stmt_end + stmt_len\n\n            if stmt_end >= current_pos:\n                text_before_cursor = full_text[stmt_start:current_pos]\n                full_text = full_text[stmt_start:]\n                break\n\n    elif parsed:\n        # A single statement\n        statement = parsed[0]\n    else:\n        # The empty string\n        statement = None\n\n    # Check for special commands and handle those separately\n    if statement:\n        # Be careful here because trivial whitespace is parsed as a statement,\n        # but the statement won't have a first token\n        tok1 = statement.token_first()\n        # lenient because \\. will parse as two tokens\n        if tok1 and tok1.value.startswith('\\\\'):\n            return suggest_special(text_before_cursor)\n        elif tok1:\n            if tok1.value.lower() in SPECIAL_COMMANDS:\n                return suggest_special(text_before_cursor)\n\n    last_token = statement and statement.token_prev(len(statement.tokens))[1] or \"\"\n\n    # todo: unsure about empty string as identifier\n    return suggest_based_on_last_token(last_token, text_before_cursor, word_before_cursor, full_text, identifier or Identifier(''))\n\n\ndef suggest_special(text: str) -> list[dict[str, Any]]:\n    text = text.lstrip()\n    cmd, _separator, _arg = parse_special_command(text)\n\n    if cmd == text:\n        # Trying to complete the special command itself\n        return [{\"type\": \"special\"}]\n\n    if cmd in (\"\\\\u\", \"\\\\r\"):\n        return [{\"type\": \"database\"}]\n\n    if cmd.lower() in ('use', 'connect'):\n        return [{'type': 'database'}]\n\n    if cmd in (r'\\T', r'\\Tr'):\n        return [{\"type\": \"table_format\"}]\n\n    if cmd.lower() in ('tableformat', 'redirectformat'):\n        return [{\"type\": \"table_format\"}]\n\n    if cmd in [\"\\\\f\", \"\\\\fs\", \"\\\\fd\"]:\n        return [{\"type\": \"favoritequery\"}]\n\n    if cmd in [\"\\\\dt\", \"\\\\dt+\"]:\n        return [\n            {\"type\": \"table\", \"schema\": []},\n            {\"type\": \"view\", \"schema\": []},\n            {\"type\": \"schema\"},\n        ]\n    elif cmd.lower() in [\n        r'\\.',\n        'source',\n        r'\\o',\n        r'\\once',\n        r'tee',\n    ]:\n        return [{\"type\": \"file_name\"}]\n    # todo: why is \\edit case-sensitive?\n    elif cmd in [\n        r'\\e',\n        r'\\edit',\n    ]:\n        return [{\"type\": \"file_name\"}]\n    if cmd in [\"\\\\llm\", \"\\\\ai\"]:\n        return [{\"type\": \"llm\"}]\n\n    return [{\"type\": \"keyword\"}, {\"type\": \"special\"}]\n\n\ndef suggest_based_on_last_token(\n    token: str | Token | None,\n    text_before_cursor: str,\n    word_before_cursor: str | None,\n    full_text: str,\n    identifier: Identifier,\n) -> list[dict[str, Any]]:\n\n    # don't suggest anything inside a string or number\n    if word_before_cursor:\n        # todo: example where this fails: completing on COLLATE with string \"0900\"\n        if re.match(r'^[\\d\\.]', word_before_cursor[0]):\n            return []\n        # more efficient if no space was typed yet in the string\n        if word_before_cursor[0] in ('\"', \"'\"):\n            return []\n        # less efficient, but handles all cases\n        # in fact, this is quite slow, but not as slow as offering completions!\n        # faster would be to peek inside the Pygments lexer run by prompt_toolkit -- how?\n        if is_inside_quotes(text_before_cursor, -1) in ['single', 'double']:\n            return []\n\n    try:\n        # todo: pass in the complete list of tokens to avoid multiple parsing passes\n        parsed = sqlparse.parse(text_before_cursor)[0]\n        tokens_wo_space = [x for x in parsed.tokens if x.ttype != sqlparse.tokens.Token.Text.Whitespace]\n    except (AttributeError, IndexError, ValueError, sqlparse.exceptions.SQLParseError):\n        parsed = sqlparse.sql.Statement()\n        tokens_wo_space = []\n\n    if isinstance(token, str):\n        token_v = token.lower()\n    elif isinstance(token, Comparison):\n        # If 'token' is a Comparison type such as\n        # 'select * FROM abc a JOIN def d ON a.id = d.'. Then calling\n        # token.value on the comparison type will only return the lhs of the\n        # comparison. In this case a.id. So we need to do token.tokens to get\n        # both sides of the comparison and pick the last token out of that\n        # list.\n        token_v = token.tokens[-1].value.lower()\n    elif isinstance(token, Where):\n        # sqlparse groups all tokens from the where clause into a single token\n        # list. This means that token.value may be something like\n        # 'where foo > 5 and '. We need to look \"inside\" token.tokens to handle\n        # suggestions in complicated where clauses correctly.\n        #\n        # This logic also needs to look even deeper in to the WHERE clause.\n        # We recapitulate some transcoding suggestions here, but cannot\n        # recapitulate the entire logic of this function.\n        where_tokens = [x for x in token.tokens if x.ttype != sqlparse.tokens.Token.Text.Whitespace]\n        if transcoding_suggestion := _charset_suggestion(where_tokens):\n            return transcoding_suggestion\n\n        original_text = text_before_cursor\n        prev_keyword, text_before_cursor = find_prev_keyword(text_before_cursor)\n        enum_suggestion = _enum_value_suggestion(original_text, full_text)\n        fallback = suggest_based_on_last_token(prev_keyword, text_before_cursor, None, full_text, identifier)\n        if enum_suggestion and _is_where_or_having(prev_keyword):\n            return [enum_suggestion] + fallback\n        return fallback\n    elif token is None:\n        return [{\"type\": \"keyword\"}]\n    else:\n        token_v = token.value.lower()\n\n    if not token:\n        return [{\"type\": \"keyword\"}, {\"type\": \"special\"}]\n\n    if token_v == \"*\":\n        return [{\"type\": \"keyword\"}]\n\n    if token_v.endswith(\"(\"):\n        if parsed.tokens and isinstance(parsed.tokens[-1], Where):\n            # Four possibilities:\n            #  1 - Parenthesized clause like \"WHERE foo AND (\"\n            #        Suggest columns/functions\n            #  2 - Function call like \"WHERE foo(\"\n            #        Suggest columns/functions\n            #  3 - Subquery expression like \"WHERE EXISTS (\"\n            #        Suggest keywords, in order to do a subquery\n            #  4 - Subquery OR array comparison like \"WHERE foo = ANY(\"\n            #        Suggest columns/functions AND keywords. (If we wanted to be\n            #        really fancy, we could suggest only array-typed columns)\n\n            column_suggestions = suggest_based_on_last_token(\"where\", text_before_cursor, None, full_text, identifier)\n\n            # Check for a subquery expression (cases 3 & 4)\n            where = parsed.tokens[-1]\n            _idx, prev_tok = where.token_prev(len(where.tokens) - 1)\n\n            if isinstance(prev_tok, Comparison):\n                # e.g. \"SELECT foo FROM bar WHERE foo = ANY(\"\n                prev_tok = prev_tok.tokens[-1]\n\n            prev_tok = prev_tok.value.lower()\n            if prev_tok == \"exists\":\n                return [{\"type\": \"keyword\"}]\n            else:\n                return column_suggestions\n\n        # Get the token before the parens\n        idx, prev_tok = parsed.token_prev(len(parsed.tokens) - 1)\n        if prev_tok and prev_tok.value and prev_tok.value.lower() == \"using\":\n            # tbl1 INNER JOIN tbl2 USING (col1, col2)\n            tables = extract_tables(full_text)\n\n            # suggest columns that are present in more than one table\n            return [{\"type\": \"column\", \"tables\": tables, \"drop_unique\": True}]\n        elif parsed.tokens and parsed.token_first().value.lower() == \"select\":\n            # If the lparen is preceeded by a space chances are we're about to\n            # do a sub-select.\n            if last_word(text_before_cursor, \"all_punctuations\").startswith(\"(\"):\n                return [{\"type\": \"keyword\"}]\n        elif parsed.tokens and parsed.token_first().value.lower() == \"show\":\n            return [{\"type\": \"show\"}]\n\n        # We're probably in a function argument list\n        return [{\"type\": \"column\", \"tables\": extract_tables(full_text)}]\n    elif token_v in (\"call\"):\n        return [{\"type\": \"procedure\", \"schema\": []}]\n    elif token_v in ('set') and len(tokens_wo_space) >= 3 and tokens_wo_space[-3].value.lower() == 'character':\n        return [{'type': 'character_set'}]\n    elif token_v in ('set') and len(tokens_wo_space) >= 2 and tokens_wo_space[-2].value.lower() == 'character':\n        return [{'type': 'character_set'}]\n    elif token_v in (\"set\", \"order by\", \"distinct\"):\n        return [{\"type\": \"column\", \"tables\": extract_tables(full_text)}]\n    elif token_v == \"as\":\n        # Don't suggest anything for an alias\n        return []\n    elif token_v in (\"show\"):\n        return [{\"type\": \"show\"}]\n    elif token_v in (\"to\",):\n        if parsed.tokens and parsed.token_first().value.lower() == \"change\":\n            return [{\"type\": \"change\"}]\n        else:\n            return [{\"type\": \"user\"}]\n    elif token_v in (\"user\", \"for\"):\n        return [{\"type\": \"user\"}]\n    elif token_v in ('collate'):\n        return [{'type': 'collation'}]\n    # some duplication with _charset_suggestion()\n    elif token_v in ('using') and len(tokens_wo_space) >= 5 and tokens_wo_space[-5].value.lower() == 'convert':\n        return [{'type': 'character_set'}]\n    elif token_v in ('using') and len(tokens_wo_space) >= 4 and tokens_wo_space[-4].value.lower() == 'convert':\n        return [{'type': 'character_set'}]\n    elif token_v in (\"select\", \"where\", \"having\"):\n        # Check for a table alias or schema qualification\n        parent = (identifier and identifier.get_parent_name()) or []\n\n        tables = extract_tables(full_text)\n        if parent:\n            tables = [t for t in tables if identifies(parent, *t)]\n            return [\n                {\"type\": \"column\", \"tables\": tables},\n                {\"type\": \"table\", \"schema\": parent},\n                {\"type\": \"view\", \"schema\": parent},\n                {\"type\": \"function\", \"schema\": parent},\n            ]\n        elif is_inside_quotes(text_before_cursor, -1) == 'backtick':\n            # todo: this should be revised, since we complete too exuberantly within\n            # backticks, including keywords\n            aliases = [alias or table for (schema, table, alias) in tables]\n            return [\n                {\"type\": \"column\", \"tables\": tables},\n                {\"type\": \"function\", \"schema\": []},\n                {\"type\": \"alias\", \"aliases\": aliases},\n                {\"type\": \"keyword\"},\n            ]\n        else:\n            aliases = [alias or table for (schema, table, alias) in tables]\n            return [\n                {\"type\": \"column\", \"tables\": tables},\n                {\"type\": \"function\", \"schema\": []},\n                {\"type\": \"introducer\"},\n                {\"type\": \"alias\", \"aliases\": aliases},\n            ]\n    elif (\n        (token_v.endswith(\"join\") and isinstance(token, Token) and token.is_keyword)\n        or (token_v in (\"copy\", \"from\", \"update\", \"into\", \"describe\", \"truncate\", \"desc\", \"explain\"))\n        # todo: the create table regex fails to match on multi-statement queries, which\n        # suggests a bug above in suggest_type()\n        or (token_v == \"like\" and re.match(r'^\\s*create\\s+table\\s', full_text, re.IGNORECASE))\n    ):\n        schema = (identifier and identifier.get_parent_name()) or []\n\n        # Suggest tables from either the currently-selected schema or the\n        # public schema if no schema has been specified\n        suggest = [{\"type\": \"table\", \"schema\": schema}]\n\n        if not schema:\n            # Suggest schemas\n            suggest.append({\"type\": \"database\"})\n\n        # Only tables can be TRUNCATED, otherwise suggest views\n        if token_v != \"truncate\":\n            suggest.append({\"type\": \"view\", \"schema\": schema})\n\n        return suggest\n\n    elif token_v in (\"table\", \"view\", \"function\"):\n        # E.g. 'DROP FUNCTION <funcname>', 'ALTER TABLE <tablname>'\n        rel_type = token_v\n        schema = (identifier and identifier.get_parent_name()) or []\n        if schema:\n            return [{\"type\": rel_type, \"schema\": schema}]\n        else:\n            return [{\"type\": \"schema\"}, {\"type\": rel_type, \"schema\": []}]\n    elif token_v == \"on\":\n        tables = extract_tables(full_text)  # [(schema, table, alias), ...]\n        parent = (identifier and identifier.get_parent_name()) or []\n        if parent:\n            # \"ON parent.<suggestion>\"\n            # parent can be either a schema name or table alias\n            tables = [t for t in tables if identifies(parent, *t)]\n            return [\n                {\"type\": \"column\", \"tables\": tables},\n                {\"type\": \"table\", \"schema\": parent},\n                {\"type\": \"view\", \"schema\": parent},\n                {\"type\": \"function\", \"schema\": parent},\n            ]\n        else:\n            # ON <suggestion>\n            # Use table alias if there is one, otherwise the table name\n            aliases = [alias or table for (schema, table, alias) in tables]\n            suggest = [{\"type\": \"alias\", \"aliases\": aliases}]\n\n            # The lists of 'aliases' could be empty if we're trying to complete\n            # a GRANT query. eg: GRANT SELECT, INSERT ON <tab>\n            # In that case we just suggest all schemata and all tables.\n            if not aliases:\n                suggest.append({\"type\": \"database\"})\n                suggest.append({\"type\": \"table\", \"schema\": parent})\n            return suggest\n\n    elif token_v in (\"database\", \"template\"):\n        # \"\\c <db\", \"use <db>\", \"DROP DATABASE <db>\",\n        # \"CREATE DATABASE <newdb> WITH TEMPLATE <db>\"\n        return [{\"type\": \"database\"}]\n\n    elif is_inside_quotes(text_before_cursor, -1) in ['single', 'double']:\n        return []\n\n    elif token_v.endswith(\",\") or token_v in BINARY_OPERANDS:\n        original_text = text_before_cursor\n        prev_keyword, text_before_cursor = find_prev_keyword(text_before_cursor)\n        enum_suggestion = _enum_value_suggestion(original_text, full_text)\n\n        # guard against non-progressing parser rewinds, which can otherwise\n        # recurse forever on some operator shapes.\n        if prev_keyword and text_before_cursor.rstrip() != original_text.rstrip():\n            fallback = suggest_based_on_last_token(prev_keyword, text_before_cursor, None, full_text, identifier)\n        else:\n            # perhaps this fallback should include columns\n            fallback = [{\"type\": \"keyword\"}]\n\n        if enum_suggestion and _is_where_or_having(prev_keyword):\n            return [enum_suggestion] + fallback\n        return fallback\n\n    else:\n        return [{\"type\": \"keyword\"}]\n\n\ndef identifies(\n    identifier: Any,\n    schema: str | None,\n    table: str,\n    alias: str,\n) -> bool:\n    if identifier == alias:\n        return True\n    if identifier == table:\n        return True\n    if schema and identifier == (schema + \".\" + table):\n        return True\n    return False\n"
  },
  {
    "path": "mycli/packages/filepaths.py",
    "content": "import os\nimport platform\n\nDEFAULT_SOCKET_DIRS: list[str] = []\nif os.name == \"posix\":\n    if platform.system() == \"Darwin\":\n        DEFAULT_SOCKET_DIRS = [\"/tmp\"]\n    else:\n        DEFAULT_SOCKET_DIRS = [\"/var/run\", \"/var/lib\"]\n\n\ndef list_path(root_dir: str) -> list[str]:\n    \"\"\"List directory if exists.\n\n    :param root_dir: str\n    :return: list\n\n    \"\"\"\n    files = []\n    dirs = []\n    if not os.path.isdir(root_dir):\n        return []\n    for name in sorted(os.listdir(root_dir)):\n        if name.startswith('.'):\n            continue\n        elif os.path.isdir(name):\n            dirs.append(f'{name}/')\n        # if .sql is too restrictive it can be made configurable with some effort\n        elif name.lower().endswith('.sql'):\n            files.append(name)\n    return files + dirs\n\n\ndef complete_path(curr_dir: str, last_dir: str) -> str:\n    \"\"\"Return the path to complete that matches the last entered component.\n\n    If the last entered component is ~, expanded path would not\n    match, so return all of the available paths.\n\n    :param curr_dir: str\n    :param last_dir: str\n    :return: str\n\n    \"\"\"\n    if not last_dir or curr_dir.startswith(last_dir):\n        return curr_dir\n    elif last_dir == \"~\":\n        return os.path.join(last_dir, curr_dir)\n    else:\n        return ''\n\n\ndef parse_path(root_dir: str) -> tuple[str, str, int]:\n    \"\"\"Split path into head and last component for the completer.\n\n    Also return position where last component starts.\n\n    :param root_dir: str path\n    :return: tuple of (string, string, int)\n\n    \"\"\"\n    base_dir, last_dir, position = \"\", \"\", 0\n    if root_dir:\n        base_dir, last_dir = os.path.split(root_dir)\n        position = -len(last_dir) if last_dir else 0\n    return base_dir, last_dir, position\n\n\ndef suggest_path(root_dir: str) -> list[str]:\n    \"\"\"List all files and subdirectories in a directory.\n\n    If the directory is not specified, suggest root directory,\n    user directory, current and parent directory.\n\n    :param root_dir: string: directory to list\n    :return: list\n\n    \"\"\"\n    if not root_dir:\n        return [\n            os.path.abspath(os.sep),\n            \"~\",\n            os.curdir,\n            os.pardir,\n            *list_path(os.curdir),\n        ]\n\n    if root_dir[0] not in ('/', '~') and root_dir[0:1] != './':\n        return list_path(os.curdir)\n\n    if \"~\" in root_dir:\n        root_dir = os.path.expanduser(root_dir)\n\n    if not os.path.exists(root_dir):\n        root_dir, _ = os.path.split(root_dir)\n\n    return list_path(root_dir)\n\n\ndef dir_path_exists(path: str) -> bool:\n    \"\"\"Check if the directory path exists for a given file.\n\n    For example, for a file /home/user/.cache/mycli/log, check if\n    /home/user/.cache/mycli exists.\n\n    :param str path: The file path.\n    :return: Whether or not the directory path exists.\n\n    \"\"\"\n    return os.path.exists(os.path.dirname(path))\n\n\ndef guess_socket_location() -> str | None:\n    \"\"\"Try to guess the location of the default mysql socket file.\"\"\"\n    socket_dirs = filter(os.path.exists, DEFAULT_SOCKET_DIRS)\n    for directory in socket_dirs:\n        for r, dirs, files in os.walk(directory, topdown=True):\n            for filename in files:\n                name, ext = os.path.splitext(filename)\n                if name.startswith(\"mysql\") and name != \"mysqlx\" and ext in (\".socket\", \".sock\"):\n                    return os.path.join(r, filename)\n            dirs[:] = [d for d in dirs if d.startswith(\"mysql\")]\n    return None\n"
  },
  {
    "path": "mycli/packages/hybrid_redirection.py",
    "content": "import functools\nimport logging\nimport warnings\n\nwith warnings.catch_warnings():\n    # for sqlglot v29.0.1\n    warnings.filterwarnings(\n        'ignore',\n        message=r'sqlglot\\[rs\\] is deprecated',\n        category=UserWarning,\n        module='sqlglot',\n    )\n    import sqlglot\n\nfrom mycli.compat import WIN\nfrom mycli.packages.special.delimitercommand import DelimiterCommand\n\nlogger = logging.getLogger(__name__)\ndelimiter_command = DelimiterCommand()\n\n\ndef find_token_indices(tokens: list[sqlglot.Token]) -> dict[str, list[int]]:\n    token_indices: dict[str, list[int]] = {\n        'raw_dollar': [],\n        'true_dollar': [],\n        'angle_bracket': [],\n        'pipe': [],\n    }\n\n    for i, tok in enumerate(tokens):\n        if tok.token_type == sqlglot.TokenType.VAR and tok.text == '$':\n            token_indices['raw_dollar'].append(i)\n            continue\n        if tok.token_type == sqlglot.TokenType.GT and (i - 1) in token_indices['raw_dollar']:\n            token_indices['angle_bracket'].append(i)\n            continue\n        if tok.token_type == sqlglot.TokenType.PIPE and (i - 1) in token_indices['raw_dollar']:\n            token_indices['pipe'].append(i)\n            continue\n\n    for i in token_indices['raw_dollar']:\n        if (i + 1) in token_indices['angle_bracket'] or (i + 1) in token_indices['pipe']:\n            token_indices['true_dollar'].append(i)\n\n    return token_indices\n\n\ndef find_sql_part(\n    command: str,\n    tokens: list[sqlglot.Token],\n    true_dollar_indices: list[int],\n):\n    leftmost_dollar_pos = tokens[true_dollar_indices[0]].start\n    sql_part = command[0:leftmost_dollar_pos].strip().removesuffix(delimiter_command.current).rstrip()\n    try:\n        statements = sqlglot.parse(sql_part, read='mysql')\n    except sqlglot.errors.ParseError:\n        return ''\n    if len(statements) != 1:\n        # buglet: the statement count doesn't respect a custom delimiter\n        return ''\n    return sql_part\n\n\ndef find_command_tokens(\n    tokens: list[sqlglot.Token],\n    true_dollar_indices: list[int],\n) -> list[sqlglot.Token]:\n    command_part_tokens = []\n\n    for i, tok in enumerate(tokens):\n        if i < true_dollar_indices[0]:\n            continue\n        if i in true_dollar_indices:\n            continue\n        command_part_tokens.append(tok)\n\n    if command_part_tokens:\n        _operator = command_part_tokens.pop(0)\n\n    return command_part_tokens\n\n\ndef find_file_tokens(\n    tokens: list[sqlglot.Token],\n    angle_bracket_indices: list[int],\n) -> tuple[list[sqlglot.Token], int, str | None]:\n    file_part_tokens: list[sqlglot.Token] = []\n    file_part_index = len(tokens)\n\n    if not angle_bracket_indices:\n        return file_part_tokens, file_part_index, None\n\n    file_part_tokens = tokens[angle_bracket_indices[-1] :]\n    file_part_index = angle_bracket_indices[-1]\n\n    file_operator_part = file_part_tokens.pop(0).text\n    if file_operator_part == '>' and file_part_tokens[0].token_type == sqlglot.TokenType.GT:\n        file_part_tokens.pop(0)\n        file_operator_part = '>>'\n\n    return file_part_tokens, file_part_index, file_operator_part\n\n\ndef assemble_tokens(tokens: list[sqlglot.Token]) -> str:\n    assembled_string = ' ' * (tokens[-1].end + 10)\n    for tok in tokens:\n        if tok.token_type == sqlglot.TokenType.IDENTIFIER:\n            text = f'\"{tok.text}\"'\n            offset = 2\n        elif tok.token_type == sqlglot.TokenType.STRING:\n            text = f\"'{tok.text}'\"\n            offset = 2\n        else:\n            text = tok.text\n            offset = 0\n        assembled_string = assembled_string[0 : tok.start] + text + assembled_string[tok.end + offset :]\n    return assembled_string.strip().removesuffix(delimiter_command.current).rstrip()\n\n\ndef invalid_shell_part(\n    file_part: str | None,\n    command_part: str | None,\n) -> bool:\n    if file_part and ' ' in file_part:\n        return True\n\n    if file_part and '>' in file_part:\n        return True\n\n    if not file_part and not command_part:\n        return True\n\n    return False\n\n\n@functools.lru_cache(maxsize=1)\ndef get_redirect_components(command: str) -> tuple[str | None, str | None, str | None, str | None]:\n    \"\"\"Get the parts of a hybrid shell-style redirect command.\"\"\"\n\n    try:\n        tokens = sqlglot.tokenize(command)\n    except sqlglot.errors.TokenError:\n        return None, None, None, None\n\n    token_indices = find_token_indices(tokens)\n\n    if not token_indices['true_dollar']:\n        return None, None, None, None\n\n    if len(token_indices['angle_bracket']) > 1:\n        return None, None, None, None\n\n    if WIN and len(token_indices['pipe']) > 1:\n        # how to give better feedback here?\n        return None, None, None, None\n\n    if token_indices['angle_bracket'] and token_indices['pipe']:\n        if token_indices['pipe'][-1] > token_indices['angle_bracket'][-1]:\n            return None, None, None, None\n\n    sql_part = find_sql_part(\n        command,\n        tokens,\n        token_indices['true_dollar'],\n    )\n    if not sql_part:\n        return None, None, None, None\n\n    (\n        file_part_tokens,\n        file_part_index,\n        file_operator_part,\n    ) = find_file_tokens(\n        tokens,\n        token_indices['angle_bracket'],\n    )\n\n    command_part_tokens = find_command_tokens(\n        tokens[0:file_part_index],\n        token_indices['true_dollar'],\n    )\n\n    if file_part_tokens:\n        file_part = assemble_tokens(file_part_tokens)\n    else:\n        file_part = None\n\n    if command_part_tokens:\n        command_part = assemble_tokens(command_part_tokens)\n    else:\n        command_part = None\n\n    if invalid_shell_part(file_part, command_part):\n        return None, None, None, None\n\n    logger.debug('redirect parse sql_part: \"{}\"'.format(sql_part))\n    logger.debug('redirect parse command_part: \"{}\"'.format(command_part))\n    logger.debug('redirect parse file_operator_part: \"{}\"'.format(file_operator_part))\n    logger.debug('redirect parse file_part: \"{}\"'.format(file_part))\n\n    return sql_part, command_part, file_operator_part, file_part\n\n\ndef is_redirect_command(command: str) -> bool:\n    \"\"\"Is this a shell-style redirect to command or file?\n\n    :param command: string\n\n    \"\"\"\n    sql_part, _command_part, _file_operator_part, _file_part = get_redirect_components(command)\n    return bool(sql_part)\n"
  },
  {
    "path": "mycli/packages/paramiko_stub/__init__.py",
    "content": "\"\"\"A module to import instead of paramiko when it is not available (to avoid\nchecking for paramiko all over the place).\n\nWhen paramiko is first invoked, this simply shuts down mycli, telling the\nuser they either have to install paramiko or should not use SSH features.\n\n\"\"\"\n\n\nclass Paramiko:\n    def __getattr__(self, name: str) -> None:\n        import sys\n        from textwrap import dedent\n\n        print(\n            dedent(\"\"\"\n            To enable certain SSH features you need to install ssh extras:\n\n                pip install 'mycli[ssh]'\n\n            or\n\n                pip install paramiko sshtunnel\n\n            This is required for the following command-line arguments:\n\n                --list-ssh-config\n                --ssh-config-host\n                --ssh-host\n            \"\"\"),\n            file=sys.stderr,\n        )\n        sys.exit(1)\n\n\nparamiko = Paramiko()\n"
  },
  {
    "path": "mycli/packages/parseutils.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom typing import Any, Generator, Literal\nimport warnings\n\nimport sqlparse\nfrom sqlparse.sql import Function, Identifier, IdentifierList, Token, TokenList\nfrom sqlparse.tokens import DML, Keyword, Punctuation\n\nwith warnings.catch_warnings():\n    # for sqlglot v29.0.1\n    warnings.filterwarnings(\n        'ignore',\n        message=r'sqlglot\\[rs\\] is deprecated',\n        category=UserWarning,\n        module='sqlglot',\n    )\n    import sqlglot\n\nsqlparse.engine.grouping.MAX_GROUPING_DEPTH = None  # type: ignore[assignment]\nsqlparse.engine.grouping.MAX_GROUPING_TOKENS = None  # type: ignore[assignment]\n\ncleanup_regex: dict[str, re.Pattern] = {\n    # This matches only alphanumerics and underscores.\n    \"alphanum_underscore\": re.compile(r\"(\\w+)$\"),\n    # This matches everything except spaces, parens, colon, and comma\n    \"many_punctuations\": re.compile(r\"([^():,\\s]+)$\"),\n    # This matches everything except spaces, parens, colon, comma, and period\n    \"most_punctuations\": re.compile(r\"([^\\.():,\\s]+)$\"),\n    # This matches everything except a space.\n    \"all_punctuations\": re.compile(r\"([^\\s]+)$\"),\n}\n\n\ndef is_valid_connection_scheme(text: str) -> tuple[bool, str | None]:\n    # exit early if the text does not resemble a DSN URI\n    if \"://\" not in text:\n        return False, None\n    scheme = text.split(\"://\")[0]\n    if scheme not in (\"mysql\", \"mysqlx\", \"tcp\", \"socket\", \"ssh\"):\n        return False, scheme\n    else:\n        return True, None\n\n\ndef last_word(\n    text: str,\n    include: Literal[\n        'alphanum_underscore',\n        'many_punctuations',\n        'most_punctuations',\n        'all_punctuations',\n    ] = 'alphanum_underscore',\n) -> str:\n    r\"\"\"\n    Find the last word in a sentence.\n\n    >>> last_word('abc')\n    'abc'\n    >>> last_word(' abc')\n    'abc'\n    >>> last_word('')\n    ''\n    >>> last_word(' ')\n    ''\n    >>> last_word('abc ')\n    ''\n    >>> last_word('abc def')\n    'def'\n    >>> last_word('abc def ')\n    ''\n    >>> last_word('abc def;')\n    ''\n    >>> last_word('bac $def')\n    'def'\n    >>> last_word('bac $def', include='most_punctuations')\n    '$def'\n    >>> last_word('bac \\def', include='most_punctuations')\n    '\\\\\\\\def'\n    >>> last_word('bac \\def;', include='most_punctuations')\n    '\\\\\\\\def;'\n    >>> last_word('bac::def', include='most_punctuations')\n    'def'\n    \"\"\"\n\n    if not text:  # Empty string\n        return \"\"\n\n    if text[-1].isspace():\n        return \"\"\n    else:\n        regex = cleanup_regex[include]\n        matches = regex.search(text)\n        if matches:\n            return matches.group(0)\n        else:\n            return \"\"\n\n\n# This code is borrowed from sqlparse example script.\n# <url>\ndef is_subselect(parsed: TokenList) -> bool:\n    if not parsed.is_group:\n        return False\n    for item in parsed.tokens:\n        if item.ttype is DML and item.value.upper() in (\"SELECT\", \"INSERT\", \"UPDATE\", \"CREATE\", \"DELETE\"):\n            return True\n    return False\n\n\ndef get_last_select(parsed: TokenList) -> TokenList:\n    \"\"\"\n    Takes a parsed sql statement and returns the last select query where applicable.\n\n    The intended use case is for when giving table suggestions based on columns, where\n    we only want to look at the columns from the most recent select. This works for a single\n    select query, or one or more sub queries (the useful part).\n\n    The custom logic is necessary because the typical sqlparse logic for things like finding\n    sub selects (i.e. is_subselect) only works on complete statements, such as:\n\n    * select c1 from t1;\n\n    However when suggesting tables based on columns, we only have partial select statements, i.e.:\n\n    * select c1\n    * select c1 from (select c2)\n\n    So given the above, we must parse them ourselves as they are not viewed as complete statements.\n\n    Returns a TokenList of the last select statement's tokens.\n    \"\"\"\n    select_indexes: list[int] = []\n\n    for token in parsed:\n        if token.match(DML, \"select\"):  # match is case insensitive\n            select_indexes.append(parsed.token_index(token))\n\n    last_select = TokenList()\n\n    if select_indexes:\n        last_select = TokenList(parsed[select_indexes[-1] :])\n\n    return last_select\n\n\ndef extract_from_part(parsed: TokenList, stop_at_punctuation: bool = True) -> Generator[Any, None, None]:\n    tbl_prefix_seen = False\n    for item in parsed.tokens:\n        if tbl_prefix_seen:\n            if is_subselect(item):\n                yield from extract_from_part(item, stop_at_punctuation)\n            elif stop_at_punctuation and item.ttype is Punctuation:\n                return None\n            # Multiple JOINs in the same query won't work properly since\n            # \"ON\" is a keyword and will trigger the next elif condition.\n            # So instead of stooping the loop when finding an \"ON\" skip it\n            # eg: 'SELECT * FROM abc JOIN def ON abc.id = def.abc_id JOIN ghi'\n            elif item.ttype is Keyword and item.value.upper() == \"ON\":\n                tbl_prefix_seen = False\n                continue\n            # An incomplete nested select won't be recognized correctly as a\n            # sub-select. eg: 'SELECT * FROM (SELECT id FROM user'. This causes\n            # the second FROM to trigger this elif condition resulting in a\n            # StopIteration. So we need to ignore the keyword if the keyword\n            # FROM.\n            # Also 'SELECT * FROM abc JOIN def' will trigger this elif\n            # condition. So we need to ignore the keyword JOIN and its variants\n            # INNER JOIN, FULL OUTER JOIN, etc.\n            elif item.ttype is Keyword and (not item.value.upper() == \"FROM\") and (not item.value.upper().endswith(\"JOIN\")):\n                return None\n            else:\n                yield item\n        elif (item.ttype is Keyword or item.ttype is Keyword.DML) and item.value.upper() in (\n            \"COPY\",\n            \"FROM\",\n            \"INTO\",\n            \"UPDATE\",\n            \"TABLE\",\n            \"JOIN\",\n        ):\n            tbl_prefix_seen = True\n        # 'SELECT a, FROM abc' will detect FROM as part of the column list.\n        # So this check here is necessary.\n        elif isinstance(item, IdentifierList):\n            for identifier in item.get_identifiers():\n                if identifier.ttype is Keyword and identifier.value.upper() == \"FROM\":\n                    tbl_prefix_seen = True\n                    break\n\n\ndef extract_table_identifiers(token_stream: Generator[Any, None, None]) -> Generator[tuple[str | None, str, str], None, None]:\n    \"\"\"yields tuples of (schema_name, table_name, table_alias)\"\"\"\n\n    for item in token_stream:\n        if isinstance(item, IdentifierList):\n            for identifier in item.get_identifiers():\n                # Sometimes Keywords (such as FROM ) are classified as\n                # identifiers which don't have the get_real_name() method.\n                try:\n                    schema_name = identifier.get_parent_name()\n                    real_name = identifier.get_real_name()\n                except AttributeError:\n                    continue\n                if real_name:\n                    yield (schema_name, real_name, identifier.get_alias())\n        elif isinstance(item, Identifier):\n            real_name = item.get_real_name()\n            schema_name = item.get_parent_name()\n\n            if real_name:\n                yield (schema_name, real_name, item.get_alias())\n            else:\n                name = item.get_name()\n                yield (None, name, item.get_alias() or name)\n        elif isinstance(item, Function):\n            yield (None, item.get_name(), item.get_name())\n\n\n# extract_tables is inspired from examples in the sqlparse lib.\ndef extract_tables(sql: str) -> list[tuple[str | None, str, str]]:\n    \"\"\"Extract the table names from an SQL statement.\n\n    Returns a list of (schema, table, alias) tuples\n\n    \"\"\"\n    parsed = sqlparse.parse(sql)\n    if not parsed:\n        return []\n\n    # INSERT statements must stop looking for tables at the sign of first\n    # Punctuation. eg: INSERT INTO abc (col1, col2) VALUES (1, 2)\n    # abc is the table name, but if we don't stop at the first lparen, then\n    # we'll identify abc, col1 and col2 as table names.\n    insert_stmt = parsed[0].token_first().value.lower() == \"insert\"\n    stream = extract_from_part(parsed[0], stop_at_punctuation=insert_stmt)\n    return list(extract_table_identifiers(stream))\n\n\ndef extract_columns_from_select(sql: str) -> list[str]:\n    \"\"\"\n    Extract the column names from a select SQL statement.\n\n    Returns a list of columns.\n    \"\"\"\n    parsed = sqlparse.parse(sql)\n    if not parsed:\n        return []\n\n    statement = get_last_select(parsed[0])\n\n    # if there is no select, skip checking for columns\n    if not statement:\n        return []\n\n    columns = []\n\n    # Loops through the tokens (pieces) of the SQL statement.\n    # Once it finds the SELECT token (generally first), it\n    # will then start looking for columns from that point on.\n    # The get_real_name() function returns the real column name\n    # even if an alias is used.\n    found_select = False\n    for token in statement.tokens:\n        if token.ttype is DML and token.value.upper() == 'SELECT':\n            found_select = True\n        elif found_select:\n            if isinstance(token, IdentifierList):\n                # multiple columns\n                for identifier in token.get_identifiers():\n                    if isinstance(identifier, Identifier):\n                        column = identifier.get_real_name()\n                    elif isinstance(identifier, Token):\n                        column = identifier.value\n                    else:\n                        continue\n                    columns.append(column)\n            elif isinstance(token, Identifier):\n                # single column\n                column = token.get_real_name()\n                columns.append(column)\n            elif token.ttype is Keyword:\n                break\n\n            if columns:\n                break\n    return columns\n\n\ndef extract_tables_from_complete_statements(sql: str) -> list[tuple[str | None, str, str | None]]:\n    \"\"\"Extract the table names from a complete and valid series of SQL\n    statements.\n\n    Returns a list of (schema, table, alias) tuples\n\n    \"\"\"\n    # sqlglot chokes entirely on things like \"\\T\" that it doesn't know about,\n    # but is much better at extracting table names from complete statements.\n    # sqlparse can extract the series of statements, though it also doesn't\n    # understand \"\\T\".\n    roughly_parsed = sqlparse.parse(sql)\n    if not roughly_parsed:\n        return []\n\n    finely_parsed = []\n    for rough_statement in roughly_parsed:\n        try:\n            finely_parsed.append(sqlglot.parse_one(str(rough_statement), read='mysql'))\n        except sqlglot.errors.ParseError:\n            pass\n\n    tables = []\n    for fine_statement in finely_parsed:\n        for identifier in fine_statement.find_all(sqlglot.exp.Table):\n            if identifier.parent_select and identifier.parent_select.sql().startswith('WITH'):\n                continue\n            tables.append((\n                None if identifier.db == '' else identifier.db,\n                identifier.name,\n                None if identifier.alias == '' else identifier.alias,\n            ))\n\n    return tables\n\n\ndef find_prev_keyword(sql: str) -> tuple[Token | None, str]:\n    \"\"\"Find the last sql keyword in an SQL statement\n\n    Returns the value of the last keyword, and the text of the query with\n    everything after the last keyword stripped\n    \"\"\"\n    if not sql.strip():\n        return None, \"\"\n\n    parsed = sqlparse.parse(sql)[0]\n    flattened = list(parsed.flatten())\n\n    logical_operators = (\"AND\", \"OR\", \"NOT\", \"BETWEEN\")\n\n    for t in reversed(flattened):\n        if t.value == \"(\" or (t.is_keyword and (t.value.upper() not in logical_operators)):\n            # Find the location of token t in the original parsed statement\n            # We can't use parsed.token_index(t) because t may be a child token\n            # inside a TokenList, in which case token_index thows an error\n            # Minimal example:\n            #   p = sqlparse.parse('select * from foo where bar')\n            #   t = list(p.flatten())[-3]  # The \"Where\" token\n            #   p.token_index(t)  # Throws ValueError: not in list\n            idx = flattened.index(t)\n\n            # Combine the string values of all tokens in the original list\n            # up to and including the target keyword token t, to produce a\n            # query string with everything after the keyword token removed\n            text = \"\".join(tok.value for tok in flattened[: idx + 1])\n            return t, text\n\n    return None, \"\"\n\n\ndef query_starts_with(query: str, prefixes: list[str]) -> bool:\n    \"\"\"Check if the query starts with any item from *prefixes*.\"\"\"\n    prefixes = [prefix.lower() for prefix in prefixes]\n    formatted_sql = sqlparse.format(query.lower(), strip_comments=True)\n    return bool(formatted_sql) and formatted_sql.split()[0] in prefixes\n\n\ndef queries_start_with(queries: str, prefixes: list[str]) -> bool:\n    \"\"\"Check if any queries start with any item from *prefixes*.\"\"\"\n    for query in sqlparse.split(queries):\n        if query and query_starts_with(query, prefixes) is True:\n            return True\n    return False\n\n\ndef query_has_where_clause(query: str) -> bool:\n    \"\"\"Check if the query contains a where-clause.\"\"\"\n    return any(isinstance(token, sqlparse.sql.Where) for token_list in sqlparse.parse(query) for token in token_list)\n\n\n# todo: handle \"UPDATE LOW_PRIORITY\" and \"UPDATE IGNORE\"\ndef query_is_single_table_update(query: str) -> bool:\n    \"\"\"Check if a query is a simple single-table UPDATE.\"\"\"\n    cleaned_query_for_parsing_only = sqlparse.format(query, strip_comments=True)\n    cleaned_query_for_parsing_only = re.sub(r'\\s+', ' ', cleaned_query_for_parsing_only)\n    if not cleaned_query_for_parsing_only:\n        return False\n    parsed = sqlparse.parse(cleaned_query_for_parsing_only)\n    if not parsed:\n        return False\n    statement = parsed[0]\n    return (\n        statement[0].value.lower() == 'update'\n        and statement[1].is_whitespace\n        and ',' not in statement[2].value  # multiple tables\n        and statement[3].is_whitespace\n        and statement[4].value.lower() == 'set'\n    )\n\n\ndef is_destructive(keywords: list[str], queries: str) -> bool:\n    \"\"\"Returns True if any of the queries in *queries* is destructive.\"\"\"\n    for query in sqlparse.split(queries):\n        if not query:\n            continue\n        # subtle: if \"UPDATE\" is one of our keywords AND \"query\" starts with \"UPDATE\"\n        if query_starts_with(query, keywords) and query_starts_with(query, [\"update\"]):\n            if query_has_where_clause(query) and query_is_single_table_update(query):\n                return False\n            else:\n                return True\n        if query_starts_with(query, keywords):\n            return True\n\n    return False\n\n\ndef is_dropping_database(queries: str, dbname: str | None) -> bool:\n    \"\"\"Determine if the query is dropping a specific database.\"\"\"\n    result = False\n    if dbname is None:\n        return False\n\n    def normalize_db_name(db: str) -> str:\n        return db.lower().strip('`\"')\n\n    dbname = normalize_db_name(dbname)\n\n    for query in sqlparse.parse(queries):\n        keywords = [t for t in query.tokens if t.is_keyword]\n        if len(keywords) < 2:\n            continue\n        if keywords[0].normalized in (\"DROP\", \"CREATE\") and keywords[1].value.lower() in (\n            \"database\",\n            \"schema\",\n        ):\n            database_token = next((t for t in query.tokens if isinstance(t, Identifier)), None)\n            if database_token is not None and normalize_db_name(database_token.get_name()) == dbname:\n                result = keywords[0].normalized == \"DROP\"\n    return result\n\n\nif __name__ == \"__main__\":\n    sql = \"select * from (select t. from tabl t\"\n    print(extract_tables(sql))\n"
  },
  {
    "path": "mycli/packages/prompt_utils.py",
    "content": "import sys\n\nimport click\n\nfrom mycli.packages.parseutils import is_destructive\n\n\nclass ConfirmBoolParamType(click.ParamType):\n    name = \"confirmation\"\n\n    def convert(self, value: bool | str, param: click.Parameter | None, ctx: click.Context | None) -> bool:\n        if isinstance(value, bool):\n            return bool(value)\n        value = value.lower()\n        if value in (\"yes\", \"y\"):\n            return True\n        if value in (\"no\", \"n\"):\n            return False\n        self.fail(f'{value} is not a valid boolean', param, ctx)\n\n    def __repr__(self):\n        return \"BOOL\"\n\n\nBOOLEAN_TYPE = ConfirmBoolParamType()\n\n\ndef confirm_destructive_query(keywords: list[str], queries: str) -> bool | None:\n    \"\"\"Check if the query is destructive and prompts the user to confirm.\n\n    Returns:\n    * None if the query is non-destructive or we can't prompt the user.\n    * True if the query is destructive and the user wants to proceed.\n    * False if the query is destructive and the user doesn't want to proceed.\n\n    \"\"\"\n    prompt_text = \"You're about to run a destructive command.\\nDo you want to proceed? (y/n)\"\n    if is_destructive(keywords, queries) and sys.stdin.isatty():\n        return prompt(prompt_text, type=BOOLEAN_TYPE)\n    else:\n        return None\n\n\ndef confirm(*args, **kwargs) -> bool:\n    \"\"\"Prompt for confirmation (yes/no) and handle any abort exceptions.\"\"\"\n    try:\n        return click.confirm(*args, **kwargs)\n    except click.Abort:\n        return False\n\n\ndef prompt(*args, **kwargs) -> bool:\n    \"\"\"Prompt the user for input and handle any abort exceptions.\"\"\"\n    try:\n        return click.prompt(*args, **kwargs)\n    except click.Abort:\n        return False\n"
  },
  {
    "path": "mycli/packages/shortcuts.py",
    "content": "from mycli.sqlexecute import SQLExecute\n\n\ndef server_date(sqlexecute: SQLExecute, quoted: bool = False) -> str:\n    server_date_str = sqlexecute.now().strftime('%Y-%m-%d')\n    if quoted:\n        return f\"'{server_date_str}'\"\n    else:\n        return server_date_str\n\n\ndef server_datetime(sqlexecute: SQLExecute, quoted: bool = False) -> str:\n    server_datetime_str = sqlexecute.now().strftime('%Y-%m-%d %H:%M:%S')\n    if quoted:\n        return f\"'{server_datetime_str}'\"\n    else:\n        return server_datetime_str\n"
  },
  {
    "path": "mycli/packages/special/__init__.py",
    "content": "from mycli.packages.special.dbcommands import (\n    list_databases,\n    list_tables,\n    status,\n)\nfrom mycli.packages.special.iocommands import (\n    clip_command,\n    close_tee,\n    copy_query_to_clipboard,\n    disable_pager,\n    editor_command,\n    flush_pipe_once_if_written,\n    forced_horizontal,\n    get_clip_query,\n    get_current_delimiter,\n    get_editor_query,\n    get_filename,\n    is_expanded_output,\n    is_pager_enabled,\n    is_redirected,\n    is_show_favorite_query,\n    is_timing_enabled,\n    open_external_editor,\n    set_delimiter,\n    set_destructive_keywords,\n    set_expanded_output,\n    set_favorite_queries,\n    set_forced_horizontal_output,\n    set_pager,\n    set_pager_enabled,\n    set_redirect,\n    set_show_favorite_query,\n    set_timing_enabled,\n    split_queries,\n    unset_once_if_written,\n    write_once,\n    write_pipe_once,\n    write_tee,\n)\nfrom mycli.packages.special.llm import (\n    FinishIteration,\n    handle_llm,\n    is_llm_command,\n    sql_using_llm,\n)\nfrom mycli.packages.special.main import (\n    CommandNotFound,\n    execute,\n    parse_special_command,\n    register_special_command,\n    special_command,\n)\n\n__all__: list[str] = [\n    'CommandNotFound',\n    'FinishIteration',\n    'clip_command',\n    'close_tee',\n    'copy_query_to_clipboard',\n    'disable_pager',\n    'editor_command',\n    'execute',\n    'flush_pipe_once_if_written',\n    'forced_horizontal',\n    'get_clip_query',\n    'get_current_delimiter',\n    'get_editor_query',\n    'get_filename',\n    'handle_llm',\n    'is_expanded_output',\n    'is_llm_command',\n    'is_pager_enabled',\n    'is_redirected',\n    'is_timing_enabled',\n    'list_databases',\n    'list_tables',\n    'open_external_editor',\n    'parse_special_command',\n    'register_special_command',\n    'set_delimiter',\n    'set_destructive_keywords',\n    'set_expanded_output',\n    'set_favorite_queries',\n    'set_forced_horizontal_output',\n    'set_pager',\n    'set_pager_enabled',\n    'set_redirect',\n    'set_timing_enabled',\n    'set_show_favorite_query',\n    'is_show_favorite_query',\n    'special_command',\n    'split_queries',\n    'sql_using_llm',\n    'status',\n    'unset_once_if_written',\n    'write_once',\n    'write_pipe_once',\n    'write_tee',\n]\n"
  },
  {
    "path": "mycli/packages/special/dbcommands.py",
    "content": "import logging\nimport os\nimport platform\n\nfrom pymysql import ProgrammingError\nfrom pymysql.cursors import Cursor\n\nfrom mycli import __version__\nfrom mycli.packages.special import iocommands\nfrom mycli.packages.special.main import ArgType, special_command\nfrom mycli.packages.special.utils import format_uptime, get_ssl_version\nfrom mycli.packages.sqlresult import SQLResult\n\nlogger = logging.getLogger(__name__)\n\n\n@special_command(\"\\\\dt\", \"\\\\dt[+] [table]\", \"List or describe tables.\", arg_type=ArgType.PARSED_QUERY, case_sensitive=True)\ndef list_tables(\n    cur: Cursor,\n    arg: str | None = None,\n    _arg_type: ArgType = ArgType.PARSED_QUERY,\n    verbose: bool = False,\n) -> list[SQLResult]:\n    if arg:\n        query = f'SHOW FIELDS FROM {arg}'\n    else:\n        query = \"SHOW TABLES\"\n    logger.debug(query)\n    cur.execute(query)\n    if cur.description:\n        header = [x[0] for x in cur.description]\n    else:\n        return [SQLResult()]\n\n    # Fetch results before potentially executing another query\n    results = list(cur.fetchall()) if verbose and arg else cur\n\n    postamble = ''\n    if verbose and arg:\n        query = f'SHOW CREATE TABLE {arg}'\n        logger.debug(query)\n        cur.execute(query)\n        if one := cur.fetchone():\n            postamble = one[1]\n\n    # todo missing a status line because sqlexecute.get_result was not used\n    return [SQLResult(header=header, rows=results, postamble=postamble)]\n\n\n@special_command(\"\\\\l\", \"\\\\l\", \"List databases.\", arg_type=ArgType.RAW_QUERY, case_sensitive=True)\ndef list_databases(cur: Cursor, **_) -> list[SQLResult]:\n    query = \"SHOW DATABASES\"\n    logger.debug(query)\n    cur.execute(query)\n    if cur.description:\n        header = [x[0] for x in cur.description]\n        # todo missing a status line because sqlexecute.get_result was not used\n        return [SQLResult(header=header, rows=cur)]\n    else:\n        return [SQLResult()]\n\n\n@special_command(\n    \"status\", \"status\", \"Get status information from the server.\", arg_type=ArgType.RAW_QUERY, aliases=[\"\\\\s\"], case_sensitive=True\n)\ndef status(cur: Cursor, **_) -> list[SQLResult]:\n    query = \"SHOW GLOBAL STATUS;\"\n    logger.debug(query)\n    try:\n        cur.execute(query)\n    except ProgrammingError:\n        # Fallback in case query fail, as it does with Mysql 4\n        query = \"SHOW STATUS;\"\n        logger.debug(query)\n        cur.execute(query)\n    status = dict(cur.fetchall())\n\n    query = \"SHOW GLOBAL VARIABLES;\"\n    logger.debug(query)\n    cur.execute(query)\n    variables = dict(cur.fetchall())\n\n    # prepare in case keys are bytes, as with Python 3 and Mysql 4\n    if isinstance(list(variables)[0], bytes) and isinstance(list(status)[0], bytes):\n        variables = {k.decode(\"utf-8\"): v.decode(\"utf-8\") for k, v in variables.items()}\n        status = {k.decode(\"utf-8\"): v.decode(\"utf-8\") for k, v in status.items()}\n\n    # Create output buffers.\n    preamble = []\n    output = []\n    footer = []\n\n    preamble.append(\"--------------\")\n\n    # Output the mycli client information.\n    implementation = platform.python_implementation()\n    version = platform.python_version()\n    client_info = []\n    client_info.append(f'mycli {__version__}')\n    client_info.append(f'running on {implementation} {version}')\n    preamble.append(\" \".join(client_info) + \"\\n\")\n\n    # Build the output that will be displayed as a table.\n    output.append((\"Connection id:\", cur.connection.thread_id()))\n\n    query = \"SELECT DATABASE(), USER();\"\n    logger.debug(query)\n    cur.execute(query)\n    if one := cur.fetchone():\n        db, user = one\n    else:\n        db = \"\"\n        user = \"\"\n\n    output.append((\"Current database:\", db))\n    output.append((\"Current user:\", user))\n\n    if iocommands.is_pager_enabled():\n        if \"PAGER\" in os.environ:\n            pager = os.environ[\"PAGER\"]\n        else:\n            pager = \"System default\"\n    else:\n        pager = \"stdout\"\n    output.append((\"Current pager:\", pager))\n\n    output.append((\"Server version:\", f'{variables[\"version\"]} {variables[\"version_comment\"]}'))\n    output.append((\"Protocol version:\", variables[\"protocol_version\"]))\n    output.append(('SSL/TLS version:', get_ssl_version(cur)))\n\n    if getattr(cur.connection, 'unix_socket', None):\n        host_info = cur.connection.host_info\n    else:\n        host_info = f'{cur.connection.host} via TCP/IP'\n\n    output.append((\"Connection:\", host_info))\n\n    query = \"SELECT @@character_set_server, @@character_set_database, @@character_set_client, @@character_set_connection LIMIT 1;\"\n    logger.debug(query)\n    cur.execute(query)\n    if one := cur.fetchone():\n        charset = one\n    else:\n        charset = (\"\", \"\", \"\", \"\")\n    output.append((\"Server characterset:\", charset[0]))\n    output.append((\"Db characterset:\", charset[1]))\n    output.append((\"Client characterset:\", charset[2]))\n    output.append((\"Conn. characterset:\", charset[3]))\n\n    if getattr(cur.connection, 'unix_socket', None):\n        output.append(('UNIX socket:', variables['socket']))\n    else:\n        output.append(('TCP port:', cur.connection.port))\n\n    if \"Uptime\" in status:\n        output.append((\"Uptime:\", format_uptime(status[\"Uptime\"])))\n\n    if \"Threads_connected\" in status:\n        # Print the current server statistics.\n        stats = []\n        stats.append(f'Connections: {status[\"Threads_connected\"]}')\n        if \"Queries\" in status:\n            stats.append(f'Queries: {status[\"Queries\"]}')\n        stats.append(f'Slow queries: {status[\"Slow_queries\"]}')\n        stats.append(f'Opens: {status[\"Opened_tables\"]}')\n        if \"Flush_commands\" in status:\n            stats.append(f'Flush tables: {status[\"Flush_commands\"]}')\n        stats.append(f'Open tables: {status[\"Open_tables\"]}')\n        if \"Queries\" in status:\n            queries_per_second = int(status[\"Queries\"]) / int(status[\"Uptime\"])\n            stats.append(f'Queries per second avg: {queries_per_second:.3f}')\n        stats_str = \"  \".join(stats)\n        footer.append(\"\\n\" + stats_str)\n\n    footer.append(\"--------------\")\n\n    return [SQLResult(preamble=\"\\n\".join(preamble), rows=output, postamble=\"\\n\".join(footer))]\n"
  },
  {
    "path": "mycli/packages/special/delimitercommand.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom typing import Generator\n\nimport sqlparse\n\nfrom mycli.packages.sqlresult import SQLResult\n\nsqlparse.engine.grouping.MAX_GROUPING_DEPTH = None  # type: ignore[assignment]\nsqlparse.engine.grouping.MAX_GROUPING_TOKENS = None  # type: ignore[assignment]\n\n\nclass DelimiterCommand:\n    def __init__(self) -> None:\n        self._delimiter = \";\"\n\n    def _split(self, sql: str) -> list[str]:\n        \"\"\"Temporary workaround until sqlparse.split() learns about custom\n        delimiters.\"\"\"\n\n        placeholder = \"\\ufffc\"  # unicode object replacement character\n\n        if self._delimiter == \";\":\n            return sqlparse.split(sql)\n\n        # We must find a string that original sql does not contain.\n        # Most likely, our placeholder is enough, but if not, keep looking\n        while placeholder in sql:\n            placeholder += placeholder[0]\n        sql = sql.replace(\";\", placeholder)\n        sql = sql.replace(self._delimiter, \";\")\n\n        split = sqlparse.split(sql)\n\n        return [stmt.replace(\";\", self._delimiter).replace(placeholder, \";\") for stmt in split]\n\n    def queries_iter(self, input_str: str) -> Generator[str, None, None]:\n        \"\"\"Iterate over queries in the input string.\"\"\"\n\n        queries = self._split(input_str)\n        while queries:\n            for sql in queries:\n                delimiter = self._delimiter\n                sql = queries.pop(0)\n                if sql.endswith(delimiter):\n                    trailing_delimiter = True\n                    sql = sql.strip(delimiter)\n                else:\n                    trailing_delimiter = False\n\n                yield sql\n\n                # if the delimiter was changed by the last command,\n                # re-split everything, and if we previously stripped\n                # the delimiter, append it to the end\n                if self._delimiter != delimiter:\n                    combined_statement = \" \".join([sql] + queries)\n                    if trailing_delimiter:\n                        combined_statement += delimiter\n                    queries = self._split(combined_statement)[1:]\n\n    def set(self, arg: str, **_) -> list[SQLResult]:\n        \"\"\"Change delimiter.\n\n        Since `arg` is everything that follows the DELIMITER token\n        after sqlparse (it may include other statements separated by\n        the new delimiter), we want to set the delimiter to the first\n        word of it.\n\n        \"\"\"\n        match = arg and re.search(r\"[^\\s]+\", arg)\n        if not match:\n            message = \"Missing required argument, delimiter\"\n            return [SQLResult(status=message)]\n\n        delimiter = match.group()\n        if delimiter.lower() == \"delimiter\":\n            return [SQLResult(status='Invalid delimiter \"delimiter\"')]\n\n        self._delimiter = delimiter\n        return [SQLResult(status=f'Changed delimiter to {delimiter}')]\n\n    @property\n    def current(self) -> str:\n        return self._delimiter\n"
  },
  {
    "path": "mycli/packages/special/favoritequeries.py",
    "content": "from __future__ import annotations\n\n\nclass FavoriteQueries:\n    section_name: str = \"favorite_queries\"\n\n    usage = \"\"\"\nFavorite Queries are a way to save frequently used queries\nwith a short name.\nExamples:\n\n    # Save a new favorite query.\n    > \\\\fs simple select * from abc where a is not Null;\n\n    # List all favorite queries.\n    > \\\\f\n    ╒════════╤═══════════════════════════════════════╕\n    │ Name   │ Query                                 │\n    ╞════════╪═══════════════════════════════════════╡\n    │ simple │ SELECT * FROM abc where a is not NULL │\n    ╘════════╧═══════════════════════════════════════╛\n\n    # Run a favorite query.\n    > \\\\f simple\n    ╒════════╤════════╕\n    │ a      │ b      │\n    ╞════════╪════════╡\n    │ 日本語  │ 日本語  │\n    ╘════════╧════════╛\n\n    # Delete a favorite query.\n    > \\\\fd simple\n    simple: Deleted.\n\"\"\"\n\n    # Class-level variable, for convenience to use as a singleton.\n    instance: FavoriteQueries\n\n    def __init__(self, config) -> None:\n        self.config = config\n\n    @classmethod\n    def from_config(cls, config):\n        return FavoriteQueries(config)\n\n    def list(self) -> list[str | None]:\n        return self.config.get(self.section_name, [])\n\n    def get(self, name) -> str | None:\n        return self.config.get(self.section_name, {}).get(name, None)\n\n    def save(self, name: str, query: str) -> None:\n        self.config.encoding = \"utf-8\"\n        if self.section_name not in self.config:\n            self.config[self.section_name] = {}\n        self.config[self.section_name][name] = query\n        self.config.write()\n\n    def delete(self, name: str) -> str:\n        try:\n            del self.config[self.section_name][name]\n        except KeyError:\n            return f'{name}: Not Found.'\n        self.config.write()\n        return f'{name}: Deleted.'\n"
  },
  {
    "path": "mycli/packages/special/iocommands.py",
    "content": "from __future__ import annotations\n\nimport locale\nimport logging\nimport os\nimport re\nimport shlex\nimport subprocess\nfrom time import sleep\nfrom typing import Any, Generator\n\nimport click\nfrom configobj import ConfigObj\nfrom prompt_toolkit.formatted_text import ANSI, FormattedText, to_plain_text\nfrom pymysql.cursors import Cursor\nimport pyperclip\nimport sqlparse\n\nfrom mycli.compat import WIN\nfrom mycli.packages.prompt_utils import confirm_destructive_query\nfrom mycli.packages.special.delimitercommand import DelimiterCommand\nfrom mycli.packages.special.favoritequeries import FavoriteQueries\nfrom mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS\nfrom mycli.packages.special.main import ArgType, special_command\nfrom mycli.packages.special.main import execute as special_execute\nfrom mycli.packages.special.utils import handle_cd_command\nfrom mycli.packages.sqlresult import SQLResult\n\nsqlparse.engine.grouping.MAX_GROUPING_DEPTH = None  # type: ignore[assignment]\nsqlparse.engine.grouping.MAX_GROUPING_TOKENS = None  # type: ignore[assignment]\n\nTIMING_ENABLED = False\nuse_expanded_output = False\nforce_horizontal_output = False\nPAGER_ENABLED = True\nSHOW_FAVORITE_QUERY = True\ntee_file = None\nonce_file = None\nwritten_to_once_file = False\nPIPE_ONCE: dict[str, Any] = {\n    'process': None,\n    'stdin': [],\n    'stdout_file': None,\n    'stdout_mode': None,\n}\ndelimiter_command = DelimiterCommand()\nfavoritequeries = FavoriteQueries(ConfigObj())\nDESTRUCTIVE_KEYWORDS: list[str] = []\n\n\ndef set_favorite_queries(config):\n    global favoritequeries\n    favoritequeries = FavoriteQueries(config)\n\n\ndef set_timing_enabled(val: bool) -> None:\n    global TIMING_ENABLED\n    TIMING_ENABLED = val\n\n\ndef set_pager_enabled(val: bool) -> None:\n    global PAGER_ENABLED\n    PAGER_ENABLED = val\n\n\ndef is_pager_enabled() -> bool:\n    return PAGER_ENABLED\n\n\ndef set_show_favorite_query(val: bool) -> None:\n    global SHOW_FAVORITE_QUERY\n    SHOW_FAVORITE_QUERY = val\n\n\ndef is_show_favorite_query() -> bool:\n    return SHOW_FAVORITE_QUERY\n\n\ndef set_destructive_keywords(val: list[str]) -> None:\n    global DESTRUCTIVE_KEYWORDS\n    DESTRUCTIVE_KEYWORDS = val\n\n\n@special_command(\n    \"pager\",\n    \"pager [command]\",\n    \"Set pager to [command]. Print query results via pager.\",\n    arg_type=ArgType.PARSED_QUERY,\n    aliases=[\"\\\\P\"],\n    case_sensitive=True,\n)\ndef set_pager(arg: str, **_) -> list[SQLResult]:\n    if arg:\n        os.environ[\"PAGER\"] = arg\n        msg = f\"PAGER set to {arg}.\"\n        set_pager_enabled(True)\n    else:\n        if \"PAGER\" in os.environ:\n            msg = f\"PAGER set to {os.environ['PAGER']}.\"\n        else:\n            # This uses click's default per echo_via_pager.\n            msg = \"Pager enabled.\"\n        set_pager_enabled(True)\n\n    return [SQLResult(status=msg)]\n\n\n@special_command(\"nopager\", \"nopager\", \"Disable pager; print to stdout.\", arg_type=ArgType.NO_QUERY, aliases=[\"\\\\n\"], case_sensitive=True)\ndef disable_pager() -> list[SQLResult]:\n    set_pager_enabled(False)\n    return [SQLResult(status=\"Pager disabled.\")]\n\n\n@special_command(\"\\\\timing\", \"\\\\timing\", \"Toggle timing of queries.\", arg_type=ArgType.NO_QUERY, aliases=[\"\\\\t\"], case_sensitive=True)\ndef toggle_timing() -> list[SQLResult]:\n    global TIMING_ENABLED\n    TIMING_ENABLED = not TIMING_ENABLED\n    message = \"Timing is \"\n    message += \"on.\" if TIMING_ENABLED else \"off.\"\n    return [SQLResult(status=message)]\n\n\ndef is_timing_enabled() -> bool:\n    return TIMING_ENABLED\n\n\ndef set_expanded_output(val: bool) -> None:\n    global use_expanded_output\n    use_expanded_output = val\n\n\ndef is_expanded_output() -> bool:\n    return use_expanded_output\n\n\ndef set_forced_horizontal_output(val: bool) -> None:\n    global force_horizontal_output\n    force_horizontal_output = val\n\n\ndef forced_horizontal() -> bool:\n    return force_horizontal_output\n\n\n_logger = logging.getLogger(__name__)\n\n\ndef editor_command(command: str) -> bool:\n    \"\"\"\n    Is this an external editor command?\n    :param command: string\n    \"\"\"\n    # It is possible to have `\\e filename` or `SELECT * FROM \\e`. So we check\n    # for both conditions.\n    return (\n        command.strip().endswith(\"\\\\e\")\n        or command.strip().startswith(\"\\\\e \")\n        or command.strip().endswith(\"\\\\edit\")\n        or command.strip().startswith(\"\\\\edit \")\n    )\n\n\ndef get_filename(sql: str) -> str | None:\n    if sql.strip().startswith(\"\\\\e \") or sql.strip().startswith(\"\\\\edit \"):\n        command, _, filename = sql.partition(\" \")\n        return filename.strip() or None\n    else:\n        return None\n\n\ndef get_editor_query(sql: str) -> str:\n    \"\"\"Get the query part of an editor command.\"\"\"\n    sql = sql.strip()\n\n    # The reason we can't simply do .strip('\\e') is that it strips characters,\n    # not a substring. So it'll strip \"e\" in the end of the sql also!\n    # Ex: \"select * from style\\e\" -> \"select * from styl\".\n    pattern = re.compile(r\"(\\\\e$|\\\\edit$)\")\n    while pattern.search(sql):\n        sql = pattern.sub(\"\", sql)\n\n    return sql\n\n\ndef open_external_editor(filename: str | None = None, sql: str | None = None) -> tuple[str, str | None]:\n    \"\"\"Open external editor, wait for the user to type in their query, return\n    the query.\n    \"\"\"\n\n    filename = filename.strip().split(\" \", 1)[0] if filename else None\n    sql = sql or \"\"\n    MARKER = \"# Type your query above this line.\\n\"\n\n    if filename:\n        query = ''\n        message = None\n        click.edit(filename=filename)\n        try:\n            with open(filename, 'r') as f:\n                query = f.read()\n        except IOError:\n            message = f'Error reading file: {filename}'\n        return (query.rstrip('\\n'), message)\n\n    # Populate the editor buffer with the partial sql (if available) and a\n    # placeholder comment.\n    query = click.edit(f\"{sql}\\n\\n{MARKER}\", extension=\".sql\") or ''\n\n    if query:\n        query = query.split(MARKER, 1)[0].rstrip(\"\\n\")\n    else:\n        # Don't return None for the caller to deal with.\n        # Empty string is ok.\n        query = sql\n\n    return (query, None)\n\n\ndef clip_command(command: str) -> bool:\n    \"\"\"Is this a clip command?\n\n    :param command: string\n\n    \"\"\"\n    # It is possible to have `\\clip` or `SELECT * FROM \\clip`. So we check\n    # for both conditions.\n    return command.strip().endswith(\"\\\\clip\") or command.strip().startswith(\"\\\\clip\")\n\n\ndef get_clip_query(sql: str) -> str:\n    \"\"\"Get the query part of a clip command.\"\"\"\n    sql = sql.strip()\n\n    # The reason we can't simply do .strip('\\clip') is that it strips characters,\n    # not a substring. So it'll strip \"c\" in the end of the sql also!\n    pattern = re.compile(r\"(^\\\\clip|\\\\clip$)\")\n    while pattern.search(sql):\n        sql = pattern.sub(\"\", sql)\n\n    return sql\n\n\ndef copy_query_to_clipboard(sql: str | None = None) -> str | None:\n    \"\"\"Send query to the clipboard.\"\"\"\n\n    sql = sql or \"\"\n    message = None\n\n    try:\n        pyperclip.copy(f\"{sql}\")\n    except RuntimeError as e:\n        message = f\"Error clipping query: {e}.\"\n\n    return message\n\n\ndef set_redirect(command_part: str | None, file_operator_part: str | None, file_part: str | None) -> list[tuple]:\n    if command_part:\n        if file_part:\n            PIPE_ONCE['stdout_file'] = file_part\n            PIPE_ONCE['stdout_mode'] = 'w' if file_operator_part == '>' else 'a'\n        return set_pipe_once(command_part)\n    elif file_operator_part == '>':\n        return set_once(f'-o {file_part}')\n    else:\n        return set_once(file_part)\n\n\n@special_command(\"\\\\f\", \"\\\\f [name [args..]]\", \"List or execute favorite queries.\", arg_type=ArgType.PARSED_QUERY, case_sensitive=True)\ndef execute_favorite_query(cur: Cursor, arg: str, **_) -> Generator[SQLResult, None, None]:\n    if arg == \"\":\n        yield from list_favorite_queries()\n\n    # Parse out favorite name and optional substitution parameters\n    name, _separator, arg_str = arg.partition(\" \")\n    args = shlex.split(arg_str)\n\n    query = FavoriteQueries.instance.get(name)\n    if query is None:\n        message = f\"No favorite query: {name}\"\n        yield SQLResult(status=message)\n    else:\n        query, arg_error = subst_favorite_query_args(query, args)\n        if query is None:\n            yield SQLResult(status=arg_error)\n        else:\n            for sql in sqlparse.split(query):\n                sql = sql.rstrip(\";\")\n                preamble = f\"> {sql}\" if is_show_favorite_query() else None\n                is_special = False\n                for special in SPECIAL_COMMANDS:\n                    if sql.lower().startswith(special.lower()):\n                        is_special = True\n                        break\n                if is_special:\n                    for result in special_execute(cur, sql):\n                        result.preamble = preamble\n                        # special_execute() already returns a SQLResult\n                        yield result\n                else:\n                    cur.execute(sql)\n                    if cur.description:\n                        header = [x[0] for x in cur.description]\n                        yield SQLResult(preamble=preamble, header=header, rows=cur)\n                    else:\n                        yield SQLResult(preamble=preamble)\n\n\ndef list_favorite_queries() -> list[SQLResult]:\n    \"\"\"List of all favorite queries.\"\"\"\n\n    header = [\"Name\", \"Query\"]\n    rows = [(r, FavoriteQueries.instance.get(r)) for r in FavoriteQueries.instance.list()]\n\n    if not rows:\n        status = \"\\nNo favorite queries found.\" + FavoriteQueries.instance.usage\n    else:\n        status = \"\"\n    return [SQLResult(header=header, rows=rows, status=status)]\n\n\ndef subst_favorite_query_args(query: str, args: list[str]) -> list[str | None]:\n    \"\"\"replace positional parameters ($1...$N) in query.\"\"\"\n    for idx, val in enumerate(args):\n        subst_var = \"$\" + str(idx + 1)\n        if subst_var not in query:\n            return [None, \"query does not have substitution parameter \" + subst_var + \":\\n  \" + query]\n\n        query = query.replace(subst_var, val)\n\n    match = re.search(r\"\\$\\d+\", query)\n    if match:\n        return [None, \"missing substitution for \" + match.group(0) + \" in query:\\n  \" + query]\n\n    return [query, None]\n\n\n@special_command(\"\\\\fs\", \"\\\\fs <name> <query>\", \"Save a favorite query.\")\ndef save_favorite_query(arg: str, **_) -> list[SQLResult]:\n    \"\"\"Save a new favorite query.\"\"\"\n\n    usage = \"Syntax: \\\\fs name query.\\n\\n\" + FavoriteQueries.instance.usage\n    if not arg:\n        return [SQLResult(status=usage)]\n\n    name, _separator, query = arg.partition(\" \")\n\n    # If either name or query is missing then print the usage and complain.\n    if (not name) or (not query):\n        return [SQLResult(status=f\"{usage} Err: Both name and query are required.\")]\n\n    FavoriteQueries.instance.save(name, query)\n    return [SQLResult(status=\"Saved.\")]\n\n\n@special_command(\"\\\\fd\", \"\\\\fd <name>\", \"Delete a favorite query.\")\ndef delete_favorite_query(arg: str, **_) -> list[SQLResult]:\n    \"\"\"Delete an existing favorite query.\"\"\"\n    usage = \"Syntax: \\\\fd name.\\n\\n\" + FavoriteQueries.instance.usage\n    if not arg:\n        return [SQLResult(status=usage)]\n\n    status = FavoriteQueries.instance.delete(arg)\n\n    return [SQLResult(status=status)]\n\n\n@special_command(\"system\", \"system [-r] <command>\", \"Execute a system shell command (raw mode with -r).\")\ndef execute_system_command(arg: str, **_) -> list[SQLResult]:\n    \"\"\"Execute a system shell command.\"\"\"\n    usage = \"Syntax: system [-r] [command].\\n-r denotes \\\"raw\\\" mode, in which output is passed through without formatting.\"\n\n    IMPLICIT_RAW_MODE_COMMANDS = {\n        'clear',\n        'vim',\n        'vi',\n        'bash',\n        'zsh',\n    }\n\n    if not arg.strip():\n        return [SQLResult(status=usage)]\n\n    try:\n        command = shlex.split(arg.strip(), posix=not WIN)\n    except ValueError as e:\n        return [SQLResult(status=f\"Cannot parse system command: {e}\")]\n\n    raw = False\n    if command[0] == '-r':\n        command.pop(0)\n        raw = True\n    elif command[0].lower() in IMPLICIT_RAW_MODE_COMMANDS:\n        raw = True\n\n    if not command:\n        return [SQLResult(status=usage)]\n\n    if command[0].lower() == 'cd':\n        ok, error_message = handle_cd_command(command)\n        if not ok:\n            return [SQLResult(status=error_message)]\n        return [SQLResult()]\n\n    try:\n        if raw:\n            completed_process = subprocess.run(command, check=False)\n            if completed_process.returncode:\n                return [SQLResult(status=f'Command exited with return code {completed_process.returncode}')]\n            else:\n                return [SQLResult()]\n        else:\n            process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n            try:\n                output, error = process.communicate(timeout=60)\n            except subprocess.TimeoutExpired:\n                process.kill()\n                output, error = process.communicate()\n            response = output if not error else error\n            encoding = locale.getpreferredencoding(False)\n            response_str = response.decode(encoding)\n            if process.returncode:\n                status = f'Command exited with return code {process.returncode}'\n            else:\n                status = None\n            return [SQLResult(preamble=response_str, status=status)]\n    except OSError as e:\n        return [SQLResult(status=f\"OSError: {e.strerror}\")]\n\n\ndef parseargfile(arg: str) -> tuple[str, str]:\n    if arg.startswith(\"-o \"):\n        mode = \"w\"\n        filename = arg[3:]\n    else:\n        mode = \"a\"\n        filename = arg\n\n    if not filename:\n        raise TypeError(\"You must provide a filename.\")\n\n    return (os.path.expanduser(filename), mode)\n\n\n@special_command(\"tee\", \"tee [-o] <filename>\", \"Append all results to an output file (overwrite using -o).\")\ndef set_tee(arg: str, **_) -> list[SQLResult]:\n    global tee_file\n\n    try:\n        tee_file = open(*parseargfile(arg))\n    except (IOError, OSError) as e:\n        raise OSError(f\"Cannot write to file '{e.filename}': {e.strerror}\") from e\n\n    return [SQLResult(status=\"\")]\n\n\ndef close_tee() -> None:\n    global tee_file\n    if tee_file:\n        tee_file.close()\n        tee_file = None\n\n\n@special_command(\"notee\", \"notee\", \"Stop writing results to an output file.\")\ndef no_tee(arg: str, **_) -> list[SQLResult]:\n    close_tee()\n    return [SQLResult(status=\"\")]\n\n\ndef write_tee(output: str | ANSI | FormattedText, nl: bool = True) -> None:\n    global tee_file\n    if not tee_file:\n        return\n    click.echo(to_plain_text(output), file=tee_file, nl=False)\n    if nl:\n        click.echo('\\n', file=tee_file, nl=False)\n    tee_file.flush()\n\n\n@special_command(\"\\\\once\", \"\\\\once [-o] <filename>\", \"Append next result to an output file (overwrite using -o).\", aliases=[\"\\\\o\"])\ndef set_once(arg: str, **_) -> list[SQLResult]:\n    global once_file, written_to_once_file\n\n    try:\n        once_file = open(*parseargfile(arg))\n    except (IOError, OSError) as e:\n        raise OSError(f\"Cannot write to file '{e.filename}': {e.strerror}\") from e\n    written_to_once_file = False\n\n    return [SQLResult(status=\"\")]\n\n\ndef is_redirected() -> bool:\n    return bool(once_file or PIPE_ONCE['process'])\n\n\ndef write_once(output: str) -> None:\n    global once_file, written_to_once_file\n    if output and once_file:\n        click.echo(output, file=once_file, nl=False)\n        click.echo(\"\\n\", file=once_file, nl=False)\n        once_file.flush()\n        written_to_once_file = True\n\n\ndef unset_once_if_written(post_redirect_command: str) -> None:\n    \"\"\"Unset the once file, if it has been written to.\"\"\"\n    global once_file, written_to_once_file\n    if written_to_once_file and once_file:\n        once_filename = once_file.name\n        once_file.close()\n        once_file = None\n        _run_post_redirect_hook(post_redirect_command, once_filename)\n\n\ndef _run_post_redirect_hook(post_redirect_command: str, filename: str) -> None:\n    if not post_redirect_command:\n        return\n    post_cmd = post_redirect_command.format(shlex.quote(filename))\n    try:\n        subprocess.run(\n            post_cmd,\n            shell=True,\n            check=True,\n            stdin=subprocess.DEVNULL,\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.DEVNULL,\n        )\n    except Exception as e:\n        raise OSError(f\"Redirect post hook failed: {e}\") from e\n\n\n@special_command(\"\\\\pipe_once\", \"\\\\pipe_once <command>\", \"Send next result to a subprocess.\", aliases=[\"\\\\|\"])\ndef set_pipe_once(arg: str, **_) -> list[SQLResult]:\n    if not arg:\n        raise OSError(\"pipe_once requires a command\")\n    if WIN:\n        # best effort, no chaining\n        pipe_once_cmd = shlex.split(arg)\n    else:\n        # to support chaining\n        pipe_once_cmd = ['sh', '-c', arg]\n    PIPE_ONCE['stdin'] = []\n    PIPE_ONCE['process'] = subprocess.Popen(\n        pipe_once_cmd,\n        stdin=subprocess.PIPE,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        encoding=\"UTF-8\",\n        universal_newlines=True,\n    )\n    return [SQLResult(status=\"\")]\n\n\ndef write_pipe_once(line: str) -> None:\n    if line and PIPE_ONCE['process']:\n        PIPE_ONCE['stdin'].append(line)\n\n\ndef flush_pipe_once_if_written(post_redirect_command: str) -> None:\n    \"\"\"Flush the pipe_once cmd, if lines have been written.\"\"\"\n    if not PIPE_ONCE['process']:\n        return\n    if not PIPE_ONCE['stdin']:\n        return\n    try:\n        (stdout_data, stderr_data) = PIPE_ONCE['process'].communicate(input='\\n'.join(PIPE_ONCE['stdin']) + '\\n', timeout=60)\n    except subprocess.TimeoutExpired:\n        PIPE_ONCE['process'].kill()\n        (stdout_data, stderr_data) = PIPE_ONCE['process'].communicate()\n    if stdout_data:\n        if PIPE_ONCE['stdout_file']:\n            with open(PIPE_ONCE['stdout_file'], PIPE_ONCE['stdout_mode']) as f:\n                print(stdout_data, file=f)\n            _run_post_redirect_hook(post_redirect_command, PIPE_ONCE['stdout_file'])\n        else:\n            click.secho(stdout_data.rstrip('\\n'))\n    if stderr_data:\n        click.secho(stderr_data.rstrip('\\n'), err=True, fg='red')\n    if returncode := PIPE_ONCE['process'].returncode:\n        PIPE_ONCE['process'] = None\n        PIPE_ONCE['stdin'] = []\n        PIPE_ONCE['stdout_file'] = None\n        PIPE_ONCE['stdout_mode'] = None\n        raise OSError(f'process exited with nonzero code {returncode}')\n    PIPE_ONCE['process'] = None\n    PIPE_ONCE['stdin'] = []\n    PIPE_ONCE['stdout_file'] = None\n    PIPE_ONCE['stdout_mode'] = None\n\n\n@special_command(\"watch\", \"watch [seconds] [-c] <query>\", \"Execute query every [seconds] seconds (5 by default).\")\ndef watch_query(arg: str, **kwargs) -> Generator[SQLResult, None, None]:\n    usage = \"\"\"Syntax: watch [seconds] [-c] query.\n    * seconds: The interval at the query will be repeated, in seconds.\n               By default 5.\n    * -c: Clears the screen between every iteration.\n\"\"\"\n    if not arg:\n        yield SQLResult(status=usage)\n        return\n    seconds = 5.0\n    clear_screen = False\n    statement = None\n    while statement is None:\n        arg = arg.strip()\n        if not arg:\n            # Oops, we parsed all the arguments without finding a statement\n            yield SQLResult(status=usage)\n            return\n        (left_arg, _, right_arg) = arg.partition(\" \")\n        arg = right_arg\n        try:\n            seconds = float(left_arg)\n            continue\n        except ValueError:\n            pass\n        if left_arg == \"-c\":\n            clear_screen = True\n            continue\n        statement = f\"{left_arg} {arg}\"\n    destructive_prompt = confirm_destructive_query(DESTRUCTIVE_KEYWORDS, statement)\n    if destructive_prompt is False:\n        click.secho(\"Wise choice!\")\n        return\n    elif destructive_prompt is True:\n        click.secho(\"Your call!\")\n    cur = kwargs[\"cur\"]\n    sql_list = [(sql.rstrip(\";\"), f\"> {sql}\") for sql in sqlparse.split(statement)]\n    old_pager_enabled = is_pager_enabled()\n    while True:\n        if clear_screen:\n            click.clear()\n        try:\n            # Somewhere in the code the pager its activated after every yield,\n            # so we disable it in every iteration\n            set_pager_enabled(False)\n            for sql, preamble in sql_list:\n                cur.execute(sql)\n                command: dict[str, str | float] = {\n                    \"name\": \"watch\",\n                    \"seconds\": seconds,\n                }\n                if cur.description:\n                    header = [x[0] for x in cur.description]\n                    yield SQLResult(preamble=preamble, header=header, rows=cur, command=command)\n                else:\n                    yield SQLResult(preamble=preamble, command=command)\n            sleep(seconds)\n        except KeyboardInterrupt:\n            # This prints the Ctrl-C character in its own line, which prevents\n            # to print a line with the cursor positioned behind the prompt\n            click.secho(\"\", nl=True)\n            return\n        finally:\n            set_pager_enabled(old_pager_enabled)\n\n\n@special_command(\"delimiter\", \"delimiter <string>\", \"Change end-of-statement delimiter.\")\ndef set_delimiter(arg: str, **_) -> list[SQLResult]:\n    return delimiter_command.set(arg)\n\n\ndef get_current_delimiter() -> str:\n    return delimiter_command.current\n\n\ndef split_queries(input_str: str) -> Generator[str, None, None]:\n    yield from delimiter_command.queries_iter(input_str)\n"
  },
  {
    "path": "mycli/packages/special/llm.py",
    "content": "import contextlib\nimport functools\nimport io\nimport logging\nimport os\nimport re\nfrom runpy import run_module\nimport shlex\nimport sys\nfrom time import time\nfrom typing import Any\n\nimport click\n\ntry:\n    if not os.environ.get('MYCLI_LLM_OFF'):\n        import llm\n\n        LLM_IMPORTED = True\n    else:\n        LLM_IMPORTED = False\nexcept ImportError:\n    LLM_IMPORTED = False\ntry:\n    if not os.environ.get('MYCLI_LLM_OFF'):\n        from llm.cli import cli\n\n        LLM_CLI_IMPORTED = True\n    else:\n        LLM_CLI_IMPORTED = False\nexcept ImportError:\n    LLM_CLI_IMPORTED = False\nfrom pymysql.cursors import Cursor\n\nfrom mycli.packages.special.main import Verbosity, parse_special_command\nfrom mycli.packages.sqlresult import SQLResult\n\nlog = logging.getLogger(__name__)\n\nLLM_TEMPLATE_NAME = \"mycli-llm-template\"\n\nSCHEMA_DATA_CACHE: dict[str, str] = {}\n\nSAMPLE_DATA_CACHE: dict[str, dict] = {}\n\n\ndef run_external_cmd(\n    cmd: str,\n    *args,\n    capture_output=False,\n    restart_cli=False,\n    raise_exception=True,\n) -> tuple[int, str]:\n    original_exe = sys.executable\n    original_args = sys.argv\n    try:\n        sys.argv = [cmd] + list(args)\n        code = 0\n        if capture_output:\n            buffer = io.StringIO()\n            redirect: contextlib.ExitStack[bool | None] | contextlib.nullcontext[None] = contextlib.ExitStack()\n            assert isinstance(redirect, contextlib.ExitStack)\n            redirect.enter_context(contextlib.redirect_stdout(buffer))\n            redirect.enter_context(contextlib.redirect_stderr(buffer))\n        else:\n            redirect = contextlib.nullcontext()\n        with redirect:\n            try:\n                run_module(cmd, run_name=\"__main__\")\n            except SystemExit as e:\n                code = int(e.code or 0)\n                if code != 0 and raise_exception:\n                    if capture_output:\n                        raise RuntimeError(buffer.getvalue()) from e\n                    raise RuntimeError(f\"Command {cmd} failed with exit code {code}.\") from e\n            except Exception as e:\n                code = 1\n                if raise_exception:\n                    if capture_output:\n                        raise RuntimeError(buffer.getvalue()) from e\n                    raise RuntimeError(f\"Command {cmd} failed: {e}\") from e\n        if restart_cli and code == 0:\n            os.execv(original_exe, [original_exe] + original_args)\n        if capture_output:\n            return code, buffer.getvalue()\n        else:\n            return code, \"\"\n    finally:\n        sys.argv = original_args\n\n\ndef _build_command_tree(cmd) -> dict[str, Any] | None:\n    tree: dict[str, Any] | None = {}\n    assert isinstance(tree, dict)\n    if isinstance(cmd, click.Group):\n        for name, subcmd in cmd.commands.items():\n            if cmd.name == \"models\" and name == \"default\":\n                tree[name] = {x.model_id: None for x in llm.get_models()}\n            else:\n                tree[name] = _build_command_tree(subcmd)\n    else:\n        tree = None\n    return tree\n\n\ndef build_command_tree(cmd) -> dict[str, Any]:\n    return _build_command_tree(cmd) or {}\n\n\n# Generate the command tree for autocompletion\nCOMMAND_TREE = build_command_tree(cli) if LLM_CLI_IMPORTED is True else {}\n\n\ndef get_completions(\n    tokens: list[str],\n    tree: dict[str, Any] | None = None,\n) -> list[str]:\n    tree = tree or COMMAND_TREE\n    for token in tokens:\n        if token.startswith(\"-\"):\n            continue\n        if tree and token in tree:\n            tree = tree[token]\n        else:\n            return []\n    return list(tree.keys()) if tree else []\n\n\nclass FinishIteration(Exception):\n    def __init__(self, results=None):\n        self.results = results\n\n\nUSAGE = \"\"\"\nUse an LLM to create SQL queries to answer questions from your database.\nExamples:\n\n# Ask a question.\n> \\\\llm 'Most visited urls?'\n\n# List available models\n> \\\\llm models\n> gpt-4o\n> gpt-3.5-turbo\n\n# Change default model\n> \\\\llm models default llama3\n\n# Set api key (not required for local models)\n> \\\\llm keys set openai\n\n# Install a model plugin\n> \\\\llm install llm-ollama\n> llm-ollama installed.\n\n# Plugins directory\n# https://llm.datasette.io/en/stable/plugins/directory.html\n\"\"\"\n\nNEED_DEPENDENCIES = \"\"\"\nTo enable LLM features you need to install mycli with LLM support:\n\n    pip install 'mycli[llm]'\n\nor\n\n    pip install 'mycli[all]'\n\nor install LLM libraries separately\n\n   pip install llm\n\nThis is required to use the \\\\llm command.\n\"\"\"\n\n_SQL_CODE_FENCE = r\"```sql\\n(.*?)\\n```\"\n\nPROMPT = \"\"\"\nYou are a helpful assistant who is a MySQL expert. You are embedded in a mysql\ncli tool called mycli.\n\nAnswer this question:\n\n$question\n\nUse the following context if it is relevant to answering the question. If the\nquestion is not about the current database then ignore the context.\n\nYou are connected to a MySQL database with the following schema:\n\n$db_schema\n\nHere is a sample row of data from each table:\n\n$sample_data\n\nIf the answer can be found using a SQL query, include a sql query in a code\nfence such as this one:\n\n```sql\nSELECT count(*) FROM table_name;\n```\nKeep your explanation concise and focused on the question asked.\n\"\"\"\n\n\ndef ensure_mycli_template(replace: bool = False) -> None:\n    if not replace:\n        code, _ = run_external_cmd(\"llm\", \"templates\", \"show\", LLM_TEMPLATE_NAME, capture_output=True, raise_exception=False)\n        if code == 0:\n            return\n    run_external_cmd(\"llm\", PROMPT, \"--save\", LLM_TEMPLATE_NAME)\n\n\n@functools.cache\ndef cli_commands() -> list[str]:\n    return list(cli.commands.keys())\n\n\ndef handle_llm(\n    text: str,\n    cur: Cursor,\n    dbname: str,\n    prompt_field_truncate: int,\n    prompt_section_truncate: int,\n) -> tuple[str, str | None, float]:\n    _, verbosity, arg = parse_special_command(text)\n    if not LLM_IMPORTED:\n        raise FinishIteration(results=[SQLResult(preamble=NEED_DEPENDENCIES)])\n    if arg.strip().lower() in ['', 'help', '?', r'\\?']:\n        raise FinishIteration(results=[SQLResult(preamble=USAGE)])\n    parts = shlex.split(arg)\n    restart = False\n    if \"-c\" in parts:\n        capture_output = True\n        use_context = False\n    elif \"prompt\" in parts:\n        capture_output = True\n        use_context = True\n    elif \"install\" in parts or \"uninstall\" in parts:\n        capture_output = False\n        use_context = False\n        restart = True\n    elif parts and parts[0] in cli_commands():\n        capture_output = False\n        use_context = False\n    elif parts and parts[0] == \"--help\":\n        capture_output = False\n        use_context = False\n    else:\n        capture_output = True\n        use_context = True\n    if not use_context:\n        args = parts\n        if capture_output:\n            click.echo(\"Calling llm command\")\n            start = time()\n            _, output = run_external_cmd(\"llm\", *args, capture_output=capture_output)\n            end = time()\n            match = re.search(_SQL_CODE_FENCE, output, re.DOTALL)\n            if match:\n                sql = match.group(1).strip()\n            else:\n                raise FinishIteration(results=[SQLResult(preamble=output)])\n            return (output if verbosity == Verbosity.SUCCINCT else \"\", sql, end - start)\n        else:\n            run_external_cmd(\"llm\", *args, restart_cli=restart)\n            raise FinishIteration(results=None)\n    try:\n        ensure_mycli_template()\n        start = time()\n        context, sql = sql_using_llm(\n            cur=cur,\n            question=arg,\n            dbname=dbname,\n            prompt_field_truncate=prompt_field_truncate,\n            prompt_section_truncate=prompt_section_truncate,\n        )\n        end = time()\n        if verbosity == Verbosity.SUCCINCT:\n            context = \"\"\n        return (context, sql, end - start)\n    except Exception as e:\n        raise RuntimeError(e) from e\n\n\ndef is_llm_command(command: str) -> bool:\n    cmd, _, _ = parse_special_command(command)\n    return cmd in (\"\\\\llm\", \"\\\\ai\")\n\n\ndef truncate_list_elements(row: list, prompt_field_truncate: int, prompt_section_truncate: int) -> list:\n    if not prompt_section_truncate and not prompt_field_truncate:\n        return row\n\n    width = prompt_field_truncate\n    while width >= 0:\n        truncated_row = [x[:width] if isinstance(x, (str, bytes)) else x for x in row]\n        if prompt_section_truncate:\n            if sum(sys.getsizeof(x) for x in truncated_row) <= prompt_section_truncate:\n                break\n            width -= 100\n        else:\n            break\n    return truncated_row\n\n\ndef truncate_table_lines(table: list[str], prompt_section_truncate: int) -> list[str]:\n    if not prompt_section_truncate:\n        return table\n\n    truncated_table = []\n    running_sum = 0\n    while table and running_sum <= prompt_section_truncate:\n        line = table.pop(0)\n        running_sum += sys.getsizeof(line)\n        truncated_table.append(line)\n    return truncated_table\n\n\ndef get_schema(cur: Cursor, dbname: str, prompt_section_truncate: int) -> str:\n    if dbname in SCHEMA_DATA_CACHE:\n        return SCHEMA_DATA_CACHE[dbname]\n    click.echo(\"Preparing schema information to feed the LLM\")\n    schema_query = f\"\"\"\n        SELECT CONCAT(table_name, '(', GROUP_CONCAT(column_name, ' ', COLUMN_TYPE SEPARATOR ', '),')') AS `schema`\n        FROM information_schema.columns\n        WHERE table_schema = '{dbname}'\n        GROUP BY table_name\n        ORDER BY table_name\n    \"\"\"\n    cur.execute(schema_query)\n    db_schema = [row for (row,) in cur.fetchall()]\n    summary = '\\n'.join(truncate_table_lines(db_schema, prompt_section_truncate))\n    SCHEMA_DATA_CACHE[dbname] = summary\n    return summary\n\n\ndef get_sample_data(\n    cur: Cursor,\n    dbname: str,\n    prompt_field_truncate: int,\n    prompt_section_truncate: int,\n) -> dict[str, Any]:\n    if dbname in SAMPLE_DATA_CACHE:\n        return SAMPLE_DATA_CACHE[dbname]\n    click.echo(\"Preparing sample data to feed the LLM\")\n    tables_query = \"SHOW TABLES\"\n    sample_row_query = \"SELECT * FROM `{dbname}`.`{table}` LIMIT 1\"\n    cur.execute(tables_query)\n    sample_data = {}\n    for (table_name,) in cur.fetchall():\n        try:\n            cur.execute(sample_row_query.format(dbname=dbname, table=table_name))\n        except Exception:\n            continue\n        cols = [desc[0] for desc in cur.description]\n        row = cur.fetchone()\n        if row is None:\n            continue\n        sample_data[table_name] = list(\n            zip(cols, truncate_list_elements(list(row), prompt_field_truncate, prompt_section_truncate), strict=False)\n        )\n    SAMPLE_DATA_CACHE[dbname] = sample_data\n    return sample_data\n\n\ndef sql_using_llm(\n    cur: Cursor | None,\n    question: str | None,\n    dbname: str = '',\n    prompt_field_truncate: int = 0,\n    prompt_section_truncate: int = 0,\n) -> tuple[str, str | None]:\n    if cur is None:\n        raise RuntimeError(\"Connect to a database and try again.\")\n    if dbname == '':\n        raise RuntimeError(\"Choose a schema and try again.\")\n    args = [\n        \"--template\",\n        LLM_TEMPLATE_NAME,\n        \"--param\",\n        \"db_schema\",\n        get_schema(cur, dbname, prompt_section_truncate),\n        \"--param\",\n        \"sample_data\",\n        get_sample_data(cur, dbname, prompt_field_truncate, prompt_section_truncate),\n        \"--param\",\n        \"question\",\n        question,\n        \" \",\n    ]\n    click.echo(\"Invoking llm command with schema information and sample data\")\n    _, result = run_external_cmd(\"llm\", *args, capture_output=True)\n    click.echo(\"Received response from the llm command\")\n    match = re.search(_SQL_CODE_FENCE, result, re.DOTALL)\n    if match:\n        sql = match.group(1).strip()\n    else:\n        sql = \"\"\n    return (result, sql)\n"
  },
  {
    "path": "mycli/packages/special/main.py",
    "content": "from collections import namedtuple\nfrom enum import Enum\nimport logging\nimport os\nfrom typing import Callable\nimport webbrowser\n\nfrom mycli.constants import DOCS_URL, ISSUES_URL\nfrom mycli.packages.sqlresult import SQLResult\n\ntry:\n    if not os.environ.get('MYCLI_LLM_OFF'):\n        import llm  # noqa: F401\n\n        LLM_IMPORTED = True\n    else:\n        LLM_IMPORTED = False\nexcept ImportError:\n    LLM_IMPORTED = False\nfrom pymysql.cursors import Cursor\n\nlogger = logging.getLogger(__name__)\n\nCOMMANDS = {}\n\nSpecialCommand = namedtuple(\n    \"SpecialCommand\",\n    [\n        \"handler\",\n        \"command\",\n        \"usage\",\n        \"description\",\n        \"arg_type\",\n        \"hidden\",\n        \"case_sensitive\",\n        \"shortcut\",\n    ],\n)\n\n\nclass ArgType(Enum):\n    NO_QUERY = 0\n    PARSED_QUERY = 1\n    RAW_QUERY = 2\n\n\nclass CommandNotFound(Exception):\n    pass\n\n\nclass Verbosity(Enum):\n    SUCCINCT = \"succinct\"\n    NORMAL = \"normal\"\n    VERBOSE = \"verbose\"\n\n\ndef parse_special_command(sql: str) -> tuple[str, Verbosity, str]:\n    command, _, arg = sql.partition(\" \")\n    verbosity = Verbosity.NORMAL\n    if \"+\" in command:\n        verbosity = Verbosity.VERBOSE\n    elif \"-\" in command:\n        verbosity = Verbosity.SUCCINCT\n    command = command.strip().strip(\"+-\")\n    return (command, verbosity, arg.strip())\n\n\ndef special_command(\n    command: str,\n    usage: str | None,\n    description: str,\n    arg_type: ArgType = ArgType.PARSED_QUERY,\n    hidden: bool = False,\n    case_sensitive: bool = False,\n    aliases: list[str] | None = None,\n) -> Callable:\n    def wrapper(wrapped):\n        register_special_command(\n            wrapped,\n            command,\n            usage,\n            description,\n            arg_type=arg_type,\n            hidden=hidden,\n            case_sensitive=case_sensitive,\n            aliases=aliases,\n        )\n        return wrapped\n\n    return wrapper\n\n\ndef register_special_command(\n    handler: Callable,\n    command: str,\n    usage: str | None,\n    description: str,\n    arg_type: ArgType = ArgType.PARSED_QUERY,\n    hidden: bool = False,\n    case_sensitive: bool = False,\n    aliases: list[str] | None = None,\n) -> None:\n    cmd = command.lower() if not case_sensitive else command\n    COMMANDS[cmd] = SpecialCommand(\n        handler,\n        command,\n        usage,\n        description,\n        arg_type=arg_type,\n        hidden=hidden,\n        case_sensitive=case_sensitive,\n        shortcut=aliases[0] if aliases else None,\n    )\n    aliases = [] if aliases is None else aliases\n    for alias in aliases:\n        cmd = alias.lower() if not case_sensitive else alias\n        COMMANDS[cmd] = SpecialCommand(\n            handler,\n            command,\n            usage,\n            description,\n            arg_type=arg_type,\n            case_sensitive=case_sensitive,\n            hidden=True,\n            shortcut=None,\n        )\n\n\ndef execute(cur: Cursor, sql: str) -> list[SQLResult]:\n    \"\"\"Execute a special command and return the results. If the special command\n    is not supported a CommandNotFound will be raised.\n    \"\"\"\n    command, verbosity, arg = parse_special_command(sql)\n\n    if (command not in COMMANDS) and (command.lower() not in COMMANDS):\n        raise CommandNotFound(f'Command not found: {command}')\n\n    try:\n        special_cmd = COMMANDS[command]\n    except KeyError as exc:\n        special_cmd = COMMANDS[command.lower()]\n        if special_cmd.case_sensitive:\n            raise CommandNotFound(f'Command not found: {command}') from exc\n\n    # \"help <SQL KEYWORD> is a special case. We want built-in help, not\n    # mycli help here.\n    if command == \"help\" and arg:\n        return show_keyword_help(cur=cur, arg=arg)\n\n    if special_cmd.arg_type == ArgType.NO_QUERY:\n        return special_cmd.handler()\n    elif special_cmd.arg_type == ArgType.PARSED_QUERY:\n        return special_cmd.handler(cur=cur, arg=arg, verbose=(verbosity == Verbosity.VERBOSE))\n    elif special_cmd.arg_type == ArgType.RAW_QUERY:\n        return special_cmd.handler(cur=cur, query=sql)\n\n    raise CommandNotFound(f\"Command type not found: {command}\")\n\n\n@special_command(\n    \"help\", \"help [term]\", \"Show this help, or search for a term on the server.\", arg_type=ArgType.NO_QUERY, aliases=[\"\\\\?\", \"?\"]\n)\ndef show_help(*_args) -> list[SQLResult]:\n    header = [\"Command\", \"Shortcut\", \"Usage\", \"Description\"]\n    result = []\n\n    for _, value in sorted(COMMANDS.items()):\n        if not value.hidden:\n            result.append((value.command, value.shortcut, value.usage, value.description))\n    return [SQLResult(header=header, rows=result, postamble=f'Docs index — {DOCS_URL}')]\n\n\ndef show_keyword_help(cur: Cursor, arg: str) -> list[SQLResult]:\n    \"\"\"\n    Call the built-in \"show <keyword>\", to display help for an SQL keyword.\n    :param cur: cursor\n    :param arg: string\n    :return: list\n    \"\"\"\n    keyword = arg.strip().strip('\"\\'')\n    query = 'help %s'\n    logger.debug(query)\n    cur.execute(query, keyword)\n    if cur.description and cur.rowcount > 0:\n        header = [x[0] for x in cur.description]\n        return [SQLResult(header=header, rows=cur)]\n    logger.debug(query)\n    cur.execute(query, (f'%{keyword}%',))\n    if cur.description and cur.rowcount > 0:\n        header = [x[0] for x in cur.description]\n        return [SQLResult(preamble='Similar terms:', header=header, rows=cur)]\n    else:\n        return [SQLResult(status=f'No help found for \"{keyword}\".')]\n\n\n@special_command('\\\\bug', '\\\\bug', 'File a bug on GitHub.', arg_type=ArgType.NO_QUERY)\ndef file_bug(*_args) -> list[SQLResult]:\n    webbrowser.open_new_tab(ISSUES_URL)\n    return [SQLResult(status=f'{ISSUES_URL} — press \"New Issue\"')]\n\n\n@special_command(\"exit\", \"exit\", \"Exit.\", arg_type=ArgType.NO_QUERY, aliases=[\"\\\\q\"])\n@special_command(\"quit\", \"quit\", \"Quit.\", arg_type=ArgType.NO_QUERY, aliases=[\"\\\\q\"])\ndef quit_(*_args):\n    raise EOFError\n\n\n@special_command(\n    \"\\\\edit\",\n    \"<query>\\\\edit | \\\\edit <filename>\",\n    \"Edit query with editor (uses $VISUAL or $EDITOR).\",\n    arg_type=ArgType.NO_QUERY,\n    case_sensitive=True,\n    aliases=['\\\\e'],\n)\n@special_command(\"\\\\clip\", \"<query>\\\\clip\", \"Copy query to the system clipboard.\", arg_type=ArgType.NO_QUERY, case_sensitive=True)\n@special_command(\"\\\\G\", \"<query>\\\\G\", \"Display query results vertically.\", arg_type=ArgType.NO_QUERY, case_sensitive=True)\ndef stub():\n    raise NotImplementedError\n\n\nif LLM_IMPORTED:\n\n    @special_command(\n        \"\\\\llm\",\n        \"\\\\llm [arguments]\",\n        \"Interrogate an LLM.  See \\\"\\\\llm help\\\".\",\n        arg_type=ArgType.RAW_QUERY,\n        case_sensitive=True,\n        aliases=[\"\\\\ai\"],\n    )\n    def llm_stub():\n        raise NotImplementedError\n"
  },
  {
    "path": "mycli/packages/special/utils.py",
    "content": "import logging\nimport os\n\nimport click\nimport pymysql\nfrom pymysql.cursors import Cursor\n\nlogger = logging.getLogger(__name__)\n\nCACHED_SSL_VERSION: dict[tuple, str | None] = {}\n\n\ndef handle_cd_command(command: list[str]) -> tuple[bool, str | None]:\n    \"\"\"Handles a `cd` shell command by calling python's os.chdir.\"\"\"\n    if not command[0].lower() == 'cd':\n        return False, 'Not a cd command.'\n    if len(command) != 2:\n        return False, 'Exactly one directory name must be provided.'\n    directory = command[1]\n    try:\n        os.chdir(directory)\n        click.echo(os.getcwd(), err=True)\n        return True, None\n    except OSError as e:\n        return False, e.strerror\n\n\ndef format_uptime(uptime_in_seconds: str) -> str:\n    \"\"\"Format number of seconds into human-readable string.\n\n    :param uptime_in_seconds: The server uptime in seconds.\n    :returns: A human-readable string representing the uptime.\n\n    >>> uptime = format_uptime('56892')\n    >>> print(uptime)\n    15 hours 48 min 12 sec\n    \"\"\"\n\n    m, s = divmod(int(uptime_in_seconds), 60)\n    h, m = divmod(m, 60)\n    d, h = divmod(h, 24)\n\n    uptime_values: list[str] = []\n\n    for value, unit in ((d, \"days\"), (h, \"hours\"), (m, \"min\"), (s, \"sec\")):\n        if value == 0 and not uptime_values:\n            # Don't include a value/unit if the unit isn't applicable to\n            # the uptime. E.g. don't do 0 days 0 hours 1 min 30 sec.\n            continue\n        if value == 1 and unit.endswith(\"s\"):\n            # Remove the \"s\" if the unit is singular.\n            unit = unit[:-1]\n        uptime_values.append(f'{value} {unit}')\n\n    uptime = \" \".join(uptime_values)\n    return uptime\n\n\ndef get_uptime(cur: Cursor) -> int:\n    query = 'SHOW STATUS LIKE \"Uptime\"'\n    logger.debug(query)\n\n    uptime = 0\n\n    try:\n        cur.execute(query)\n        if one := cur.fetchone():\n            uptime = int(one[1] or 0)\n    except pymysql.err.OperationalError:\n        pass\n\n    return uptime\n\n\ndef get_warning_count(cur: Cursor) -> int:\n    query = 'SHOW COUNT(*) WARNINGS'\n    logger.debug(query)\n\n    warning_count = 0\n\n    try:\n        cur.execute(query)\n        if one := cur.fetchone():\n            warning_count = int(one[0] or 0)\n    except pymysql.err.OperationalError:\n        pass\n\n    return warning_count\n\n\ndef get_ssl_version(cur: Cursor) -> str | None:\n    cache_key = (id(cur.connection), cur.connection.thread_id())\n\n    if cache_key in CACHED_SSL_VERSION:\n        return CACHED_SSL_VERSION[cache_key] or None\n\n    query = 'SHOW STATUS LIKE \"Ssl_version\"'\n    logger.debug(query)\n\n    ssl_version = None\n\n    try:\n        cur.execute(query)\n        if one := cur.fetchone():\n            CACHED_SSL_VERSION[cache_key] = one[1]\n            ssl_version = one[1] or None\n        else:\n            CACHED_SSL_VERSION[cache_key] = ''\n    except pymysql.err.OperationalError:\n        pass\n\n    return ssl_version\n"
  },
  {
    "path": "mycli/packages/sqlresult.py",
    "content": "from dataclasses import dataclass\nfrom functools import cached_property\n\nfrom prompt_toolkit.formatted_text import FormattedText, to_plain_text\nfrom pymysql.cursors import Cursor\n\n\n@dataclass\nclass SQLResult:\n    preamble: str | None = None\n    header: list[str] | str | None = None\n    rows: Cursor | list[tuple] | None = None\n    postamble: str | None = None\n    status: str | FormattedText | None = None\n    command: dict[str, str | float] | None = None\n\n    def __iter__(self):\n        return self\n\n    def __str__(self):\n        return f\"{self.preamble}, {self.header}, {self.rows}, {self.postamble}, {self.status}, {self.command}\"\n\n    @cached_property\n    def status_plain(self):\n        if self.status is None:\n            return None\n        return to_plain_text(self.status)\n"
  },
  {
    "path": "mycli/packages/string_utils.py",
    "content": "import re\n\nfrom cli_helpers.utils import strip_ansi\n\n\ndef sanitize_terminal_title(title: str) -> str:\n    sanitized = strip_ansi(title)\n    sanitized = sanitized.replace('\\n', ' ')\n    sanitized = re.sub('[\\x00-\\x1f\\x7f]', '', sanitized)\n    return sanitized\n"
  },
  {
    "path": "mycli/packages/tabular_output/__init__.py",
    "content": ""
  },
  {
    "path": "mycli/packages/tabular_output/sql_format.py",
    "content": "\"\"\"Format adapter for sql.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Generator, Union\n\nfrom cli_helpers.tabular_output import TabularOutputFormatter\n\nfrom mycli.packages.parseutils import extract_tables_from_complete_statements\n\nsupported_formats = (\n    \"sql-insert\",\n    \"sql-update\",\n    \"sql-update-1\",\n    \"sql-update-2\",\n)\n\npreprocessors = ()\n\nformatter: TabularOutputFormatter\n\n\ndef escape_for_sql_statement(value: Union[bytes, str]) -> str:\n    if isinstance(value, bytes):\n        return f\"0x{value.hex()}\"\n    else:\n        return formatter.mycli.sqlexecute.conn.escape(value)\n\n\ndef adapter(data: list[str], headers: list[str], table_format: Union[str, None] = None, **kwargs) -> Generator[str, None, None]:\n    tables = extract_tables_from_complete_statements(formatter.query)\n    if len(tables) > 0:\n        table = tables[0]\n        if table[0]:\n            table_name = f'{table[0]}.{table[1]}'\n        else:\n            table_name = table[1]\n    else:\n        table_name = \"`DUAL`\"\n    if table_format == \"sql-insert\":\n        h = \"`, `\".join(headers)\n        yield f'INSERT INTO {table_name} (`{h}`) VALUES'\n        prefix = \"  \"\n        for d in data:\n            values = \", \".join(escape_for_sql_statement(v) for i, v in enumerate(d))\n            yield f'{prefix}({values})'\n            if prefix == \"  \":\n                prefix = \", \"\n        yield \";\"\n    if table_format and table_format.startswith(\"sql-update\"):\n        s = table_format.split(\"-\")\n        keys = 1\n        if len(s) > 2:\n            keys = int(s[-1])\n        for d in data:\n            yield f'UPDATE {table_name} SET'\n            prefix = \"  \"\n            for i, v in enumerate(d[keys:], keys):\n                yield f'{prefix}`{headers[i]}` = {escape_for_sql_statement(v)}'\n                if prefix == \"  \":\n                    prefix = \", \"\n            f = \"`{}` = {}\"\n            where = (f.format(headers[i], escape_for_sql_statement(d[i])) for i in range(keys))\n            yield f'WHERE {\" AND \".join(where)};'\n\n\ndef register_new_formatter(tof: TabularOutputFormatter):\n    global formatter\n    formatter = tof\n    for sql_format in supported_formats:\n        tof.register_new_formatter(sql_format, adapter, preprocessors, {\"table_format\": sql_format})\n"
  },
  {
    "path": "mycli/packages/toolkit/__init__.py",
    "content": ""
  },
  {
    "path": "mycli/packages/toolkit/fzf.py",
    "content": "import re\nimport shlex\nfrom shutil import which\n\nfrom prompt_toolkit import search\nfrom prompt_toolkit.key_binding.key_processor import KeyPressEvent\nfrom pyfzf import FzfPrompt\n\nfrom mycli.packages.toolkit.history import FileHistoryWithTimestamp\nfrom mycli.packages.toolkit.utils import safe_invalidate_display\n\n\nclass Fzf(FzfPrompt):\n    def __init__(self):\n        self.executable = which(\"fzf\")\n        if self.executable:\n            super().__init__()\n\n    def is_available(self) -> bool:\n        return self.executable is not None\n\n\ndef search_history(\n    event: KeyPressEvent,\n    highlight_preview: bool = False,\n    highlight_style: str = 'default',\n    incremental: bool = False,\n) -> None:\n    buffer = event.current_buffer\n    history = buffer.history\n\n    fzf = Fzf()\n\n    if incremental or not fzf.is_available() or not isinstance(history, FileHistoryWithTimestamp):\n        # Fallback to default reverse incremental search\n        search.start_search(direction=search.SearchDirection.BACKWARD)\n        return\n\n    history_items_with_timestamp = history.load_history_with_timestamp()\n\n    formatted_history_items = []\n    original_history_items = []\n    seen = {}\n    for item, timestamp in history_items_with_timestamp:\n        formatted_item = re.sub(r'\\s+', ' ', item)\n        timestamp = timestamp.split(\".\")[0] if \".\" in timestamp else timestamp\n        if formatted_item in seen:\n            continue\n        seen[formatted_item] = True\n        formatted_history_items.append(f\"{timestamp}  {formatted_item}\")\n        original_history_items.append(item)\n\n    options = [\n        '--info=hidden',\n        '--scheme=history',\n        '--tiebreak=index',\n        '--bind=ctrl-r:up,alt-r:up',\n        '--preview-window=down:wrap:nohidden',\n        '--no-height',\n    ]\n\n    if highlight_preview and which('pygmentize'):\n        options.append(f'--preview=\"printf \\'%s\\' {{}} | pygmentize -l mysql -P style={shlex.quote(highlight_style)}\"')\n    else:\n        options.append('--preview=\"printf \\'%s\\' {}\"')\n\n    result = fzf.prompt(\n        formatted_history_items,\n        fzf_options=' '.join(options),\n    )\n    safe_invalidate_display(event.app)\n\n    if result:\n        selected_index = formatted_history_items.index(result[0])\n        buffer.text = original_history_items[selected_index]\n        buffer.cursor_position = len(buffer.text)\n"
  },
  {
    "path": "mycli/packages/toolkit/history.py",
    "content": "import os\nfrom typing import Union\n\nfrom prompt_toolkit.history import FileHistory\n\n_StrOrBytesPath = Union[str, bytes, os.PathLike]\n\n\nclass FileHistoryWithTimestamp(FileHistory):\n    \"\"\"\n    :class:`.FileHistory` class that stores all strings in a file with timestamp.\n    \"\"\"\n\n    def __init__(self, filename: _StrOrBytesPath) -> None:\n        self.filename = filename\n        super().__init__(filename)\n\n    def load_history_with_timestamp(self) -> list[tuple[str, str]]:\n        \"\"\"\n        Load history entries along with their timestamps.\n\n        Returns:\n            list[tuple[str, str]]: A list of tuples where each tuple contains\n                                   a history entry and its corresponding timestamp.\n        \"\"\"\n        history_with_timestamp: list[tuple[str, str]] = []\n        lines: list[str] = []\n        timestamp: str = \"\"\n\n        def add() -> None:\n            if lines:\n                # Join and drop trailing newline.\n                string = \"\".join(lines)[:-1]\n                history_with_timestamp.append((string, timestamp))\n\n        if os.path.exists(self.filename):\n            with open(self.filename, 'r', encoding='utf-8') as f:\n                for line in f:\n                    if line.startswith(\"#\"):\n                        # Extract timestamp\n                        timestamp = line[2:].strip()\n                    elif line.startswith(\"+\"):\n                        lines.append(line[1:])\n                    else:\n                        add()\n                        lines = []\n\n                add()\n\n        return list(reversed(history_with_timestamp))\n"
  },
  {
    "path": "mycli/packages/toolkit/utils.py",
    "content": "from prompt_toolkit.application import Application, run_in_terminal\n\n\ndef safe_invalidate_display(app: Application) -> None:\n    \"\"\"\n    fzf can confuse the terminal/app when certain values are set in\n    environment variable FZF_DEFAULT_OPTS.\n\n    The same could happen after running other external programs.\n\n    This function invalidates the prompt_toolkit display, causing a\n    refresh of the prompt message and pending user input, without\n    leading to exceptions at exit time, as the built-in\n    app.invalidate() does.\n    \"\"\"\n\n    def print_empty_string():\n        app.print_text('')\n\n    try:\n        run_in_terminal(print_empty_string)\n    except RuntimeError:\n        pass\n"
  },
  {
    "path": "mycli/sqlcompleter.py",
    "content": "from __future__ import annotations\n\nfrom collections import Counter\nfrom enum import IntEnum\nimport logging\nimport re\nfrom typing import Any, Collection, Generator, Iterable, Literal\n\nfrom prompt_toolkit.completion import CompleteEvent, Completer, Completion\nfrom prompt_toolkit.completion.base import Document\nfrom pygments.lexers._mysql_builtins import MYSQL_DATATYPES, MYSQL_FUNCTIONS, MYSQL_KEYWORDS\nimport rapidfuzz\n\nfrom mycli.packages.completion_engine import is_inside_quotes, suggest_type\nfrom mycli.packages.filepaths import complete_path, parse_path, suggest_path\nfrom mycli.packages.parseutils import extract_columns_from_select, last_word\nfrom mycli.packages.special import llm\nfrom mycli.packages.special.favoritequeries import FavoriteQueries\nfrom mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS\n\n_logger = logging.getLogger(__name__)\n\n\nclass Fuzziness(IntEnum):\n    PERFECT = 0\n    REGEX = 1\n    UNDER_WORDS = 2\n    CAMEL_CASE = 3\n    RAPIDFUZZ = 4\n\n\nclass SQLCompleter(Completer):\n    favorite_keywords = [\n        'SELECT',\n        'FROM',\n        'WHERE',\n        'UPDATE',\n        'DELETE FROM',\n        'GROUP BY',\n        'ORDER BY',\n        'JOIN',\n        'LEFT JOIN',\n        'INSERT INTO',\n        'LIKE',\n        'LIMIT',\n        'WITH',\n        'EXPLAIN',\n    ]\n    keywords_raw = [\n        x.upper()\n        for x in favorite_keywords\n        + list(MYSQL_DATATYPES)\n        + list(MYSQL_KEYWORDS)\n        + ['ALTER TABLE', 'CHANGE MASTER TO', 'CHARACTER SET', 'FOREIGN KEY']\n    ]\n    keywords_d = dict.fromkeys(keywords_raw)\n    for x in SPECIAL_COMMANDS:\n        if x.upper() in keywords_d:\n            del keywords_d[x.upper()]\n    keywords = list(keywords_d)\n\n    tidb_keywords = [\n        \"SELECT\",\n        \"FROM\",\n        \"WHERE\",\n        \"DELETE FROM\",\n        \"UPDATE\",\n        \"GROUP BY\",\n        \"JOIN\",\n        \"INSERT INTO\",\n        \"LIKE\",\n        \"LIMIT\",\n        \"ACCOUNT\",\n        \"ACTION\",\n        \"ADD\",\n        \"ADDDATE\",\n        \"ADMIN\",\n        \"ADVISE\",\n        \"AFTER\",\n        \"AGAINST\",\n        \"AGO\",\n        \"ALGORITHM\",\n        \"ALL\",\n        \"ALTER\",\n        \"ALWAYS\",\n        \"ANALYZE\",\n        \"AND\",\n        \"ANY\",\n        \"APPROX_COUNT_DISTINCT\",\n        \"APPROX_PERCENTILE\",\n        \"AS\",\n        \"ASC\",\n        \"ASCII\",\n        \"ATTRIBUTES\",\n        \"AUTO_ID_CACHE\",\n        \"AUTO_INCREMENT\",\n        \"AUTO_RANDOM\",\n        \"AUTO_RANDOM_BASE\",\n        \"AVG\",\n        \"AVG_ROW_LENGTH\",\n        \"BACKEND\",\n        \"BACKUP\",\n        \"BACKUPS\",\n        \"BATCH\",\n        \"BEGIN\",\n        \"BERNOULLI\",\n        \"BETWEEN\",\n        \"BIGINT\",\n        \"BINARY\",\n        \"BINDING\",\n        \"BINDINGS\",\n        \"BINDING_CACHE\",\n        \"BINLOG\",\n        \"BIT\",\n        \"BIT_AND\",\n        \"BIT_OR\",\n        \"BIT_XOR\",\n        \"BLOB\",\n        \"BLOCK\",\n        \"BOOL\",\n        \"BOOLEAN\",\n        \"BOTH\",\n        \"BOUND\",\n        \"BRIEF\",\n        \"BTREE\",\n        \"BUCKETS\",\n        \"BUILTINS\",\n        \"BY\",\n        \"BYTE\",\n        \"CACHE\",\n        \"CALL\",\n        \"CANCEL\",\n        \"CAPTURE\",\n        \"CARDINALITY\",\n        \"CASCADE\",\n        \"CASCADED\",\n        \"CASE\",\n        \"CAST\",\n        \"CAUSAL\",\n        \"CHAIN\",\n        \"CHANGE\",\n        \"CHAR\",\n        \"CHARACTER\",\n        \"CHARSET\",\n        \"CHECK\",\n        \"CHECKPOINT\",\n        \"CHECKSUM\",\n        \"CIPHER\",\n        \"CLEANUP\",\n        \"CLIENT\",\n        \"CLIENT_ERRORS_SUMMARY\",\n        \"CLUSTERED\",\n        \"CMSKETCH\",\n        \"COALESCE\",\n        \"COLLATE\",\n        \"COLLATION\",\n        \"COLUMN\",\n        \"COLUMNS\",\n        \"COLUMN_FORMAT\",\n        \"COLUMN_STATS_USAGE\",\n        \"COMMENT\",\n        \"COMMIT\",\n        \"COMMITTED\",\n        \"COMPACT\",\n        \"COMPRESSED\",\n        \"COMPRESSION\",\n        \"CONCURRENCY\",\n        \"CONFIG\",\n        \"CONNECTION\",\n        \"CONSISTENCY\",\n        \"CONSISTENT\",\n        \"CONSTRAINT\",\n        \"CONSTRAINTS\",\n        \"CONTEXT\",\n        \"CONVERT\",\n        \"COPY\",\n        \"CORRELATION\",\n        \"CPU\",\n        \"CREATE\",\n        \"CROSS\",\n        \"CSV_BACKSLASH_ESCAPE\",\n        \"CSV_DELIMITER\",\n        \"CSV_HEADER\",\n        \"CSV_NOT_NULL\",\n        \"CSV_NULL\",\n        \"CSV_SEPARATOR\",\n        \"CSV_TRIM_LAST_SEPARATORS\",\n        \"CUME_DIST\",\n        \"CURRENT\",\n        \"CURRENT_DATE\",\n        \"CURRENT_ROLE\",\n        \"CURRENT_TIME\",\n        \"CURRENT_TIMESTAMP\",\n        \"CURRENT_USER\",\n        \"CURTIME\",\n        \"CYCLE\",\n        \"DATA\",\n        \"DATABASE\",\n        \"DATABASES\",\n        \"DATE\",\n        \"DATETIME\",\n        \"DATE_ADD\",\n        \"DATE_SUB\",\n        \"DAY\",\n        \"DAY_HOUR\",\n        \"DAY_MICROSECOND\",\n        \"DAY_MINUTE\",\n        \"DAY_SECOND\",\n        \"DDL\",\n        \"DEALLOCATE\",\n        \"DECIMAL\",\n        \"DEFAULT\",\n        \"DEFINER\",\n        \"DELAYED\",\n        \"DELAY_KEY_WRITE\",\n        \"DENSE_RANK\",\n        \"DEPENDENCY\",\n        \"DEPTH\",\n        \"DESC\",\n        \"DESCRIBE\",\n        \"DIRECTORY\",\n        \"DISABLE\",\n        \"DISABLED\",\n        \"DISCARD\",\n        \"DISK\",\n        \"DISTINCT\",\n        \"DISTINCTROW\",\n        \"DIV\",\n        \"DO\",\n        \"DOT\",\n        \"DOUBLE\",\n        \"DRAINER\",\n        \"DROP\",\n        \"DRY\",\n        \"DUAL\",\n        \"DUMP\",\n        \"DUPLICATE\",\n        \"DYNAMIC\",\n        \"ELSE\",\n        \"ENABLE\",\n        \"ENABLED\",\n        \"ENCLOSED\",\n        \"ENCRYPTION\",\n        \"END\",\n        \"ENFORCED\",\n        \"ENGINE\",\n        \"ENGINES\",\n        \"ENUM\",\n        \"ERROR\",\n        \"ERRORS\",\n        \"ESCAPE\",\n        \"ESCAPED\",\n        \"EVENT\",\n        \"EVENTS\",\n        \"EVOLVE\",\n        \"EXACT\",\n        \"EXCEPT\",\n        \"EXCHANGE\",\n        \"EXCLUSIVE\",\n        \"EXECUTE\",\n        \"EXISTS\",\n        \"EXPANSION\",\n        \"EXPIRE\",\n        \"EXPLAIN\",\n        \"EXPR_PUSHDOWN_BLACKLIST\",\n        \"EXTENDED\",\n        \"EXTRACT\",\n        \"FALSE\",\n        \"FAST\",\n        \"FAULTS\",\n        \"FETCH\",\n        \"FIELDS\",\n        \"FILE\",\n        \"FIRST\",\n        \"FIRST_VALUE\",\n        \"FIXED\",\n        \"FLASHBACK\",\n        \"FLOAT\",\n        \"FLUSH\",\n        \"FOLLOWER\",\n        \"FOLLOWERS\",\n        \"FOLLOWER_CONSTRAINTS\",\n        \"FOLLOWING\",\n        \"FOR\",\n        \"FORCE\",\n        \"FOREIGN\",\n        \"FORMAT\",\n        \"FULL\",\n        \"FULLTEXT\",\n        \"FUNCTION\",\n        \"GENERAL\",\n        \"GENERATED\",\n        \"GET_FORMAT\",\n        \"GLOBAL\",\n        \"GRANT\",\n        \"GRANTS\",\n        \"GROUPS\",\n        \"GROUP_CONCAT\",\n        \"HASH\",\n        \"HAVING\",\n        \"HELP\",\n        \"HIGH_PRIORITY\",\n        \"HISTOGRAM\",\n        \"HISTOGRAMS_IN_FLIGHT\",\n        \"HISTORY\",\n        \"HOSTS\",\n        \"HOUR\",\n        \"HOUR_MICROSECOND\",\n        \"HOUR_MINUTE\",\n        \"HOUR_SECOND\",\n        \"IDENTIFIED\",\n        \"IF\",\n        \"IGNORE\",\n        \"IMPORT\",\n        \"IMPORTS\",\n        \"IN\",\n        \"INCREMENT\",\n        \"INCREMENTAL\",\n        \"INDEX\",\n        \"INDEXES\",\n        \"INFILE\",\n        \"INNER\",\n        \"INPLACE\",\n        \"INSERT_METHOD\",\n        \"INSTANCE\",\n        \"INSTANT\",\n        \"INT\",\n        \"INT1\",\n        \"INT2\",\n        \"INT3\",\n        \"INT4\",\n        \"INT8\",\n        \"INTEGER\",\n        \"INTERNAL\",\n        \"INTERSECT\",\n        \"INTERVAL\",\n        \"INTO\",\n        \"INVISIBLE\",\n        \"INVOKER\",\n        \"IO\",\n        \"IPC\",\n        \"IS\",\n        \"ISOLATION\",\n        \"ISSUER\",\n        \"JOB\",\n        \"JOBS\",\n        \"JSON\",\n        \"JSON_ARRAYAGG\",\n        \"JSON_OBJECTAGG\",\n        \"KEY\",\n        \"KEYS\",\n        \"KEY_BLOCK_SIZE\",\n        \"KILL\",\n        \"LABELS\",\n        \"LAG\",\n        \"LANGUAGE\",\n        \"LAST\",\n        \"LASTVAL\",\n        \"LAST_BACKUP\",\n        \"LAST_VALUE\",\n        \"LEAD\",\n        \"LEADER\",\n        \"LEADER_CONSTRAINTS\",\n        \"LEADING\",\n        \"LEARNER\",\n        \"LEARNERS\",\n        \"LEARNER_CONSTRAINTS\",\n        \"LEFT\",\n        \"LESS\",\n        \"LEVEL\",\n        \"LINEAR\",\n        \"LINES\",\n        \"LIST\",\n        \"LOAD\",\n        \"LOCAL\",\n        \"LOCALTIME\",\n        \"LOCALTIMESTAMP\",\n        \"LOCATION\",\n        \"LOCK\",\n        \"LOCKED\",\n        \"LOGS\",\n        \"LONG\",\n        \"LONGBLOB\",\n        \"LONGTEXT\",\n        \"LOW_PRIORITY\",\n        \"MASTER\",\n        \"MATCH\",\n        \"MAX\",\n        \"MAXVALUE\",\n        \"MAX_CONNECTIONS_PER_HOUR\",\n        \"MAX_IDXNUM\",\n        \"MAX_MINUTES\",\n        \"MAX_QUERIES_PER_HOUR\",\n        \"MAX_ROWS\",\n        \"MAX_UPDATES_PER_HOUR\",\n        \"MAX_USER_CONNECTIONS\",\n        \"MB\",\n        \"MEDIUMBLOB\",\n        \"MEDIUMINT\",\n        \"MEDIUMTEXT\",\n        \"MEMORY\",\n        \"MERGE\",\n        \"MICROSECOND\",\n        \"MIN\",\n        \"MINUTE\",\n        \"MINUTE_MICROSECOND\",\n        \"MINUTE_SECOND\",\n        \"MINVALUE\",\n        \"MIN_ROWS\",\n        \"MOD\",\n        \"MODE\",\n        \"MODIFY\",\n        \"MONTH\",\n        \"NAMES\",\n        \"NATIONAL\",\n        \"NATURAL\",\n        \"NCHAR\",\n        \"NEVER\",\n        \"NEXT\",\n        \"NEXTVAL\",\n        \"NEXT_ROW_ID\",\n        \"NO\",\n        \"NOCACHE\",\n        \"NOCYCLE\",\n        \"NODEGROUP\",\n        \"NODE_ID\",\n        \"NODE_STATE\",\n        \"NOMAXVALUE\",\n        \"NOMINVALUE\",\n        \"NONCLUSTERED\",\n        \"NONE\",\n        \"NORMAL\",\n        \"NOT\",\n        \"NOW\",\n        \"NOWAIT\",\n        \"NO_WRITE_TO_BINLOG\",\n        \"NTH_VALUE\",\n        \"NTILE\",\n        \"NULL\",\n        \"NULLS\",\n        \"NUMERIC\",\n        \"NVARCHAR\",\n        \"OF\",\n        \"OFF\",\n        \"OFFSET\",\n        \"ON\",\n        \"ONLINE\",\n        \"ONLY\",\n        \"ON_DUPLICATE\",\n        \"OPEN\",\n        \"OPTIMISTIC\",\n        \"OPTIMIZE\",\n        \"OPTION\",\n        \"OPTIONAL\",\n        \"OPTIONALLY\",\n        \"OPT_RULE_BLACKLIST\",\n        \"OR\",\n        \"ORDER\",\n        \"OUTER\",\n        \"OUTFILE\",\n        \"OVER\",\n        \"PACK_KEYS\",\n        \"PAGE\",\n        \"PARSER\",\n        \"PARTIAL\",\n        \"PARTITION\",\n        \"PARTITIONING\",\n        \"PARTITIONS\",\n        \"PASSWORD\",\n        \"PERCENT\",\n        \"PERCENT_RANK\",\n        \"PER_DB\",\n        \"PER_TABLE\",\n        \"PESSIMISTIC\",\n        \"PLACEMENT\",\n        \"PLAN\",\n        \"PLAN_CACHE\",\n        \"PLUGINS\",\n        \"POLICY\",\n        \"POSITION\",\n        \"PRECEDING\",\n        \"PRECISION\",\n        \"PREDICATE\",\n        \"PREPARE\",\n        \"PRESERVE\",\n        \"PRE_SPLIT_REGIONS\",\n        \"PRIMARY\",\n        \"PRIMARY_REGION\",\n        \"PRIVILEGES\",\n        \"PROCEDURE\",\n        \"PROCESS\",\n        \"PROCESSLIST\",\n        \"PROFILE\",\n        \"PROFILES\",\n        \"PROXY\",\n        \"PUMP\",\n        \"PURGE\",\n        \"QUARTER\",\n        \"QUERIES\",\n        \"QUERY\",\n        \"QUICK\",\n        \"RANGE\",\n        \"RANK\",\n        \"RATE_LIMIT\",\n        \"READ\",\n        \"REAL\",\n        \"REBUILD\",\n        \"RECENT\",\n        \"RECOVER\",\n        \"RECURSIVE\",\n        \"REDUNDANT\",\n        \"REFERENCES\",\n        \"REGEXP\",\n        \"REGION\",\n        \"REGIONS\",\n        \"RELEASE\",\n        \"RELOAD\",\n        \"REMOVE\",\n        \"RENAME\",\n        \"REORGANIZE\",\n        \"REPAIR\",\n        \"REPEAT\",\n        \"REPEATABLE\",\n        \"REPLACE\",\n        \"REPLAYER\",\n        \"REPLICA\",\n        \"REPLICAS\",\n        \"REPLICATION\",\n        \"REQUIRE\",\n        \"REQUIRED\",\n        \"RESET\",\n        \"RESPECT\",\n        \"RESTART\",\n        \"RESTORE\",\n        \"RESTORES\",\n        \"RESTRICT\",\n        \"RESUME\",\n        \"REVERSE\",\n        \"REVOKE\",\n        \"RIGHT\",\n        \"RLIKE\",\n        \"ROLE\",\n        \"ROLLBACK\",\n        \"ROUTINE\",\n        \"ROW\",\n        \"ROWS\",\n        \"ROW_COUNT\",\n        \"ROW_FORMAT\",\n        \"ROW_NUMBER\",\n        \"RTREE\",\n        \"RUN\",\n        \"RUNNING\",\n        \"S3\",\n        \"SAMPLERATE\",\n        \"SAMPLES\",\n        \"SAN\",\n        \"SAVEPOINT\",\n        \"SCHEDULE\",\n        \"SECOND\",\n        \"SECONDARY_ENGINE\",\n        \"SECONDARY_LOAD\",\n        \"SECONDARY_UNLOAD\",\n        \"SECOND_MICROSECOND\",\n        \"SECURITY\",\n        \"SEND_CREDENTIALS_TO_TIKV\",\n        \"SEPARATOR\",\n        \"SEQUENCE\",\n        \"SERIAL\",\n        \"SERIALIZABLE\",\n        \"SESSION\",\n        \"SESSION_STATES\",\n        \"SET\",\n        \"SETVAL\",\n        \"SHARD_ROW_ID_BITS\",\n        \"SHARE\",\n        \"SHARED\",\n        \"SHOW\",\n        \"SHUTDOWN\",\n        \"SIGNED\",\n        \"SIMPLE\",\n        \"SKIP\",\n        \"SKIP_SCHEMA_FILES\",\n        \"SLAVE\",\n        \"SLOW\",\n        \"SMALLINT\",\n        \"SNAPSHOT\",\n        \"SOME\",\n        \"SOURCE\",\n        \"SPATIAL\",\n        \"SPLIT\",\n        \"SQL\",\n        \"SQL_BIG_RESULT\",\n        \"SQL_BUFFER_RESULT\",\n        \"SQL_CACHE\",\n        \"SQL_CALC_FOUND_ROWS\",\n        \"SQL_NO_CACHE\",\n        \"SQL_SMALL_RESULT\",\n        \"SQL_TSI_DAY\",\n        \"SQL_TSI_HOUR\",\n        \"SQL_TSI_MINUTE\",\n        \"SQL_TSI_MONTH\",\n        \"SQL_TSI_QUARTER\",\n        \"SQL_TSI_SECOND\",\n        \"SQL_TSI_WEEK\",\n        \"SQL_TSI_YEAR\",\n        \"SSL\",\n        \"STALENESS\",\n        \"START\",\n        \"STARTING\",\n        \"STATISTICS\",\n        \"STATS\",\n        \"STATS_AUTO_RECALC\",\n        \"STATS_BUCKETS\",\n        \"STATS_COL_CHOICE\",\n        \"STATS_COL_LIST\",\n        \"STATS_EXTENDED\",\n        \"STATS_HEALTHY\",\n        \"STATS_HISTOGRAMS\",\n        \"STATS_META\",\n        \"STATS_OPTIONS\",\n        \"STATS_PERSISTENT\",\n        \"STATS_SAMPLE_PAGES\",\n        \"STATS_SAMPLE_RATE\",\n        \"STATS_TOPN\",\n        \"STATUS\",\n        \"STD\",\n        \"STDDEV\",\n        \"STDDEV_POP\",\n        \"STDDEV_SAMP\",\n        \"STOP\",\n        \"STORAGE\",\n        \"STORED\",\n        \"STRAIGHT_JOIN\",\n        \"STRICT\",\n        \"STRICT_FORMAT\",\n        \"STRONG\",\n        \"SUBDATE\",\n        \"SUBJECT\",\n        \"SUBPARTITION\",\n        \"SUBPARTITIONS\",\n        \"SUBSTRING\",\n        \"SUM\",\n        \"SUPER\",\n        \"SWAPS\",\n        \"SWITCHES\",\n        \"SYSTEM\",\n        \"SYSTEM_TIME\",\n        \"TABLE\",\n        \"TABLES\",\n        \"TABLESAMPLE\",\n        \"TABLESPACE\",\n        \"TABLE_CHECKSUM\",\n        \"TARGET\",\n        \"TELEMETRY\",\n        \"TELEMETRY_ID\",\n        \"TEMPORARY\",\n        \"TEMPTABLE\",\n        \"TERMINATED\",\n        \"TEXT\",\n        \"THAN\",\n        \"THEN\",\n        \"TIDB\",\n        \"TIFLASH\",\n        \"TIKV_IMPORTER\",\n        \"TIME\",\n        \"TIMESTAMP\",\n        \"TIMESTAMPADD\",\n        \"TIMESTAMPDIFF\",\n        \"TINYBLOB\",\n        \"TINYINT\",\n        \"TINYTEXT\",\n        \"TLS\",\n        \"TO\",\n        \"TOKUDB_DEFAULT\",\n        \"TOKUDB_FAST\",\n        \"TOKUDB_LZMA\",\n        \"TOKUDB_QUICKLZ\",\n        \"TOKUDB_SMALL\",\n        \"TOKUDB_SNAPPY\",\n        \"TOKUDB_UNCOMPRESSED\",\n        \"TOKUDB_ZLIB\",\n        \"TOP\",\n        \"TOPN\",\n        \"TRACE\",\n        \"TRADITIONAL\",\n        \"TRAILING\",\n        \"TRANSACTION\",\n        \"TRIGGER\",\n        \"TRIGGERS\",\n        \"TRIM\",\n        \"TRUE\",\n        \"TRUE_CARD_COST\",\n        \"TRUNCATE\",\n        \"TYPE\",\n        \"UNBOUNDED\",\n        \"UNCOMMITTED\",\n        \"UNDEFINED\",\n        \"UNICODE\",\n        \"UNION\",\n        \"UNIQUE\",\n        \"UNKNOWN\",\n        \"UNLOCK\",\n        \"UNSIGNED\",\n        \"USAGE\",\n        \"USE\",\n        \"USER\",\n        \"USING\",\n        \"UTC_DATE\",\n        \"UTC_TIME\",\n        \"UTC_TIMESTAMP\",\n        \"VALIDATION\",\n        \"VALUE\",\n        \"VALUES\",\n        \"VARBINARY\",\n        \"VARCHAR\",\n        \"VARCHARACTER\",\n        \"VARIABLES\",\n        \"VARIANCE\",\n        \"VARYING\",\n        \"VAR_POP\",\n        \"VAR_SAMP\",\n        \"VERBOSE\",\n        \"VIEW\",\n        \"VIRTUAL\",\n        \"VISIBLE\",\n        \"VOTER\",\n        \"VOTERS\",\n        \"VOTER_CONSTRAINTS\",\n        \"WAIT\",\n        \"WARNINGS\",\n        \"WEEK\",\n        \"WEIGHT_STRING\",\n        \"WHEN\",\n        \"WIDTH\",\n        \"WINDOW\",\n        \"WITH\",\n        \"WITHOUT\",\n        \"WRITE\",\n        \"X509\",\n        \"XOR\",\n        \"YEAR\",\n        \"YEAR_MONTH\",\n        \"ZEROFILL\",\n    ]\n\n    # misclassified as keywords\n    # do they need to also be subtracted from keywords?\n    pygments_misclassified_functions = [\n        'ASCII',\n        'AVG',\n        'CHARSET',\n        'COALESCE',\n        'COLLATION',\n        'CONVERT',\n        'CUME_DIST',\n        'CURRENT_DATE',\n        'CURRENT_TIME',\n        'CURRENT_TIMESTAMP',\n        'CURRENT_USER',\n        'DATABASE',\n        'DAY',\n        'DEFAULT',\n        'DENSE_RANK',\n        'EXISTS',\n        'FIRST_VALUE',\n        'FORMAT',\n        'GEOMCOLLECTION',\n        'GET_FORMAT',\n        'GROUPING',\n        'HOUR',\n        'IF',\n        'INSERT',\n        'INTERVAL',\n        'JSON_TABLE',\n        'JSON_VALUE',\n        'LAG',\n        'LAST_VALUE',\n        'LEAD',\n        'LEFT',\n        'LOCALTIME',\n        'LOCALTIMESTAMP',\n        'MATCH',\n        'MICROSECOND',\n        'MINUTE',\n        'MOD',\n        'MONTH',\n        'NTH_VALUE',\n        'NTILE',\n        'PERCENT_RANK',\n        'QUARTER',\n        'RANK',\n        'REPEAT',\n        'REPLACE',\n        'REVERSE',\n        'RIGHT',\n        'ROW_COUNT',\n        'ROW_NUMBER',\n        'SCHEMA',\n        'SECOND',\n        'TIMESTAMPADD',\n        'TIMESTAMPDIFF',\n        'TRUNCATE',\n        'USER',\n        'UTC_DATE',\n        'UTC_TIME',\n        'UTC_TIMESTAMP',\n        'VALUES',\n        'WEEK',\n        'WEIGHT_STRING',\n    ]\n\n    # should case be respected for functions styled as CamelCase?\n    pygments_missing_functions = [\n        'BINARY',  # deprecated function, but available everywhere\n        'CHAR',\n        'DATE',\n        'DISTANCE',\n        'ETAG',\n        'GeometryCollection',\n        'JSON_DUALITY_OBJECT',\n        'LineString',\n        'MultiLineString',\n        'MultiPoint',\n        'MultiPolygon',\n        'Point',\n        'Polygon',\n        'STRING_TO_VECTOR',\n        'TIME',\n        'TIMESTAMP',\n        'VECTOR_DIM',\n        'VECTOR_TO_STRING',\n        'YEAR',\n    ]\n\n    # so far an incomplete list\n    # these should be spun out and completed independently from functions in the value position\n    pygments_value_position_nonfunction_keywords = [\n        'BETWEEN',\n        'CASE',\n        'DISTINCT',\n        'FALSE',\n        'NOT',\n        'NULL',\n        'TRUE',\n    ]\n\n    # should https://dev.mysql.com/doc/refman/9.6/en/loadable-function-reference.html also be added?\n    pygments_functions_supplemented = sorted(\n        [x.upper() for x in MYSQL_FUNCTIONS]\n        + [x.upper() for x in pygments_misclassified_functions]\n        + [x.upper() for x in pygments_missing_functions]\n        + [x.upper() for x in pygments_value_position_nonfunction_keywords]\n    )\n\n    favorite_functions = [\n        'COUNT',\n        'CONVERT',\n        'BINARY',\n        'CAST',\n        'COALESCE',\n        'MAX',\n        'MIN',\n        'SUM',\n        'AVG',\n        'JSON_EXTRACT',\n        'JSON_VALUE',\n        'JSON_REMOVE',\n        'JSON_SET',\n        'CONCAT',\n        'GROUP_CONCAT',\n        'CHAR_LENGTH',\n        'ROUND',\n        'FLOOR',\n        'CEIL',\n        'IF',\n        'IFNULL',\n        'SUBSTR',\n        'SUBSTRING_INDEX',\n        'REPLACE',\n        'RIGHT',\n        'LEFT',\n        'UNIX_TIMESTAMP',\n        'FROM_UNIXTIME',\n        'RAND',\n        'DATEDIFF',\n        'DATE_SUB',\n    ]\n    functions_raw = favorite_functions + pygments_functions_supplemented\n    functions = list(dict.fromkeys(functions_raw))\n\n    # https://docs.pingcap.com/tidb/dev/tidb-functions\n    tidb_functions = [\n        \"TIDB_BOUNDED_STALENESS\",\n        \"TIDB_DECODE_KEY\",\n        \"TIDB_DECODE_PLAN\",\n        \"TIDB_IS_DDL_OWNER\",\n        \"TIDB_PARSE_TSO\",\n        \"TIDB_VERSION\",\n        \"TIDB_DECODE_SQL_DIGESTS\",\n        \"VITESS_HASH\",\n        \"TIDB_SHARD\",\n    ]\n\n    show_items: list[Completion] = []\n\n    change_items = [\n        \"MASTER_BIND\",\n        \"MASTER_HOST\",\n        \"MASTER_USER\",\n        \"MASTER_PASSWORD\",\n        \"MASTER_PORT\",\n        \"MASTER_CONNECT_RETRY\",\n        \"MASTER_HEARTBEAT_PERIOD\",\n        \"MASTER_LOG_FILE\",\n        \"MASTER_LOG_POS\",\n        \"RELAY_LOG_FILE\",\n        \"RELAY_LOG_POS\",\n        \"MASTER_SSL\",\n        \"MASTER_SSL_CA\",\n        \"MASTER_SSL_CAPATH\",\n        \"MASTER_SSL_CERT\",\n        \"MASTER_SSL_KEY\",\n        \"MASTER_SSL_CIPHER\",\n        \"MASTER_SSL_VERIFY_SERVER_CERT\",\n        \"IGNORE_SERVER_IDS\",\n    ]\n\n    users: list[str] = []\n\n    character_sets: list[str] = []\n\n    collations: list[str] = []\n\n    def __init__(\n        self,\n        smart_completion: bool = True,\n        supported_formats: tuple = (),\n        keyword_casing: str = \"auto\",\n    ) -> None:\n        super(self.__class__, self).__init__()\n        self.smart_completion = smart_completion\n        self.reserved_words = set()\n        for x in self.keywords:\n            self.reserved_words.update(x.split())\n        self.name_pattern = re.compile(r\"^[_a-zA-Z][_a-zA-Z0-9\\$]*$\")\n\n        self.special_commands: list[str] = []\n        self.table_formats = supported_formats\n        if keyword_casing not in (\"upper\", \"lower\", \"auto\"):\n            keyword_casing = \"auto\"\n        self.keyword_casing = keyword_casing\n        self.reset_completions()\n\n    def escape_name(self, name: str) -> str:\n        if name and ((not self.name_pattern.match(name)) or (name.upper() in self.reserved_words) or (name.upper() in self.functions)):\n            name = f'`{name}`'\n\n        return name\n\n    def escaped_names(self, names: Collection[str]) -> list[str]:\n        return [self.escape_name(name) for name in names]\n\n    def extend_special_commands(self, special_commands: list[str]) -> None:\n        # Special commands are not part of all_completions since they can only\n        # be at the beginning of a line.\n        self.special_commands.extend(special_commands)\n\n    def extend_database_names(self, databases: list[str]) -> None:\n        self.databases.extend([self.escape_name(db) for db in databases])\n\n    def extend_keywords(self, keywords: list[str], replace: bool = False) -> None:\n        if replace:\n            self.keywords = keywords\n        else:\n            self.keywords.extend(keywords)\n        self.all_completions.update(keywords)\n\n    def extend_show_items(self, show_items: Iterable[tuple]) -> None:\n        for show_item in show_items:\n            self.show_items.extend(show_item)\n            self.all_completions.update(show_item)\n\n    def extend_change_items(self, change_items: Iterable[tuple]) -> None:\n        for change_item in change_items:\n            self.change_items.extend(change_item)\n            self.all_completions.update(change_item)\n\n    def extend_users(self, users: Iterable[tuple]) -> None:\n        for user in users:\n            self.users.extend(user)\n            self.all_completions.update(user)\n\n    def extend_schemata(self, schema: str | None) -> None:\n        if schema is None:\n            return\n        metadata = self.dbmetadata[\"tables\"]\n        metadata[schema] = {}\n\n        # dbmetadata.values() are the 'tables' and 'functions' dicts\n        for metadata in self.dbmetadata.values():\n            metadata[schema] = {}\n        self.all_completions.update(schema)\n\n    def extend_relations(self, data: list[tuple[str, str]], kind: Literal['tables', 'views']) -> None:\n        \"\"\"Extend metadata for tables or views\n\n        :param data: list of (rel_name, ) tuples\n        :param kind: either 'tables' or 'views'\n        :return:\n        \"\"\"\n        data_ll = [self.escaped_names(d) for d in data]\n\n        # dbmetadata['tables'][$schema_name][$table_name] should be a list of\n        # column names. Default to an asterisk\n        metadata = self.dbmetadata[kind]\n        for relname in data_ll:\n            try:\n                metadata[self.dbname][relname[0]] = [\"*\"]\n            except KeyError:\n                _logger.error(\"%r %r listed in unrecognized schema %r\", kind, relname[0], self.dbname)\n            self.all_completions.add(relname[0])\n\n    def extend_columns(self, column_data: list[tuple[str, str]], kind: Literal['tables', 'views']) -> None:\n        \"\"\"Extend column metadata\n\n        :param column_data: list of (rel_name, column_name) tuples\n        :param kind: either 'tables' or 'views'\n        :return:\n        \"\"\"\n        column_data_ll = [self.escaped_names(d) for d in column_data]\n\n        metadata = self.dbmetadata[kind]\n        for relname, column in column_data_ll:\n            if relname not in metadata[self.dbname]:\n                _logger.error(\"relname '%s' was not found in db '%s'\", relname, self.dbname)\n                # this could happen back when the completer populated via two calls:\n                # SHOW TABLES then SELECT table_name, column_name from information_schema.columns\n                # it's a slight race, but much more likely on Vitess picking random shards for each.\n                # see discussion in https://github.com/dbcli/mycli/pull/1182 (tl;dr - let's keep it)\n                continue\n            metadata[self.dbname][relname].append(column)\n            self.all_completions.add(column)\n\n    def extend_enum_values(self, enum_data: Iterable[tuple[str, str, list[str]]]) -> None:\n        metadata = self.dbmetadata[\"enum_values\"]\n        if self.dbname not in metadata:\n            metadata[self.dbname] = {}\n\n        for relname, column, values in enum_data:\n            relname_escaped = self.escape_name(relname)\n            column_escaped = self.escape_name(column)\n            table_meta = metadata[self.dbname].setdefault(relname_escaped, {})\n            table_meta[column_escaped] = values\n\n    def extend_functions(self, func_data: list[str] | Generator[tuple[str, str]], builtin: bool = False) -> None:\n        # if 'builtin' is set this is extending the list of builtin functions\n        if builtin:\n            if isinstance(func_data, list):\n                self.functions.extend(func_data)\n            return\n\n        # 'func_data' is a generator object. It can throw an exception while\n        # being consumed. This could happen if the user has launched the app\n        # without specifying a database name. This exception must be handled to\n        # prevent crashing.\n        try:\n            func_data_ll = [self.escaped_names(d) for d in func_data]\n        except Exception:\n            func_data_ll = []\n\n        # dbmetadata['functions'][$schema_name][$function_name] should return\n        # function metadata.\n        metadata = self.dbmetadata[\"functions\"]\n\n        for func in func_data_ll:\n            metadata[self.dbname][func[0]] = None\n            self.all_completions.add(func[0])\n\n    def extend_procedures(self, procedure_data: Generator[tuple]) -> None:\n        metadata = self.dbmetadata[\"procedures\"]\n        if self.dbname not in metadata:\n            metadata[self.dbname] = {}\n\n        for elt in procedure_data:\n            # not sure why this happens on MariaDB in some cases\n            # see https://github.com/dbcli/mycli/issues/1531\n            if not elt:\n                continue\n            if not elt[0]:\n                continue\n            metadata[self.dbname][elt[0]] = None\n\n    def extend_character_sets(self, character_set_data: Generator[tuple]) -> None:\n        for elt in character_set_data:\n            if not elt:\n                continue\n            if not elt[0]:\n                continue\n            self.character_sets.append(elt[0])\n            self.all_completions.update(elt[0])\n\n    def extend_collations(self, collation_data: Generator[tuple]) -> None:\n        for elt in collation_data:\n            if not elt:\n                continue\n            if not elt[0]:\n                continue\n            self.collations.append(elt[0])\n            self.all_completions.update(elt[0])\n\n    def set_dbname(self, dbname: str | None) -> None:\n        self.dbname = dbname or ''\n\n    def reset_completions(self) -> None:\n        self.databases: list[str] = []\n        self.users: list[str] = []\n        self.character_sets: list[str] = []\n        self.collations: list[str] = []\n        self.show_items: list[Completion] = []\n        self.dbname = \"\"\n        self.dbmetadata: dict[str, Any] = {\n            \"tables\": {},\n            \"views\": {},\n            \"functions\": {},\n            \"procedures\": {},\n            \"enum_values\": {},\n        }\n        self.all_completions = set(self.keywords + self.functions)\n\n    @staticmethod\n    def find_matches(\n        orig_text: str,\n        collection: Collection,\n        start_only: bool = False,\n        fuzzy: bool = True,\n        casing: str | None = None,\n        text_before_cursor: str = '',\n    ) -> Generator[tuple[str, int], None, None]:\n        \"\"\"Find completion matches for the given text.\n\n        Given the user's input text and a collection of available\n        completions, find completions matching the last word of the\n        text.\n\n        If `start_only` is True, the text will match an available\n        completion only at the beginning. Otherwise, a completion is\n        considered a match if the text appears anywhere within it.\n\n        yields prompt_toolkit Completion instances for any matches found\n        in the collection of available completions.\n        \"\"\"\n        last = last_word(orig_text, include=\"most_punctuations\")\n        text = last.lower()\n        # unicode support not possible without adding the regex dependency\n        case_change_pat = re.compile(\"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])\")\n\n        completions: list[tuple[str, int]] = []\n\n        def maybe_quote_identifier(item: str) -> str:\n            if item.startswith('`'):\n                return item\n            if item == '*':\n                return item\n            return '`' + item + '`'\n\n        # checking text.startswith() first is an optimization; is_inside_quotes() covers more cases\n        if text.startswith('`') or is_inside_quotes(text_before_cursor, len(text_before_cursor)) == 'backtick':\n            quoted_collection: Collection[Any] = [maybe_quote_identifier(x) if isinstance(x, str) else x for x in collection]\n        else:\n            quoted_collection = collection\n\n        if fuzzy:\n            regex = \".{0,3}?\".join(map(re.escape, text))\n            pat = re.compile(f'({regex})')\n            under_words_text = [x for x in text.split('_') if x]\n            case_words_text = re.split(case_change_pat, last)\n\n            for item in quoted_collection:\n                r = pat.search(item.lower())\n                if r:\n                    completions.append((item, Fuzziness.REGEX))\n                    continue\n\n                under_words_item = [x for x in item.lower().split('_') if x]\n                occurrences = 0\n                for elt_word in under_words_text:\n                    for elt_item in under_words_item:\n                        if elt_item.startswith(elt_word):\n                            occurrences += 1\n                            break\n                if occurrences >= len(under_words_text):\n                    completions.append((item, Fuzziness.UNDER_WORDS))\n                    continue\n\n                case_words_item = re.split(case_change_pat, item)\n                occurrences = 0\n                for elt_word in case_words_text:\n                    for elt_item in case_words_item:\n                        if elt_item.startswith(elt_word):\n                            occurrences += 1\n                            break\n                if occurrences >= len(case_words_text):\n                    completions.append((item, Fuzziness.CAMEL_CASE))\n                    continue\n\n            if len(text) >= 4:\n                rapidfuzz_matches = rapidfuzz.process.extract(\n                    text,\n                    quoted_collection,\n                    scorer=rapidfuzz.fuzz.WRatio,\n                    # todo: maybe make our own processor which only does case-folding\n                    # because underscores are valuable info\n                    processor=rapidfuzz.utils.default_process,\n                    limit=20,\n                    score_cutoff=75,\n                )\n                for elt in rapidfuzz_matches:\n                    item, _score, _type = elt\n                    if len(item) < len(text) / 1.5:\n                        continue\n                    if item in completions:\n                        continue\n                    completions.append((item, Fuzziness.RAPIDFUZZ))\n\n        else:\n            match_end_limit = len(text) if start_only else None\n            for item in quoted_collection:\n                match_point = item.lower().find(text, 0, match_end_limit)\n                if match_point >= 0:\n                    completions.append((item, Fuzziness.PERFECT))\n\n        if casing == \"auto\":\n            casing = \"lower\" if last and (last[0].islower() or last[-1].islower()) else \"upper\"\n\n        def apply_case(tup: tuple[str, int]) -> tuple[str, int]:\n            kw, fuzziness = tup\n            if casing == \"upper\":\n                return (kw.upper(), fuzziness)\n            return (kw.lower(), fuzziness)\n\n        return (x if casing is None else apply_case(x) for x in completions)\n\n    def get_completions(\n        self,\n        document: Document,\n        complete_event: CompleteEvent | None,\n        smart_completion: bool | None = None,\n    ) -> Iterable[Completion]:\n        word_before_cursor = document.get_word_before_cursor(WORD=True)\n        last_for_len = last_word(word_before_cursor, include=\"most_punctuations\")\n        text_for_len = last_for_len.lower()\n        last_for_len_paths = last_word(word_before_cursor, include='alphanum_underscore')\n\n        if smart_completion is None:\n            smart_completion = self.smart_completion\n\n        # If smart_completion is off then match any word that starts with\n        # 'word_before_cursor'.\n        if not smart_completion:\n            matches = self.find_matches(\n                word_before_cursor,\n                self.all_completions,\n                start_only=True,\n                fuzzy=False,\n                text_before_cursor=document.text_before_cursor,\n            )\n            return (Completion(x[0], -len(text_for_len)) for x in matches)\n\n        completions: list[tuple[str, int, int]] = []\n        suggestions = suggest_type(document.text, document.text_before_cursor)\n        rigid_sort = False\n        length_based_on_path = False\n\n        rank = 0\n        for suggestion in suggestions:\n            _logger.debug(\"Suggestion type: %r\", suggestion[\"type\"])\n            rank += 1\n\n            if suggestion[\"type\"] == \"column\":\n                tables = suggestion[\"tables\"]\n                _logger.debug(\"Completion column scope: %r\", tables)\n                scoped_cols = self.populate_scoped_cols(tables)\n                if suggestion.get(\"drop_unique\"):\n                    # drop_unique is used for 'tb11 JOIN tbl2 USING (...'\n                    # which should suggest only columns that appear in more than\n                    # one table\n                    scoped_cols = [col for (col, count) in Counter(scoped_cols).items() if count > 1 and col != \"*\"]\n                elif not tables:\n                    # if tables was empty, this is a naked SELECT and we are\n                    # showing all columns. So make them unique and sort them.\n                    scoped_cols = sorted(set(scoped_cols), key=lambda s: s.strip('`'))\n\n                cols = self.find_matches(\n                    word_before_cursor,\n                    scoped_cols,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in cols])\n\n            elif suggestion[\"type\"] == \"function\":\n                # suggest user-defined functions using substring matching\n                funcs = self.populate_schema_objects(suggestion[\"schema\"], \"functions\")\n                user_funcs = self.find_matches(\n                    word_before_cursor,\n                    funcs,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in user_funcs])\n\n                # suggest hardcoded functions using startswith matching only if\n                # there is no schema qualifier. If a schema qualifier is\n                # present it probably denotes a table.\n                # eg: SELECT * FROM users u WHERE u.\n                if not suggestion[\"schema\"]:\n                    predefined_funcs = self.find_matches(\n                        word_before_cursor,\n                        self.functions,\n                        start_only=True,\n                        fuzzy=False,\n                        casing=self.keyword_casing,\n                        text_before_cursor=document.text_before_cursor,\n                    )\n                    completions.extend([(*x, rank) for x in predefined_funcs])\n\n            elif suggestion[\"type\"] == \"procedure\":\n                procs = self.populate_schema_objects(suggestion[\"schema\"], \"procedures\")\n                procs_m = self.find_matches(\n                    word_before_cursor,\n                    procs,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in procs_m])\n\n            elif suggestion['type'] == 'introducer':\n                introducers = [f'_{x}' for x in self.character_sets]\n                introducers_m = self.find_matches(\n                    word_before_cursor,\n                    introducers,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in introducers_m])\n\n            elif suggestion['type'] == 'character_set':\n                charsets_m = self.find_matches(\n                    word_before_cursor,\n                    self.character_sets,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in charsets_m])\n\n            elif suggestion['type'] == 'collation':\n                collations_m = self.find_matches(\n                    word_before_cursor,\n                    self.collations,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in collations_m])\n\n            elif suggestion[\"type\"] == \"table\":\n                # If this is a select and columns are given, parse the columns and\n                # then only return tables that have one or more of the given columns.\n                # If no columns are given (or able to be parsed), return all tables\n                # as usual.\n                columns = extract_columns_from_select(document.text)\n                if columns:\n                    tables = self.populate_schema_objects(suggestion[\"schema\"], \"tables\", columns)\n                else:\n                    tables = self.populate_schema_objects(suggestion[\"schema\"], \"tables\")\n                tables_m = self.find_matches(\n                    word_before_cursor,\n                    tables,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in tables_m])\n\n            elif suggestion[\"type\"] == \"view\":\n                views = self.populate_schema_objects(suggestion[\"schema\"], \"views\")\n                views_m = self.find_matches(\n                    word_before_cursor,\n                    views,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in views_m])\n\n            elif suggestion[\"type\"] == \"alias\":\n                aliases = suggestion[\"aliases\"]\n                aliases_m = self.find_matches(\n                    word_before_cursor,\n                    aliases,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in aliases_m])\n\n            elif suggestion[\"type\"] == \"database\":\n                dbs_m = self.find_matches(\n                    word_before_cursor,\n                    self.databases,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in dbs_m])\n\n            elif suggestion[\"type\"] == \"keyword\":\n                keywords_m = self.find_matches(\n                    word_before_cursor,\n                    self.keywords,\n                    casing=self.keyword_casing,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in keywords_m])\n\n            elif suggestion[\"type\"] == \"show\":\n                show_items_m = self.find_matches(\n                    word_before_cursor,\n                    self.show_items,\n                    start_only=False,\n                    fuzzy=True,\n                    casing=self.keyword_casing,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in show_items_m])\n\n            elif suggestion[\"type\"] == \"change\":\n                change_items_m = self.find_matches(\n                    word_before_cursor,\n                    self.change_items,\n                    start_only=False,\n                    fuzzy=True,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in change_items_m])\n\n            elif suggestion[\"type\"] == \"user\":\n                users_m = self.find_matches(\n                    word_before_cursor,\n                    self.users,\n                    start_only=False,\n                    fuzzy=True,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in users_m])\n\n            elif suggestion[\"type\"] == \"special\":\n                special_m = self.find_matches(\n                    word_before_cursor,\n                    self.special_commands,\n                    start_only=True,\n                    fuzzy=False,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                # specials are special, and go early in the candidates, first if possible\n                completions.extend([(*x, 0) for x in special_m])\n\n            elif suggestion[\"type\"] == \"favoritequery\":\n                if hasattr(FavoriteQueries, 'instance') and hasattr(FavoriteQueries.instance, 'list'):\n                    queries_m = self.find_matches(\n                        word_before_cursor,\n                        FavoriteQueries.instance.list(),\n                        start_only=False,\n                        fuzzy=True,\n                        text_before_cursor=document.text_before_cursor,\n                    )\n                    completions.extend([(*x, rank) for x in queries_m])\n\n            elif suggestion[\"type\"] == \"table_format\":\n                formats_m = self.find_matches(\n                    word_before_cursor,\n                    self.table_formats,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in formats_m])\n\n            elif suggestion[\"type\"] == \"file_name\":\n                file_names_m = self.find_files(word_before_cursor)\n                completions.extend([(*x, rank) for x in file_names_m])\n                # for filenames we _really_ want directories to go last\n                rigid_sort = True\n                length_based_on_path = True\n            elif suggestion[\"type\"] == \"llm\":\n                if not word_before_cursor:\n                    tokens = document.text.split()[1:]\n                else:\n                    tokens = document.text.split()[1:-1]\n                possible_entries = llm.get_completions(tokens)\n                subcommands_m = self.find_matches(\n                    word_before_cursor,\n                    possible_entries,\n                    start_only=False,\n                    fuzzy=True,\n                    text_before_cursor=document.text_before_cursor,\n                )\n                completions.extend([(*x, rank) for x in subcommands_m])\n\n            elif suggestion[\"type\"] == \"enum_value\":\n                enum_values = self.populate_enum_values(\n                    suggestion[\"tables\"],\n                    suggestion[\"column\"],\n                    suggestion.get(\"parent\"),\n                )\n                if enum_values:\n                    quoted_values = [self._quote_sql_string(value) for value in enum_values]\n                    completions = [\n                        (*x, rank)\n                        for x in self.find_matches(\n                            word_before_cursor,\n                            quoted_values,\n                            text_before_cursor=document.text_before_cursor,\n                        )\n                    ]\n                    break\n\n        def completion_sort_key(item: tuple[str, int, int], text_for_len: str):\n            candidate, fuzziness, rank = item\n            if not text_for_len:\n                # sort only by the rank (the order of the completion type)\n                return (0, rank, 0)\n            elif candidate.lower().startswith(text_for_len):\n                # sort only by the length of the candidate\n                return (0, 0, -1000 + len(candidate))\n            # sort by fuzziness and rank\n            # todo add alpha here, or original order?\n            return (fuzziness, rank, 0)\n\n        if rigid_sort:\n            uniq_completions_str = dict.fromkeys(x[0] for x in completions)\n        else:\n            sorted_completions = sorted(completions, key=lambda item: completion_sort_key(item, text_for_len.lower()))\n            uniq_completions_str = dict.fromkeys(x[0] for x in sorted_completions)\n\n        if length_based_on_path:\n            return (Completion(x, -len(last_for_len_paths)) for x in uniq_completions_str)\n        else:\n            return (Completion(x, -len(text_for_len)) for x in uniq_completions_str)\n\n    def find_files(self, word: str) -> Generator[tuple[str, int], None, None]:\n        \"\"\"Yield matching directory or file names.\n\n        :param word:\n        :return: iterable\n\n        \"\"\"\n        # todo position is ignored, but may need to be used\n        # todo fuzzy matches for filenames\n        base_path, last_path, position = parse_path(word)\n        paths = suggest_path(word)\n        for name in paths:\n            suggestion = complete_path(name, last_path)\n            if suggestion:\n                yield (suggestion, Fuzziness.PERFECT)\n\n    def populate_scoped_cols(self, scoped_tbls: list[tuple[str | None, str, str | None]]) -> list[str]:\n        \"\"\"Find all columns in a set of scoped_tables\n        :param scoped_tbls: list of (schema, table, alias) tuples\n        :return: list of column names\n        \"\"\"\n        columns = []\n        meta = self.dbmetadata\n\n        # if scoped tables is empty, this is just after a SELECT so we\n        # show all columns for all tables in the schema.\n        if len(scoped_tbls) == 0 and self.dbname:\n            for table in meta[\"tables\"][self.dbname]:\n                columns.extend(meta[\"tables\"][self.dbname][table])\n            return columns or ['*']\n\n        # query includes tables, so use those to populate columns\n        for tbl in scoped_tbls:\n            # A fully qualified schema.relname reference or default_schema\n            # DO NOT escape schema names.\n            schema = tbl[0] or self.dbname\n            relname = tbl[1]\n            escaped_relname = self.escape_name(tbl[1])\n\n            # We don't know if schema.relname is a table or view. Since\n            # tables and views cannot share the same name, we can check one\n            # at a time\n            try:\n                columns.extend(meta[\"tables\"][schema][relname])\n\n                # Table exists, so don't bother checking for a view\n                continue\n            except KeyError:\n                try:\n                    columns.extend(meta[\"tables\"][schema][escaped_relname])\n                    # Table exists, so don't bother checking for a view\n                    continue\n                except KeyError:\n                    pass\n\n            try:\n                columns.extend(meta[\"views\"][schema][relname])\n            except KeyError:\n                pass\n\n        return columns\n\n    def populate_enum_values(\n        self,\n        scoped_tbls: list[tuple[str | None, str, str | None]],\n        column: str,\n        parent: str | None = None,\n    ) -> list[str]:\n        values: list[str] = []\n        meta = self.dbmetadata[\"enum_values\"]\n        column_key = self._escape_identifier(column)\n        parent_key = self._strip_backticks(parent) if parent else None\n\n        for schema, relname, alias in scoped_tbls:\n            if parent_key and not self._matches_parent(parent_key, schema, relname, alias):\n                continue\n\n            schema = schema or self.dbname\n            table_meta = meta.get(schema, {})\n            escaped_relname = self.escape_name(relname)\n\n            for rel_key in {relname, escaped_relname}:\n                columns = table_meta.get(rel_key)\n                if columns and column_key in columns:\n                    values.extend(columns[column_key])\n\n        return list(dict.fromkeys(values))\n\n    def _escape_identifier(self, name: str) -> str:\n        return self.escape_name(self._strip_backticks(name))\n\n    @staticmethod\n    def _strip_backticks(name: str | None) -> str:\n        if name and name[0] == \"`\" and name[-1] == \"`\":\n            return name[1:-1]\n        return name or \"\"\n\n    @staticmethod\n    def _matches_parent(parent: str, schema: str | None, relname: str, alias: str | None) -> bool:\n        if alias and parent == alias:\n            return True\n        if parent == relname:\n            return True\n        if schema and parent == f\"{schema}.{relname}\":\n            return True\n        return False\n\n    @staticmethod\n    def _quote_sql_string(value: str) -> str:\n        return \"'\" + value.replace(\"'\", \"''\") + \"'\"\n\n    def populate_schema_objects(self, schema: str | None, obj_type: str, columns: list[str] | None = None) -> list[str]:\n        \"\"\"Returns list of tables or functions for a (optional) schema\"\"\"\n        metadata = self.dbmetadata[obj_type]\n        schema = schema or self.dbname\n        try:\n            objects = list(metadata[schema].keys())\n        except KeyError:\n            # schema doesn't exist\n            objects = []\n\n        filtered_objects: list[str] = []\n        remaining_objects: list[str] = []\n\n        # If the requested object type is tables and the user already entered\n        # columns, return a filtered list of tables (or views) that contain\n        # one or more of the given columns. If a table does not contain the\n        # given columns, add it to a separate list to add to the end of the\n        # filtered suggestions.\n        if obj_type == \"tables\" and columns and objects:\n            for obj in objects:\n                matched = False\n                for column in metadata[schema][obj]:\n                    if column in columns:\n                        filtered_objects.append(obj)\n                        matched = True\n                        break\n                if not matched:\n                    remaining_objects.append(obj)\n        else:\n            filtered_objects = objects\n        return filtered_objects + remaining_objects\n"
  },
  {
    "path": "mycli/sqlexecute.py",
    "content": "from __future__ import annotations\n\nimport datetime\nimport enum\nimport logging\nimport re\nimport ssl\nfrom typing import Any, Generator, Iterable\n\nfrom prompt_toolkit.formatted_text import FormattedText\nimport pymysql\nfrom pymysql.connections import Connection\nfrom pymysql.constants import FIELD_TYPE\nfrom pymysql.converters import conversions, convert_date, convert_datetime, convert_time, decoders\nfrom pymysql.cursors import Cursor\n\nfrom mycli.packages.special import iocommands\nfrom mycli.packages.special.main import CommandNotFound, execute\nfrom mycli.packages.sqlresult import SQLResult\n\ntry:\n    import paramiko  # noqa: F401\n    import sshtunnel\nexcept ImportError:\n    pass\n\n_logger = logging.getLogger(__name__)\n\nFIELD_TYPES = decoders.copy()\nFIELD_TYPES.update({FIELD_TYPE.NULL: type(None)})\n\n\nERROR_CODE_ACCESS_DENIED = 1045\n\n\nclass ServerSpecies(enum.Enum):\n    MySQL = \"MySQL\"\n    MariaDB = \"MariaDB\"\n    Percona = \"Percona\"\n    TiDB = \"TiDB\"\n    Unknown = \"Unknown\"\n\n\nclass ServerInfo:\n    def __init__(self, species: ServerSpecies | None, version_str: str) -> None:\n        self.species = species\n        self.version_str = version_str\n        self.version = self.calc_mysql_version_value(version_str)\n\n    @staticmethod\n    def calc_mysql_version_value(version_str: str) -> int:\n        if not version_str or not isinstance(version_str, str):\n            return 0\n        try:\n            major, minor, patch = version_str.split(\".\")\n        except ValueError:\n            return 0\n        else:\n            return int(major) * 10_000 + int(minor) * 100 + int(patch)\n\n    @classmethod\n    def from_version_string(cls, version_string: str) -> ServerInfo:\n        if not version_string:\n            return cls(ServerSpecies.MySQL, \"\")\n\n        re_species = (\n            (r\"(?P<version>[0-9\\.]+)-MariaDB\", ServerSpecies.MariaDB),\n            (r\"[0-9\\.]*-TiDB-v(?P<version>[0-9\\.]+)-?(?P<comment>[a-z0-9\\-]*)\", ServerSpecies.TiDB),\n            (r\"(?P<version>[0-9\\.]+)[a-z0-9]*-(?P<comment>[0-9]+$)\", ServerSpecies.Percona),\n            (r\"(?P<version>[0-9\\.]+)[a-z0-9]*-(?P<comment>[A-Za-z0-9_]+)\", ServerSpecies.MySQL),\n        )\n        for regexp, species in re_species:\n            match = re.search(regexp, version_string)\n            if match is not None:\n                parsed_version = match.group(\"version\")\n                detected_species = species\n                break\n        else:\n            detected_species = ServerSpecies.MySQL\n            parsed_version = \"\"\n\n        return cls(detected_species, parsed_version)\n\n    def __str__(self) -> str:\n        if self.species:\n            return f\"{self.species.value} {self.version_str}\"\n        else:\n            return self.version_str\n\n\nclass SQLExecute:\n    databases_query = \"\"\"SHOW DATABASES\"\"\"\n\n    tables_query = \"\"\"SHOW TABLES\"\"\"\n\n    show_candidates_query = '''SELECT name from mysql.help_topic WHERE name like \"SHOW %\"'''\n\n    users_query = \"\"\"SELECT CONCAT(\"'\", user, \"'@'\",host,\"'\") FROM mysql.user\"\"\"\n\n    functions_query = '''SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES\n    WHERE ROUTINE_TYPE=\"FUNCTION\" AND ROUTINE_SCHEMA = %s'''\n\n    procedures_query = '''SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES\n    WHERE ROUTINE_TYPE=\"PROCEDURE\" AND ROUTINE_SCHEMA = %s'''\n\n    character_sets_query = '''SHOW CHARACTER SET'''\n\n    collations_query = '''SHOW COLLATION'''\n\n    table_columns_query = \"\"\"select TABLE_NAME, COLUMN_NAME from information_schema.columns\n                                    where table_schema = %s\n                                    order by table_name,ordinal_position\"\"\"\n\n    enum_values_query = \"\"\"select TABLE_NAME, COLUMN_NAME, COLUMN_TYPE from information_schema.columns\n                                    where table_schema = %s and data_type = 'enum'\n                                    order by table_name,ordinal_position\"\"\"\n\n    now_query = \"\"\"SELECT NOW()\"\"\"\n\n    @staticmethod\n    def _parse_enum_values(column_type: str) -> list[str]:\n        if not column_type or not column_type.lower().startswith(\"enum(\"):\n            return []\n\n        values: list[str] = []\n        current: list[str] = []\n        in_quote = False\n        i = column_type.find(\"(\") + 1\n\n        while i < len(column_type):\n            ch = column_type[i]\n\n            if not in_quote:\n                if ch == \"'\":\n                    in_quote = True\n                    current = []\n                elif ch == \")\":\n                    break\n            else:\n                if ch == \"\\\\\" and i + 1 < len(column_type):\n                    current.append(column_type[i + 1])\n                    i += 1\n                elif ch == \"'\":\n                    if i + 1 < len(column_type) and column_type[i + 1] == \"'\":\n                        current.append(\"'\")\n                        i += 1\n                    else:\n                        values.append(\"\".join(current))\n                        in_quote = False\n                else:\n                    current.append(ch)\n            i += 1\n\n        return values\n\n    def __init__(\n        self,\n        database: str | None,\n        user: str | None,\n        password: str | None,\n        host: str | None,\n        port: int | None,\n        socket: str | None,\n        character_set: str | None,\n        local_infile: bool | None,\n        ssl: dict[str, Any] | None,\n        ssh_user: str | None,\n        ssh_host: str | None,\n        ssh_port: int | None,\n        ssh_password: str | None,\n        ssh_key_filename: str | None,\n        init_command: str | None = None,\n        unbuffered: bool | None = None,\n    ) -> None:\n        self.dbname = database\n        self.user = user\n        self.password = password\n        self.host = host\n        self.port = port\n        self.socket = socket\n        self.character_set = character_set\n        self.local_infile = local_infile\n        self.ssl = ssl\n        self.server_info: ServerInfo | None = None\n        self.connection_id: int | None = None\n        self.ssh_user = ssh_user\n        self.ssh_host = ssh_host\n        self.ssh_port = ssh_port\n        self.ssh_password = ssh_password\n        self.ssh_key_filename = ssh_key_filename\n        self.init_command = init_command\n        self.unbuffered = unbuffered\n        self.conn: Connection | None = None\n        self.connect()\n\n    def connect(\n        self,\n        database: str | None = None,\n        user: str | None = None,\n        password: str | None = None,\n        host: str | None = None,\n        port: int | None = None,\n        socket: str | None = None,\n        character_set: str | None = None,\n        local_infile: bool | None = None,\n        ssl: dict[str, Any] | None = None,\n        ssh_host: str | None = None,\n        ssh_port: int | None = None,\n        ssh_user: str | None = None,\n        ssh_password: str | None = None,\n        ssh_key_filename: str | None = None,\n        init_command: str | None = None,\n        unbuffered: bool | None = None,\n    ):\n        db = database if database is not None else self.dbname\n        user = user if user is not None else self.user\n        password = password if password is not None else self.password\n        host = host if host is not None else self.host\n        port = port if port is not None else self.port\n        socket = socket if socket is not None else self.socket\n        character_set = character_set if character_set is not None else self.character_set\n        local_infile = local_infile if local_infile is not None else self.local_infile\n        ssl = ssl if ssl is not None else self.ssl\n        ssh_user = ssh_user if ssh_user is not None else self.ssh_user\n        ssh_host = ssh_host if ssh_host is not None else self.ssh_host\n        ssh_port = ssh_port if ssh_port is not None else self.ssh_port\n        ssh_password = ssh_password if ssh_password is not None else self.ssh_password\n        ssh_key_filename = ssh_key_filename if ssh_key_filename is not None else self.ssh_key_filename\n        init_command = init_command if init_command is not None else self.init_command\n        unbuffered = unbuffered if unbuffered is not None else self.unbuffered\n        _logger.debug(\n            \"Connection DB Params: \\n\"\n            \"\\tdatabase: %r\"\n            \"\\tuser: %r\"\n            \"\\thost: %r\"\n            \"\\tport: %r\"\n            \"\\tsocket: %r\"\n            \"\\tcharacter_set: %r\"\n            \"\\tlocal_infile: %r\"\n            \"\\tssl: %r\"\n            \"\\tssh_user: %r\"\n            \"\\tssh_host: %r\"\n            \"\\tssh_port: %r\"\n            \"\\tssh_password: %r\"\n            \"\\tssh_key_filename: %r\"\n            \"\\tinit_command: %r\"\n            \"\\tunbuffered: %r\",\n            db,\n            user,\n            host,\n            port,\n            socket,\n            character_set,\n            local_infile,\n            ssl,\n            ssh_user,\n            ssh_host,\n            ssh_port,\n            ssh_password,\n            ssh_key_filename,\n            init_command,\n            unbuffered,\n        )\n        conv = conversions.copy()\n        conv.update({\n            FIELD_TYPE.TIMESTAMP: lambda obj: convert_datetime(obj) or obj,\n            FIELD_TYPE.DATETIME: lambda obj: convert_datetime(obj) or obj,\n            FIELD_TYPE.TIME: lambda obj: convert_time(obj) or obj,\n            FIELD_TYPE.DATE: lambda obj: convert_date(obj) or obj,\n        })\n\n        defer_connect = False\n\n        if ssh_host:\n            defer_connect = True\n\n        client_flag = pymysql.constants.CLIENT.INTERACTIVE\n        if init_command and len(list(iocommands.split_queries(init_command))) > 1:\n            client_flag |= pymysql.constants.CLIENT.MULTI_STATEMENTS\n\n        ssl_context = None\n        if ssl:\n            ssl_context = self._create_ssl_ctx(ssl)\n\n        conn = pymysql.connect(\n            database=db,\n            user=user,\n            password=password or '',\n            host=host,\n            port=port or 0,\n            unix_socket=socket,\n            use_unicode=True,\n            charset=character_set or '',\n            autocommit=True,\n            client_flag=client_flag,\n            local_infile=local_infile or False,\n            conv=conv,\n            ssl=ssl_context,  # type: ignore[arg-type]\n            program_name=\"mycli\",\n            defer_connect=defer_connect,\n            init_command=init_command or None,\n            cursorclass=pymysql.cursors.SSCursor if unbuffered else pymysql.cursors.Cursor,\n        )  # type: ignore[misc]\n\n        if ssh_host:\n            ##### paramiko.Channel is a bad socket implementation overall if you want SSL through an SSH tunnel\n            #####\n            # instead let's open a tunnel and rewrite host:port to local bind\n            try:\n                chan = sshtunnel.SSHTunnelForwarder(\n                    (ssh_host, ssh_port),\n                    ssh_username=ssh_user,\n                    ssh_pkey=ssh_key_filename,\n                    ssh_password=ssh_password,\n                    remote_bind_address=(host, port),\n                )\n                chan.start()\n\n                conn.host = chan.local_bind_host\n                conn.port = chan.local_bind_port\n                conn.connect()\n            except Exception as e:\n                raise e\n\n        if self.conn is not None:\n            try:\n                self.conn.close()\n            except pymysql.err.Error:\n                pass\n        self.conn = conn\n        # Update them after the connection is made to ensure that it was a\n        # successful connection.\n        self.dbname = db\n        self.user = user\n        self.password = password\n        self.host = host\n        self.port = port\n        self.socket = socket\n        self.character_set = character_set\n        self.ssl = ssl\n        self.init_command = init_command\n        self.unbuffered = unbuffered\n        # retrieve connection id\n        self.reset_connection_id()\n        self.server_info = ServerInfo.from_version_string(conn.server_version)  # type: ignore[attr-defined]\n\n    def run(self, statement: str) -> Generator[SQLResult, None, None]:\n        \"\"\"Execute the sql in the database and return the results.\"\"\"\n\n        # Remove spaces and EOL\n        statement = statement.strip()\n        if not statement:  # Empty string\n            yield SQLResult()\n\n        # Split the sql into separate queries and run each one.\n        # Unless it's saving a favorite query, in which case we\n        # want to save them all together.\n        if statement.startswith(\"\\\\fs\"):\n            components: Iterable[str] = [statement]\n        else:\n            components = iocommands.split_queries(statement)\n\n        for sql in components:\n            # \\G is treated specially since we have to set the expanded output.\n            if sql.endswith(\"\\\\G\"):\n                iocommands.set_expanded_output(True)\n                sql = sql[:-2].strip()\n            # \\g is treated specially since we might want collapsed output when\n            # auto vertical output is enabled\n            elif sql.endswith('\\\\g'):\n                iocommands.set_expanded_output(False)\n                iocommands.set_forced_horizontal_output(True)\n                sql = sql[:-2].strip()\n\n            assert isinstance(self.conn, Connection)\n            cur = self.conn.cursor()\n            try:  # Special command\n                _logger.debug(\"Trying a dbspecial command. sql: %r\", sql)\n                yield from execute(cur, sql)\n            except CommandNotFound:  # Regular SQL\n                _logger.debug(\"Regular sql statement. sql: %r\", sql)\n                cur.execute(sql)\n                while True:\n                    yield self.get_result(cur)\n\n                    # PyMySQL returns an extra, empty result set with stored\n                    # procedures. We skip it (rowcount is zero and no\n                    # description).\n                    if not cur.nextset() or (not cur.rowcount and cur.description is None):\n                        break\n\n    def get_result(self, cursor: Cursor) -> SQLResult:\n        \"\"\"Get the current result's data from the cursor.\"\"\"\n        preamble = header = None\n\n        # cursor.description is not None for queries that return result sets,\n        # e.g. SELECT or SHOW.\n        plural = '' if cursor.rowcount == 1 else 's'\n        if cursor.description:\n            header = [x[0] for x in cursor.description]\n            status = FormattedText([('', f'{cursor.rowcount} row{plural} in set')])\n        else:\n            _logger.debug(\"No rows in result.\")\n            status = FormattedText([('', f'Query OK, {cursor.rowcount} row{plural} affected')])\n\n        if cursor.warning_count > 0:\n            plural = '' if cursor.warning_count == 1 else 's'\n            comma = FormattedText([('', ', ')])\n            warning_count = FormattedText([('class:output.status.warning-count', f'{cursor.warning_count} warning{plural}')])\n            status.extend(comma)\n            status.extend(warning_count)\n\n        return SQLResult(preamble=preamble, header=header, rows=cursor, status=status)\n\n    def tables(self) -> Generator[tuple[str], None, None]:\n        \"\"\"Yields table names\"\"\"\n\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Tables Query. sql: %r\", self.tables_query)\n            cur.execute(self.tables_query)\n            yield from cur\n\n    def table_columns(self) -> Generator[tuple[str, str], None, None]:\n        \"\"\"Yields (table name, column name) pairs\"\"\"\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Columns Query. sql: %r\", self.table_columns_query)\n            cur.execute(self.table_columns_query, (self.dbname,))\n            yield from cur\n\n    def enum_values(self) -> Generator[tuple[str, str, list[str]], None, None]:\n        \"\"\"Yields (table name, column name, enum values) tuples\"\"\"\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Enum Values Query. sql: %r\", self.enum_values_query)\n            cur.execute(self.enum_values_query, (self.dbname,))\n            for table_name, column_name, column_type in cur:\n                values = self._parse_enum_values(column_type)\n                if values:\n                    yield (table_name, column_name, values)\n\n    def databases(self) -> list[str]:\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Databases Query. sql: %r\", self.databases_query)\n            cur.execute(self.databases_query)\n            return [x[0] for x in cur.fetchall()]\n\n    def functions(self) -> Generator[tuple[str, str], None, None]:\n        \"\"\"Yields tuples of (schema_name, function_name)\"\"\"\n\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Functions Query. sql: %r\", self.functions_query)\n            cur.execute(self.functions_query, (self.dbname,))\n            yield from cur\n\n    def procedures(self) -> Generator[tuple, None, None]:\n        \"\"\"Yields tuples of (procedure_name, )\"\"\"\n\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Procedures Query. sql: %r\", self.procedures_query)\n            try:\n                cur.execute(self.procedures_query, (self.dbname,))\n            except pymysql.DatabaseError as e:\n                _logger.error('No procedure completions due to %r', e)\n                yield ()\n            else:\n                yield from cur\n\n    def character_sets(self) -> Generator[tuple, None, None]:\n        \"\"\"Yields tuples of (character_set_name, )\"\"\"\n\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Character sets Query. sql: %r\", self.character_sets_query)\n            try:\n                cur.execute(self.character_sets_query)\n            except pymysql.DatabaseError as e:\n                _logger.error('No character_set completions due to %r', e)\n                yield ()\n            else:\n                yield from cur\n\n    def collations(self) -> Generator[tuple, None, None]:\n        \"\"\"Yields tuples of (collation_name, )\"\"\"\n\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Collations Query. sql: %r\", self.collations_query)\n            try:\n                cur.execute(self.collations_query)\n            except pymysql.DatabaseError as e:\n                _logger.error('No collations completions due to %r', e)\n                yield ()\n            else:\n                yield from cur\n\n    def show_candidates(self) -> Generator[tuple, None, None]:\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Show Query. sql: %r\", self.show_candidates_query)\n            try:\n                cur.execute(self.show_candidates_query)\n            except pymysql.DatabaseError as e:\n                _logger.error(\"No show completions due to %r\", e)\n                yield ()\n            else:\n                for row in cur:\n                    yield (row[0].split(None, 1)[-1],)\n\n    def users(self) -> Generator[tuple, None, None]:\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Users Query. sql: %r\", self.users_query)\n            try:\n                cur.execute(self.users_query)\n            except pymysql.DatabaseError as e:\n                _logger.error(\"No user completions due to %r\", e)\n                yield ()\n            else:\n                yield from cur\n\n    def now(self) -> datetime.datetime:\n        assert isinstance(self.conn, Connection)\n        with self.conn.cursor() as cur:\n            _logger.debug(\"Now Query. sql: %r\", self.now_query)\n            cur.execute(self.now_query)\n            if one := cur.fetchone():\n                return one[0]\n            else:\n                return datetime.datetime.now()\n\n    def get_connection_id(self) -> int | None:\n        if not self.connection_id:\n            self.reset_connection_id()\n        return self.connection_id\n\n    def reset_connection_id(self) -> None:\n        # Remember current connection id\n        _logger.debug(\"Get current connection id\")\n        try:\n            results = self.run(\"select connection_id()\")\n            for result in results:\n                cur = result.rows\n                if isinstance(cur, Cursor):\n                    v = cur.fetchone()\n                    self.connection_id = v[0] if v is not None else -1\n                else:\n                    raise ValueError\n        except Exception as e:\n            # See #1054\n            self.connection_id = -1\n            _logger.error(\"Failed to get connection id: %s\", e)\n        else:\n            _logger.debug(\"Current connection id: %s\", self.connection_id)\n\n    def change_db(self, db: str) -> None:\n        assert isinstance(self.conn, Connection)\n        self.conn.select_db(db)\n        self.dbname = db\n\n    def _create_ssl_ctx(self, sslp: dict) -> ssl.SSLContext:\n        ca = sslp.get(\"ca\")\n        capath = sslp.get(\"capath\")\n        hasnoca = ca is None and capath is None\n        ctx = ssl.create_default_context(cafile=ca, capath=capath)\n        ctx.check_hostname = not hasnoca and sslp.get(\"check_hostname\", True)\n        ctx.verify_mode = ssl.CERT_NONE if hasnoca else ssl.CERT_REQUIRED\n        if \"cert\" in sslp:\n            ctx.load_cert_chain(sslp[\"cert\"], keyfile=sslp.get(\"key\"))\n        if \"cipher\" in sslp:\n            ctx.set_ciphers(sslp[\"cipher\"])\n\n        ctx.minimum_version = ssl.TLSVersion.TLSv1_2\n\n        if \"tls_version\" in sslp:\n            tls_version = sslp[\"tls_version\"]\n\n            if tls_version == \"TLSv1\":\n                ctx.minimum_version = ssl.TLSVersion.TLSv1\n                ctx.maximum_version = ssl.TLSVersion.TLSv1\n            elif tls_version == \"TLSv1.1\":\n                ctx.minimum_version = ssl.TLSVersion.TLSv1_1\n                ctx.maximum_version = ssl.TLSVersion.TLSv1_1\n            elif tls_version == \"TLSv1.2\":\n                ctx.minimum_version = ssl.TLSVersion.TLSv1_2\n                ctx.maximum_version = ssl.TLSVersion.TLSv1_2\n            elif tls_version == \"TLSv1.3\":\n                ctx.minimum_version = ssl.TLSVersion.TLSv1_3\n                ctx.maximum_version = ssl.TLSVersion.TLSv1_3\n            else:\n                _logger.error(\"Invalid tls version: %s\", tls_version)\n\n        return ctx\n\n    def close(self) -> None:\n        if self.conn is not None:\n            try:\n                self.conn.close()\n            except pymysql.err.Error:\n                pass\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"mycli\"\ndynamic = [\"version\"]\ndescription = \"CLI for MySQL Database. With auto-completion and syntax highlighting.\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nlicense = \"BSD-3-Clause\"\nauthors = [{ name = \"Mycli Core Team\" }]\n\ndependencies = [\n    \"click ~= 8.3.1\",\n    \"cryptography ~= 46.0.5\",\n    \"Pygments ~= 2.19.2\",\n    \"prompt_toolkit>=3.0.6,<4.0.0\",\n    \"PyMySQL ~= 1.1.2\",\n    \"sqlparse>=0.3.0,<0.6.0\",\n    \"sqlglot[c] ~= 30.0.0\",\n    \"configobj ~= 5.0.9\",\n    \"cli_helpers[styles] ~= 2.11.0\",\n    \"wcwidth ~= 0.6.0\",\n    \"pyperclip ~= 1.11.0\",\n    \"pycryptodomex ~= 3.23.0\",\n    \"pyfzf ~= 0.3.1\",\n    \"rapidfuzz ~= 3.14.3\",\n    \"keyring ~= 25.7.0\",\n]\n\n[project.urls]\nHomepage = 'https://mycli.net'\nDocumentation = 'https://mycli.net/docs'\nSource = 'https://github.com/dbcli/mycli'\nIssues = 'https://github.com/dbcli/mycli/issues'\nChangelog = 'https://github.com/dbcli/mycli/blob/main/changelog.md'\n\n[build-system]\nrequires = [\"setuptools>=64.0\", \"setuptools-scm>=8\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.setuptools_scm]\n\n\n[project.optional-dependencies]\nssh = [\n    \"paramiko ~= 3.5.1\",\n    \"sshtunnel ~= 0.4.0\",\n]\nllm = [\n    \"llm ~= 0.28.0\",\n    \"setuptools == 82.*\",                   # Required by llm commands to install models\n    \"pip == 26.*\",\n]\nall = [\n  \"mycli[ssh]\",\n  \"mycli[llm]\",\n]\ndev = [\n    \"behave ~= 1.3.3\",\n    \"coverage ~= 7.13.4\",\n    \"mypy ~= 1.19.1\",\n    \"pexpect ~= 4.9.0\",\n    \"pytest ~= 9.0.2\",\n    \"pytest-cov ~= 7.0.0\",\n    \"tox ~= 4.35.0\",\n    \"pdbpp ~= 0.11.7\",\n    \"paramiko ~= 3.5.1\",\n    \"sshtunnel ~= 0.4.0\",\n    \"llm ~= 0.28.0\",\n    \"setuptools == 82.*\",                   # Required by llm commands to install models\n    \"pip == 26.*\",\n    \"ruff ~= 0.15.0\",\n]\n\n[project.scripts]\nmycli = \"mycli.main:cli\"\n\n[tool.setuptools.package-data]\nmycli = [\"myclirc\", \"AUTHORS\", \"SPONSORS\", \"TIPS\"]\n\n[tool.setuptools.packages.find]\ninclude = [\"mycli*\"]\n\n[tool.ruff]\ntarget-version = 'py310'\nline-length = 140\n\n[tool.ruff.lint]\nselect = ['A', 'B', 'I', 'E', 'W', 'F', 'C4', 'PIE', 'TID']\nignore = [\n    'B005',   # Multi-character strip()\n    'E401',   # Multiple imports on one line\n    'E402',   # Module level import not at top of file\n    'PIE808', # range() starting with 0\n    # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules\n    'E111', # indentation-with-invalid-multiple\n    'E114', # indentation-with-invalid-multiple-comment\n    'E117', # over-indented\n    'W191', # tab-indentation\n]\n\n[tool.ruff.lint.isort]\nforce-sort-within-sections = true\nknown-first-party = ['mycli', 'test', 'steps']\n\n[tool.ruff.lint.flake8-tidy-imports]\nban-relative-imports = 'all'\n\n[tool.ruff.format]\npreview = true\nquote-style = 'preserve'\nexclude = ['build', 'mycli_dev']\n\n[tool.mypy]\npretty = true\nstrict_equality = true\nignore_missing_imports = true\nwarn_unreachable = true\nwarn_redundant_casts = true\nwarn_no_return = true\nwarn_unused_configs = true\nshow_column_numbers = true\nexclude = ['^build/', '^dist/']\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\naddopts = --ignore=mycli/packages/paramiko_stub/__init__.py\n"
  },
  {
    "path": "test/__init__.py",
    "content": ""
  },
  {
    "path": "test/conftest.py",
    "content": "# type: ignore\n\nimport pytest\n\nimport mycli.sqlexecute\nfrom test.utils import CHARACTER_SET, DATABASE, HOST, PASSWORD, PORT, SSH_HOST, SSH_PORT, SSH_USER, USER, create_db, db_connection\n\n\n@pytest.fixture(scope=\"function\")\ndef connection():\n    create_db(DATABASE)\n    connection = db_connection(DATABASE)\n    yield connection\n\n    connection.close()\n\n\n@pytest.fixture\ndef cursor(connection):\n    with connection.cursor() as cur:\n        return cur\n\n\n@pytest.fixture\ndef executor(connection):\n    return mycli.sqlexecute.SQLExecute(\n        database=DATABASE,\n        user=USER,\n        host=HOST,\n        password=PASSWORD,\n        port=PORT,\n        socket=None,\n        character_set=CHARACTER_SET,\n        local_infile=False,\n        ssl=None,\n        ssh_user=SSH_USER,\n        ssh_host=SSH_HOST,\n        ssh_port=SSH_PORT,\n        ssh_password=None,\n        ssh_key_filename=None,\n    )\n"
  },
  {
    "path": "test/features/__init__.py",
    "content": ""
  },
  {
    "path": "test/features/auto_vertical.feature",
    "content": "Feature: auto_vertical mode:\n  on, off\n\n  Scenario: auto_vertical on with small query\n    When we run dbcli with --auto-vertical-output\n      and we execute a small query\n      then we see small results in horizontal format\n\n  Scenario: auto_vertical on with large query\n    When we run dbcli with --auto-vertical-output\n      and we execute a large query\n      then we see large results in vertical format\n"
  },
  {
    "path": "test/features/basic_commands.feature",
    "content": "Feature: run the cli,\n  call the help command,\n  check our application name,\n  insert the date,\n  exit the cli\n\n  Scenario: run \"\\?\" command\n     When we send \"\\?\" command\n      then we see help output\n\n  Scenario: run source command\n     When we send source command\n      then we see help output\n\n  Scenario: check our application_name\n     When we run query to check application_name\n      then we see found\n\n  Scenario: insert the date\n     When we send \"ctrl + o, ctrl + d\"\n      then we see the date\n\n  Scenario: run the cli and exit\n     When we send \"ctrl + d\"\n      then dbcli exits\n"
  },
  {
    "path": "test/features/connection.feature",
    "content": "Feature: connect to a database:\n\n  @requires_local_db\n  Scenario: run mycli on localhost without port\n    When we run mycli with arguments \"host=localhost\" without arguments \"port\"\n      When we query \"status\"\n      Then status contains \"via UNIX socket\"\n\n  Scenario: run mycli on TCP host without port\n    When we run mycli without arguments \"port\"\n      When we query \"status\"\n      Then status contains \"via TCP/IP\"\n\n  Scenario: run mycli with port but without host\n    When we run mycli without arguments \"host\"\n      When we query \"status\"\n      Then status contains \"via TCP/IP\"\n\n  @requires_local_db\n  Scenario: run mycli without host and port\n    When we run mycli without arguments \"host port\"\n      When we query \"status\"\n      Then status contains \"via UNIX socket\"\n\n  Scenario: run mycli with my.cnf configuration\n    When we create my.cnf file\n    When we run mycli without arguments \"host port user pass defaults_file\"\n      Then we are logged in\n\n  Scenario: run mycli with mylogin.cnf configuration\n    When we create mylogin.cnf file\n    When we run mycli with arguments \"login_path=test_login_path\" without arguments \"host port user pass defaults_file\"\n      Then we are logged in\n\n\n"
  },
  {
    "path": "test/features/crud_database.feature",
    "content": "Feature: manipulate databases:\n  create, drop, connect, disconnect\n\n  Scenario: create and drop temporary database\n     When we create database\n      then we see database created\n      when we drop database\n      then we confirm the destructive warning\n      then we see database dropped\n      when we connect to dbserver\n      then we see database connected\n\n  Scenario: connect and disconnect from test database\n     When we connect to test database\n      then we see database connected\n      when we connect to dbserver\n      then we see database connected\n\n  Scenario: connect and disconnect from quoted test database\n     When we connect to quoted test database\n      then we see database connected\n\n  Scenario: create and drop default database\n     When we create database\n      then we see database created\n      when we connect to tmp database\n      then we see database connected\n      when we drop database\n      then we confirm the destructive warning\n      then we see database dropped and no default database\n"
  },
  {
    "path": "test/features/crud_table.feature",
    "content": "Feature: manipulate tables:\n  create, insert, update, select, delete from, drop\n\n  Scenario: create, insert, select from, update, drop table\n     When we connect to test database\n      then we see database connected\n      when we create table\n      then we see table created\n      when we insert into table\n      then we see record inserted\n      when we update table\n      then we see record updated\n      when we select from table\n      then we see data selected\n      when we delete from table\n      then we confirm the destructive warning\n      then we see record deleted\n      when we drop table\n      then we confirm the destructive warning\n      then we see table dropped\n      when we connect to dbserver\n      then we see database connected\n\n  Scenario: select null values\n    When we connect to test database\n      then we see database connected\n      when we select null\n      then we see null selected\n\n  Scenario: confirm destructive query\n     When we query \"create table foo(x integer);\"\n      and we query \"delete from foo;\"\n      and we answer the destructive warning with \"y\"\n      then we see text \"Your call!\"\n\n  Scenario: decline destructive query\n     When we query \"delete from foo;\"\n      and we answer the destructive warning with \"n\"\n      then we see text \"Wise choice!\"\n\n   # TODO (amjith). This scenario fails in GH actions but only in 3.12. Unable\n   # to reproduce locally.\n   @skip_py312\n   Scenario: no destructive warning if disabled in config\n     When we run dbcli with --no-warn\n      and we query \"create table blabla(x integer);\"\n      and we query \"delete from blabla;\"\n     Then we see text \"Query OK\"\n\n  Scenario: confirm destructive query with invalid response\n     When we query \"delete from foo;\"\n      then we answer the destructive warning with invalid \"1\" and see text \"is not a valid boolean\"\n"
  },
  {
    "path": "test/features/db_utils.py",
    "content": "# type: ignore\n\nimport pymysql\n\nfrom mycli.constants import DEFAULT_CHARSET, DEFAULT_HOST, DEFAULT_PORT\n\n\ndef create_db(hostname=DEFAULT_HOST, port=DEFAULT_PORT, username=None, password=None, dbname=None):\n    \"\"\"Create test database.\n\n    :param hostname: string\n    :param port: int\n    :param username: string\n    :param password: string\n    :param dbname: string\n    :return:\n\n    \"\"\"\n    cn = pymysql.connect(\n        host=hostname, port=port, user=username, password=password, charset=DEFAULT_CHARSET, cursorclass=pymysql.cursors.DictCursor\n    )\n\n    with cn.cursor() as cr:\n        cr.execute(\"drop database if exists \" + dbname)\n        cr.execute(\"create database \" + dbname)\n\n    cn.close()\n\n    cn = create_cn(hostname, port, password, username, dbname)\n    return cn\n\n\ndef create_cn(hostname, port, password, username, dbname):\n    \"\"\"Open connection to database.\n\n    :param hostname:\n    :param port:\n    :param password:\n    :param username:\n    :param dbname: string\n    :return: psycopg2.connection\n\n    \"\"\"\n    cn = pymysql.connect(\n        host=hostname,\n        port=port,\n        user=username,\n        password=password,\n        db=dbname,\n        charset=DEFAULT_CHARSET,\n        cursorclass=pymysql.cursors.DictCursor,\n    )\n\n    return cn\n\n\ndef drop_db(hostname=DEFAULT_HOST, port=DEFAULT_PORT, username=None, password=None, dbname=None):\n    \"\"\"Drop database.\n\n    :param hostname: string\n    :param port: int\n    :param username: string\n    :param password: string\n    :param dbname: string\n\n    \"\"\"\n    cn = pymysql.connect(\n        host=hostname,\n        port=port,\n        user=username,\n        password=password,\n        db=dbname,\n        charset=DEFAULT_CHARSET,\n        cursorclass=pymysql.cursors.DictCursor,\n    )\n\n    with cn.cursor() as cr:\n        cr.execute(\"drop database if exists \" + dbname)\n\n    close_cn(cn)\n\n\ndef close_cn(cn=None):\n    \"\"\"Close connection.\n\n    :param connection: pymysql.connection\n\n    \"\"\"\n    if cn:\n        cn.close()\n"
  },
  {
    "path": "test/features/environment.py",
    "content": "# type: ignore\n\nimport os\nimport shutil\nimport sys\nfrom tempfile import NamedTemporaryFile\n\nimport db_utils as dbutils\nimport fixture_utils as fixutils\nimport pexpect\n\nfrom mycli.constants import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER\nfrom steps.wrappers import run_cli, wait_prompt\nfrom test.utils import TEMPFILE_PREFIX\n\ntest_log_file = os.path.join(os.environ[\"HOME\"], \".mycli.test.log\")\n\n\nSELF_CONNECTING_FEATURES = (\"test/features/connection.feature\",)\n\n\nMY_CNF_PATH = os.path.expanduser(\"~/.my.cnf\")\nMY_CNF_BACKUP_PATH = f\"{MY_CNF_PATH}.backup\"\nMYLOGIN_CNF_PATH = os.path.expanduser(\"~/.mylogin.cnf\")\nMYLOGIN_CNF_BACKUP_PATH = f\"{MYLOGIN_CNF_PATH}.backup\"\n\n\ndef get_db_name_from_context(context):\n    return context.config.userdata.get(\"my_test_db\", None) or \"mycli_behave_tests\"\n\n\ndef before_all(context):\n    \"\"\"Set env parameters.\"\"\"\n    os.environ[\"LINES\"] = \"100\"\n    os.environ[\"COLUMNS\"] = \"100\"\n    os.environ[\"VISUAL\"] = \"ex\"\n    os.environ[\"EDITOR\"] = \"ex\"\n    os.environ[\"LC_ALL\"] = \"en_US.UTF-8\"\n    os.environ[\"PROMPT_TOOLKIT_NO_CPR\"] = \"1\"\n    os.environ[\"MYCLI_HISTFILE\"] = os.devnull\n\n    # test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))\n    # login_path_file = os.path.join(test_dir, \"mylogin.cnf\")\n    #    os.environ['MYSQL_TEST_LOGIN_FILE'] = login_path_file\n\n    context.package_root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))\n\n    os.environ[\"COVERAGE_PROCESS_START\"] = os.path.join(context.package_root, \".coveragerc\")\n\n    context.exit_sent = False\n\n    vi = \"_\".join([str(x) for x in sys.version_info[:3]])\n    db_name = get_db_name_from_context(context)\n    db_name_full = f\"{db_name}_{vi}\"\n\n    # Store get params from config/environment variables\n    context.conf = {\n        \"host\": context.config.userdata.get(\"my_test_host\", os.getenv(\"PYTEST_HOST\", DEFAULT_HOST)),\n        \"port\": context.config.userdata.get(\"my_test_port\", int(os.getenv(\"PYTEST_PORT\", DEFAULT_PORT))),\n        \"user\": context.config.userdata.get(\"my_test_user\", os.getenv(\"PYTEST_USER\", DEFAULT_USER)),\n        \"pass\": context.config.userdata.get(\"my_test_pass\", os.getenv(\"PYTEST_PASSWORD\", None)),\n        \"cli_command\": context.config.userdata.get(\"my_cli_command\", None)\n        or sys.executable + ' -c \"import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()\"',\n        \"dbname\": db_name,\n        \"dbname_tmp\": db_name_full + \"_tmp\",\n        \"vi\": vi,\n        \"pager_boundary\": \"---boundary---\",\n    }\n\n    with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode='w', delete=False) as my_cnf:\n        my_cnf.write(\n            f'[client]\\npager={sys.executable} '\n            f'{os.path.join(context.package_root, \"test/features/wrappager.py\")} {context.conf[\"pager_boundary\"]}\\n'\n        )\n    context.conf[\"defaults-file\"] = my_cnf.name\n    context.conf[\"myclirc\"] = os.path.join(context.package_root, \"test\", \"myclirc\")\n\n    context.cn = dbutils.create_db(\n        context.conf[\"host\"], context.conf[\"port\"], context.conf[\"user\"], context.conf[\"pass\"], context.conf[\"dbname\"]\n    )\n\n    context.fixture_data = fixutils.read_fixture_files()\n\n\ndef after_all(context):\n    \"\"\"Unset env parameters.\"\"\"\n    dbutils.close_cn(context.cn)\n    dbutils.drop_db(context.conf[\"host\"], context.conf[\"port\"], context.conf[\"user\"], context.conf[\"pass\"], context.conf[\"dbname\"])\n    try:\n        if os.path.exists(context.conf[\"defaults-file\"]):\n            os.remove(context.conf[\"defaults-file\"])\n    except Exception:\n        pass\n\n    # Restore env vars.\n    # for k, v in context.pgenv.items():\n    #    if k in os.environ and v is None:\n    #        del os.environ[k]\n    #    elif v:\n    #        os.environ[k] = v\n\n\ndef before_step(context, _):\n    context.atprompt = False\n\n\ndef before_scenario(context, arg):\n    # Skip scenarios marked skip_py312 when running on Python 3.12\n    if sys.version_info[:2] == (3, 12) and \"skip_py312\" in arg.tags:\n        arg.skip(\"Skipped on Python 3.12\")\n    with open(test_log_file, \"w\") as f:\n        f.write(\"\")\n    if arg.location.filename not in SELF_CONNECTING_FEATURES:\n        run_cli(context)\n        wait_prompt(context)\n\n    if os.path.exists(MY_CNF_PATH):\n        shutil.move(MY_CNF_PATH, MY_CNF_BACKUP_PATH)\n\n    if os.path.exists(MYLOGIN_CNF_PATH):\n        shutil.move(MYLOGIN_CNF_PATH, MYLOGIN_CNF_BACKUP_PATH)\n\n\ndef after_scenario(context, _):\n    \"\"\"Cleans up after each test complete.\"\"\"\n    with open(test_log_file) as f:\n        for line in f:\n            if \"error\" in line.lower():\n                raise RuntimeError(f\"Error in log file: {line}\")\n\n    if hasattr(context, \"cli\") and not context.exit_sent:\n        # Quit nicely.\n        if not context.atprompt:\n            user = context.conf[\"user\"]\n            host = context.conf[\"host\"]\n            dbname = context.currentdb\n            context.cli.expect_exact(f\"{user}@{host}:{dbname}>\", timeout=5)\n        context.cli.sendcontrol(\"c\")\n        context.cli.sendcontrol(\"d\")\n        context.cli.expect_exact(pexpect.EOF, timeout=5)\n\n    if os.path.exists(MY_CNF_BACKUP_PATH):\n        shutil.move(MY_CNF_BACKUP_PATH, MY_CNF_PATH)\n\n    if os.path.exists(MYLOGIN_CNF_BACKUP_PATH):\n        shutil.move(MYLOGIN_CNF_BACKUP_PATH, MYLOGIN_CNF_PATH)\n    elif os.path.exists(MYLOGIN_CNF_PATH):\n        # This file was moved in `before_scenario`.\n        # If it exists now, it has been created during a test\n        os.remove(MYLOGIN_CNF_PATH)\n\n\n# TODO: uncomment to debug a failure\n# def after_step(context, step):\n#     if step.status == \"failed\":\n#         import ipdb; ipdb.set_trace()\n"
  },
  {
    "path": "test/features/fixture_data/help.txt",
    "content": "+--------------------------+-----------------------------------------------+\n| Command                  | Description                                   |\n|--------------------------+-----------------------------------------------|\n| \\#                       | Refresh auto-completions.                     |\n| \\?                       | Show Help.                                    |\n| \\c[onnect] database_name | Change to a new database.                     |\n| \\d [pattern]             | List or describe tables, views and sequences. |\n| \\dT[S+] [pattern]        | List data types                               |\n| \\df[+] [pattern]         | List functions.                               |\n| \\di[+] [pattern]         | List indexes.                                 |\n| \\dn[+] [pattern]         | List schemas.                                 |\n| \\ds[+] [pattern]         | List sequences.                               |\n| \\dt[+] [pattern]         | List tables.                                  |\n| \\du[+] [pattern]         | List roles.                                   |\n| \\dv[+] [pattern]         | List views.                                   |\n| \\e [file]                | Edit the query with external editor.          |\n| \\l                       | List databases.                               |\n| \\n[+] [name]             | List or execute named queries.                |\n| \\nd [name [query]]       | Delete a named query.                         |\n| \\ns name query           | Save a named query.                           |\n| \\refresh                 | Refresh auto-completions.                     |\n| \\timing                  | Toggle timing of commands.                    |\n| \\x                       | Toggle expanded output.                       |\n+--------------------------+-----------------------------------------------+\n"
  },
  {
    "path": "test/features/fixture_data/help_commands.txt",
    "content": "+----------------+----------+---------------------------------+-------------------------------------------------------------+\r\n| Command        | Shortcut | Usage                           | Description                                                 |\r\n+----------------+----------+---------------------------------+-------------------------------------------------------------+\r\n| \\G             | <null>   | <query>\\G                       | Display query results vertically.                           |\r\n| \\bug           | <null>   | \\bug                            | File a bug on GitHub.                                       |\r\n| \\clip          | <null>   | <query>\\clip                    | Copy query to the system clipboard.                         |\r\n| \\dt            | <null>   | \\dt[+] [table]                  | List or describe tables.                                    |\r\n| \\edit          | \\e       | <query>\\edit | \\edit <filename> | Edit query with editor (uses $VISUAL or $EDITOR).           |\r\n| \\f             | <null>   | \\f [name [args..]]              | List or execute favorite queries.                           |\r\n| \\fd            | <null>   | \\fd <name>                      | Delete a favorite query.                                    |\r\n| \\fs            | <null>   | \\fs <name> <query>              | Save a favorite query.                                      |\r\n| \\l             | <null>   | \\l                              | List databases.                                             |\r\n| \\llm           | \\ai      | \\llm [arguments]                | Interrogate an LLM.  See \"\\llm help\".                       |\r\n| \\once          | \\o       | \\once [-o] <filename>           | Append next result to an output file (overwrite using -o).  |\r\n| \\pipe_once     | \\|       | \\pipe_once <command>            | Send next result to a subprocess.                           |\r\n| \\timing        | \\t       | \\timing                         | Toggle timing of queries.                                   |\r\n| connect        | \\r       | connect [database]              | Reconnect to the server, optionally switching databases.    |\r\n| delimiter      | <null>   | delimiter <string>              | Change end-of-statement delimiter.                          |\r\n| exit           | \\q       | exit                            | Exit.                                                       |\r\n| help           | \\?       | help [term]                     | Show this help, or search for a term on the server.         |\r\n| nopager        | \\n       | nopager                         | Disable pager; print to stdout.                             |\r\n| notee          | <null>   | notee                           | Stop writing results to an output file.                     |\r\n| nowarnings     | \\w       | nowarnings                      | Disable automatic warnings display.                         |\r\n| pager          | \\P       | pager [command]                 | Set pager to [command]. Print query results via pager.      |\r\n| prompt         | \\R       | prompt <string>                 | Change prompt format.                                       |\r\n| quit           | \\q       | quit                            | Quit.                                                       |\r\n| redirectformat | \\Tr      | redirectformat <format>         | Change the table format used to output redirected results.  |\r\n| rehash         | \\#       | rehash                          | Refresh auto-completions.                                   |\r\n| source         | \\.       | source <filename>               | Execute queries from a file.                                |\r\n| status         | \\s       | status                          | Get status information from the server.                     |\r\n| system         | <null>   | system [-r] <command>           | Execute a system shell command (raw mode with -r).          |\r\n| tableformat    | \\T       | tableformat <format>            | Change the table format used to output interactive results. |\r\n| tee            | <null>   | tee [-o] <filename>             | Append all results to an output file (overwrite using -o).  |\r\n| use            | \\u       | use <database>                  | Change to a new database.                                   |\r\n| warnings       | \\W       | warnings                        | Enable automatic warnings display.                          |\r\n| watch          | <null>   | watch [seconds] [-c] <query>    | Execute query every [seconds] seconds (5 by default).       |\r\n+----------------+----------+---------------------------------+-------------------------------------------------------------+\r\n"
  },
  {
    "path": "test/features/fixture_utils.py",
    "content": "# type: ignore\n\nimport os\n\n\ndef read_fixture_lines(filename):\n    \"\"\"Read lines of text from file.\n\n    :param filename: string name\n    :return: list of strings\n\n    \"\"\"\n    lines = []\n    for line in open(filename):\n        lines.append(line.strip())\n    return lines\n\n\ndef read_fixture_files():\n    \"\"\"Read all files inside fixture_data directory.\"\"\"\n    fixture_dict = {}\n\n    current_dir = os.path.dirname(__file__)\n    fixture_dir = os.path.join(current_dir, \"fixture_data/\")\n    for filename in os.listdir(fixture_dir):\n        if filename not in [\".\", \"..\"]:\n            fullname = os.path.join(fixture_dir, filename)\n            fixture_dict[filename] = read_fixture_lines(fullname)\n\n    return fixture_dict\n"
  },
  {
    "path": "test/features/iocommands.feature",
    "content": "Feature: I/O commands\n\n  Scenario: edit sql in file with external editor\n     When we start external editor providing a file name\n      and we type \"select * from abc\" in the editor\n      and we exit the editor\n      then we see dbcli prompt\n      and we see \"select * from abc\" in prompt\n\n  Scenario: tee output from query\n     When we tee output\n      and we wait for prompt\n      and we select \"select 123456\"\n      and we wait for prompt\n      and we notee output\n      and we wait for prompt\n      then we see 123456 in tee output\n\n   Scenario: set delimiter\n      When we query \"delimiter $\"\n      then delimiter is set to \"$\"\n\n   Scenario: set delimiter twice\n      When we query \"delimiter $\"\n      and we query \"delimiter ]]\"\n      then delimiter is set to \"]]\"\n\n   Scenario: set delimiter and query on same line\n      When we query \"select 123; delimiter $ select 456 $ delimiter %\"\n      then we see tabular result \"123\"\n      and we see tabular result \"456\"\n      and delimiter is set to \"%\"\n\n   Scenario: send output to file\n      When we query \"\\o /tmp/output1.sql\"\n      and we query \"select 123\"\n      and we query \"system cat /tmp/output1.sql\"\n      then we see csv result \"123\"\n\n   Scenario: send output to file two times\n      When we query \"\\o /tmp/output1.sql\"\n      and we query \"select 123\"\n      and we query \"\\o /tmp/output2.sql\"\n      and we query \"select 456\"\n      and we query \"system cat /tmp/output2.sql\"\n      then we see csv result \"456\"\n  \n   Scenario: shell style redirect to file\n      When we query \"select 123 as constant $> /tmp/output1.csv\"\n      and we query \"system cat /tmp/output1.csv\"\n      then we see csv 123 in file output\n\n   Scenario: shell style redirect to command\n      When we query \"select 100 $| wc\"\n      then we see space 12 in command output\n\n   Scenario: shell style redirect to multiple commands\n      When we query \"select 100 $| head -1 $| wc\"\n      then we see space 6 in command output\n\n   Scenario: shell style redirect to multiple commands with minimal spaces\n      When we query \"select 100$|head -1$|wc\"\n      then we see space 6 in command output\n\n   Scenario: shell style redirect to multiple commands containing single quotes\n      When we query \"select 100 $| head '-1' $| wc\"\n      then we see space 6 in command output\n\n   Scenario: shell style redirect to multiple commands containing single quotes and minimal spaces\n      When we query \"select 100$|head '-1'$|wc\"\n      then we see space 6 in command output\n\n   Scenario: shell style redirect to multiple commands containing mixed quoted and unquoted arg\n      When we query \"select 100 $| head -'1' $| wc\"\n      then we see space 6 in command output\n\n   Scenario: shell style redirect to multiple commands containing double quotes\n      When we query \"select 100 $| head \"\"-1\"\" $| wc\"\n      then we see space 6 in command output\n\n   Scenario: shell style redirect with commands and capture to file\n      When we query \"select 100 $| head -1 $| wc $> /tmp/output1.txt\"\n      and we query \"system cat /tmp/output1.txt\"\n      then we see text 6 in file output\n\n   Scenario: shell style redirect with append to file\n      When we query \"select 100 $> /tmp/output1.csv\"\n      and we query \"select 200 $>> /tmp/output1.csv\"\n      and we query \"system cat /tmp/output1.csv\"\n      then we see csv 100 in file output\n      and we see csv 200 in file output\n\n   Scenario: shell style redirect with command and append to file\n      When we query \"select 300 $| grep 0 $> /tmp/output1.csv\"\n      and we query \"select 400 $| grep 0 $>> /tmp/output1.csv\"\n      and we query \"system cat /tmp/output1.csv\"\n      then we see csv 300 in file output\n      and we see csv 400 in file output\n"
  },
  {
    "path": "test/features/named_queries.feature",
    "content": "Feature: named queries:\n  save, use and delete named queries\n\n  Scenario: save, use and delete named queries\n     When we connect to test database\n      then we see database connected\n      when we save a named query\n      then we see the named query saved\n      when we use a named query\n      then we see the named query executed\n      when we delete a named query\n      then we see the named query deleted\n\n  Scenario: save, use and delete named queries with parameters\n     When we connect to test database\n      then we see database connected\n      when we save a named query with parameters\n      then we see the named query saved\n      when we use named query with parameters\n      then we see the named query with parameters executed\n      when we use named query with too few parameters\n      then we see the named query with parameters fail with missing parameters\n      when we use named query with too many parameters\n      then we see the named query with parameters fail with extra parameters\n"
  },
  {
    "path": "test/features/specials.feature",
    "content": "Feature: Special commands\n\n  @wip\n  Scenario: run refresh command\n     When we refresh completions\n      and we wait for prompt\n      then we see completions refresh started\n"
  },
  {
    "path": "test/features/steps/__init__.py",
    "content": ""
  },
  {
    "path": "test/features/steps/auto_vertical.py",
    "content": "# type: ignore\n\nfrom textwrap import dedent\n\nfrom behave import then, when\nfrom utils import parse_cli_args_to_dict\nimport wrappers\n\n\n@when(\"we run dbcli with {arg}\")\ndef step_run_cli_with_arg(context, arg):\n    wrappers.run_cli(context, run_args=parse_cli_args_to_dict(arg))\n\n\n@when(\"we execute a small query\")\ndef step_execute_small_query(context):\n    context.cli.sendline(\"select 1\")\n\n\n@when(\"we execute a large query\")\ndef step_execute_large_query(context):\n    context.cli.sendline(f\"select {','.join([str(n) for n in range(1, 50)])}\")\n\n\n@then(\"we see small results in horizontal format\")\ndef step_see_small_results(context):\n    expected = (\n        dedent(\n            \"\"\"\n            +---+\\r\n            | 1 |\\r\n            +---+\\r\n            | 1 |\\r\n            +---+\n            \"\"\"\n        ).strip()\n        + '\\r\\n\\r\\n'\n    )\n\n    wrappers.expect_pager(\n        context,\n        expected,\n        timeout=5,\n    )\n    wrappers.expect_exact(context, \"1 row in set\", timeout=2)\n\n\n@then(\"we see large results in vertical format\")\ndef step_see_large_results(context):\n    rows = [f\"{str(n):3}| {n}\" for n in range(1, 50)]\n    delimited_rows = '\\r\\n'.join(rows) + '\\r\\n'\n    expected = \"***************************[ 1. row ]***************************\\r\\n\" + delimited_rows + \"\\r\\n\"\n\n    wrappers.expect_pager(context, expected, timeout=10)\n    wrappers.expect_exact(context, \"1 row in set\", timeout=2)\n"
  },
  {
    "path": "test/features/steps/basic_commands.py",
    "content": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string decorating it. This string is used\nto call the step in \"*.feature\" file.\n\n\"\"\"\n\nimport datetime\nimport tempfile\nfrom textwrap import dedent\n\nfrom behave import then, when\nimport wrappers\n\nfrom test.utils import TEMPFILE_PREFIX\n\n\n@when(\"we run dbcli\")\ndef step_run_cli(context):\n    wrappers.run_cli(context)\n\n\n@when(\"we wait for prompt\")\ndef step_wait_prompt(context):\n    wrappers.wait_prompt(context)\n\n\n@when('we send \"ctrl + d\"')\ndef step_ctrl_d(context):\n    \"\"\"Send Ctrl + D to hopefully exit.\"\"\"\n    context.cli.sendcontrol(\"d\")\n    context.exit_sent = True\n\n\n@when('we send \"ctrl + o, ctrl + d\"')\ndef step_ctrl_o_ctrl_d(context):\n    \"\"\"Send ctrl + o, ctrl + d to insert the quoted date.\"\"\"\n    context.cli.send(\"SELECT \")\n    context.cli.sendcontrol(\"o\")\n    context.cli.sendcontrol(\"d\")\n    context.cli.send(\" AS dt\")\n    context.cli.sendline(\"\")\n\n\n@when(r'we send \"\\?\" command')\ndef step_send_help(context):\n    r\"\"\"Send \\?\n\n    to see help.\n\n    \"\"\"\n    context.cli.sendline(\"\\\\?\")\n    wrappers.expect_exact(context, context.conf[\"pager_boundary\"] + \"\\r\\n\", timeout=5)\n\n\n@when(\"we send source command\")\ndef step_send_source_command(context):\n    with tempfile.NamedTemporaryFile(prefix=TEMPFILE_PREFIX) as f:\n        f.write(b\"\\\\?\")\n        f.flush()\n        context.cli.sendline(f\"\\\\. {f.name}\")\n        wrappers.expect_exact(context, context.conf[\"pager_boundary\"] + \"\\r\\n\", timeout=5)\n\n\n@when(\"we run query to check application_name\")\ndef step_check_application_name(context):\n    context.cli.sendline(\n        \"SELECT 'found' FROM performance_schema.session_connect_attrs WHERE attr_name = 'program_name' AND attr_value = 'mycli'\"\n    )\n\n\n@then(\"we see found\")\ndef step_see_found(context):\n    expected = (\n        dedent(\n            \"\"\"\n            +-------+\\r\n            | found |\\r\n            +-------+\\r\n            | found |\\r\n            +-------+\n            \"\"\"\n        ).strip()\n        + '\\r\\n\\r\\n'\n    )\n\n    wrappers.expect_exact(\n        context,\n        context.conf[\"pager_boundary\"] + '\\r\\n' + expected + context.conf[\"pager_boundary\"],\n        timeout=5,\n    )\n\n\n@then(\"we see the date\")\ndef step_see_date(context):\n    # There are some edge cases in which this test could fail,\n    # such as running near midnight when the test database has\n    # a different TZ setting than the system.\n    date_str = datetime.datetime.now().strftime(\"%Y-%m-%d\")\n    expected = (\n        dedent(\n            f\"\"\"\n            +------------+\\r\n            | dt         |\\r\n            +------------+\\r\n            | {date_str} |\\r\n            +------------+\n            \"\"\"\n        ).strip()\n        + '\\r\\n\\r\\n'\n    )\n\n    wrappers.expect_exact(\n        context,\n        context.conf[\"pager_boundary\"] + '\\r\\n' + expected + context.conf[\"pager_boundary\"],\n        timeout=5,\n    )\n\n\n@then(\"we confirm the destructive warning\")\ndef step_confirm_destructive_command(context):  # noqa\n    \"\"\"Confirm destructive command.\"\"\"\n    wrappers.expect_exact(context, \"You're about to run a destructive command.\\r\\nDo you want to proceed? (y/n):\", timeout=2)\n    context.cli.sendline(\"y\")\n\n\n@when('we answer the destructive warning with \"{confirmation}\"')\ndef step_confirm_destructive_command(context, confirmation):  # noqa\n    \"\"\"Confirm destructive command.\"\"\"\n    wrappers.expect_exact(context, \"You're about to run a destructive command.\\r\\nDo you want to proceed? (y/n):\", timeout=2)\n    context.cli.sendline(confirmation)\n\n\n@then('we answer the destructive warning with invalid \"{confirmation}\" and see text \"{text}\"')\ndef step_confirm_destructive_command(context, confirmation, text):  # noqa\n    \"\"\"Confirm destructive command.\"\"\"\n    wrappers.expect_exact(context, \"You're about to run a destructive command.\\r\\nDo you want to proceed? (y/n):\", timeout=2)\n    context.cli.sendline(confirmation)\n    wrappers.expect_exact(context, text, timeout=2)\n    # we must exit the Click loop, or the feature will hang\n    context.cli.sendline(\"n\")\n"
  },
  {
    "path": "test/features/steps/connection.py",
    "content": "# type: ignore\n\nimport io\nimport os\n\nfrom behave import then, when\nimport wrappers\n\nfrom mycli.config import encrypt_mylogin_cnf\nfrom test.features.environment import MY_CNF_PATH, MYLOGIN_CNF_PATH, get_db_name_from_context\nfrom test.features.steps.utils import parse_cli_args_to_dict\nfrom test.utils import HOST, PASSWORD, PORT, USER\n\nTEST_LOGIN_PATH = \"test_login_path\"\n\n\n@when('we run mycli with arguments \"{exact_args}\" without arguments \"{excluded_args}\"')\n@when('we run mycli without arguments \"{excluded_args}\"')\ndef step_run_cli_without_args(context, excluded_args, exact_args=\"\"):\n    wrappers.run_cli(context, run_args=parse_cli_args_to_dict(exact_args), exclude_args=parse_cli_args_to_dict(excluded_args).keys())\n\n\n@then('status contains \"{expression}\"')\ndef status_contains(context, expression):\n    wrappers.expect_exact(context, f\"{expression}\", timeout=5)\n\n    # Normally, the shutdown after scenario waits for the prompt.\n    # But we may have changed the prompt, depending on parameters,\n    # so let's wait for its last character\n    context.cli.expect_exact(\">\")\n    context.atprompt = True\n\n\n@when(\"we create my.cnf file\")\ndef step_create_my_cnf_file(context):\n    my_cnf = f\"[client]\\nhost = {HOST}\\nport = {PORT}\\nuser = {USER}\\npassword = {PASSWORD}\\n\"\n    with open(MY_CNF_PATH, \"w\") as f:\n        f.write(my_cnf)\n\n\n@when(\"we create mylogin.cnf file\")\ndef step_create_mylogin_cnf_file(context):\n    os.environ.pop(\"MYSQL_TEST_LOGIN_FILE\", None)\n    mylogin_cnf = f\"[{TEST_LOGIN_PATH}]\\nhost = {HOST}\\nport = {PORT}\\nuser = {USER}\\npassword = {PASSWORD}\\n\"\n    with open(MYLOGIN_CNF_PATH, \"wb\") as f:\n        input_file = io.StringIO(mylogin_cnf)\n        f.write(encrypt_mylogin_cnf(input_file).read())\n\n\n@then(\"we are logged in\")\ndef we_are_logged_in(context):\n    db_name = get_db_name_from_context(context)\n    context.cli.expect_exact(f\"{db_name}>\", timeout=5)\n    context.atprompt = True\n"
  },
  {
    "path": "test/features/steps/crud_database.py",
    "content": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string decorating it. This string is used\nto call the step in \"*.feature\" file.\n\n\"\"\"\n\nfrom behave import then, when\nimport pexpect\nimport wrappers\n\nfrom mycli.constants import DEFAULT_DATABASE\n\n\n@when(\"we create database\")\ndef step_db_create(context):\n    \"\"\"Send create database.\"\"\"\n    context.cli.sendline(f\"create database {context.conf['dbname_tmp']};\")\n\n    context.response = {\"database_name\": context.conf[\"dbname_tmp\"]}\n\n\n@when(\"we drop database\")\ndef step_db_drop(context):\n    \"\"\"Send drop database.\"\"\"\n    context.cli.sendline(f\"drop database {context.conf['dbname_tmp']};\")\n\n\n@when(\"we connect to test database\")\ndef step_db_connect_test(context):\n    \"\"\"Send connect to database.\"\"\"\n    db_name = context.conf[\"dbname\"]\n    context.currentdb = db_name\n    context.cli.sendline(f\"use {db_name};\")\n\n\n@when(\"we connect to quoted test database\")\ndef step_db_connect_quoted_tmp(context):\n    \"\"\"Send connect to database.\"\"\"\n    db_name = context.conf[\"dbname\"]\n    context.currentdb = db_name\n    context.cli.sendline(f\"use `{db_name}`;\")\n\n\n@when(\"we connect to tmp database\")\ndef step_db_connect_tmp(context):\n    \"\"\"Send connect to database.\"\"\"\n    db_name = context.conf[\"dbname_tmp\"]\n    context.currentdb = db_name\n    context.cli.sendline(f\"use {db_name}\")\n\n\n@when(\"we connect to dbserver\")\ndef step_db_connect_dbserver(context):\n    \"\"\"Send connect to database.\"\"\"\n    context.currentdb = DEFAULT_DATABASE\n    context.cli.sendline(f\"use {DEFAULT_DATABASE}\")\n\n\n@then(\"dbcli exits\")\ndef step_wait_exit(context):\n    \"\"\"Make sure the cli exits.\"\"\"\n    wrappers.expect_exact(context, pexpect.EOF, timeout=5)\n\n\n@then(\"we see dbcli prompt\")\ndef step_see_prompt(context):\n    \"\"\"Wait to see the prompt.\"\"\"\n    user = context.conf[\"user\"]\n    host = context.conf[\"host\"]\n    dbname = context.currentdb\n    wrappers.wait_prompt(context, f\"{user}@{host}:{dbname}> \")\n\n\n@then(\"we see help output\")\ndef step_see_help(context):\n    for expected_line in context.fixture_data[\"help_commands.txt\"]:\n        # in case tests are run without extras\n        if 'LLM' in expected_line:\n            continue\n        wrappers.expect_exact(context, expected_line, timeout=1)\n\n\n@then(\"we see database created\")\ndef step_see_db_created(context):\n    \"\"\"Wait to see create database output.\"\"\"\n    wrappers.expect_exact(context, \"Query OK, 1 row affected\", timeout=2)\n\n\n@then(\"we see database dropped\")\ndef step_see_db_dropped(context):\n    \"\"\"Wait to see drop database output.\"\"\"\n    wrappers.expect_exact(context, \"Query OK, 0 rows affected\", timeout=2)\n\n\n@then(\"we see database dropped and no default database\")\ndef step_see_db_dropped_no_default(context):\n    \"\"\"Wait to see drop database output.\"\"\"\n    user = context.conf[\"user\"]\n    host = context.conf[\"host\"]\n    database = \"(none)\"\n    context.currentdb = None\n\n    wrappers.expect_exact(context, \"Query OK, 0 rows affected\", timeout=2)\n    wrappers.wait_prompt(context, f\"{user}@{host}:{database}>\")\n\n\n@then(\"we see database connected\")\ndef step_see_db_connected(context):\n    \"\"\"Wait to see drop database output.\"\"\"\n    wrappers.expect_exact(context, 'connected to database \"', timeout=2)\n    wrappers.expect_exact(context, '\"', timeout=2)\n    wrappers.expect_exact(context, f' as user \"{context.conf[\"user\"]}\"', timeout=2)\n"
  },
  {
    "path": "test/features/steps/crud_table.py",
    "content": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string decorating it. This string is used\nto call the step in \"*.feature\" file.\n\n\"\"\"\n\nfrom textwrap import dedent\n\nfrom behave import then, when\nimport wrappers\n\n\n@when(\"we create table\")\ndef step_create_table(context):\n    \"\"\"Send create table.\"\"\"\n    context.cli.sendline(\"create table a(x text);\")\n\n\n@when(\"we insert into table\")\ndef step_insert_into_table(context):\n    \"\"\"Send insert into table.\"\"\"\n    context.cli.sendline(\"\"\"insert into a(x) values('xxx');\"\"\")\n\n\n@when(\"we update table\")\ndef step_update_table(context):\n    \"\"\"Send insert into table.\"\"\"\n    context.cli.sendline(\"\"\"update a set x = 'yyy' where x = 'xxx';\"\"\")\n\n\n@when(\"we select from table\")\ndef step_select_from_table(context):\n    \"\"\"Send select from table.\"\"\"\n    context.cli.sendline(\"select * from a;\")\n\n\n@when(\"we delete from table\")\ndef step_delete_from_table(context):\n    \"\"\"Send deete from table.\"\"\"\n    context.cli.sendline(\"\"\"delete from a where x = 'yyy';\"\"\")\n\n\n@when(\"we drop table\")\ndef step_drop_table(context):\n    \"\"\"Send drop table.\"\"\"\n    context.cli.sendline(\"drop table a;\")\n\n\n@then(\"we see table created\")\ndef step_see_table_created(context):\n    \"\"\"Wait to see create table output.\"\"\"\n    wrappers.expect_exact(context, \"Query OK, 0 rows affected\", timeout=2)\n\n\n@then(\"we see record inserted\")\ndef step_see_record_inserted(context):\n    \"\"\"Wait to see insert output.\"\"\"\n    wrappers.expect_exact(context, \"Query OK, 1 row affected\", timeout=2)\n\n\n@then(\"we see record updated\")\ndef step_see_record_updated(context):\n    \"\"\"Wait to see update output.\"\"\"\n    wrappers.expect_exact(context, \"Query OK, 1 row affected\", timeout=2)\n\n\n@then(\"we see data selected\")\ndef step_see_data_selected(context):\n    \"\"\"Wait to see select output.\"\"\"\n    expected = (\n        dedent(\n            \"\"\"\n            +-----+\\r\n            | x   |\\r\n            +-----+\\r\n            | yyy |\\r\n            +-----+\n            \"\"\"\n        ).strip()\n        + '\\r\\n\\r\\n'\n    )\n\n    wrappers.expect_pager(\n        context,\n        expected,\n        timeout=2,\n    )\n    wrappers.expect_exact(context, \"1 row in set\", timeout=2)\n\n\n@then(\"we see record deleted\")\ndef step_see_data_deleted(context):\n    \"\"\"Wait to see delete output.\"\"\"\n    wrappers.expect_exact(context, \"Query OK, 1 row affected\", timeout=2)\n\n\n@then(\"we see table dropped\")\ndef step_see_table_dropped(context):\n    \"\"\"Wait to see drop output.\"\"\"\n    wrappers.expect_exact(context, \"Query OK, 0 rows affected\", timeout=2)\n\n\n@when(\"we select null\")\ndef step_select_null(context):\n    \"\"\"Send select null.\"\"\"\n    context.cli.sendline(\"select null;\")\n\n\n@then(\"we see null selected\")\ndef step_see_null_selected(context):\n    \"\"\"Wait to see null output.\"\"\"\n    expected = (\n        dedent(\n            \"\"\"\n            +--------+\\r\n            | NULL   |\\r\n            +--------+\\r\n            | <null> |\\r\n            +--------+\n            \"\"\"\n        ).strip()\n        + '\\r\\n\\r\\n'\n    )\n\n    wrappers.expect_pager(\n        context,\n        expected,\n        timeout=2,\n    )\n    wrappers.expect_exact(context, \"1 row in set\", timeout=2)\n"
  },
  {
    "path": "test/features/steps/iocommands.py",
    "content": "# type: ignore\n\nimport os\nfrom textwrap import dedent\n\nfrom behave import then, when\nimport wrappers\n\n\n@when(\"we start external editor providing a file name\")\ndef step_edit_file(context):\n    \"\"\"Edit file with external editor.\"\"\"\n    context.editor_file_name = os.path.join(context.package_root, f\"test_file_{context.conf['vi']}.sql\")\n    if os.path.exists(context.editor_file_name):\n        os.remove(context.editor_file_name)\n    context.cli.sendline(f\"\\\\e {os.path.basename(context.editor_file_name)}\")\n    wrappers.expect_exact(context, 'Entering Ex mode.  Type \"visual\" to go to Normal mode.', timeout=4)\n    wrappers.expect_exact(context, \"\\r\\n:\", timeout=4)\n\n\n@when('we type \"{query}\" in the editor')\ndef step_edit_type_sql(context, query):\n    context.cli.sendline(\"i\")\n    context.cli.sendline(query)\n    context.cli.sendline(\".\")\n    wrappers.expect_exact(context, \"\\r\\n:\", timeout=4)\n\n\n@when(\"we exit the editor\")\ndef step_edit_quit(context):\n    context.cli.sendline(\"x\")\n    wrappers.expect_exact(context, \"written\", timeout=4)\n\n\n@then('we see \"{query}\" in prompt')\ndef step_edit_done_sql(context, query):\n    for match in query.split(\" \"):\n        wrappers.expect_exact(context, match, timeout=5)\n    # Cleanup the command line.\n    context.cli.sendcontrol(\"c\")\n    # Cleanup the edited file.\n    if context.editor_file_name and os.path.exists(context.editor_file_name):\n        os.remove(context.editor_file_name)\n\n\n@when(\"we tee output\")\ndef step_tee_ouptut(context):\n    context.tee_file_name = os.path.join(context.package_root, f\"tee_file_{context.conf['vi']}.sql\")\n    if os.path.exists(context.tee_file_name):\n        os.remove(context.tee_file_name)\n    context.cli.sendline(f\"tee {os.path.basename(context.tee_file_name)}\")\n\n\n@when('we select \"select {param}\"')\ndef step_query_select_number(context, param):\n    context.cli.sendline(f\"select {param}\")\n    expected = (\n        dedent(\n            f\"\"\"\n            +{'-' * (len(param) + 2)}+\\r\n            | {param} |\\r\n            +{'-' * (len(param) + 2)}+\\r\n            | {param} |\\r\n            +{'-' * (len(param) + 2)}+\n            \"\"\"\n        ).strip()\n        + '\\r\\n\\r\\n'\n    )\n\n    wrappers.expect_pager(\n        context,\n        expected,\n        timeout=5,\n    )\n    wrappers.expect_exact(context, \"1 row in set\", timeout=2)\n\n\n@then('we see tabular result \"{result}\"')\ndef step_see_tabular_result(context, result):\n    wrappers.expect_exact(context, f'| {result} |', timeout=2)\n\n\n@then('we see csv result \"{result}\"')\ndef step_see_csv_result(context, result):\n    wrappers.expect_exact(context, f'\"{result}\"', timeout=2)\n\n\n@when('we query \"{query}\"')\ndef step_query(context, query):\n    context.cli.sendline(query)\n\n\n@when(\"we notee output\")\ndef step_notee_output(context):\n    context.cli.sendline(\"notee\")\n\n\n@then(\"we see 123456 in tee output\")\ndef step_see_123456_in_ouput(context):\n    with open(context.tee_file_name) as f:\n        assert \"123456\" in f.read()\n    if os.path.exists(context.tee_file_name):\n        os.remove(context.tee_file_name)\n\n\n@then('we see csv {result} in file output')\ndef step_see_csv_result_in_redirected_ouput(context, result):\n    wrappers.expect_exact(context, f'\"{result}\"', timeout=2)\n    temp_filename = \"/tmp/output1.csv\"\n    if os.path.exists(temp_filename):\n        os.remove(temp_filename)\n\n\n@then('we see text {result} in file output')\ndef step_see_text_result_in_redirected_ouput(context, result):\n    wrappers.expect_exact(context, f' {result}', timeout=2)\n    temp_filename = \"/tmp/output1.txt\"\n    if os.path.exists(temp_filename):\n        os.remove(temp_filename)\n\n\n@then(\"we see space 12 in command output\")\ndef step_see_space_12_in_command_ouput(context):\n    wrappers.expect_exact(context, ' 12', timeout=2)\n\n\n@then(\"we see space 6 in command output\")\ndef step_see_space_6_in_command_ouput(context):\n    wrappers.expect_exact(context, ' 6', timeout=2)\n\n\n@then('delimiter is set to \"{delimiter}\"')\ndef delimiter_is_set(context, delimiter):\n    wrappers.expect_exact(context, f\"Changed delimiter to {delimiter}\", timeout=2)\n"
  },
  {
    "path": "test/features/steps/named_queries.py",
    "content": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string decorating it. This string is used\nto call the step in \"*.feature\" file.\n\n\"\"\"\n\nfrom behave import then, when\nimport wrappers\n\n\n@when(\"we save a named query\")\ndef step_save_named_query(context):\n    \"\"\"Send \\fs command.\"\"\"\n    context.cli.sendline(\"\\\\fs foo SELECT 12345\")\n\n\n@when(\"we use a named query\")\ndef step_use_named_query(context):\n    \"\"\"Send \\f command.\"\"\"\n    context.cli.sendline(\"\\\\f foo\")\n\n\n@when(\"we delete a named query\")\ndef step_delete_named_query(context):\n    \"\"\"Send \\fd command.\"\"\"\n    context.cli.sendline(\"\\\\fd foo\")\n\n\n@then(\"we see the named query saved\")\ndef step_see_named_query_saved(context):\n    \"\"\"Wait to see query saved.\"\"\"\n    wrappers.expect_exact(context, \"Saved.\", timeout=2)\n\n\n@then(\"we see the named query executed\")\ndef step_see_named_query_executed(context):\n    \"\"\"Wait to see select output.\"\"\"\n    wrappers.expect_exact(context, \"SELECT 12345\", timeout=2)\n\n\n@then(\"we see the named query deleted\")\ndef step_see_named_query_deleted(context):\n    \"\"\"Wait to see query deleted.\"\"\"\n    wrappers.expect_exact(context, \"foo: Deleted\", timeout=2)\n\n\n@when(\"we save a named query with parameters\")\ndef step_save_named_query_with_parameters(context):\n    \"\"\"Send \\fs command for query with parameters.\"\"\"\n    context.cli.sendline('\\\\fs foo_args SELECT $1, \"$2\", \"$3\"')\n\n\n@when(\"we use named query with parameters\")\ndef step_use_named_query_with_parameters(context):\n    \"\"\"Send \\f command with parameters.\"\"\"\n    context.cli.sendline('\\\\f foo_args 101 second \"third value\"')\n\n\n@then(\"we see the named query with parameters executed\")\ndef step_see_named_query_with_parameters_executed(context):\n    \"\"\"Wait to see select output.\"\"\"\n    wrappers.expect_exact(context, 'SELECT 101, \"second\", \"third value\"', timeout=2)\n\n\n@when(\"we use named query with too few parameters\")\ndef step_use_named_query_with_too_few_parameters(context):\n    \"\"\"Send \\f command with missing parameters.\"\"\"\n    context.cli.sendline(\"\\\\f foo_args 101\")\n\n\n@then(\"we see the named query with parameters fail with missing parameters\")\ndef step_see_named_query_with_parameters_fail_with_missing_parameters(context):\n    \"\"\"Wait to see select output.\"\"\"\n    wrappers.expect_exact(context, \"missing substitution for $2 in query:\", timeout=2)\n\n\n@when(\"we use named query with too many parameters\")\ndef step_use_named_query_with_too_many_parameters(context):\n    \"\"\"Send \\f command with extra parameters.\"\"\"\n    context.cli.sendline(\"\\\\f foo_args 101 102 103 104\")\n\n\n@then(\"we see the named query with parameters fail with extra parameters\")\ndef step_see_named_query_with_parameters_fail_with_extra_parameters(context):\n    \"\"\"Wait to see select output.\"\"\"\n    wrappers.expect_exact(context, \"query does not have substitution parameter $4:\", timeout=2)\n"
  },
  {
    "path": "test/features/steps/specials.py",
    "content": "# type: ignore\n\n\"\"\"Steps for behavioral style tests are defined in this module.\n\nEach step is defined by the string decorating it. This string is used\nto call the step in \"*.feature\" file.\n\n\"\"\"\n\nfrom behave import then, when\nimport wrappers\n\n\n@when(\"we refresh completions\")\ndef step_refresh_completions(context):\n    \"\"\"Send refresh command.\"\"\"\n    context.cli.sendline(\"rehash\")\n\n\n@then('we see text \"{text}\"')\ndef step_see_text(context, text):\n    \"\"\"Wait to see given text message.\"\"\"\n    wrappers.expect_exact(context, text, timeout=2)\n\n\n@then(\"we see completions refresh started\")\ndef step_see_refresh_started(context):\n    \"\"\"Wait to see refresh output.\"\"\"\n    wrappers.expect_exact(context, \"Auto-completion refresh started in the background.\", timeout=2)\n"
  },
  {
    "path": "test/features/steps/utils.py",
    "content": "# 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(cli_args):\n        if \"=\" in arg:\n            key, value = arg.split(\"=\")\n            args_dict[key] = value\n        else:\n            args_dict[arg] = None\n    return args_dict\n"
  },
  {
    "path": "test/features/steps/wrappers.py",
    "content": "# type: ignore\n\nfrom io import StringIO\nimport re\nimport sys\nimport textwrap\n\nimport pexpect\n\n\ndef expect_exact(context, expected, timeout):\n    timedout = False\n    try:\n        context.cli.expect_exact(expected, timeout=timeout)\n    except pexpect.TIMEOUT:\n        timedout = True\n    if timedout:\n        # Strip color codes out of the output.\n        actual = re.sub(r\"\\x1b\\[([0-9A-Za-z;?])+[m|K]?\", \"\", context.cli.before)\n        raise Exception(\n            textwrap.dedent(\n                f\"\"\"\\\n                Expected:\n                ---\n                {expected!r}\n                ---\n                Actual:\n                ---\n                {actual!r}\n                ---\n                Full log:\n                ---\n                {context.logfile.getvalue()!r}\n                ---\n                \"\"\"\n            )\n        )\n\n\ndef expect_pager(context, expected, timeout):\n    expect_exact(context, f\"{context.conf['pager_boundary']}\\r\\n{expected}{context.conf['pager_boundary']}\\r\\n\", timeout=timeout)\n\n\ndef run_cli(context, run_args=None, exclude_args=None):\n    \"\"\"Run the process using pexpect.\"\"\"\n    run_args = run_args or {}\n    rendered_args = []\n    exclude_args = set(exclude_args) if exclude_args else set()\n\n    conf = dict(**context.conf)\n    conf.update(run_args)\n\n    def add_arg(name, key, value):\n        if name not in exclude_args:\n            if value is not None:\n                rendered_args.extend((key, value))\n            else:\n                rendered_args.append(key)\n\n    if conf.get(\"host\", None):\n        add_arg(\"host\", \"-h\", conf[\"host\"])\n    if conf.get(\"user\", None):\n        add_arg(\"user\", \"-u\", conf[\"user\"])\n    if conf.get(\"pass\", None):\n        add_arg(\"pass\", \"-p\", conf[\"pass\"])\n    if conf.get(\"port\", None):\n        add_arg(\"port\", \"-P\", str(conf[\"port\"]))\n    if conf.get(\"dbname\", None):\n        add_arg(\"dbname\", \"-D\", conf[\"dbname\"])\n    if conf.get(\"defaults-file\", None):\n        add_arg(\"defaults_file\", \"--defaults-file\", conf[\"defaults-file\"])\n    if conf.get(\"myclirc\", None):\n        add_arg(\"myclirc\", \"--myclirc\", conf[\"myclirc\"])\n    if conf.get(\"login_path\"):\n        add_arg(\"login_path\", \"--login-path\", conf[\"login_path\"])\n\n    for arg_name, arg_value in conf.items():\n        if arg_name.startswith(\"-\"):\n            add_arg(arg_name, arg_name, arg_value)\n\n    try:\n        cli_cmd = context.conf[\"cli_command\"]\n    except KeyError:\n        cli_cmd = f'{sys.executable} -c \"import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()\"'\n\n    cmd_parts = [cli_cmd] + rendered_args\n    cmd = \" \".join(cmd_parts)\n    context.cli = pexpect.spawnu(cmd, cwd=context.package_root)\n    context.logfile = StringIO()\n    context.cli.logfile = context.logfile\n    context.exit_sent = False\n    context.currentdb = context.conf[\"dbname\"]\n\n\ndef wait_prompt(context, prompt=None):\n    \"\"\"Make sure prompt is displayed.\"\"\"\n    if prompt is None:\n        user = context.conf[\"user\"]\n        host = context.conf[\"host\"]\n        dbname = context.currentdb\n        prompt = (f\"{user}@{host}:{dbname}>\",)\n    expect_exact(context, prompt, timeout=5)\n    context.atprompt = True\n"
  },
  {
    "path": "test/features/wrappager.py",
    "content": "#!/usr/bin/env python\n\nimport sys\n\n\ndef wrappager(boundary: str) -> None:\n    print(boundary)\n    while 1:\n        buf = sys.stdin.read(2048)\n        if not buf:\n            break\n        sys.stdout.write(buf)\n    print(boundary)\n\n\nif __name__ == \"__main__\":\n    wrappager(sys.argv[1])\n"
  },
  {
    "path": "test/myclirc",
    "content": "# vi: ft=dosini\n[main]\n\n# Enable or disable the automatic displaying of warnings (\"SHOW WARNINGS\")\n# after executing a SQL statement when applicable.\nshow_warnings = False\n\n# Enables context sensitive auto-completion. If this is disabled the all\n# possible completions will be listed.\nsmart_completion = True\n\n# Minimum characters typed before offering completion suggestions.\n# Suggestion: 3.\nmin_completion_trigger = 1\n\n# Multi-line mode allows breaking up the sql statements into multiple lines. If\n# this is set to True, then the end of the statements must have a semi-colon.\n# If this is set to False then sql statements can't be split into multiple\n# lines. End of line (return) is considered as the end of the statement.\nmulti_line = False\n\n# Destructive warning mode will alert you before executing a sql statement\n# that may cause harm to the database such as \"drop table\", \"drop database\"\n# or \"shutdown\".\ndestructive_warning = True\n\n# Queries starting with these keywords will activate the destructive warning.\n# UPDATE will not activate the warning if the statement includes a WHERE\n# clause.\ndestructive_keywords = DROP SHUTDOWN DELETE TRUNCATE ALTER UPDATE\n\n# interactive query history location.\nhistory_file = ~/.mycli-history\n\n# log_file location.\nlog_file = ~/.mycli.test.log\n\n# Default log level. Possible values: \"CRITICAL\", \"ERROR\", \"WARNING\", \"INFO\"\n# and \"DEBUG\". \"NONE\" disables logging.\nlog_level = DEBUG\n\n# Log every query and its results to a file. Enable this by uncommenting the\n# line below.\n# audit_log = ~/.mycli-audit.log\n\n# Timing of sql statements and table rendering.\ntiming = True\n\n# Show the full SQL when running a favorite query. Set to False to hide.\nshow_favorite_query = True\n\n# Beep after long-running queries are completed; 0 to disable.\nbeep_after_seconds = 0\n\n# Table format. Possible values: ascii, double, github,\n# psql, plain, simple, grid, fancy_grid, pipe, orgtbl, rst, mediawiki, html,\n# latex, latex_booktabs, textile, moinmoin, jira, vertical, tsv, tsv_noheader,\n# csv, csv-noheader, jsonl, jsonl_unescaped.\n# Recommended: ascii\ntable_format = ascii\n\n# Redirected otuput format\n# Recommended: csv.\nredirect_format = csv\n\n# How to display the missing value (ie NULL).  Only certain table formats\n# support configuring the missing value.  CSV for example always uses the\n# empty string, and JSON formats use native nulls.\nnull_string = <null>\n\n# How to align numeric data in tabular output: right or left.\nnumeric_alignment = right\n\n# How to display binary values in tabular output: \"hex\", or \"utf8\".  \"utf8\"\n# means attempt to render valid UTF-8 sequences as strings, then fall back\n# to hex rendering if not possible.\nbinary_display = hex\n\n# A command to run after a successful output redirect, with {} to be replaced\n# with the escaped filename.  Mac example: echo {} | pbcopy.  Escaping is not\n# reliable/safe on Windows.\npost_redirect_command = \"\"\n\n# Syntax coloring style. Possible values (many support the \"-dark\" suffix):\n# manni, igor, xcode, vim, autumn, vs, rrt, native, perldoc, borland, tango, emacs,\n# friendly, monokai, paraiso, colorful, murphy, bw, pastie, paraiso, trac, default,\n# fruity.\n# Screenshots at https://mycli.net/syntax\n# Can be further modified in [colors]\nsyntax_style = default\n\n# Keybindings: Possible values: emacs, vi.\n# Emacs mode: Ctrl-A is home, Ctrl-E is end. All emacs keybindings are available in the REPL.\n# When Vi mode is enabled you can use modal editing features offered by Vi in the REPL.\nkey_bindings = emacs\n\n# Enabling this option will show the suggestions in a wider menu. Thus more items are suggested.\nwider_completion_menu = False\n\n# MySQL prompt\n# * \\D - full current date, e.g. Sat Feb 14 15:55:48 2026\n# * \\R - current hour in 24-hour time (00–23)\n# * \\r - current hour in 12-hour time (01–12)\n# * \\m - minutes of the current time\n# * \\s - seconds of the current time\n# * \\P - AM/PM\n# * \\d - selected database/schema\n# * \\h - hostname of the server\n# * \\H - shortened hostname of the server\n# * \\p - connection port\n# * \\j - connection socket basename\n# * \\J - full connection socket path\n# * \\k - connection socket basename OR the port\n# * \\K - full connection socket path OR the port\n# * \\T - connection SSL/TLS version\n# * \\t - database vendor (Percona, MySQL, MariaDB, TiDB)\n# * \\w - number of warnings, or \"(none)\" (requires frequent trips to the server)\n# * \\W - number of warnings, or the empty string  (requires frequent trips to the server)\n# * \\y - uptime in seconds (requires frequent trips to the server)\n# * \\Y - uptime in words (requires frequent trips to the server)\n# * \\u - username\n# * \\A - DSN alias\n# * \\n - a newline\n# * \\_ - a space\n# * \\\\ - a literal backslash\n# * \\x1b[...m - an ANSI escape sequence (can style with color)\nprompt = \"\\t \\u@\\h:\\d> \"\nprompt_continuation = ->\n\n# Use the same prompt format strings to construct a status line in the toolbar,\n# where \\B in the first position refers to the default toolbar showing keystrokes\n# and state.  Example:\n#\n#     toolbar = '\\B\\d \\D'\n#\n# If \\B is included, the additional content will begin on the next line.  More\n# lines can be added with \\n.  If \\B is not included, the customized toolbar\n# can be a single line.  An empty value is the same as the default \"\\B\".  The\n# special literal value \"None\" will suppress the toolbar from appearing.\ntoolbar = ''\n\n# Use the same prompt format strings to construct a terminal tab title.\n# The original XTerm docs call this title the \"window title\", but it now\n# probably refers to a terminal tab.  This title is only updated as frequently\n# as the database is changed.\nterminal_tab_title = ''\n\n# Use the same prompt format strings to construct a terminal window title.\n# The original XTerm docs call this title the \"icon title\", but it now\n# probably refers to a terminal window which contains tabs.  This title is\n# only updated as frequently as the database is changed.\nterminal_window_title = ''\n\n# Use the same prompt format strings to construct a window title in a terminal\n# multiplexer.  Currently only tmux is supported.  This title is only updated\n# as frequently as the database is changed.\nmultiplex_window_title = ''\n\n# Use the same prompt format strings to construct a pane title in a terminal\n# multiplexer.  Currently only tmux is supported.  This title is only updated\n# as frequently as the database is changed.\nmultiplex_pane_title = ''\n\n# Skip intro info on startup and outro info on exit\nless_chatty = True\n\n# Use alias from --login-path instead of host name in prompt\nlogin_path_as_host = False\n\n# Cause result sets to be displayed vertically if they are too wide for the current window,\n# and using normal tabular format otherwise. (This applies to statements terminated by ; or \\G.)\nauto_vertical_output = False\n\n# keyword casing preference. Possible values \"lower\", \"upper\", \"auto\"\nkeyword_casing = auto\n\n# disabled pager on startup\nenable_pager = True\n\n# Choose a specific pager\npager = less\n\n# whether to show verbose warnings about the transition away from reading my.cnf\nmy_cnf_transition_done = False\n\n# Whether to store and retrieve passwords from the system keyring.\n# See the documentation for https://pypi.org/project/keyring/ for your OS.\n# Note that the hostname is considered to be different if short or qualified.\n# This can be overridden with --use-keyring= at the CLI.\n# A password can be reset with --use-keyring=reset at the CLI.\nuse_keyring = False\n\n[search]\n\n# Whether to apply syntax highlighting to the preview window in fuzzy history\n# search.  There is a small performance penalty to enabling this.  The \"pygmentize\"\n# CLI tool must also be available.  The syntax style from the \"syntax_style\"\n# option will be respected, though additional customizations from [colors] will\n# not be applied.\nhighlight_preview = False\n\n[connection]\n\n# character set for connections without --character-set being set\ndefault_character_set = utf8mb4\n\n# whether to enable LOAD DATA LOCAL INFILE for connections without --local-infile being set\ndefault_local_infile = False\n\n# How often to send periodic background pings to the server when input is idle.  Ticks are\n# roughly in seconds, but may be faster.  Set to zero to disable.  Suggestion: 300.\ndefault_keepalive_ticks = 0\n\n# Sets the desired behavior for handling secure connections to the database server.\n# Possible values:\n# auto = SSL is preferred for TCP/IP connections. Will attempt to connect via SSL, but will fall\n#        back to cleartext as needed.  Will not attempt to connect with SSL over local sockets.\n# on   = SSL is required. Will attempt to connect via SSL even on a local socket, and will fail if\n#        a secure connection is not established.\n# off  = do not use SSL. Will fail if the server requires a secure connection.\ndefault_ssl_mode = auto\n\n# SSL CA file for connections without --ssl-ca being set\ndefault_ssl_ca =\n\n# SSL CA directory for connections without --ssl-capath being set\ndefault_ssl_capath =\n\n# SSL X509 cert path for connections without --ssl-cert being set\ndefault_ssl_cert =\n\n# SSL X509 key for connections without --ssl-key being set\ndefault_ssl_key =\n\n# SSL cipher to use for connections without --ssl-cipher being set\ndefault_ssl_cipher =\n\n# whether to verify server's \"Common Name\" in its cert, for connections without\n# --ssl-verify-server-cert being set\ndefault_ssl_verify_server_cert = False\n\n[llm]\n\n# If set to a positive integer, truncate text/binary fields to that width\n# in bytes when sending sample data, to conserve tokens.  Suggestion: 1024.\nprompt_field_truncate = None\n\n# If set to a positive integer, attempt to truncate various sections of LLM\n# prompt input to that number in bytes, to conserve tokens.  Suggestion:\n# 1000000.\nprompt_section_truncate = None\n\n[keys]\n\n# possible values: exit, none\ncontrol_d = exit\n\n# possible values: auto, fzf, reverse_isearch\ncontrol_r = auto\n\n# comma-separated list: toolkit_default, summon, advancing_summon, prefixing_summon, advance, cancel\n#\n# * toolkit_default - ignore other behaviors and use prompt_toolkit's default bindings\n# * summon - when completions are not visible, summon them\n# * advancing_summon - when completions are not visible, summon them _and_ advance in the list\n# * prefixing_summon - when completions are not visible, summon them _and_ insert the common prefix\n# * advance - when completions are visible, advance in the list\n# * cancel - when completions are visible, toggle the list off\ncontrol_space = summon, advance\n\n# comma-separated list: toolkit_default, summon, advancing_summon, prefixing_summon, advance, cancel\ntab = advancing_summon, advance\n\n# How long to wait for an Escape key sequence in vi mode.\n# 0.5 seconds is the prompt_toolkit default, but vi users may find that too long.\n# Shorter values mean that \"Escape\" alone is recognized more quickly.\nvi_ttimeoutlen = 0.1\n\n# How long to wait for an Escape key sequence in Emacs mode.\nemacs_ttimeoutlen = 0.5\n\n# Custom colors for the completion menu, toolbar, etc, with actual support\n# depending on the terminal, and the property being set.\n# Colors: #ffffff, bg:#ffffff, border:#ffffff.\n# Attributes: (no)blink, bold, dim, hidden, inherit, italic, reverse, strike, underline.\n[colors]\ncompletion-menu.completion.current = \"bg:#ffffff #000000\"\ncompletion-menu.completion = \"bg:#008888 #ffffff\"\ncompletion-menu.meta.completion.current = \"bg:#44aaaa #000000\"\ncompletion-menu.meta.completion = \"bg:#448888 #ffffff\"\ncompletion-menu.multi-column-meta = \"bg:#aaffff #000000\"\nscrollbar.arrow = \"bg:#003333\"\nscrollbar = \"bg:#00aaaa\"\nselected = \"#ffffff bg:#6666aa\"\nsearch = \"#ffffff bg:#4444aa\"\nsearch.current = \"#ffffff bg:#44aa44\"\nbottom-toolbar = \"bg:#222222 #aaaaaa\"\nbottom-toolbar.off = \"bg:#222222 #888888\"\nbottom-toolbar.on = \"bg:#222222 #ffffff\"\nsearch-toolbar = noinherit bold\nsearch-toolbar.text = nobold\nsystem-toolbar = noinherit bold\narg-toolbar = noinherit bold\narg-toolbar.text = nobold\nbottom-toolbar.transaction.valid = \"bg:#222222 #00ff5f bold\"\nbottom-toolbar.transaction.failed = \"bg:#222222 #ff005f bold\"\nprompt = ''\ncontinuation = ''\n\n# style classes for colored table output\noutput.table-separator = \"\"\noutput.header = \"#00ff5f bold\"\noutput.odd-row = \"\"\noutput.even-row = \"\"\noutput.null = \"#808080\"\noutput.status = \"\"\noutput.status.warning-count = \"\"\noutput.timing = \"\"\n\n# SQL syntax highlighting overrides\n# sql.comment = 'italic #408080'\n# sql.comment.multi-line = ''\n# sql.comment.single-line = ''\n# sql.comment.optimizer-hint = ''\n# sql.escape = 'border:#FF0000'\n# sql.keyword = 'bold #008000'\n# sql.datatype = 'nobold #B00040'\n# sql.literal = ''\n# sql.literal.date = ''\n# sql.symbol = ''\n# sql.quoted-schema-object = ''\n# sql.quoted-schema-object.escape = ''\n# sql.constant = '#880000'\n# sql.function = '#0000FF'\n# sql.variable = '#19177C'\n# sql.number = '#666666'\n# sql.number.binary = ''\n# sql.number.float = ''\n# sql.number.hex = ''\n# sql.number.integer = ''\n# sql.operator = '#666666'\n# sql.punctuation = ''\n# sql.string = '#BA2121'\n# sql.string.double-quouted = ''\n# sql.string.escape = 'bold #BB6622'\n# sql.string.single-quoted = ''\n# sql.whitespace = ''\n\n# Favorite queries.\n# You can add your favorite queries here. They will be available in the\n# REPL when you type `\\f` or `\\f <query_name>`.\n[favorite_queries]\ncheck = 'select \"✔\"'\nfoo_args = 'SELECT $1, \"$2\", \"$3\"'\n# example = \"SELECT * FROM example_table WHERE id = 1\"\n\n# Initial commands to execute when connecting to any database.\n[init-commands]\nglobal_limit = set sql_select_limit=9999\n# read_only = \"SET SESSION TRANSACTION READ ONLY\"\n\n\n# Use the -d option to reference a DSN.\n# Special characters in passwords and other strings can be escaped with URL encoding.\n[alias_dsn]\n# example_dsn = mysql://[user[:password]@][host][:port][/dbname]\n\n# Initial commands to execute when connecting to a DSN alias.\n[alias_dsn.init-commands]\n# Define one or more SQL statements per alias (semicolon-separated).\n# example_dsn = \"SET sql_select_limit=1000; SET time_zone='+00:00'\"\n"
  },
  {
    "path": "test/test.txt",
    "content": "mycli rocks!\n"
  },
  {
    "path": "test/test_clistyle.py",
    "content": "# type: ignore\n\n\"\"\"Test the mycli.clistyle module.\"\"\"\n\nfrom pygments.style import Style\nfrom pygments.token import Token\nimport pytest\n\nfrom mycli.clistyle import style_factory_toolkit\n\n\n@pytest.mark.skip(reason=\"incompatible with new prompt toolkit\")\ndef test_style_factory_toolkit():\n    \"\"\"Test that a Pygments Style class is created.\"\"\"\n    header = \"bold underline #ansired\"\n    cli_style = {\"Token.Output.Header\": header}\n    style = style_factory_toolkit(\"default\", cli_style)\n\n    assert isinstance(style(), Style)\n    assert Token.Output.Header in style.styles\n    assert header == style.styles[Token.Output.Header]\n\n\n@pytest.mark.skip(reason=\"incompatible with new prompt toolkit\")\ndef test_style_factory_toolkit_unknown_name():\n    \"\"\"Test that an unrecognized name will not throw an error.\"\"\"\n    style = style_factory_toolkit(\"foobar\", {})\n\n    assert isinstance(style(), Style)\n"
  },
  {
    "path": "test/test_clitoolbar.py",
    "content": "from prompt_toolkit.shortcuts import PromptSession\n\nfrom mycli.clitoolbar import create_toolbar_tokens_func\nfrom mycli.main import MyCli\n\n\ndef test_create_toolbar_tokens_func_initial():\n    m = MyCli()\n    m.prompt_app = PromptSession()\n    iteration = 0\n    f = create_toolbar_tokens_func(m, lambda: iteration == 0, m.toolbar_format)\n    result = f()\n    assert any(\"right-arrow accepts full-line suggestion\" in token for token in result)\n\n\ndef test_create_toolbar_tokens_func_short():\n    m = MyCli()\n    m.prompt_app = PromptSession()\n    iteration = 1\n    f = create_toolbar_tokens_func(m, lambda: iteration == 0, m.toolbar_format)\n    result = f()\n    assert not any(\"right-arrow accepts full-line suggestion\" in token for token in result)\n"
  },
  {
    "path": "test/test_completion_engine.py",
    "content": "# type: ignore\n\nimport pytest\n\nfrom mycli.packages import special\nfrom mycli.packages.completion_engine import (\n    _find_doubled_backticks,\n    is_inside_quotes,\n    suggest_type,\n)\n\n\ndef sorted_dicts(dicts):\n    \"\"\"input is a list of dicts.\"\"\"\n    return sorted(tuple(x.items()) for x in dicts)\n\n\ndef test_select_suggests_cols_with_visible_table_scope():\n    suggestions = suggest_type(\"SELECT  FROM tabl\", \"SELECT \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"tabl\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_select_suggests_cols_with_qualified_table_scope():\n    suggestions = suggest_type(\"SELECT  FROM sch.tabl\", \"SELECT \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"tabl\"]},\n        {\"type\": \"column\", \"tables\": [(\"sch\", \"tabl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT * FROM tabl WHERE \",\n        \"SELECT * FROM tabl WHERE (\",\n        \"SELECT * FROM tabl WHERE bar OR \",\n        \"SELECT * FROM tabl WHERE foo = 1 AND \",\n        \"SELECT * FROM tabl WHERE (bar > 10 AND \",\n        \"SELECT * FROM tabl WHERE (bar AND (baz OR (qux AND (\",\n        \"SELECT * FROM tabl WHERE 10 < \",\n        \"SELECT * FROM tabl WHERE foo BETWEEN \",\n        \"SELECT * FROM tabl WHERE foo BETWEEN foo AND \",\n    ],\n)\ndef test_where_suggests_columns_functions(expression):\n    suggestions = suggest_type(expression, expression)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"tabl\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_where_equals_suggests_enum_values_first():\n    expression = \"SELECT * FROM tabl WHERE foo = \"\n    suggestions = suggest_type(expression, expression)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"enum_value\", \"tables\": [(None, \"tabl\", None)], \"column\": \"foo\", \"parent\": None},\n        {\"type\": \"alias\", \"aliases\": [\"tabl\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT * FROM tabl WHERE foo IN (\",\n        \"SELECT * FROM tabl WHERE foo IN (bar, \",\n    ],\n)\ndef test_where_in_suggests_columns(expression):\n    suggestions = suggest_type(expression, expression)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"tabl\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_where_equals_any_suggests_columns_or_keywords():\n    text = \"SELECT * FROM tabl WHERE foo = ANY(\"\n    suggestions = suggest_type(text, text)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"tabl\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_where_convert_using_suggests_character_set():\n    text = 'SELECT * FROM tabl WHERE CONVERT(foo USING '\n    suggestions = suggest_type(text, text)\n    assert suggestions == [{\"type\": \"character_set\"}]\n\n\ndef test_where_cast_character_set_suggests_character_set():\n    text = 'SELECT * FROM tabl WHERE CAST(foo AS CHAR CHARACTER SET '\n    suggestions = suggest_type(text, text)\n    assert suggestions == [{\"type\": \"character_set\"}]\n\n\ndef test_lparen_suggests_cols():\n    suggestion = suggest_type(\"SELECT MAX( FROM tbl\", \"SELECT MAX(\")\n    assert suggestion == [{\"type\": \"column\", \"tables\": [(None, \"tbl\", None)]}]\n\n\ndef test_operand_inside_function_suggests_cols1():\n    suggestion = suggest_type(\"SELECT MAX(col1 +  FROM tbl\", \"SELECT MAX(col1 + \")\n    assert suggestion == [{\"type\": \"column\", \"tables\": [(None, \"tbl\", None)]}]\n\n\ndef test_operand_inside_function_suggests_cols2():\n    suggestion = suggest_type(\"SELECT MAX(col1 + col2 +  FROM tbl\", \"SELECT MAX(col1 + col2 + \")\n    assert suggestion == [{\"type\": \"column\", \"tables\": [(None, \"tbl\", None)]}]\n\n\ndef test_operand_inside_function_suggests_cols3():\n    suggestion = suggest_type(\"SELECT MAX(col1 ||  FROM tbl\", \"SELECT MAX(col1 || \")\n    assert suggestion == [{\"type\": \"column\", \"tables\": [(None, \"tbl\", None)]}]\n\n\ndef test_operand_inside_function_suggests_cols4():\n    suggestion = suggest_type(\"SELECT MAX(col1 LIKE  FROM tbl\", \"SELECT MAX(col1 LIKE \")\n    assert suggestion == [{\"type\": \"column\", \"tables\": [(None, \"tbl\", None)]}]\n\n\ndef test_operand_inside_function_suggests_cols5():\n    suggestion = suggest_type(\"SELECT MAX(col1 DIV  FROM tbl\", \"SELECT MAX(col1 DIV \")\n    assert suggestion == [{\"type\": \"column\", \"tables\": [(None, \"tbl\", None)]}]\n\n\n@pytest.mark.xfail\ndef test_arrow_op_inside_function_suggests_nothing():\n    suggestion = suggest_type(\"SELECT MAX(col1->  FROM tbl\", \"SELECT MAX(col1->\")\n    assert suggestion == []\n\n\ndef test_select_suggests_cols_and_funcs():\n    suggestions = suggest_type(\"SELECT \", \"SELECT \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": []},\n        {\"type\": \"column\", \"tables\": []},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT * FROM \",\n        \"INSERT INTO \",\n        \"COPY \",\n        \"UPDATE \",\n        \"DESCRIBE \",\n        \"DESC \",\n        \"EXPLAIN \",\n        \"SELECT * FROM foo JOIN \",\n    ],\n)\ndef test_expression_suggests_tables_views_and_schemas(expression):\n    suggestions = suggest_type(expression, expression)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n        {\"type\": \"database\"},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT * FROM sch.\",\n        \"INSERT INTO sch.\",\n        \"COPY sch.\",\n        \"UPDATE sch.\",\n        \"DESCRIBE sch.\",\n        \"DESC sch.\",\n        \"EXPLAIN sch.\",\n        \"SELECT * FROM foo JOIN sch.\",\n    ],\n)\ndef test_expression_suggests_qualified_tables_views_and_schemas(expression):\n    suggestions = suggest_type(expression, expression)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"table\", \"schema\": \"sch\"},\n        {\"type\": \"view\", \"schema\": \"sch\"},\n    ])\n\n\ndef test_truncate_suggests_tables_and_schemas():\n    suggestions = suggest_type(\"TRUNCATE \", \"TRUNCATE \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"database\"},\n    ])\n\n\ndef test_truncate_suggests_qualified_tables():\n    suggestions = suggest_type(\"TRUNCATE sch.\", \"TRUNCATE sch.\")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"table\", \"schema\": \"sch\"},\n    ])\n\n\ndef test_distinct_suggests_cols():\n    suggestions = suggest_type(\"SELECT DISTINCT \", \"SELECT DISTINCT \")\n    assert suggestions == [{\"type\": \"column\", \"tables\": []}]\n\n\ndef test_col_comma_suggests_cols():\n    suggestions = suggest_type(\"SELECT a, b, FROM tbl\", \"SELECT a, b,\")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"tbl\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"tbl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_table_comma_suggests_tables_and_schemas():\n    suggestions = suggest_type(\"SELECT a, b FROM tbl1, \", \"SELECT a, b FROM tbl1, \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"database\"},\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n    ])\n\n\ndef test_into_suggests_tables_and_schemas():\n    suggestion = suggest_type(\"INSERT INTO \", \"INSERT INTO \")\n    assert sorted_dicts(suggestion) == sorted_dicts([\n        {\"type\": \"database\"},\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n    ])\n\n\ndef test_insert_into_lparen_suggests_cols():\n    suggestions = suggest_type(\"INSERT INTO abc (\", \"INSERT INTO abc (\")\n    assert suggestions == [{\"type\": \"column\", \"tables\": [(None, \"abc\", None)]}]\n\n\ndef test_insert_into_lparen_partial_text_suggests_cols():\n    suggestions = suggest_type(\"INSERT INTO abc (i\", \"INSERT INTO abc (i\")\n    assert suggestions == [{\"type\": \"column\", \"tables\": [(None, \"abc\", None)]}]\n\n\ndef test_insert_into_lparen_comma_suggests_cols():\n    suggestions = suggest_type(\"INSERT INTO abc (id,\", \"INSERT INTO abc (id,\")\n    assert suggestions == [{\"type\": \"column\", \"tables\": [(None, \"abc\", None)]}]\n\n\ndef test_partially_typed_col_name_suggests_col_names():\n    suggestions = suggest_type(\"SELECT * FROM tabl WHERE col_n\", \"SELECT * FROM tabl WHERE col_n\")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"tabl\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_dot_suggests_cols_of_a_table_or_schema_qualified_table():\n    suggestions = suggest_type(\"SELECT tabl. FROM tabl\", \"SELECT tabl.\")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", None)]},\n        {\"type\": \"table\", \"schema\": \"tabl\"},\n        {\"type\": \"view\", \"schema\": \"tabl\"},\n        {\"type\": \"function\", \"schema\": \"tabl\"},\n    ])\n\n\ndef test_dot_suggests_cols_of_an_alias():\n    suggestions = suggest_type(\"SELECT t1. FROM tabl1 t1, tabl2 t2\", \"SELECT t1.\")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"table\", \"schema\": \"t1\"},\n        {\"type\": \"view\", \"schema\": \"t1\"},\n        {\"type\": \"column\", \"tables\": [(None, \"tabl1\", \"t1\")]},\n        {\"type\": \"function\", \"schema\": \"t1\"},\n    ])\n\n\ndef test_dot_col_comma_suggests_cols_or_schema_qualified_table():\n    suggestions = suggest_type(\"SELECT t1.a, t2. FROM tabl1 t1, tabl2 t2\", \"SELECT t1.a, t2.\")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"column\", \"tables\": [(None, \"tabl2\", \"t2\")]},\n        {\"type\": \"table\", \"schema\": \"t2\"},\n        {\"type\": \"view\", \"schema\": \"t2\"},\n        {\"type\": \"function\", \"schema\": \"t2\"},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT * FROM (\",\n        \"SELECT * FROM foo WHERE EXISTS (\",\n        \"SELECT * FROM foo WHERE bar AND NOT EXISTS (\",\n        \"SELECT 1 AS\",\n    ],\n)\ndef test_sub_select_suggests_keyword(expression):\n    suggestion = suggest_type(expression, expression)\n    assert suggestion == [{\"type\": \"keyword\"}]\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT * FROM (S\",\n        \"SELECT * FROM foo WHERE EXISTS (S\",\n        \"SELECT * FROM foo WHERE bar AND NOT EXISTS (S\",\n    ],\n)\ndef test_sub_select_partial_text_suggests_keyword(expression):\n    suggestion = suggest_type(expression, expression)\n    assert suggestion == [{\"type\": \"keyword\"}]\n\n\ndef test_outer_table_reference_in_exists_subquery_suggests_columns():\n    q = \"SELECT * FROM foo f WHERE EXISTS (SELECT 1 FROM bar WHERE f.\"\n    suggestions = suggest_type(q, q)\n    assert suggestions == [\n        {\"type\": \"column\", \"tables\": [(None, \"foo\", \"f\")]},\n        {\"type\": \"table\", \"schema\": \"f\"},\n        {\"type\": \"view\", \"schema\": \"f\"},\n        {\"type\": \"function\", \"schema\": \"f\"},\n    ]\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT * FROM (SELECT * FROM \",\n        \"SELECT * FROM foo WHERE EXISTS (SELECT * FROM \",\n        \"SELECT * FROM foo WHERE bar AND NOT EXISTS (SELECT * FROM \",\n    ],\n)\ndef test_sub_select_table_name_completion(expression):\n    suggestion = suggest_type(expression, expression)\n    assert sorted_dicts(suggestion) == sorted_dicts([\n        {\"type\": \"database\"},\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n    ])\n\n\ndef test_sub_select_col_name_completion():\n    suggestions = suggest_type(\"SELECT * FROM (SELECT  FROM abc\", \"SELECT * FROM (SELECT \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"abc\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"abc\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\n@pytest.mark.xfail\ndef test_sub_select_multiple_col_name_completion():\n    suggestions = suggest_type(\"SELECT * FROM (SELECT a, FROM abc\", \"SELECT * FROM (SELECT a, \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"column\", \"tables\": [(None, \"abc\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_sub_select_dot_col_name_completion():\n    suggestions = suggest_type(\"SELECT * FROM (SELECT t. FROM tabl t\", \"SELECT * FROM (SELECT t.\")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"column\", \"tables\": [(None, \"tabl\", \"t\")]},\n        {\"type\": \"table\", \"schema\": \"t\"},\n        {\"type\": \"view\", \"schema\": \"t\"},\n        {\"type\": \"function\", \"schema\": \"t\"},\n    ])\n\n\n@pytest.mark.parametrize(\"join_type\", [\"\", \"INNER\", \"LEFT\", \"RIGHT OUTER\"])\n@pytest.mark.parametrize(\"tbl_alias\", [\"\", \"foo\"])\ndef test_join_suggests_tables_and_schemas(tbl_alias, join_type):\n    text = f\"SELECT * FROM abc {tbl_alias} {join_type} JOIN \"\n    suggestion = suggest_type(text, text)\n    assert sorted_dicts(suggestion) == sorted_dicts([\n        {\"type\": \"database\"},\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"sql\",\n    [\n        \"SELECT * FROM abc a JOIN def d ON a.\",\n        \"SELECT * FROM abc a JOIN def d ON a.id = d.id AND a.\",\n    ],\n)\ndef test_join_alias_dot_suggests_cols1(sql):\n    suggestions = suggest_type(sql, sql)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"column\", \"tables\": [(None, \"abc\", \"a\")]},\n        {\"type\": \"table\", \"schema\": \"a\"},\n        {\"type\": \"view\", \"schema\": \"a\"},\n        {\"type\": \"function\", \"schema\": \"a\"},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"sql\",\n    [\n        \"SELECT * FROM abc a JOIN def d ON a.id = d.\",\n        \"SELECT * FROM abc a JOIN def d ON a.id = d.id AND a.id2 = d.\",\n    ],\n)\ndef test_join_alias_dot_suggests_cols2(sql):\n    suggestions = suggest_type(sql, sql)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"column\", \"tables\": [(None, \"def\", \"d\")]},\n        {\"type\": \"table\", \"schema\": \"d\"},\n        {\"type\": \"view\", \"schema\": \"d\"},\n        {\"type\": \"function\", \"schema\": \"d\"},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"sql\",\n    [\n        \"select a.x, b.y from abc a join bcd b on \",\n        \"select a.x, b.y from abc a join bcd b on a.id = b.id OR \",\n        \"select a.x, b.y from abc a join bcd b on a.id = b.id + \",\n        \"select a.x, b.y from abc a join bcd b on a.id = b.id < \",\n    ],\n)\ndef test_on_suggests_aliases(sql):\n    suggestions = suggest_type(sql, sql)\n    assert suggestions == [{\"type\": \"alias\", \"aliases\": [\"a\", \"b\"]}]\n\n\n@pytest.mark.parametrize(\n    \"sql\",\n    [\n        \"select abc.x, bcd.y from abc join bcd on \",\n        \"select abc.x, bcd.y from abc join bcd on abc.id = bcd.id AND \",\n    ],\n)\ndef test_on_suggests_tables(sql):\n    suggestions = suggest_type(sql, sql)\n    assert suggestions == [{\"type\": \"alias\", \"aliases\": [\"abc\", \"bcd\"]}]\n\n\n@pytest.mark.parametrize(\n    \"sql\",\n    [\n        \"select a.x, b.y from abc a join bcd b on a.id = \",\n        \"select a.x, b.y from abc a join bcd b on a.id = b.id AND a.id2 = \",\n    ],\n)\ndef test_on_suggests_aliases_right_side(sql):\n    suggestions = suggest_type(sql, sql)\n    assert suggestions == [{\"type\": \"alias\", \"aliases\": [\"a\", \"b\"]}]\n\n\n@pytest.mark.parametrize(\n    \"sql\",\n    [\n        \"select abc.x, bcd.y from abc join bcd on \",\n        \"select abc.x, bcd.y from abc join bcd on abc.id = bcd.id and \",\n    ],\n)\ndef test_on_suggests_tables_right_side(sql):\n    suggestions = suggest_type(sql, sql)\n    assert suggestions == [{\"type\": \"alias\", \"aliases\": [\"abc\", \"bcd\"]}]\n\n\n@pytest.mark.parametrize(\"col_list\", [\"\", \"col1, \"])\ndef test_join_using_suggests_common_columns(col_list):\n    text = \"select * from abc inner join def using (\" + col_list\n    assert suggest_type(text, text) == [{\"type\": \"column\", \"tables\": [(None, \"abc\", None), (None, \"def\", None)], \"drop_unique\": True}]\n\n\n@pytest.mark.parametrize(\n    \"sql\",\n    [\n        \"SELECT * FROM abc a JOIN def d ON a.id = d.id JOIN ghi g ON g.\",\n        \"SELECT * FROM abc a JOIN def d ON a.id = d.id AND a.id2 = d.id2 JOIN ghi g ON d.id = g.id AND g.\",\n    ],\n)\ndef test_two_join_alias_dot_suggests_cols1(sql):\n    suggestions = suggest_type(sql, sql)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"column\", \"tables\": [(None, \"ghi\", \"g\")]},\n        {\"type\": \"table\", \"schema\": \"g\"},\n        {\"type\": \"view\", \"schema\": \"g\"},\n        {\"type\": \"function\", \"schema\": \"g\"},\n    ])\n\n\ndef test_2_statements_2nd_current():\n    suggestions = suggest_type(\"select * from a; select * from \", \"select * from a; select * from \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n        {\"type\": \"database\"},\n    ])\n\n    suggestions = suggest_type(\"select * from a; select  from b\", \"select * from a; select \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"b\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"b\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n    # Should work even if first statement is invalid\n    suggestions = suggest_type(\"select * from; select * from \", \"select * from; select * from \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n        {\"type\": \"database\"},\n    ])\n\n\ndef test_2_statements_1st_current():\n    suggestions = suggest_type(\"select * from ; select * from b\", \"select * from \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"database\"},\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n    ])\n\n    suggestions = suggest_type(\"select  from a; select * from b\", \"select \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"a\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"a\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_3_statements_2nd_current():\n    suggestions = suggest_type(\"select * from a; select * from ; select * from c\", \"select * from a; select * from \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"database\"},\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n    ])\n\n    suggestions = suggest_type(\"select * from a; select  from b; select * from c\", \"select * from a; select \")\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"alias\", \"aliases\": [\"b\"]},\n        {\"type\": \"column\", \"tables\": [(None, \"b\", None)]},\n        {\"type\": \"function\", \"schema\": []},\n        {\"type\": \"introducer\"},\n    ])\n\n\ndef test_create_db_with_template():\n    suggestions = suggest_type(\"create database foo with template \", \"create database foo with template \")\n\n    assert sorted_dicts(suggestions) == sorted_dicts([{\"type\": \"database\"}])\n\n\n@pytest.mark.parametrize(\"initial_text\", [\"\", \"    \", \"\\t \\t\"])\ndef test_specials_included_for_initial_completion(initial_text):\n    suggestions = suggest_type(initial_text, initial_text)\n\n    assert sorted_dicts(suggestions) == sorted_dicts([{\"type\": \"keyword\"}, {\"type\": \"special\"}])\n\n\n@pytest.mark.parametrize('initial_text', ['REDIRECT'])\ndef test_specials_included_with_caps(initial_text):\n    suggestions = suggest_type(initial_text, initial_text)\n\n    assert sorted_dicts(suggestions) == sorted_dicts([{'type': 'keyword'}, {'type': 'special'}])\n\n\ndef test_specials_not_included_after_initial_token():\n    suggestions = suggest_type(\"create table foo (dt d\", \"create table foo (dt d\")\n\n    assert sorted_dicts(suggestions) == sorted_dicts([{\"type\": \"keyword\"}])\n\n\ndef test_drop_schema_qualified_table_suggests_only_tables():\n    text = \"DROP TABLE schema_name.table_name\"\n    suggestions = suggest_type(text, text)\n    assert suggestions == [{\"type\": \"table\", \"schema\": \"schema_name\"}]\n\n\n@pytest.mark.parametrize(\"text\", [\",\", \"  ,\", \"sel ,\"])\ndef test_handle_pre_completion_comma_gracefully(text):\n    suggestions = suggest_type(text, text)\n\n    assert iter(suggestions)\n\n\ndef test_cross_join():\n    text = \"select * from v1 cross join v2 JOIN v1.id, \"\n    suggestions = suggest_type(text, text)\n    assert sorted_dicts(suggestions) == sorted_dicts([\n        {\"type\": \"database\"},\n        {\"type\": \"table\", \"schema\": []},\n        {\"type\": \"view\", \"schema\": []},\n    ])\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"SELECT 1 AS \",\n        \"SELECT 1 FROM tabl AS \",\n    ],\n)\ndef test_after_as(expression):\n    suggestions = suggest_type(expression, expression)\n    assert set(suggestions) == set()\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"\\\\. \",\n        \"select 1; \\\\. \",\n        \"select 1;\\\\. \",\n        \"select 1 ; \\\\. \",\n        \"source \",\n        \"truncate table test; source \",\n        \"truncate table test ; source \",\n        \"truncate table test;source \",\n    ],\n)\ndef test_source_is_file(expression):\n    # \"source\" has to be registered by hand because that usually happens inside MyCLI in mycli/main.py\n    special.register_special_command(..., 'source', '\\\\. <filename>', 'Execute commands from file.', aliases=['\\\\.'])\n    suggestions = suggest_type(expression, expression)\n    assert suggestions == [{\"type\": \"file_name\"}]\n\n\n@pytest.mark.parametrize(\n    \"expression\",\n    [\n        \"\\\\f \",\n    ],\n)\ndef test_favorite_name_suggestion(expression):\n    suggestions = suggest_type(expression, expression)\n    assert suggestions == [{\"type\": \"favoritequery\"}]\n\n\ndef test_order_by():\n    text = \"select * from foo order by \"\n    suggestions = suggest_type(text, text)\n    assert suggestions == [{\"tables\": [(None, \"foo\", None)], \"type\": \"column\"}]\n\n\ndef test_quoted_where():\n    text = \"'where i=';\"\n    suggestions = suggest_type(text, text)\n    assert suggestions == [{\"type\": \"keyword\"}]\n\n\ndef test_find_doubled_backticks_none():\n    text = 'select `ab`'\n    assert _find_doubled_backticks(text) == []\n\n\ndef test_find_doubled_backticks_some():\n    text = 'select `a``b`'\n    assert _find_doubled_backticks(text) == [9, 10]\n\n\ndef test_inside_quotes_01():\n    text = \"select '\"\n    assert is_inside_quotes(text, len(text)) == 'single'\n\n\ndef test_inside_quotes_02():\n    text = \"select '\\\\'\"\n    assert is_inside_quotes(text, len(text)) == 'single'\n\n\ndef test_inside_quotes_03():\n    text = \"select '`\"\n    assert is_inside_quotes(text, len(text)) == 'single'\n\n\ndef test_inside_quotes_04():\n    text = 'select \"'\n    assert is_inside_quotes(text, len(text)) == 'double'\n\n\ndef test_inside_quotes_05():\n    text = 'select \"\\\\\"\\''\n    assert is_inside_quotes(text, len(text)) == 'double'\n\n\ndef test_inside_quotes_06():\n    text = 'select \"\"'\n    assert is_inside_quotes(text, len(text)) is False\n\n\n@pytest.mark.parametrize(\n    [\"text\", \"position\", \"expected\"],\n    [\n        (\"select `'\",      len(\"select `'\"),  'backtick'),\n        (\"select `' \",     len(\"select `' \"), 'backtick'),\n        (\"select `'\",      -1,  'backtick'),\n        (\"select `'\",      -2,  False),\n        ('select `ab` ',   -1,  False),\n        ('select `ab` ',   -2,  'backtick'),\n        ('select `a``b` ', -1,  False),\n        ('select `a``b` ', -2,  'backtick'),\n        ('select `a``b` ', -3,  'backtick'),\n        ('select `a``b` ', -4,  'backtick'),\n        ('select `a``b` ', -5,  'backtick'),\n        ('select `a``b` ', -6,  'backtick'),\n        ('select `a``b` ', -7,  False),\n    ]\n)  # fmt: skip\ndef test_inside_quotes_backtick_01(text, position, expected):\n    assert is_inside_quotes(text, position) == expected\n\n\ndef test_inside_quotes_backtick_02():\n    \"\"\"Empty backtick pairs are treated as a doubled (escaped) backtick.\n    This is okay because it is invalid SQL, and we don't have to complete on it.\n    \"\"\"\n    text = 'select ``'\n    assert is_inside_quotes(text, -1) is False\n\n\ndef test_inside_quotes_backtick_03():\n    \"\"\"Empty backtick pairs are treated as a doubled (escaped) backtick.\n    This is okay because it is invalid SQL, and we don't have to complete on it.\n    \"\"\"\n    text = 'select ``'\n    assert is_inside_quotes(text, -2) is False\n"
  },
  {
    "path": "test/test_completion_refresher.py",
    "content": "# type: ignore\n\nimport time\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\n\n@pytest.fixture\ndef refresher():\n    from mycli.completion_refresher import CompletionRefresher\n\n    return CompletionRefresher()\n\n\ndef test_ctor(refresher):\n    \"\"\"Refresher object should contain a few handlers.\n\n    :param refresher:\n    :return:\n\n    \"\"\"\n    assert len(refresher.refreshers) > 0\n    actual_handlers = list(refresher.refreshers.keys())\n    expected_handlers = [\n        \"databases\",\n        \"schemata\",\n        \"tables\",\n        \"enum_values\",\n        \"users\",\n        \"functions\",\n        \"procedures\",\n        'character_sets',\n        'collations',\n        \"special_commands\",\n        \"show_commands\",\n        \"keywords\",\n    ]\n    assert expected_handlers == actual_handlers\n\n\ndef test_refresh_called_once(refresher):\n    \"\"\"\n\n    :param refresher:\n    :return:\n    \"\"\"\n    callbacks = Mock()\n    sqlexecute = Mock()\n\n    with patch.object(refresher, \"_bg_refresh\") as bg_refresh:\n        actual = refresher.refresh(sqlexecute, callbacks)\n        time.sleep(1)  # Wait for the thread to work.\n        assert actual[0].preamble is None\n        assert actual[0].header is None\n        assert actual[0].rows is None\n        assert actual[0].status == \"Auto-completion refresh started in the background.\"\n        bg_refresh.assert_called_with(sqlexecute, callbacks, {})\n\n\ndef test_refresh_called_twice(refresher):\n    \"\"\"If refresh is called a second time, it should be restarted.\n\n    :param refresher:\n    :return:\n\n    \"\"\"\n    callbacks = Mock()\n\n    sqlexecute = Mock()\n\n    def dummy_bg_refresh(*args):\n        time.sleep(3)  # seconds\n\n    refresher._bg_refresh = dummy_bg_refresh\n\n    actual1 = refresher.refresh(sqlexecute, callbacks)\n    time.sleep(1)  # Wait for the thread to work.\n    assert actual1[0].preamble is None\n    assert actual1[0].header is None\n    assert actual1[0].rows is None\n    assert actual1[0].status == \"Auto-completion refresh started in the background.\"\n\n    actual2 = refresher.refresh(sqlexecute, callbacks)\n    time.sleep(1)  # Wait for the thread to work.\n    assert actual2[0].preamble is None\n    assert actual2[0].header is None\n    assert actual2[0].rows is None\n    assert actual2[0].status == \"Auto-completion refresh restarted.\"\n\n\ndef test_refresh_with_callbacks(refresher):\n    \"\"\"Callbacks must be called.\n\n    :param refresher:\n\n    \"\"\"\n    callbacks = [Mock()]\n    sqlexecute_class = Mock()\n    sqlexecute = Mock()\n\n    with patch(\"mycli.completion_refresher.SQLExecute\", sqlexecute_class):\n        # Set refreshers to 0: we're not testing refresh logic here\n        refresher.refreshers = {}\n        refresher.refresh(sqlexecute, callbacks)\n        time.sleep(1)  # Wait for the thread to work.\n        assert callbacks[0].call_count == 1\n"
  },
  {
    "path": "test/test_config.py",
    "content": "# type: ignore\n\n\"\"\"Unit tests for the mycli.config module.\"\"\"\n\nfrom io import BytesIO, StringIO, TextIOWrapper\nimport os\nimport struct\nimport sys\nfrom tempfile import NamedTemporaryFile\n\nimport pytest\n\nfrom mycli.config import (\n    get_mylogin_cnf_path,\n    open_mylogin_cnf,\n    read_and_decrypt_mylogin_cnf,\n    read_config_file,\n    str_to_bool,\n    strip_matching_quotes,\n)\nfrom test.utils import TEMPFILE_PREFIX\n\nLOGIN_PATH_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), \"mylogin.cnf\"))\n\n\ndef open_bmylogin_cnf(name):\n    \"\"\"Open contents of *name* in a BytesIO buffer.\"\"\"\n    with open(name, \"rb\") as f:\n        buf = BytesIO()\n        buf.write(f.read())\n    return buf\n\n\ndef test_read_mylogin_cnf():\n    \"\"\"Tests that a login path file can be read and decrypted.\"\"\"\n    mylogin_cnf = open_mylogin_cnf(LOGIN_PATH_FILE)\n\n    assert isinstance(mylogin_cnf, TextIOWrapper)\n\n    contents = mylogin_cnf.read()\n    for word in (\"[test]\", \"user\", \"password\", \"host\", \"port\"):\n        assert word in contents\n\n\ndef test_decrypt_blank_mylogin_cnf():\n    \"\"\"Test that a blank login path file is handled correctly.\"\"\"\n    mylogin_cnf = read_and_decrypt_mylogin_cnf(BytesIO())\n    assert mylogin_cnf is None\n\n\ndef test_corrupted_login_key():\n    \"\"\"Test that a corrupted login path key is handled correctly.\"\"\"\n    buf = open_bmylogin_cnf(LOGIN_PATH_FILE)\n\n    # Skip past the unused bytes\n    buf.seek(4)\n\n    # Write null bytes over half the login key\n    buf.write(b\"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\")\n\n    buf.seek(0)\n    mylogin_cnf = read_and_decrypt_mylogin_cnf(buf)\n\n    assert mylogin_cnf is None\n\n\ndef test_corrupted_pad():\n    \"\"\"Tests that a login path file with a corrupted pad is partially read.\"\"\"\n    buf = open_bmylogin_cnf(LOGIN_PATH_FILE)\n\n    # Skip past the login key\n    buf.seek(24)\n\n    # Skip option group\n    len_buf = buf.read(4)\n    (cipher_len,) = struct.unpack(\"<i\", len_buf)\n    buf.read(cipher_len)\n\n    # Corrupt the pad for the user line\n    len_buf = buf.read(4)\n    (cipher_len,) = struct.unpack(\"<i\", len_buf)\n    buf.read(cipher_len - 1)\n    buf.write(b\"\\0\")\n\n    buf.seek(0)\n    mylogin_cnf = TextIOWrapper(read_and_decrypt_mylogin_cnf(buf))\n    contents = mylogin_cnf.read()\n    for word in (\"[test]\", \"password\", \"host\", \"port\"):\n        assert word in contents\n    assert \"user\" not in contents\n\n\ndef test_get_mylogin_cnf_path(monkeypatch):\n    \"\"\"Tests that the path for .mylogin.cnf is detected.\"\"\"\n    monkeypatch.delenv('MYSQL_TEST_LOGIN_FILE', raising=False)\n    is_windows = sys.platform == \"win32\"\n\n    login_cnf_path = get_mylogin_cnf_path()\n\n    if login_cnf_path is not None:\n        assert login_cnf_path.endswith(\".mylogin.cnf\")\n\n        if is_windows is True:\n            assert \"MySQL\" in login_cnf_path\n        else:\n            home_dir = os.path.expanduser(\"~\")\n            assert login_cnf_path.startswith(home_dir)\n\n\ndef test_alternate_get_mylogin_cnf_path(monkeypatch):\n    \"\"\"Tests that the alternate path for .mylogin.cnf is detected.\"\"\"\n\n    with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode='w', delete=False) as login_file:\n        monkeypatch.setenv('MYSQL_TEST_LOGIN_FILE', login_file.name)\n        login_cnf_path = get_mylogin_cnf_path()\n\n    try:\n        assert login_file.name == login_cnf_path\n    except AssertionError as e:\n        assert AssertionError(e)\n    finally:\n        if os.path.exists(login_file.name):\n            os.remove(login_file.name)\n\n\ndef test_str_to_bool():\n    \"\"\"Tests that str_to_bool function converts values correctly.\"\"\"\n\n    assert str_to_bool(False) is False\n    assert str_to_bool(True) is True\n    assert str_to_bool(\"False\") is False\n    assert str_to_bool(\"True\") is True\n    assert str_to_bool(\"TRUE\") is True\n    assert str_to_bool(\"1\") is True\n    assert str_to_bool(\"0\") is False\n    assert str_to_bool(\"on\") is True\n    assert str_to_bool(\"off\") is False\n    assert str_to_bool(\"off\") is False\n\n    with pytest.raises(ValueError):\n        str_to_bool(\"foo\")\n\n    with pytest.raises(TypeError):\n        str_to_bool(None)\n\n\ndef test_read_config_file_list_values_default():\n    \"\"\"Test that reading a config file uses list_values by default.\"\"\"\n\n    f = StringIO(\"[main]\\nweather='cloudy with a chance of meatballs'\\n\")\n    config = read_config_file(f)\n\n    assert config[\"main\"][\"weather\"] == \"cloudy with a chance of meatballs\"\n\n\ndef test_read_config_file_list_values_off():\n    \"\"\"Test that you can disable list_values when reading a config file.\"\"\"\n\n    f = StringIO(\"[main]\\nweather='cloudy with a chance of meatballs'\\n\")\n    config = read_config_file(f, list_values=False)\n\n    assert config[\"main\"][\"weather\"] == \"'cloudy with a chance of meatballs'\"\n\n\ndef test_strip_quotes_with_matching_quotes():\n    \"\"\"Test that a string with matching quotes is unquoted.\"\"\"\n\n    s = \"May the force be with you.\"\n    assert s == strip_matching_quotes(f'\"{s}\"')\n    assert s == strip_matching_quotes(f\"'{s}'\")\n\n\ndef test_strip_quotes_with_unmatching_quotes():\n    \"\"\"Test that a string with unmatching quotes is not unquoted.\"\"\"\n\n    s = \"May the force be with you.\"\n    assert '\"' + s == strip_matching_quotes(f'\"{s}')\n    assert s + \"'\" == strip_matching_quotes(f\"{s}'\")\n\n\ndef test_strip_quotes_with_empty_string():\n    \"\"\"Test that an empty string is handled during unquoting.\"\"\"\n\n    assert \"\" == strip_matching_quotes(\"\")\n\n\ndef test_strip_quotes_with_none():\n    \"\"\"Test that None is handled during unquoting.\"\"\"\n\n    assert None is strip_matching_quotes(None)\n\n\ndef test_strip_quotes_with_quotes():\n    \"\"\"Test that strings with quotes in them are handled during unquoting.\"\"\"\n\n    s1 = 'Darth Vader said, \"Luke, I am your father.\"'\n    assert s1 == strip_matching_quotes(s1)\n\n    s2 = '\"Darth Vader said, \"Luke, I am your father.\"\"'\n    assert s2[1:-1] == strip_matching_quotes(s2)\n"
  },
  {
    "path": "test/test_dbspecial.py",
    "content": "# type: ignore\n\nfrom unittest.mock import MagicMock\n\nfrom mycli.packages.completion_engine import suggest_type\nfrom mycli.packages.special.dbcommands import list_tables\nfrom mycli.packages.special.utils import format_uptime\nfrom test.test_completion_engine import sorted_dicts\n\n\ndef test_list_tables_verbose_preserves_field_results():\n    \"\"\"Test that \\\\dt+ table_name returns SHOW FIELDS results, not SHOW CREATE TABLE results.\n\n    This is a regression test for a bug where the cursor was reused for SHOW CREATE TABLE,\n    which overwrote the SHOW FIELDS results.\n    \"\"\"\n    # Mock cursor that simulates MySQL behavior\n    cur = MagicMock()\n\n    # Track which query is being executed\n    query_results = {\n        'SHOW FIELDS FROM test_table': {\n            'description': [('Field',), ('Type',), ('Null',), ('Key',), ('Default',), ('Extra',)],\n            'rows': [\n                ('id', 'int', 'NO', 'PRI', None, 'auto_increment'),\n                ('name', 'varchar(255)', 'YES', '', None, ''),\n            ],\n        },\n        'SHOW CREATE TABLE test_table': {\n            'description': [('Table',), ('Create Table',)],\n            'rows': [('test_table', 'CREATE TABLE `test_table` ...')],\n        },\n    }\n\n    current_query = [None]  # Use list to allow mutation in nested function\n\n    def execute_side_effect(query):\n        current_query[0] = query\n        cur.description = query_results[query]['description']\n        cur.rowcount = len(query_results[query]['rows'])\n\n    def fetchall_side_effect():\n        return query_results[current_query[0]]['rows']\n\n    def fetchone_side_effect():\n        rows = query_results[current_query[0]]['rows']\n        return rows[0] if rows else None\n\n    cur.execute.side_effect = execute_side_effect\n    cur.fetchall.side_effect = fetchall_side_effect\n    cur.fetchone.side_effect = fetchone_side_effect\n\n    # Call list_tables with verbose=True (simulating \\dt+ table_name)\n    results = list_tables(cur, arg='test_table', verbose=True)\n\n    assert len(results) == 1\n    result = results[0]\n\n    # The header should be from SHOW FIELDS\n    assert result.header == ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra']\n\n    # The results should contain the field data, not be empty\n    # Convert to list if it's a cursor or iterable\n    result_data = list(result.rows) if hasattr(result.rows, '__iter__') else result.rows\n    assert len(result_data) == 2\n    assert result_data[0][0] == 'id'\n    assert result_data[1][0] == 'name'\n\n    # The postamble should contain the CREATE TABLE statement\n    assert 'CREATE TABLE' in result.postamble\n\n\ndef test_u_suggests_databases():\n    suggestions = suggest_type(\"\\\\u \", \"\\\\u \")\n    assert sorted_dicts(suggestions) == sorted_dicts([{\"type\": \"database\"}])\n\n\ndef test_describe_table():\n    suggestions = suggest_type(\"\\\\dt\", \"\\\\dt \")\n    assert sorted_dicts(suggestions) == sorted_dicts([{\"type\": \"table\", \"schema\": []}, {\"type\": \"view\", \"schema\": []}, {\"type\": \"schema\"}])\n\n\ndef test_list_or_show_create_tables():\n    suggestions = suggest_type(\"\\\\dt+\", \"\\\\dt+ \")\n    assert sorted_dicts(suggestions) == sorted_dicts([{\"type\": \"table\", \"schema\": []}, {\"type\": \"view\", \"schema\": []}, {\"type\": \"schema\"}])\n\n\ndef test_format_uptime():\n    seconds = 59\n    assert \"59 sec\" == format_uptime(seconds)\n\n    seconds = 120\n    assert \"2 min 0 sec\" == format_uptime(seconds)\n\n    seconds = 54890\n    assert \"15 hours 14 min 50 sec\" == format_uptime(seconds)\n\n    seconds = 598244\n    assert \"6 days 22 hours 10 min 44 sec\" == format_uptime(seconds)\n\n    seconds = 522600\n    assert \"6 days 1 hour 10 min 0 sec\" == format_uptime(seconds)\n"
  },
  {
    "path": "test/test_llm_special.py",
    "content": "from unittest.mock import patch\n\nimport pytest\n\nfrom mycli.packages.special.llm import (\n    USAGE,\n    FinishIteration,\n    handle_llm,\n    is_llm_command,\n    sql_using_llm,\n)\nfrom mycli.packages.sqlresult import SQLResult\n\n\n# Override executor fixture to avoid real DB connections during llm tests\n@pytest.fixture\ndef executor():\n    \"\"\"Dummy executor fixture\"\"\"\n    return None\n\n\n@patch(\"mycli.packages.special.llm.llm\")\ndef test_llm_command_without_args(mock_llm, executor):\n    r\"\"\"\n    Invoking \\llm without any arguments should print the usage and raise FinishIteration.\n    \"\"\"\n    assert mock_llm is not None\n    test_text = r\"\\llm\"\n    with pytest.raises(FinishIteration) as exc_info:\n        handle_llm(test_text, executor, 'mysql', 0, 0)\n    # Should return usage message when no args provided\n    assert exc_info.value.results == [SQLResult(preamble=USAGE)]\n\n\n@patch(\"mycli.packages.special.llm.llm\")\ndef test_llm_command_with_help_subcommand(mock_llm, executor):\n    r\"\"\"\n    Invoking \\llm with \"help\" should print the usage and raise FinishIteration.\n    \"\"\"\n    assert mock_llm is not None\n    test_text = r\"\\llm help\"\n    with pytest.raises(FinishIteration) as exc_info:\n        handle_llm(test_text, executor, 'mysql', 0, 0)\n    # Should return usage message when \"help\" subcommand or variant is provided\n    assert exc_info.value.results == [SQLResult(preamble=USAGE)]\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.run_external_cmd\")\ndef test_llm_command_with_c_flag(mock_run_cmd, mock_llm, executor):\n    string = \"Hello, no SQL today.\"\n    # Suppose the LLM returns some text without fenced SQL\n    mock_run_cmd.return_value = (0, string)\n    test_text = r\"\\llm -c 'Something?'\"\n    with pytest.raises(FinishIteration) as exc_info:\n        handle_llm(test_text, executor, 'mysql', 0, 0)\n    # Expect raw output when no SQL fence found\n    assert exc_info.value.results == [SQLResult(preamble=string)]\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.run_external_cmd\")\ndef test_llm_command_with_c_flag_and_fenced_sql(mock_run_cmd, mock_llm, executor):\n    # Return text containing a fenced SQL block\n    sql_text = \"SELECT * FROM users;\"\n    fenced = f\"Here you go:\\n```sql\\n{sql_text}\\n```\"\n    mock_run_cmd.return_value = (0, fenced)\n    test_text = r\"\\llm -c 'Rewrite SQL'\"\n    result, sql, duration = handle_llm(test_text, executor, 'mysql', 0, 0)\n    # Without verbose, result is empty, sql extracted\n    assert sql == sql_text\n    assert result == \"\"\n    assert isinstance(duration, float)\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.run_external_cmd\")\ndef test_llm_command_known_subcommand(mock_run_cmd, mock_llm, executor):\n    # 'models' is a known subcommand\n    test_text = r\"\\llm models\"\n    with pytest.raises(FinishIteration) as exc_info:\n        handle_llm(test_text, executor, 'mysql', 0, 0)\n    mock_run_cmd.assert_called_once_with(\"llm\", \"models\", restart_cli=False)\n    assert exc_info.value.results is None\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.run_external_cmd\")\ndef test_llm_command_with_help_flag(mock_run_cmd, mock_llm, executor):\n    test_text = r\"\\llm --help\"\n    with pytest.raises(FinishIteration) as exc_info:\n        handle_llm(test_text, executor, 'mysql', 0, 0)\n    mock_run_cmd.assert_called_once_with(\"llm\", \"--help\", restart_cli=False)\n    assert exc_info.value.results is None\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.run_external_cmd\")\ndef test_llm_command_with_install_flag(mock_run_cmd, mock_llm, executor):\n    test_text = r\"\\llm install openai\"\n    with pytest.raises(FinishIteration) as exc_info:\n        handle_llm(test_text, executor, 'mysql', 0, 0)\n    mock_run_cmd.assert_called_once_with(\"llm\", \"install\", \"openai\", restart_cli=True)\n    assert exc_info.value.results is None\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.ensure_mycli_template\")\n@patch(\"mycli.packages.special.llm.sql_using_llm\")\ndef test_llm_command_with_prompt(mock_sql_using_llm, mock_ensure_template, mock_llm, executor):\n    r\"\"\"\n    \\llm prompt 'question' should use template and call sql_using_llm\n    \"\"\"\n    mock_sql_using_llm.return_value = (\"CTX\", \"SELECT 1;\")\n    test_text = r\"\\llm prompt 'Test?'\"\n    context, sql, duration = handle_llm(test_text, executor, 'mysql', 0, 0)\n    mock_ensure_template.assert_called_once()\n    mock_sql_using_llm.assert_called()\n    assert context == \"CTX\"\n    assert sql == \"SELECT 1;\"\n    assert isinstance(duration, float)\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.ensure_mycli_template\")\n@patch(\"mycli.packages.special.llm.sql_using_llm\")\ndef test_llm_command_question_with_context(mock_sql_using_llm, mock_ensure_template, mock_llm, executor):\n    r\"\"\"\n    \\llm 'question' treats as prompt and returns SQL\n    \"\"\"\n    mock_sql_using_llm.return_value = (\"CTX2\", \"SELECT 2;\")\n    test_text = r\"\\llm 'Top 10?'\"\n    context, sql, duration = handle_llm(test_text, executor, 'mysql', 0, 0)\n    mock_ensure_template.assert_called_once()\n    mock_sql_using_llm.assert_called()\n    assert context == \"CTX2\"\n    assert sql == \"SELECT 2;\"\n    assert isinstance(duration, float)\n\n\n@patch(\"mycli.packages.special.llm.llm\")\n@patch(\"mycli.packages.special.llm.ensure_mycli_template\")\n@patch(\"mycli.packages.special.llm.sql_using_llm\")\ndef test_llm_command_question_verbose(mock_sql_using_llm, mock_ensure_template, mock_llm, executor):\n    r\"\"\"\n    \\llm+ returns verbose context and SQL\n    \"\"\"\n    mock_sql_using_llm.return_value = (\"NO_CTX\", \"SELECT 42;\")\n    test_text = r\"\\llm- 'Succinct?'\"\n    context, sql, duration = handle_llm(test_text, executor, 'mysql', 0, 0)\n    assert context == \"\"\n    assert sql == \"SELECT 42;\"\n    assert isinstance(duration, float)\n\n\ndef test_is_llm_command():\n    # Valid llm command variants\n    for cmd in [\"\\\\llm\", \"\\\\ai\"]:\n        assert is_llm_command(cmd + \" 'x'\")\n    # Invalid commands\n    assert not is_llm_command(\"select * from table;\")\n\n\ndef test_sql_using_llm_no_connection():\n    # Should error if no database cursor provided\n    with pytest.raises(RuntimeError) as exc_info:\n        sql_using_llm(None, question=\"test\")\n    assert \"Connect to a database\" in str(exc_info.value)\n\n\n# Test sql_using_llm with dummy cursor and fenced SQL output\n@patch(\"mycli.packages.special.llm.run_external_cmd\")\ndef test_sql_using_llm_success(mock_run_cmd):\n    # Dummy cursor simulating database schema and sample data\n    class DummyCursor:\n        def __init__(self):\n            self._last = []\n\n        def execute(self, query):\n            if \"information_schema.columns\" in query:\n                self._last = [(\"table1(col1 int,col2 text)\",), (\"table2(colA varchar(20))\",)]\n            elif query.strip().upper().startswith(\"SHOW TABLES\"):\n                self._last = [(\"table1\",), (\"table2\",)]\n            elif query.strip().upper().startswith(\"SELECT * FROM\"):\n                self.description = [(\"col1\", None), (\"col2\", None)]\n                self._row = (1, \"abc\")\n\n        def fetchall(self):\n            return getattr(self, \"_last\", [])\n\n        def fetchone(self):\n            return getattr(self, \"_row\", None)\n\n    dummy_cur = DummyCursor()\n    # Simulate llm CLI returning a fenced SQL result\n    sql_text = \"SELECT 1, 'abc';\"\n    fenced = f\"Note\\n```sql\\n{sql_text}\\n```\"\n    mock_run_cmd.return_value = (0, fenced)\n    result, sql = sql_using_llm(dummy_cur, question=\"dummy\", dbname='mysql')\n    assert result == fenced\n    assert sql == sql_text\n\n\n# Test handle_llm supports alias prefixes without args\n@pytest.mark.parametrize(\"prefix\", [r\"\\\\llm\", r\".llm\", r\"\\\\ai\", r\".ai\"])\ndef test_handle_llm_aliases_without_args(prefix, executor, monkeypatch):\n    # Ensure llm is available\n    from mycli.packages.special import llm as llm_module\n\n    monkeypatch.setattr(llm_module, \"llm\", object())\n    with pytest.raises(FinishIteration) as exc_info:\n        handle_llm(prefix, executor, 'mysql', 0, 0)\n    assert exc_info.value.results == [SQLResult(preamble=USAGE)]\n"
  },
  {
    "path": "test/test_main.py",
    "content": "# type: ignore\n\nfrom collections import namedtuple\nfrom contextlib import redirect_stdout\nimport csv\nimport io\nimport os\nimport shutil\nfrom tempfile import NamedTemporaryFile\nfrom textwrap import dedent\n\nimport click\nfrom click.testing import CliRunner\nfrom pymysql.err import OperationalError\n\nfrom mycli.constants import (\n    DEFAULT_DATABASE,\n    DEFAULT_HOST,\n    DEFAULT_PORT,\n    DEFAULT_USER,\n    TEST_DATABASE,\n)\nfrom mycli.main import EMPTY_PASSWORD_FLAG_SENTINEL, MyCli, cli, thanks_picker\nfrom mycli.packages.parseutils import is_valid_connection_scheme\nimport mycli.packages.special\nfrom mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS\nfrom mycli.packages.sqlresult import SQLResult\nfrom mycli.sqlexecute import ServerInfo, SQLExecute\nfrom test.utils import DATABASE, HOST, PASSWORD, PORT, TEMPFILE_PREFIX, USER, dbtest, run\n\ntest_dir = os.path.abspath(os.path.dirname(__file__))\nproject_dir = os.path.dirname(test_dir)\ndefault_config_file = os.path.join(project_dir, \"test\", \"myclirc\")\nlogin_path_file = os.path.join(test_dir, \"mylogin.cnf\")\n\nos.environ[\"MYSQL_TEST_LOGIN_FILE\"] = login_path_file\nCLI_ARGS = [\n    \"--user\",\n    USER,\n    \"--host\",\n    HOST,\n    \"--port\",\n    PORT,\n    \"--password\",\n    PASSWORD,\n    \"--myclirc\",\n    default_config_file,\n    \"--defaults-file\",\n    default_config_file,\n    TEST_DATABASE,\n]\n\n\n@dbtest\ndef test_binary_display_hex(executor):\n    m = MyCli()\n    m.sqlexecute = SQLExecute(\n        None,\n        USER,\n        PASSWORD,\n        HOST,\n        PORT,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    )\n    m.explicit_pager = False\n    sqlresult = next(m.sqlexecute.run(\"select b'01101010' AS binary_test\"))\n    formatted = m.format_sqlresult(\n        sqlresult,\n        is_expanded=False,\n        is_redirected=False,\n        null_string=\"<null>\",\n        numeric_alignment=\"right\",\n        binary_display=\"hex\",\n        max_width=None,\n    )\n    f = io.StringIO()\n    with redirect_stdout(f):\n        m.output(formatted, sqlresult)\n    expected = \" 0x6a \"\n    output = f.getvalue()\n    assert expected in output\n\n\n@dbtest\ndef test_binary_display_utf8(executor):\n    m = MyCli()\n    m.sqlexecute = SQLExecute(\n        None,\n        USER,\n        PASSWORD,\n        HOST,\n        PORT,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    )\n    m.explicit_pager = False\n    sqlresult = next(m.sqlexecute.run(\"select b'01101010' AS binary_test\"))\n    formatted = m.format_sqlresult(\n        sqlresult,\n        is_expanded=False,\n        is_redirected=False,\n        null_string=\"<null>\",\n        numeric_alignment=\"right\",\n        binary_display=\"utf8\",\n        max_width=None,\n    )\n    f = io.StringIO()\n    with redirect_stdout(f):\n        m.output(formatted, sqlresult)\n    expected = \" j \"\n    output = f.getvalue()\n    assert expected in output\n\n\n@dbtest\ndef test_select_from_empty_table(executor):\n    run(executor, \"\"\"create table t1(id int)\"\"\")\n    sql = \"select * from t1\"\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"-t\"], input=sql)\n    expected = dedent(\"\"\"\\\n        +----+\n        | id |\n        +----+\n        +----+\"\"\")\n    assert expected in result.output\n\n\ndef test_is_valid_connection_scheme_valid(executor, capsys):\n    is_valid, scheme = is_valid_connection_scheme(f\"mysql://test@{DEFAULT_HOST}:{DEFAULT_PORT}/dev\")\n    assert is_valid\n\n\ndef test_is_valid_connection_scheme_invalid(executor, capsys):\n    is_valid, scheme = is_valid_connection_scheme(f\"nope://test@{DEFAULT_HOST}:{DEFAULT_PORT}/dev\")\n    assert not is_valid\n\n\n@dbtest\ndef test_ssl_mode_on(executor, capsys):\n    runner = CliRunner()\n    ssl_mode = \"on\"\n    sql = \"select * from performance_schema.session_status where variable_name = 'Ssl_cipher'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--csv\", \"--ssl-mode\", ssl_mode], input=sql)\n    result_dict = next(csv.DictReader(result.stdout.split(\"\\n\")))\n    ssl_cipher = result_dict.get(\"VARIABLE_VALUE\", None)\n    assert ssl_cipher\n\n\n@dbtest\ndef test_ssl_mode_auto(executor, capsys):\n    runner = CliRunner()\n    ssl_mode = \"auto\"\n    sql = \"select * from performance_schema.session_status where variable_name = 'Ssl_cipher'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--csv\", \"--ssl-mode\", ssl_mode], input=sql)\n    result_dict = next(csv.DictReader(result.stdout.split(\"\\n\")))\n    ssl_cipher = result_dict.get(\"VARIABLE_VALUE\", None)\n    assert ssl_cipher\n\n\n@dbtest\ndef test_ssl_mode_off(executor, capsys):\n    runner = CliRunner()\n    ssl_mode = \"off\"\n    sql = \"select * from performance_schema.session_status where variable_name = 'Ssl_cipher'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--csv\", \"--ssl-mode\", ssl_mode], input=sql)\n    result_dict = next(csv.DictReader(result.stdout.split(\"\\n\")))\n    ssl_cipher = result_dict.get(\"VARIABLE_VALUE\", None)\n    assert not ssl_cipher\n\n\n@dbtest\ndef test_ssl_mode_overrides_ssl(executor, capsys):\n    runner = CliRunner()\n    ssl_mode = \"off\"\n    sql = \"select * from performance_schema.session_status where variable_name = 'Ssl_cipher'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--csv\", \"--ssl-mode\", ssl_mode, \"--ssl\"], input=sql)\n    result_dict = next(csv.DictReader(result.stdout.split(\"\\n\")))\n    ssl_cipher = result_dict.get(\"VARIABLE_VALUE\", None)\n    assert not ssl_cipher\n\n\n@dbtest\ndef test_ssl_mode_overrides_no_ssl(executor, capsys):\n    runner = CliRunner()\n    ssl_mode = \"on\"\n    sql = \"select * from performance_schema.session_status where variable_name = 'Ssl_cipher'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--csv\", \"--ssl-mode\", ssl_mode, \"--no-ssl\"], input=sql)\n    result_dict = next(csv.DictReader(result.stdout.split(\"\\n\")))\n    ssl_cipher = result_dict.get(\"VARIABLE_VALUE\", None)\n    assert ssl_cipher\n\n\n@dbtest\ndef test_reconnect_database_is_selected(executor, capsys):\n    m = MyCli()\n    m.register_special_commands()\n    m.sqlexecute = SQLExecute(\n        None,\n        USER,\n        PASSWORD,\n        HOST,\n        PORT,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    )\n    try:\n        next(m.sqlexecute.run(f\"use {DATABASE}\"))\n        next(m.sqlexecute.run(f\"kill {m.sqlexecute.connection_id}\"))\n    except OperationalError:\n        pass  # expected as the connection was killed\n    except Exception as e:\n        raise e\n    m.reconnect()\n    try:\n        next(m.sqlexecute.run(\"show tables\")).rows.fetchall()\n    except Exception as e:\n        raise e\n\n\n@dbtest\ndef test_reconnect_no_database(executor, capsys):\n    m = MyCli()\n    m.register_special_commands()\n    m.sqlexecute = SQLExecute(\n        None,\n        USER,\n        PASSWORD,\n        HOST,\n        PORT,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    )\n    sql = \"\\\\r\"\n    result = next(mycli.packages.special.execute(executor, sql))\n    stdout, _stderr = capsys.readouterr()\n    assert result.status is None\n    assert \"Already connected\" in stdout\n\n\n@dbtest\ndef test_reconnect_with_different_database(executor):\n    m = MyCli()\n    m.register_special_commands()\n    m.sqlexecute = SQLExecute(\n        None,\n        USER,\n        PASSWORD,\n        HOST,\n        PORT,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    )\n    database_1 = TEST_DATABASE\n    database_2 = DEFAULT_DATABASE\n    sql_1 = f\"use {database_1}\"\n    sql_2 = f\"\\\\r {database_2}\"\n    _result_1 = next(mycli.packages.special.execute(executor, sql_1))\n    result_2 = next(mycli.packages.special.execute(executor, sql_2))\n    expected = f'You are now connected to database \"{database_2}\" as user \"{USER}\"'\n    assert expected in result_2.status\n\n\n@dbtest\ndef test_reconnect_with_same_database(executor):\n    m = MyCli()\n    m.register_special_commands()\n    m.sqlexecute = SQLExecute(\n        None,\n        USER,\n        PASSWORD,\n        HOST,\n        PORT,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    )\n    database = DEFAULT_DATABASE\n    sql = f\"\\\\u {database}\"\n    result = next(mycli.packages.special.execute(executor, sql))\n    sql = f\"\\\\r {database}\"\n    result = next(mycli.packages.special.execute(executor, sql))\n    expected = f'You are already connected to database \"{database}\" as user \"{USER}\"'\n    assert expected in result.status\n\n\n@dbtest\ndef test_prompt_no_host_only_socket(executor):\n    mycli = MyCli()\n    mycli.prompt_format = \"\\\\t \\\\u@\\\\h:\\\\d> \"\n    mycli.sqlexecute = SQLExecute\n    mycli.sqlexecute.server_info = ServerInfo.from_version_string(\"8.0.44-0ubuntu0.24.04.1\")\n    mycli.sqlexecute.host = None\n    mycli.sqlexecute.socket = \"/var/run/mysqld/mysqld.sock\"\n    mycli.sqlexecute.user = DEFAULT_USER\n    mycli.sqlexecute.dbname = DEFAULT_DATABASE\n    mycli.sqlexecute.port = DEFAULT_PORT\n    prompt = mycli.get_prompt(mycli.prompt_format, 0)\n    assert prompt == f\"MySQL {DEFAULT_USER}@{DEFAULT_HOST}:{DEFAULT_DATABASE}> \"\n\n\n@dbtest\ndef test_prompt_socket_overrides_port(executor):\n    mycli = MyCli()\n    mycli.prompt_format = \"\\\\t \\\\u@\\\\h:\\\\k \\\\d> \"\n    mycli.sqlexecute = SQLExecute\n    mycli.sqlexecute.server_info = ServerInfo.from_version_string(\"8.0.44-0ubuntu0.24.04.1\")\n    mycli.sqlexecute.host = None\n    mycli.sqlexecute.socket = \"/var/run/mysqld/mysqld.sock\"\n    mycli.sqlexecute.user = DEFAULT_USER\n    mycli.sqlexecute.dbname = DEFAULT_DATABASE\n    mycli.sqlexecute.port = DEFAULT_PORT\n    prompt = mycli.get_prompt(mycli.prompt_format, 0)\n    assert prompt == f\"MySQL {DEFAULT_USER}@{DEFAULT_HOST}:mysqld.sock {DEFAULT_DATABASE}> \"\n\n\n@dbtest\ndef test_prompt_socket_short_host(executor):\n    mycli = MyCli()\n    mycli.prompt_format = \"\\\\t \\\\u@\\\\H:\\\\k \\\\d> \"\n    mycli.sqlexecute = SQLExecute\n    mycli.sqlexecute.server_info = ServerInfo.from_version_string(\"8.0.44-0ubuntu0.24.04.1\")\n    mycli.sqlexecute.host = f'{DEFAULT_HOST}.localdomain'\n    mycli.sqlexecute.socket = None\n    mycli.sqlexecute.user = DEFAULT_USER\n    mycli.sqlexecute.dbname = DEFAULT_DATABASE\n    mycli.sqlexecute.port = DEFAULT_PORT\n    prompt = mycli.get_prompt(mycli.prompt_format, 0)\n    assert prompt == f\"MySQL {DEFAULT_USER}@{DEFAULT_HOST}:{DEFAULT_PORT} {DEFAULT_DATABASE}> \"\n\n\n@dbtest\ndef test_enable_show_warnings(executor):\n    mycli = MyCli()\n    mycli.register_special_commands()\n    sql = \"\\\\W\"\n    result = run(executor, sql)\n    assert result[0][\"status\"] == \"Show warnings enabled.\"\n\n\n@dbtest\ndef test_disable_show_warnings(executor):\n    mycli = MyCli()\n    mycli.register_special_commands()\n    sql = \"\\\\w\"\n    result = run(executor, sql)\n    assert result[0][\"status\"] == \"Show warnings disabled.\"\n\n\n@dbtest\ndef test_output_ddl_with_warning_and_show_warnings_enabled(executor):\n    runner = CliRunner()\n    db = TEST_DATABASE\n    table = \"table_that_definitely_does_not_exist_1234\"\n    sql = f\"DROP TABLE IF EXISTS {db}.{table}\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--show-warnings\", \"--no-warn\"], input=sql)\n    expected = f\"Level\\tCode\\tMessage\\nNote\\t1051\\tUnknown table '{db}.table_that_definitely_does_not_exist_1234'\\n\"\n    assert expected in result.output\n\n\n@dbtest\ndef test_output_with_warning_and_show_warnings_enabled(executor):\n    runner = CliRunner()\n    sql = \"SELECT 1 + '0 foo'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--show-warnings\"], input=sql)\n    expected = \"1 + '0 foo'\\n1.0\\nLevel\\tCode\\tMessage\\nWarning\\t1292\\tTruncated incorrect DOUBLE value: '0 foo'\\n\"\n    assert expected in result.output\n\n\n@dbtest\ndef test_output_with_warning_and_show_warnings_disabled(executor):\n    runner = CliRunner()\n    sql = \"SELECT 1 + '0 foo'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--no-show-warnings\"], input=sql)\n    expected = \"1 + '0 foo'\\n1.0\\nLevel\\tCode\\tMessage\\nWarning\\t1292\\tTruncated incorrect DOUBLE value: '0 foo'\\n\"\n    assert expected not in result.output\n\n\n@dbtest\ndef test_output_with_multiple_warnings_in_single_statement(executor):\n    runner = CliRunner()\n    sql = \"SELECT 1 + '0 foo', 2 + '0 foo'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--show-warnings\"], input=sql)\n    expected = (\n        \"1 + '0 foo'\\t2 + '0 foo'\\n\"\n        \"1.0\\t2.0\\n\"\n        \"Level\\tCode\\tMessage\\n\"\n        \"Warning\\t1292\\tTruncated incorrect DOUBLE value: '0 foo'\\n\"\n        \"Warning\\t1292\\tTruncated incorrect DOUBLE value: '0 foo'\\n\"\n    )\n    assert expected in result.output\n\n\n@dbtest\ndef test_output_with_multiple_warnings_in_multiple_statements(executor):\n    runner = CliRunner()\n    sql = \"SELECT 1 + '0 foo'; SELECT 2 + '0 foo'\"\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--show-warnings\"], input=sql)\n    expected = (\n        \"1 + '0 foo'\\n\"\n        \"1.0\\n\"\n        \"Level\\tCode\\tMessage\\n\"\n        \"Warning\\t1292\\tTruncated incorrect DOUBLE value: '0 foo'\\n\"\n        \"2 + '0 foo'\\n\"\n        \"2.0\\n\"\n        \"Level\\tCode\\tMessage\\n\"\n        \"Warning\\t1292\\tTruncated incorrect DOUBLE value: '0 foo'\\n\"\n    )\n    assert expected in result.output\n\n\n@dbtest\ndef test_execute_arg(executor):\n    run(executor, \"create table test (a text)\")\n    run(executor, 'insert into test values(\"abc\")')\n\n    sql = \"select * from test;\"\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"-e\", sql])\n\n    assert result.exit_code == 0\n    assert \"abc\" in result.output\n\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--execute\", sql])\n\n    assert result.exit_code == 0\n    assert \"abc\" in result.output\n\n    expected = \"a\\nabc\\n\"\n\n    assert expected in result.output\n\n\n@dbtest\ndef test_execute_arg_with_checkpoint(executor):\n    run(executor, \"create table test (a text)\")\n    run(executor, 'insert into test values(\"abc\")')\n\n    sql = \"select * from test;\"\n    runner = CliRunner()\n\n    with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode=\"w\", delete=False) as checkpoint:\n        checkpoint.close()\n\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--execute\", sql, f\"--checkpoint={checkpoint.name}\"])\n    assert result.exit_code == 0\n\n    with open(checkpoint.name, 'r') as f:\n        contents = f.read()\n    assert sql in contents\n    os.remove(checkpoint.name)\n\n    sql = 'select 10 from nonexistent_table;'\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--execute\", sql, f\"--checkpoint={checkpoint.name}\"])\n    assert result.exit_code != 0\n\n    with open(checkpoint.name, 'r') as f:\n        contents = f.read()\n    assert sql not in contents\n\n    # delete=False means we should try to clean up\n    # we don't really need \"try\" here as open() would have already failed\n    try:\n        if os.path.exists(checkpoint.name):\n            os.remove(checkpoint.name)\n    except Exception as e:\n        print(f\"An error occurred while attempting to delete the file: {e}\")\n\n\n@dbtest\ndef test_execute_arg_with_table(executor):\n    run(executor, \"create table test (a text)\")\n    run(executor, 'insert into test values(\"abc\")')\n\n    sql = \"select * from test;\"\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"-e\", sql] + [\"--table\"])\n    expected = \"+-----+\\n| a   |\\n+-----+\\n| abc |\\n+-----+\\n\"\n\n    assert result.exit_code == 0\n    assert expected in result.output\n\n\n@dbtest\ndef test_execute_arg_with_csv(executor):\n    run(executor, \"create table test (a text)\")\n    run(executor, 'insert into test values(\"abc\")')\n\n    sql = \"select * from test;\"\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"-e\", sql] + [\"--csv\"])\n    expected = '\"a\"\\n\"abc\"\\n'\n\n    assert result.exit_code == 0\n    assert expected in \"\".join(result.output)\n\n\n@dbtest\ndef test_batch_mode(executor):\n    run(executor, \"\"\"create table test(a text)\"\"\")\n    run(executor, \"\"\"insert into test values('abc'), ('def'), ('ghi')\"\"\")\n\n    sql = \"select count(*) from test;\\nselect * from test limit 1;\"\n\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS, input=sql)\n\n    assert result.exit_code == 0\n    assert \"count(*)\\n3\\na\\nabc\\n\" in \"\".join(result.output)\n\n\n@dbtest\ndef test_batch_mode_multiline_statement(executor):\n    run(executor, \"\"\"create table test(a text)\"\"\")\n    run(executor, \"\"\"insert into test values('abc'), ('def'), ('ghi')\"\"\")\n\n    sql = \"select count(*)\\nfrom test;\\nselect * from test limit 1;\"\n\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS, input=sql)\n\n    assert result.exit_code == 0\n    assert \"count(*)\\n3\\na\\nabc\\n\" in \"\".join(result.output)\n\n\n@dbtest\ndef test_batch_mode_table(executor):\n    run(executor, \"\"\"create table test(a text)\"\"\")\n    run(executor, \"\"\"insert into test values('abc'), ('def'), ('ghi')\"\"\")\n\n    sql = \"select count(*) from test;\\nselect * from test limit 1;\"\n\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"-t\"], input=sql)\n\n    expected = dedent(\"\"\"\\\n        +----------+\n        | count(*) |\n        +----------+\n        |        3 |\n        +----------+\n        +-----+\n        | a   |\n        +-----+\n        | abc |\n        +-----+\"\"\")\n\n    assert result.exit_code == 0\n    assert expected in result.output\n\n\n@dbtest\ndef test_batch_mode_csv(executor):\n    run(executor, \"\"\"create table test(a text, b text)\"\"\")\n    run(executor, \"\"\"insert into test (a, b) values('abc', 'de\\nf'), ('ghi', 'jkl')\"\"\")\n\n    sql = \"select * from test;\"\n\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--csv\"], input=sql)\n\n    expected = '\"a\",\"b\"\\n\"abc\",\"de\\nf\"\\n\"ghi\",\"jkl\"\\n'\n\n    assert result.exit_code == 0\n    assert expected in \"\".join(result.output)\n\n\ndef test_thanks_picker_utf8():\n    name = thanks_picker()\n    assert name and isinstance(name, str)\n\n\ndef test_help_strings_end_with_periods():\n    \"\"\"Make sure click options have help text that end with a period.\"\"\"\n    for param in cli.params:\n        if isinstance(param, click.core.Option):\n            assert hasattr(param, \"help\")\n            assert param.help.endswith(\".\")\n\n\ndef test_command_descriptions_end_with_periods():\n    \"\"\"Make sure that mycli commands' descriptions end with a period.\"\"\"\n    MyCli()\n    for _, command in SPECIAL_COMMANDS.items():\n        assert command[3].endswith(\".\")\n\n\ndef output(monkeypatch, terminal_size, testdata, explicit_pager, expect_pager):\n    global clickoutput\n    clickoutput = \"\"\n    m = MyCli(myclirc=default_config_file)\n\n    class TestOutput:\n        def get_size(self):\n            size = namedtuple(\"Size\", \"rows columns\")\n            size.columns, size.rows = terminal_size\n            return size\n\n    class TestExecute:\n        host = \"test\"\n        user = \"test\"\n        dbname = \"test\"\n        server_info = ServerInfo.from_version_string(\"unknown\")\n        port = 0\n        socket = ''\n\n        def server_type(self):\n            return [\"test\"]\n\n    class PromptBuffer:\n        output = TestOutput()\n        app = None\n\n    m.prompt_app = PromptBuffer()\n    m.sqlexecute = TestExecute()\n    m.explicit_pager = explicit_pager\n\n    def echo_via_pager(s):\n        assert expect_pager\n        global clickoutput\n        clickoutput += \"\".join(s)\n\n    def secho(s):\n        assert not expect_pager\n        global clickoutput\n        clickoutput += s + \"\\n\"\n\n    monkeypatch.setattr(click, \"echo_via_pager\", echo_via_pager)\n    monkeypatch.setattr(click, \"secho\", secho)\n    m.output(testdata, SQLResult())\n    if clickoutput.endswith(\"\\n\"):\n        clickoutput = clickoutput[:-1]\n    assert clickoutput == \"\\n\".join(testdata)\n\n\ndef test_conditional_pager(monkeypatch):\n    testdata = \"Lorem ipsum dolor sit amet consectetur adipiscing elit sed do\".split(\" \")\n    # User didn't set pager, output doesn't fit screen -> pager\n    output(monkeypatch, terminal_size=(5, 10), testdata=testdata, explicit_pager=False, expect_pager=True)\n    # User didn't set pager, output fits screen -> no pager\n    output(monkeypatch, terminal_size=(20, 20), testdata=testdata, explicit_pager=False, expect_pager=False)\n    # User manually configured pager, output doesn't fit screen -> pager\n    output(monkeypatch, terminal_size=(5, 10), testdata=testdata, explicit_pager=True, expect_pager=True)\n    # User manually configured pager, output fit screen -> pager\n    output(monkeypatch, terminal_size=(20, 20), testdata=testdata, explicit_pager=True, expect_pager=True)\n\n    SPECIAL_COMMANDS[\"nopager\"].handler()\n    output(monkeypatch, terminal_size=(5, 10), testdata=testdata, explicit_pager=False, expect_pager=False)\n    SPECIAL_COMMANDS[\"pager\"].handler(\"\")\n\n\ndef test_reserved_space_is_integer(monkeypatch):\n    \"\"\"Make sure that reserved space is returned as an integer.\"\"\"\n\n    def stub_terminal_size():\n        return (5, 5)\n\n    with monkeypatch.context() as m:\n        m.setattr(shutil, \"get_terminal_size\", stub_terminal_size)\n        mycli = MyCli()\n        assert isinstance(mycli.get_reserved_space(), int)\n\n\ndef test_list_dsn(monkeypatch):\n    monkeypatch.setattr(MyCli, \"system_config_files\", [])\n    monkeypatch.setattr(MyCli, \"pwd_config_file\", os.devnull)\n    runner = CliRunner()\n    # keep Windows from locking the file with delete=False\n    with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode=\"w\", delete=False) as myclirc:\n        myclirc.write(\n            dedent(\"\"\"\\\n            [alias_dsn]\n            test = mysql://test/test\n            \"\"\")\n        )\n        myclirc.flush()\n        args = [\"--list-dsn\", \"--myclirc\", myclirc.name]\n        result = runner.invoke(cli, args=args)\n        assert result.output == \"test\\n\"\n        result = runner.invoke(cli, args=args + [\"--verbose\"])\n        assert result.output == \"test : mysql://test/test\\n\"\n\n    # delete=False means we should try to clean up\n    try:\n        if os.path.exists(myclirc.name):\n            os.remove(myclirc.name)\n    except Exception as e:\n        print(f\"An error occurred while attempting to delete the file: {e}\")\n\n\ndef test_prettify_statement():\n    statement = \"SELECT 1\"\n    m = MyCli()\n    pretty_statement = m.handle_prettify_binding(statement)\n    assert pretty_statement == \"SELECT\\n    1;\"\n\n\ndef test_unprettify_statement():\n    statement = \"SELECT\\n    1\"\n    m = MyCli()\n    unpretty_statement = m.handle_unprettify_binding(statement)\n    assert unpretty_statement == \"SELECT 1;\"\n\n\ndef test_list_ssh_config():\n    runner = CliRunner()\n    # keep Windows from locking the file with delete=False\n    with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode=\"w\", delete=False) as ssh_config:\n        ssh_config.write(\n            dedent(\"\"\"\\\n            Host test\n                Hostname test.example.com\n                User joe\n                Port 22222\n                IdentityFile ~/.ssh/gateway\n        \"\"\")\n        )\n        ssh_config.flush()\n        args = [\"--list-ssh-config\", \"--ssh-config-path\", ssh_config.name]\n        result = runner.invoke(cli, args=args)\n        assert \"test\\n\" in result.output\n        result = runner.invoke(cli, args=args + [\"--verbose\"])\n        assert \"test : test.example.com\\n\" in result.output\n\n    # delete=False means we should try to clean up\n    try:\n        if os.path.exists(ssh_config.name):\n            os.remove(ssh_config.name)\n    except Exception as e:\n        print(f\"An error occurred while attempting to delete the file: {e}\")\n\n\ndef test_dsn(monkeypatch):\n    # Setup classes to mock mycli.main.MyCli\n    class Formatter:\n        format_name = None\n\n    class Logger:\n        def debug(self, *args, **args_dict):\n            pass\n\n        def warning(self, *args, **args_dict):\n            pass\n\n    class MockMyCli:\n        config = {\n            \"main\": {},\n            \"alias_dsn\": {},\n            \"connection\": {\n                \"default_keepalive_ticks\": 0,\n            },\n        }\n\n        def __init__(self, **args):\n            self.logger = Logger()\n            self.destructive_warning = False\n            self.main_formatter = Formatter()\n            self.redirect_formatter = Formatter()\n            self.ssl_mode = \"auto\"\n            self.my_cnf = {\"client\": {}, \"mysqld\": {}}\n            self.default_keepalive_ticks = 0\n\n        def connect(self, **args):\n            MockMyCli.connect_args = args\n\n        def run_query(self, query, new_line=True):\n            pass\n\n    import mycli.main\n\n    monkeypatch.setattr(mycli.main, \"MyCli\", MockMyCli)\n    runner = CliRunner()\n\n    # When a user supplies a DSN as database argument to mycli,\n    # use these values.\n    result = runner.invoke(mycli.main.cli, args=[\"mysql://dsn_user:dsn_passwd@dsn_host:1/dsn_database\"])\n    assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n    assert (\n        MockMyCli.connect_args[\"user\"] == \"dsn_user\"\n        and MockMyCli.connect_args[\"passwd\"] == \"dsn_passwd\"\n        and MockMyCli.connect_args[\"host\"] == \"dsn_host\"\n        and MockMyCli.connect_args[\"port\"] == 1\n        and MockMyCli.connect_args[\"database\"] == \"dsn_database\"\n    )\n\n    MockMyCli.connect_args = None\n\n    # When a use supplies a DSN as database argument to mycli,\n    # and used command line arguments, use the command line\n    # arguments.\n    result = runner.invoke(\n        mycli.main.cli,\n        args=[\n            \"mysql://dsn_user:dsn_passwd@dsn_host:2/dsn_database\",\n            \"--user\",\n            \"arg_user\",\n            \"--password\",\n            \"arg_password\",\n            \"--host\",\n            \"arg_host\",\n            \"--port\",\n            \"3\",\n            \"--database\",\n            \"arg_database\",\n        ],\n    )\n    assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n    assert (\n        MockMyCli.connect_args[\"user\"] == \"arg_user\"\n        and MockMyCli.connect_args[\"passwd\"] == \"arg_password\"\n        and MockMyCli.connect_args[\"host\"] == \"arg_host\"\n        and MockMyCli.connect_args[\"port\"] == 3\n        and MockMyCli.connect_args[\"database\"] == \"arg_database\"\n    )\n\n    MockMyCli.config = {\n        \"main\": {},\n        \"alias_dsn\": {\"test\": \"mysql://alias_dsn_user:alias_dsn_passwd@alias_dsn_host:4/alias_dsn_database\"},\n        \"connection\": {\n            \"default_keepalive_ticks\": 0,\n        },\n    }\n    MockMyCli.connect_args = None\n\n    # When a user uses a DSN from the configuration file (alias_dsn),\n    # use these values.\n    result = runner.invoke(cli, args=[\"--dsn\", \"test\"])\n    assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n    assert (\n        MockMyCli.connect_args[\"user\"] == \"alias_dsn_user\"\n        and MockMyCli.connect_args[\"passwd\"] == \"alias_dsn_passwd\"\n        and MockMyCli.connect_args[\"host\"] == \"alias_dsn_host\"\n        and MockMyCli.connect_args[\"port\"] == 4\n        and MockMyCli.connect_args[\"database\"] == \"alias_dsn_database\"\n    )\n\n    MockMyCli.config = {\n        \"main\": {},\n        \"alias_dsn\": {\"test\": \"mysql://alias_dsn_user:alias_dsn_passwd@alias_dsn_host:4/alias_dsn_database\"},\n        \"connection\": {\n            \"default_keepalive_ticks\": 0,\n        },\n    }\n    MockMyCli.connect_args = None\n\n    # When a user uses a DSN from the configuration file (alias_dsn)\n    # and used command line arguments, use the command line arguments.\n    result = runner.invoke(\n        cli,\n        args=[\n            \"--dsn\",\n            \"test\",\n            \"\",\n            \"--user\",\n            \"arg_user\",\n            \"--password\",\n            \"arg_password\",\n            \"--host\",\n            \"arg_host\",\n            \"--port\",\n            \"5\",\n            \"--database\",\n            \"arg_database\",\n        ],\n    )\n    assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n    assert (\n        MockMyCli.connect_args[\"user\"] == \"arg_user\"\n        and MockMyCli.connect_args[\"passwd\"] == \"arg_password\"\n        and MockMyCli.connect_args[\"host\"] == \"arg_host\"\n        and MockMyCli.connect_args[\"port\"] == 5\n        and MockMyCli.connect_args[\"database\"] == \"arg_database\"\n    )\n\n    # Use a DSN without password\n    result = runner.invoke(mycli.main.cli, args=[\"mysql://dsn_user@dsn_host:6/dsn_database\"])\n    assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n    assert (\n        MockMyCli.connect_args[\"user\"] == \"dsn_user\"\n        and MockMyCli.connect_args[\"passwd\"] is None\n        and MockMyCli.connect_args[\"host\"] == \"dsn_host\"\n        and MockMyCli.connect_args[\"port\"] == 6\n        and MockMyCli.connect_args[\"database\"] == \"dsn_database\"\n    )\n\n    # Use a DSN with query parameters\n    result = runner.invoke(mycli.main.cli, args=[\"mysql://dsn_user:dsn_passwd@dsn_host:6/dsn_database?ssl_mode=off\"])\n    assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n    assert (\n        MockMyCli.connect_args[\"user\"] == \"dsn_user\"\n        and MockMyCli.connect_args[\"passwd\"] == \"dsn_passwd\"\n        and MockMyCli.connect_args[\"host\"] == \"dsn_host\"\n        and MockMyCli.connect_args[\"port\"] == 6\n        and MockMyCli.connect_args[\"database\"] == \"dsn_database\"\n        and MockMyCli.connect_args[\"ssl\"] is None\n    )\n\n    # When a user uses a DSN with query parameters, and also used command line\n    # arguments, prefer the command line arguments.\n    MockMyCli.connect_args = None\n    MockMyCli.config = {\n        \"main\": {},\n        \"alias_dsn\": {},\n        \"connection\": {\n            \"default_keepalive_ticks\": 0,\n        },\n    }\n\n    # keepalive_ticks as a query parameter\n    result = runner.invoke(mycli.main.cli, args=[\"mysql://dsn_user:dsn_passwd@dsn_host:6/dsn_database?keepalive_ticks=30\"])\n    assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n    assert MockMyCli.connect_args[\"keepalive_ticks\"] == 30\n\n    MockMyCli.connect_args = None\n\n    # When a user uses a DSN with query parameters, and also used command line\n    # arguments, use the command line arguments.\n    result = runner.invoke(\n        mycli.main.cli,\n        args=[\n            'mysql://dsn_user:dsn_passwd@dsn_host:6/dsn_database?ssl_mode=off',\n            '--ssl-mode=on',\n        ],\n    )\n    assert result.exit_code == 0, result.output + ' ' + str(result.exception)\n    assert MockMyCli.connect_args['user'] == 'dsn_user'\n    assert MockMyCli.connect_args['passwd'] == 'dsn_passwd'\n    assert MockMyCli.connect_args['host'] == 'dsn_host'\n    assert MockMyCli.connect_args['port'] == 6\n    assert MockMyCli.connect_args['database'] == 'dsn_database'\n    assert MockMyCli.connect_args['ssl']['mode'] == 'on'\n\n    # Accept a literal DSN with the --dsn flag (not only an alias)\n    result = runner.invoke(\n        mycli.main.cli,\n        args=[\n            '--dsn',\n            'mysql://dsn_user:dsn_passwd@dsn_host:6/dsn_database',\n        ],\n    )\n    assert result.exit_code == 0, result.output + ' ' + str(result.exception)\n    assert (\n        MockMyCli.connect_args['user'] == 'dsn_user'\n        and MockMyCli.connect_args['passwd'] == 'dsn_passwd'\n        and MockMyCli.connect_args['host'] == 'dsn_host'\n        and MockMyCli.connect_args['port'] == 6\n        and MockMyCli.connect_args['database'] == 'dsn_database'\n    )\n\n    # accept socket as a query parameter\n    result = runner.invoke(\n        mycli.main.cli,\n        args=[\n            f'mysql://dsn_user:dsn_passwd@{DEFAULT_HOST}/dsn_database?socket=mysql.sock',\n        ],\n    )\n    assert result.exit_code == 0, result.output + ' ' + str(result.exception)\n    assert MockMyCli.connect_args['user'] == 'dsn_user'\n    assert MockMyCli.connect_args['passwd'] == 'dsn_passwd'\n    assert MockMyCli.connect_args['host'] == DEFAULT_HOST\n    assert MockMyCli.connect_args['database'] == 'dsn_database'\n    assert MockMyCli.connect_args['socket'] == 'mysql.sock'\n\n    # accept character_set as a query parameter\n    result = runner.invoke(\n        mycli.main.cli,\n        args=[\n            f'mysql://dsn_user:dsn_passwd@{DEFAULT_HOST}/dsn_database?character_set=latin1',\n        ],\n    )\n    assert result.exit_code == 0, result.output + ' ' + str(result.exception)\n    assert MockMyCli.connect_args['user'] == 'dsn_user'\n    assert MockMyCli.connect_args['passwd'] == 'dsn_passwd'\n    assert MockMyCli.connect_args['host'] == DEFAULT_HOST\n    assert MockMyCli.connect_args['database'] == 'dsn_database'\n    assert MockMyCli.connect_args['character_set'] == 'latin1'\n\n    # --character_set overrides character_set as a query parameter\n    result = runner.invoke(\n        mycli.main.cli,\n        args=[\n            f'mysql://dsn_user:dsn_passwd@{DEFAULT_HOST}/dsn_database?character_set=latin1',\n            '--character-set=utf8mb3',\n        ],\n    )\n    assert result.exit_code == 0, result.output + ' ' + str(result.exception)\n    assert MockMyCli.connect_args['user'] == 'dsn_user'\n    assert MockMyCli.connect_args['passwd'] == 'dsn_passwd'\n    assert MockMyCli.connect_args['host'] == DEFAULT_HOST\n    assert MockMyCli.connect_args['database'] == 'dsn_database'\n    assert MockMyCli.connect_args['character_set'] == 'utf8mb3'\n\n\ndef test_password_flag_uses_sentinel(monkeypatch):\n    class Formatter:\n        format_name = None\n\n    class Logger:\n        def debug(self, *args, **args_dict):\n            pass\n\n        def warning(self, *args, **args_dict):\n            pass\n\n    class MockMyCli:\n        config = {\n            'main': {},\n            'alias_dsn': {},\n            'connection': {\n                'default_keepalive_ticks': 0,\n            },\n        }\n\n        def __init__(self, **_args):\n            self.logger = Logger()\n            self.destructive_warning = False\n            self.main_formatter = Formatter()\n            self.redirect_formatter = Formatter()\n            self.ssl_mode = 'auto'\n            self.my_cnf = {'client': {}, 'mysqld': {}}\n            self.default_keepalive_ticks = 0\n\n        def connect(self, **args):\n            MockMyCli.connect_args = args\n\n        def run_query(self, query, new_line=True):\n            pass\n\n    import mycli.main\n\n    monkeypatch.setattr(mycli.main, 'MyCli', MockMyCli)\n    runner = CliRunner()\n\n    result = runner.invoke(\n        mycli.main.cli,\n        args=[\n            '--user',\n            'user',\n            '--host',\n            DEFAULT_HOST,\n            '--port',\n            f'{DEFAULT_PORT}',\n            '--database',\n            'database',\n            '--password',\n        ],\n    )\n    assert result.exit_code == 0, result.output + ' ' + str(result.exception)\n    assert MockMyCli.connect_args['passwd'] == EMPTY_PASSWORD_FLAG_SENTINEL\n\n\ndef test_ssh_config(monkeypatch):\n    # Setup classes to mock mycli.main.MyCli\n    class Formatter:\n        format_name = None\n\n    class Logger:\n        def debug(self, *args, **args_dict):\n            pass\n\n        def warning(self, *args, **args_dict):\n            pass\n\n    class MockMyCli:\n        config = {\n            \"main\": {},\n            \"alias_dsn\": {},\n            \"connection\": {\n                \"default_keepalive_ticks\": 0,\n            },\n        }\n\n        def __init__(self, **args):\n            self.logger = Logger()\n            self.destructive_warning = False\n            self.main_formatter = Formatter()\n            self.redirect_formatter = Formatter()\n            self.ssl_mode = \"auto\"\n            self.my_cnf = {\"client\": {}, \"mysqld\": {}}\n            self.default_keepalive_ticks = 0\n\n        def connect(self, **args):\n            MockMyCli.connect_args = args\n\n        def run_query(self, query, new_line=True):\n            pass\n\n    import mycli.main\n\n    monkeypatch.setattr(mycli.main, \"MyCli\", MockMyCli)\n    runner = CliRunner()\n\n    # Setup temporary configuration\n    # keep Windows from locking the file with delete=False\n    with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode=\"w\", delete=False) as ssh_config:\n        ssh_config.write(\n            dedent(\"\"\"\\\n            Host test\n                Hostname test.example.com\n                User joe\n                Port 22222\n                IdentityFile ~/.ssh/gateway\n        \"\"\")\n        )\n        ssh_config.flush()\n\n        # When a user supplies a ssh config.\n        result = runner.invoke(mycli.main.cli, args=[\"--ssh-config-path\", ssh_config.name, \"--ssh-config-host\", \"test\"])\n        assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n        assert (\n            MockMyCli.connect_args[\"ssh_user\"] == \"joe\"\n            and MockMyCli.connect_args[\"ssh_host\"] == \"test.example.com\"\n            and MockMyCli.connect_args[\"ssh_port\"] == 22222\n            and MockMyCli.connect_args[\"ssh_key_filename\"] == os.path.expanduser(\"~\") + \"/.ssh/gateway\"\n        )\n\n        # When a user supplies a ssh config host as argument to mycli,\n        # and used command line arguments, use the command line\n        # arguments.\n        result = runner.invoke(\n            mycli.main.cli,\n            args=[\n                \"--ssh-config-path\",\n                ssh_config.name,\n                \"--ssh-config-host\",\n                \"test\",\n                \"--ssh-user\",\n                \"arg_user\",\n                \"--ssh-host\",\n                \"arg_host\",\n                \"--ssh-port\",\n                \"3\",\n                \"--ssh-key-filename\",\n                \"/path/to/key\",\n            ],\n        )\n        assert result.exit_code == 0, result.output + \" \" + str(result.exception)\n        assert (\n            MockMyCli.connect_args[\"ssh_user\"] == \"arg_user\"\n            and MockMyCli.connect_args[\"ssh_host\"] == \"arg_host\"\n            and MockMyCli.connect_args[\"ssh_port\"] == 3\n            and MockMyCli.connect_args[\"ssh_key_filename\"] == \"/path/to/key\"\n        )\n\n    # delete=False means we should try to clean up\n    try:\n        if os.path.exists(ssh_config.name):\n            os.remove(ssh_config.name)\n    except Exception as e:\n        print(f\"An error occurred while attempting to delete the file: {e}\")\n\n\n@dbtest\ndef test_init_command_arg(executor):\n    init_command = \"set sql_select_limit=1000\"\n    sql = 'show variables like \"sql_select_limit\";'\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--init-command\", init_command], input=sql)\n\n    expected = \"sql_select_limit\\t1000\\n\"\n    assert result.exit_code == 0\n    assert expected in result.output\n\n\n@dbtest\ndef test_init_command_multiple_arg(executor):\n    init_command = \"set sql_select_limit=2000; set max_join_size=20000\"\n    sql = 'show variables like \"sql_select_limit\";\\nshow variables like \"max_join_size\"'\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS + [\"--init-command\", init_command], input=sql)\n\n    expected_sql_select_limit = \"sql_select_limit\\t2000\\n\"\n    expected_max_join_size = \"max_join_size\\t20000\\n\"\n\n    assert result.exit_code == 0\n    assert expected_sql_select_limit in result.output\n    assert expected_max_join_size in result.output\n\n\n@dbtest\ndef test_global_init_commands(executor):\n    \"\"\"Tests that global init-commands from config are executed by default.\"\"\"\n    # The global init-commands section in test/myclirc sets sql_select_limit=9999\n    sql = 'show variables like \"sql_select_limit\";'\n    runner = CliRunner()\n    result = runner.invoke(cli, args=CLI_ARGS, input=sql)\n    expected = \"sql_select_limit\\t9999\\n\"\n    assert result.exit_code == 0\n    assert expected in result.output\n\n\n@dbtest\ndef test_execute_with_logfile(executor):\n    \"\"\"Test that --execute combines with --logfile\"\"\"\n    sql = 'select 1'\n    runner = CliRunner()\n\n    with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode=\"w\", delete=False) as logfile:\n        result = runner.invoke(mycli.main.cli, args=CLI_ARGS + [\"--logfile\", logfile.name, \"--execute\", sql])\n        assert result.exit_code == 0\n\n    assert os.path.getsize(logfile.name) > 0\n\n    try:\n        if os.path.exists(logfile.name):\n            os.remove(logfile.name)\n    except Exception as e:\n        print(f\"An error occurred while attempting to delete the file: {e}\")\n\n\ndef test_null_string_config(monkeypatch):\n    monkeypatch.setattr(MyCli, 'system_config_files', [])\n    monkeypatch.setattr(MyCli, 'pwd_config_file', os.devnull)\n    runner = CliRunner()\n    # keep Windows from locking the file with delete=False\n    with NamedTemporaryFile(mode='w', delete=False) as myclirc:\n        myclirc.write(\n            dedent(\"\"\"\\\n            [main]\n            null_string = <nope>\n            \"\"\")\n        )\n        myclirc.flush()\n        args = CLI_ARGS + ['--myclirc', myclirc.name, '--format=table', '--execute', 'SELECT NULL']\n        result = runner.invoke(mycli.main.cli, args=args)\n        assert '<nope>' in result.output\n        assert '<null>' not in result.output\n\n    # delete=False means we should try to clean up\n    try:\n        if os.path.exists(myclirc.name):\n            os.remove(myclirc.name)\n    except Exception as e:\n        print(f'An error occurred while attempting to delete the file: {e}')\n"
  },
  {
    "path": "test/test_naive_completion.py",
    "content": "# type: ignore\n\nfrom prompt_toolkit.completion import Completion\nfrom prompt_toolkit.document import Document\nimport pytest\n\n\n@pytest.fixture\ndef completer():\n    import mycli.sqlcompleter as sqlcompleter\n\n    return sqlcompleter.SQLCompleter(smart_completion=False)\n\n\n@pytest.fixture\ndef complete_event():\n    from unittest.mock import Mock\n\n    return Mock()\n\n\ndef test_empty_string_completion(completer, complete_event):\n    text = \"\"\n    position = 0\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == list(map(Completion, completer.all_completions))\n\n\ndef test_select_keyword_completion(completer, complete_event):\n    text = \"SEL\"\n    position = len(\"SEL\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [Completion(text=\"SELECT\", start_position=-3)]\n\n\ndef test_function_name_completion(completer, complete_event):\n    text = \"SELECT MA\"\n    position = len(\"SELECT MA\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert sorted(x.text for x in result) == [\n        'MAKEDATE',\n        'MAKETIME',\n        'MAKE_SET',\n        'MASTER',\n        'MASTER_AUTO_POSITION',\n        'MASTER_BIND',\n        'MASTER_COMPRESSION_ALGORITHMS',\n        'MASTER_CONNECT_RETRY',\n        'MASTER_DELAY',\n        'MASTER_HEARTBEAT_PERIOD',\n        'MASTER_HOST',\n        'MASTER_LOG_FILE',\n        'MASTER_LOG_POS',\n        'MASTER_PASSWORD',\n        'MASTER_PORT',\n        'MASTER_POS_WAIT',\n        'MASTER_PUBLIC_KEY_PATH',\n        'MASTER_RETRY_COUNT',\n        'MASTER_SSL',\n        'MASTER_SSL_CA',\n        'MASTER_SSL_CAPATH',\n        'MASTER_SSL_CERT',\n        'MASTER_SSL_CIPHER',\n        'MASTER_SSL_CRL',\n        'MASTER_SSL_CRLPATH',\n        'MASTER_SSL_KEY',\n        'MASTER_SSL_VERIFY_SERVER_CERT',\n        'MASTER_TLS_CIPHERSUITES',\n        'MASTER_TLS_VERSION',\n        'MASTER_USER',\n        'MASTER_ZSTD_COMPRESSION_LEVEL',\n        'MATCH',\n        'MAX',\n        'MAXVALUE',\n        'MAX_CONNECTIONS_PER_HOUR',\n        'MAX_QUERIES_PER_HOUR',\n        'MAX_ROWS',\n        'MAX_SIZE',\n        'MAX_UPDATES_PER_HOUR',\n        'MAX_USER_CONNECTIONS',\n    ]\n\n\ndef test_column_name_completion(completer, complete_event):\n    text = \"SELECT  FROM users\"\n    position = len(\"SELECT \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == list(map(Completion, completer.all_completions))\n\n\ndef test_special_name_completion(completer, complete_event):\n    text = \"\\\\\"\n    position = len(\"\\\\\")\n    result = set(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    # Special commands will NOT be suggested during naive completion mode.\n    assert result == set()\n"
  },
  {
    "path": "test/test_parseutils.py",
    "content": "# type: ignore\n\nimport pytest\n\nfrom mycli.packages.parseutils import (\n    extract_columns_from_select,\n    extract_tables,\n    extract_tables_from_complete_statements,\n    is_destructive,\n    is_dropping_database,\n    queries_start_with,\n    query_has_where_clause,\n    query_starts_with,\n)\n\n\ndef test_extract_columns_from_select():\n    try:\n        columns = extract_columns_from_select(\"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS\")\n    except Exception:\n        columns = []\n    assert columns == [\"COLUMN_NAME\", \"DATA_TYPE\", \"IS_NULLABLE\", \"COLUMN_DEFAULT\"]\n\n\ndef test_empty_string():\n    tables = extract_tables(\"\")\n    assert tables == []\n\n\ndef test_simple_select_single_table():\n    tables = extract_tables(\"select * from abc\")\n    assert tables == [(None, \"abc\", None)]\n\n\ndef test_simple_select_single_table_schema_qualified():\n    tables = extract_tables(\"select * from abc.def\")\n    assert tables == [(\"abc\", \"def\", None)]\n\n\ndef test_simple_select_multiple_tables():\n    tables = extract_tables(\"select * from abc, def\")\n    assert sorted(tables) == [(None, \"abc\", None), (None, \"def\", None)]\n\n\ndef test_simple_select_multiple_tables_schema_qualified():\n    tables = extract_tables(\"select * from abc.def, ghi.jkl\")\n    assert sorted(tables) == [(\"abc\", \"def\", None), (\"ghi\", \"jkl\", None)]\n\n\ndef test_simple_select_with_cols_single_table():\n    tables = extract_tables(\"select a,b from abc\")\n    assert tables == [(None, \"abc\", None)]\n\n\ndef test_simple_select_with_cols_single_table_schema_qualified():\n    tables = extract_tables(\"select a,b from abc.def\")\n    assert tables == [(\"abc\", \"def\", None)]\n\n\ndef test_simple_select_with_cols_multiple_tables():\n    tables = extract_tables(\"select a,b from abc, def\")\n    assert sorted(tables) == [(None, \"abc\", None), (None, \"def\", None)]\n\n\ndef test_simple_select_with_cols_multiple_tables_with_schema():\n    tables = extract_tables(\"select a,b from abc.def, def.ghi\")\n    assert sorted(tables) == [(\"abc\", \"def\", None), (\"def\", \"ghi\", None)]\n\n\ndef test_select_with_hanging_comma_single_table():\n    tables = extract_tables(\"select a, from abc\")\n    assert tables == [(None, \"abc\", None)]\n\n\ndef test_select_with_hanging_comma_multiple_tables():\n    tables = extract_tables(\"select a, from abc, def\")\n    assert sorted(tables) == [(None, \"abc\", None), (None, \"def\", None)]\n\n\ndef test_select_with_hanging_period_multiple_tables():\n    tables = extract_tables(\"SELECT t1. FROM tabl1 t1, tabl2 t2\")\n    assert sorted(tables) == [(None, \"tabl1\", \"t1\"), (None, \"tabl2\", \"t2\")]\n\n\ndef test_simple_insert_single_table():\n    tables = extract_tables('insert into abc (id, name) values (1, \"def\")')\n\n    # sqlparse mistakenly assigns an alias to the table\n    # assert tables == [(None, 'abc', None)]\n    assert tables == [(None, \"abc\", \"abc\")]\n\n\ndef test_simple_insert_single_table_schema_qualified():\n    tables = extract_tables('insert into abc.def (id, name) values (1, \"def\")')\n    assert tables == [(\"abc\", \"def\", None)]\n\n\ndef test_simple_update_table():\n    tables = extract_tables(\"update abc set id = 1\")\n    assert tables == [(None, \"abc\", None)]\n\n\ndef test_simple_update_table_with_schema():\n    tables = extract_tables(\"update abc.def set id = 1\")\n    assert tables == [(\"abc\", \"def\", None)]\n\n\ndef test_join_table():\n    tables = extract_tables(\"SELECT * FROM abc a JOIN def d ON a.id = d.num\")\n    assert sorted(tables) == [(None, \"abc\", \"a\"), (None, \"def\", \"d\")]\n\n\ndef test_join_table_schema_qualified():\n    tables = extract_tables(\"SELECT * FROM abc.def x JOIN ghi.jkl y ON x.id = y.num\")\n    assert tables == [(\"abc\", \"def\", \"x\"), (\"ghi\", \"jkl\", \"y\")]\n\n\ndef test_join_as_table():\n    tables = extract_tables(\"SELECT * FROM my_table AS m WHERE m.a > 5\")\n    assert tables == [(None, \"my_table\", \"m\")]\n\n\ndef test_extract_tables_from_complete_statements():\n    tables = extract_tables_from_complete_statements(\"SELECT * FROM my_table AS m WHERE m.a > 5\")\n    assert tables == [(None, \"my_table\", \"m\")]\n\n\ndef test_extract_tables_from_complete_statements_cte():\n    tables = extract_tables_from_complete_statements(\"WITH my_cte (id, num) AS ( SELECT id, COUNT(1) FROM my_table GROUP BY id ) SELECT *\")\n    assert tables == [(None, \"my_table\", None)]\n\n\n# this would confuse plain extract_tables() per #1122\ndef test_extract_tables_from_multiple_complete_statements():\n    tables = extract_tables_from_complete_statements(r'\\T sql-insert; SELECT * FROM my_table AS m WHERE m.a > 5')\n    assert tables == [(None, \"my_table\", \"m\")]\n\n\ndef test_query_starts_with():\n    query = \"USE test;\"\n    assert query_starts_with(query, (\"use\",)) is True\n\n    query = \"DROP DATABASE test;\"\n    assert query_starts_with(query, (\"use\",)) is False\n\n\ndef test_query_starts_with_comment():\n    query = \"# comment\\nUSE test;\"\n    assert query_starts_with(query, (\"use\",)) is True\n\n\ndef test_queries_start_with():\n    sql = \"# comment\\nshow databases;use foo;\"\n    assert queries_start_with(sql, [\"show\", \"select\"]) is True\n    assert queries_start_with(sql, [\"use\", \"drop\"]) is True\n    assert queries_start_with(sql, [\"delete\", \"update\"]) is False\n\n\ndef test_is_destructive():\n    sql = \"use test;\\nshow databases;\\ndrop database foo;\"\n    assert is_destructive([\"drop\"], sql) is True\n\n\ndef test_is_destructive_update_with_where_clause():\n    sql = \"use test;\\nshow databases;\\nUPDATE test SET x = 1 WHERE id = 1;\"\n    assert is_destructive([\"update\"], sql) is False\n\n\ndef test_is_destructive_update_with_where_clause_and_comment():\n    sql = \"use test;\\nshow databases;\\nUPDATE /* inline comment */ test SET x = 1 WHERE id = 1;\"\n    assert is_destructive([\"update\"], sql) is False\n\n\ndef test_is_destructive_update_multiple_tables_with_where_clause():\n    sql = \"use test;\\nshow databases;\\nUPDATE test, foo SET x = 1 WHERE id = 1;\"\n    assert is_destructive([\"update\"], sql) is True\n\n\ndef test_is_destructive_update_without_where_clause():\n    sql = \"use test;\\nshow databases;\\nUPDATE test SET x = 1;\"\n    assert is_destructive([\"update\"], sql) is True\n\n\n@pytest.mark.parametrize(\n    (\"sql\", \"has_where_clause\"),\n    [\n        (\"update test set dummy = 1;\", False),\n        (\"update test set dummy = 1 where id = 1);\", True),\n    ],\n)\ndef test_query_has_where_clause(sql, has_where_clause):\n    assert query_has_where_clause(sql) is has_where_clause\n\n\n@pytest.mark.parametrize(\n    (\"sql\", \"dbname\", \"is_dropping\"),\n    [\n        (\"select bar from foo\", \"foo\", False),\n        ('drop database \"foo\";', \"`foo`\", True),\n        (\"drop schema foo\", \"foo\", True),\n        (\"drop schema foo\", \"bar\", False),\n        (\"drop database bar\", \"foo\", False),\n        (\"drop database foo\", None, False),\n        (\"drop database foo; create database foo\", \"foo\", False),\n        (\"drop database foo; create database bar\", \"foo\", True),\n        (\"select bar from foo; drop database bazz\", \"foo\", False),\n        (\"select bar from foo; drop database bazz\", \"bazz\", True),\n        (\"-- dropping database \\n drop -- really dropping \\n schema abc -- now it is dropped\", \"abc\", True),\n    ],\n)\ndef test_is_dropping_database(sql, dbname, is_dropping):\n    assert is_dropping_database(sql, dbname) == is_dropping\n"
  },
  {
    "path": "test/test_plan.wiki",
    "content": "= Gross Checks =\n    * [ ] Check connecting to a local database.\n    * [ ] Check connecting to a remote database.\n    * [ ] Check connecting to a database with a user/password.\n    * [ ] Check connecting to a non-existent database.\n    * [ ] Test changing the database.\n\n    == PGExecute ==\n    * [ ] Test successful execution given a cursor.\n    * [ ] Test unsuccessful execution with a syntax error.\n    * [ ] Test a series of executions with the same cursor without failure.\n    * [ ] Test a series of executions with the same cursor with failure.\n    * [ ] Test passing in a special command. \n\n    == Naive Autocompletion ==\n    * [ ] Input empty string, ask for completions - Everything.\n    * [ ] Input partial prefix, ask for completions - Stars with prefix.\n    * [ ] Input fully autocompleted string, ask for completions - Only full match\n    * [ ] Input non-existent prefix, ask for completions - nothing\n    * [ ] Input lowercase prefix - case insensitive completions\n\n    == Smart Autocompletion ==\n    * [ ] Input empty string and check if only keywords are returned.\n    * [ ] Input SELECT prefix and check if only columns and '*' are returned.\n    * [ ] Input SELECT blah - only keywords are returned.\n    * [ ] Input SELECT * FROM - Table names only\n\n    == PGSpecial ==\n    * [ ] Test \\d\n    * [ ] Test \\d tablename\n    * [ ] Test \\d tablena*\n    * [ ] Test \\d non-existent-tablename\n    * [ ] Test \\d index\n    * [ ] Test \\d sequence\n    * [ ] Test \\d view\n    \n    == Exceptionals ==\n    * [ ] Test the 'use' command to change db.\n"
  },
  {
    "path": "test/test_prompt_utils.py",
    "content": "# type: ignore\n\nimport click\n\nfrom mycli.packages.prompt_utils import confirm_destructive_query\n\n\ndef test_confirm_destructive_query_notty() -> None:\n    stdin = click.get_text_stream(\"stdin\")\n    assert stdin.isatty() is False\n\n    sql = \"drop database foo;\"\n    assert confirm_destructive_query([\"drop\"], sql) is None\n"
  },
  {
    "path": "test/test_smart_completion_public_schema_only.py",
    "content": "# type: ignore\n\nimport os.path\nfrom unittest.mock import patch\n\nfrom prompt_toolkit.completion import Completion\nfrom prompt_toolkit.document import Document\nimport pytest\n\nimport mycli.packages.special.main as special\n\nmetadata = {\n    \"users\": [\"id\", \"email\", \"first_name\", \"last_name\"],\n    \"orders\": [\"id\", \"ordered_date\", \"status\"],\n    \"select\": [\"id\", \"insert\", \"ABC\"],\n    \"réveillé\": [\"id\", \"insert\", \"ABC\"],\n    \"time_zone\": [\"Time_zone_id\"],\n    \"time_zone_leap_second\": [\"Time_zone_id\"],\n    \"time_zone_name\": [\"Time_zone_id\"],\n    \"time_zone_transition\": [\"Time_zone_id\"],\n    \"time_zone_transition_type\": [\"Time_zone_id\"],\n}\n\n\n@pytest.fixture\ndef completer():\n    import mycli.sqlcompleter as sqlcompleter\n\n    comp = sqlcompleter.SQLCompleter(smart_completion=True)\n\n    tables, columns = [], []\n\n    for table, cols in metadata.items():\n        tables.append((table,))\n        columns.extend([(table, col) for col in cols])\n\n    databases = [\"test\", \"test 2\"]\n\n    for db in databases:\n        comp.extend_schemata(db)\n    comp.extend_database_names(databases)\n    comp.set_dbname(\"test\")\n    comp.extend_relations(tables, kind=\"tables\")\n    comp.extend_columns(columns, kind=\"tables\")\n    comp.extend_enum_values([(\"orders\", \"status\", [\"pending\", \"shipped\"])])\n    comp.extend_special_commands(special.COMMANDS)\n\n    return comp\n\n\n@pytest.fixture\ndef empty_completer():\n    import mycli.sqlcompleter as sqlcompleter\n\n    comp = sqlcompleter.SQLCompleter(smart_completion=True)\n\n    tables, columns = [], []\n\n    for table, cols in metadata.items():\n        tables.append((table,))\n        columns.extend([(table, col) for col in cols])\n\n    db = 'empty'\n\n    comp.extend_schemata(db)\n    comp.extend_database_names([db])\n    comp.set_dbname(db)\n    comp.extend_special_commands(special.COMMANDS)\n\n    return comp\n\n\n@pytest.fixture\ndef complete_event():\n    from unittest.mock import Mock\n\n    return Mock()\n\n\ndef test_use_database_completion(completer, complete_event):\n    text = \"USE \"\n    position = len(text)\n    special.register_special_command(..., 'use', '\\\\u [database]', 'Change to a new database.', aliases=['\\\\u'])\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [\n        Completion(text=\"test\", start_position=0),\n        Completion(text=\"`test 2`\", start_position=0),\n    ]\n\n\ndef test_special_name_completion(completer, complete_event):\n    text = \"\\\\d\"\n    position = len(\"\\\\d\")\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [Completion(text=\"\\\\dt\", start_position=-2)]\n\n\ndef test_empty_string_completion(completer, complete_event):\n    text = \"\"\n    position = 0\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert list(map(Completion, completer.special_commands + completer.keywords)) == result\n\n\ndef test_select_keyword_completion(completer, complete_event):\n    text = \"SEL\"\n    position = len(\"SEL\")\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [\n        Completion(text='SELECT', start_position=-3),\n        Completion(text='SERIAL', start_position=-3),\n        Completion(text='MASTER_LOG_FILE', start_position=-3),\n        Completion(text='MASTER_LOG_POS', start_position=-3),\n        Completion(text='MASTER_TLS_CIPHERSUITES', start_position=-3),\n        Completion(text='MASTER_TLS_VERSION', start_position=-3),\n        Completion(text='SCHEDULE', start_position=-3),\n        Completion(text='SERIALIZABLE', start_position=-3),\n    ]\n\n\ndef test_select_star(completer, complete_event):\n    text = \"SELECT * \"\n    position = len(text)\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == list(map(Completion, completer.keywords))\n\n\ndef test_introducer_completion(completer, complete_event):\n    completer.extend_character_sets([('latin1',), ('utf8mb4',)])\n    text = 'SELECT _'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert '_latin1' in result_text\n    assert '_utf8mb4' in result_text\n\n\ndef test_collation_completion(completer, complete_event):\n    completer.extend_collations([('utf16le_bin',), ('utf8mb4_unicode_ci',)])\n    text = 'SELECT \"text\" COLLATE '\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert 'utf16le_bin' in result_text\n    assert 'utf8mb4_unicode_ci' in result_text\n\n\ndef test_transcoding_completion_1(completer, complete_event):\n    completer.extend_character_sets([('latin1',), ('utf8mb4',)])\n    text = 'SELECT CONVERT(\"text\" USING '\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert 'latin1' in result_text\n    assert 'utf8mb4' in result_text\n\n\ndef test_transcoding_completion_2(completer, complete_event):\n    completer.extend_character_sets([('utf8mb3',), ('utf8mb4',)])\n    text = 'SELECT CONVERT(\"text\" USING u'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert 'utf8mb3' in result_text\n    assert 'utf8mb4' in result_text\n\n\ndef test_transcoding_completion_3(completer, complete_event):\n    completer.extend_character_sets([('latin1',), ('utf8mb4',)])\n    text = 'SELECT CAST(\"text\" AS CHAR CHARACTER SET '\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert 'latin1' in result_text\n    assert 'utf8mb4' in result_text\n\n\ndef test_transcoding_completion_4(completer, complete_event):\n    completer.extend_character_sets([('utf8mb3',), ('utf8mb4',)])\n    text = 'SELECT CAST(\"text\" AS CHAR CHARACTER SET u'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert 'utf8mb3' in result_text\n    assert 'utf8mb4' in result_text\n\n\ndef test_where_transcoding_completion_1(completer, complete_event):\n    completer.extend_character_sets([('latin1',), ('utf8mb4',)])\n    text = 'SELECT * FROM users WHERE CONVERT(email USING '\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert 'latin1' in result_text\n    assert 'utf8mb4' in result_text\n\n\ndef test_where_transcoding_completion_2(completer, complete_event):\n    completer.extend_character_sets([('latin1',), ('utf8mb4',)])\n    text = 'SELECT * FROM users WHERE CAST(email AS CHAR CHARACTER SET '\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    result_text = [item.text for item in result]\n    assert 'latin1' in result_text\n    assert 'utf8mb4' in result_text\n\n\ndef test_table_completion(completer, complete_event):\n    text = \"SELECT * FROM \"\n    position = len(text)\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [\n        Completion(text=\"users\", start_position=0),\n        Completion(text=\"orders\", start_position=0),\n        Completion(text=\"`select`\", start_position=0),\n        Completion(text=\"`réveillé`\", start_position=0),\n        Completion(text=\"time_zone\", start_position=0),\n        Completion(text=\"time_zone_leap_second\", start_position=0),\n        Completion(text=\"time_zone_name\", start_position=0),\n        Completion(text=\"time_zone_transition\", start_position=0),\n        Completion(text=\"time_zone_transition_type\", start_position=0),\n        Completion(text=\"test\", start_position=0),\n        Completion(text=\"`test 2`\", start_position=0),\n    ]\n\n\ndef test_select_filtered_table_completion(completer, complete_event):\n    text = \"SELECT ABC FROM \"\n    position = len(text)\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [\n        Completion(text=\"`select`\", start_position=0),\n        Completion(text=\"`réveillé`\", start_position=0),\n        Completion(text=\"users\", start_position=0),\n        Completion(text=\"orders\", start_position=0),\n        Completion(text=\"time_zone\", start_position=0),\n        Completion(text=\"time_zone_leap_second\", start_position=0),\n        Completion(text=\"time_zone_name\", start_position=0),\n        Completion(text=\"time_zone_transition\", start_position=0),\n        Completion(text=\"time_zone_transition_type\", start_position=0),\n        Completion(text=\"test\", start_position=0),\n        Completion(text=\"`test 2`\", start_position=0),\n    ]\n\n\ndef test_sub_select_filtered_table_completion(completer, complete_event):\n    text = \"SELECT * FROM (SELECT ordered_date FROM \"\n    position = len(text)\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [\n        Completion(text=\"orders\", start_position=0),\n        Completion(text=\"users\", start_position=0),\n        Completion(text=\"`select`\", start_position=0),\n        Completion(text=\"`réveillé`\", start_position=0),\n        Completion(text=\"time_zone\", start_position=0),\n        Completion(text=\"time_zone_leap_second\", start_position=0),\n        Completion(text=\"time_zone_name\", start_position=0),\n        Completion(text=\"time_zone_transition\", start_position=0),\n        Completion(text=\"time_zone_transition_type\", start_position=0),\n        Completion(text=\"test\", start_position=0),\n        Completion(text=\"`test 2`\", start_position=0),\n    ]\n\n\ndef test_enum_value_completion(completer, complete_event):\n    text = \"SELECT * FROM orders WHERE status = \"\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"'pending'\", start_position=0),\n        Completion(text=\"'shipped'\", start_position=0),\n    ]\n\n\ndef test_function_name_completion(completer, complete_event):\n    text = \"SELECT MA\"\n    position = len(\"SELECT MA\")\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [\n        Completion(text='MAX', start_position=-2),\n        Completion(text='MATCH', start_position=-2),\n        Completion(text='MAKEDATE', start_position=-2),\n        Completion(text='MAKETIME', start_position=-2),\n        Completion(text='MAKE_SET', start_position=-2),\n        Completion(text='MASTER_POS_WAIT', start_position=-2),\n        Completion(text='email', start_position=-2),\n    ]\n\n\ndef test_suggested_column_names(completer, complete_event):\n    \"\"\"Suggest column and function names when selecting from table.\n\n    :param completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT  from users\"\n    position = len(\"SELECT \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == list(\n        [\n            Completion(text=\"*\", start_position=0),\n            Completion(text=\"id\", start_position=0),\n            Completion(text=\"email\", start_position=0),\n            Completion(text=\"first_name\", start_position=0),\n            Completion(text=\"last_name\", start_position=0),\n        ]\n        + list(map(Completion, completer.functions))\n        + [Completion(text=\"users\", start_position=0)]\n    )\n\n\ndef test_suggested_column_names_empty_db(empty_completer, complete_event):\n    \"\"\"Suggest * and function when selecting from no-table db.\n\n    :param empty_completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT \"\n    position = len(\"SELECT \")\n    result = list(empty_completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == list(\n        [\n            Completion(text=\"*\", start_position=0),\n        ]\n        + list(map(Completion, empty_completer.functions))\n    )\n\n\ndef test_suggested_column_names_in_function(completer, complete_event):\n    \"\"\"Suggest column and function names when selecting multiple columns from\n    table.\n\n    :param completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT MAX( from users\"\n    position = len(\"SELECT MAX(\")\n    result = completer.get_completions(Document(text=text, cursor_position=position), complete_event)\n    assert list(result) == [\n        Completion(text=\"*\", start_position=0),\n        Completion(text=\"id\", start_position=0),\n        Completion(text=\"email\", start_position=0),\n        Completion(text=\"first_name\", start_position=0),\n        Completion(text=\"last_name\", start_position=0),\n    ]\n\n\ndef test_suggested_column_names_with_table_dot(completer, complete_event):\n    \"\"\"Suggest column names on table name and dot.\n\n    :param completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT users. from users\"\n    position = len(\"SELECT users.\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"*\", start_position=0),\n        Completion(text=\"id\", start_position=0),\n        Completion(text=\"email\", start_position=0),\n        Completion(text=\"first_name\", start_position=0),\n        Completion(text=\"last_name\", start_position=0),\n    ]\n\n\ndef test_suggested_column_names_with_alias(completer, complete_event):\n    \"\"\"Suggest column names on table alias and dot.\n\n    :param completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT u. from users u\"\n    position = len(\"SELECT u.\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"*\", start_position=0),\n        Completion(text=\"id\", start_position=0),\n        Completion(text=\"email\", start_position=0),\n        Completion(text=\"first_name\", start_position=0),\n        Completion(text=\"last_name\", start_position=0),\n    ]\n\n\ndef test_suggested_multiple_column_names(completer, complete_event):\n    \"\"\"Suggest column and function names when selecting multiple columns from\n    table.\n\n    :param completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT id,  from users u\"\n    position = len(\"SELECT id, \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == list(\n        [\n            Completion(text=\"*\", start_position=0),\n            Completion(text=\"id\", start_position=0),\n            Completion(text=\"email\", start_position=0),\n            Completion(text=\"first_name\", start_position=0),\n            Completion(text=\"last_name\", start_position=0),\n        ]\n        + list(map(Completion, completer.functions))\n        + [Completion(text=\"u\", start_position=0)]\n    )\n\n\ndef test_suggested_multiple_column_names_with_alias(completer, complete_event):\n    \"\"\"Suggest column names on table alias and dot when selecting multiple\n    columns from table.\n\n    :param completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT u.id, u. from users u\"\n    position = len(\"SELECT u.id, u.\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"*\", start_position=0),\n        Completion(text=\"id\", start_position=0),\n        Completion(text=\"email\", start_position=0),\n        Completion(text=\"first_name\", start_position=0),\n        Completion(text=\"last_name\", start_position=0),\n    ]\n\n\ndef test_suggested_multiple_column_names_with_dot(completer, complete_event):\n    \"\"\"Suggest column names on table names and dot when selecting multiple\n    columns from table.\n\n    :param completer:\n    :param complete_event:\n    :return:\n\n    \"\"\"\n    text = \"SELECT users.id, users. from users u\"\n    position = len(\"SELECT users.id, users.\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"*\", start_position=0),\n        Completion(text=\"id\", start_position=0),\n        Completion(text=\"email\", start_position=0),\n        Completion(text=\"first_name\", start_position=0),\n        Completion(text=\"last_name\", start_position=0),\n    ]\n\n\ndef test_suggested_aliases_after_on(completer, complete_event):\n    text = \"SELECT u.name, o.id FROM users u JOIN orders o ON \"\n    position = len(\"SELECT u.name, o.id FROM users u JOIN orders o ON \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"u\", start_position=0),\n        Completion(text=\"o\", start_position=0),\n    ]\n\n\ndef test_suggested_aliases_after_on_right_side(completer, complete_event):\n    text = \"SELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = \"\n    position = len(\"SELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"u\", start_position=0),\n        Completion(text=\"o\", start_position=0),\n    ]\n\n\ndef test_suggested_tables_after_on(completer, complete_event):\n    text = \"SELECT users.name, orders.id FROM users JOIN orders ON \"\n    position = len(\"SELECT users.name, orders.id FROM users JOIN orders ON \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"users\", start_position=0),\n        Completion(text=\"orders\", start_position=0),\n    ]\n\n\ndef test_suggested_tables_after_on_right_side(completer, complete_event):\n    text = \"SELECT users.name, orders.id FROM users JOIN orders ON orders.user_id = \"\n    position = len(\"SELECT users.name, orders.id FROM users JOIN orders ON orders.user_id = \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"users\", start_position=0),\n        Completion(text=\"orders\", start_position=0),\n    ]\n\n\ndef test_table_names_after_from(completer, complete_event):\n    text = \"SELECT * FROM \"\n    position = len(\"SELECT * FROM \")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"users\", start_position=0),\n        Completion(text=\"orders\", start_position=0),\n        Completion(text=\"`select`\", start_position=0),\n        Completion(text=\"`réveillé`\", start_position=0),\n        Completion(text=\"time_zone\", start_position=0),\n        Completion(text=\"time_zone_leap_second\", start_position=0),\n        Completion(text=\"time_zone_name\", start_position=0),\n        Completion(text=\"time_zone_transition\", start_position=0),\n        Completion(text=\"time_zone_transition_type\", start_position=0),\n        Completion(text=\"test\", start_position=0),\n        Completion(text=\"`test 2`\", start_position=0),\n    ]\n\n\ndef test_table_names_leading_partial(completer, complete_event):\n    text = \"SELECT * FROM time_zone\"\n    position = len(\"SELECT * FROM time_zone\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"time_zone\", start_position=-9),\n        Completion(text=\"time_zone_name\", start_position=-9),\n        Completion(text=\"time_zone_transition\", start_position=-9),\n        Completion(text=\"time_zone_leap_second\", start_position=-9),\n        Completion(text=\"time_zone_transition_type\", start_position=-9),\n    ]\n\n\ndef test_table_names_inter_partial(completer, complete_event):\n    text = \"SELECT * FROM time_leap\"\n    position = len(\"SELECT * FROM time_leap\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"time_zone_leap_second\", start_position=-9),\n        Completion(text='time_zone_name', start_position=-9),\n        Completion(text='time_zone_transition', start_position=-9),\n        Completion(text='time_zone_transition_type', start_position=-9),\n    ]\n\n\ndef test_table_names_fuzzy(completer, complete_event):\n    text = \"SELECT * FROM tim_leap\"\n    position = len(\"SELECT * FROM tim_leap\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"time_zone_leap_second\", start_position=-8),\n    ]\n\n\ndef test_auto_escaped_col_names(completer, complete_event):\n    text = \"SELECT  from `select`\"\n    position = len(\"SELECT \")\n    result = [x.text for x in completer.get_completions(Document(text=text, cursor_position=position), complete_event)]\n    expected = (\n        [\n            \"*\",\n            \"id\",\n            \"`insert`\",\n            \"ABC\",\n        ]\n        + completer.functions\n        + [\"select\"]\n    )\n    assert result == expected\n\n\ndef test_un_escaped_table_names(completer, complete_event):\n    text = \"SELECT  from réveillé\"\n    position = len(\"SELECT \")\n    result = [x.text for x in completer.get_completions(Document(text=text, cursor_position=position), complete_event)]\n    assert result == [\n        \"*\",\n        \"id\",\n        \"`insert`\",\n        \"ABC\",\n    ] + completer.functions + [\"réveillé\"]\n\n\n# todo: the fixtures are insufficient; the database name should also appear in the result\ndef test_grant_on_suggets_tables_and_schemata(completer, complete_event):\n    text = \"GRANT ALL ON \"\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"test\", start_position=0),\n        Completion(text=\"`test 2`\", start_position=0),\n        Completion(text='users', start_position=0),\n        Completion(text='orders', start_position=0),\n        Completion(text='`select`', start_position=0),\n        Completion(text='`réveillé`', start_position=0),\n        Completion(text='time_zone', start_position=0),\n        Completion(text='time_zone_leap_second', start_position=0),\n        Completion(text='time_zone_name', start_position=0),\n        Completion(text='time_zone_transition', start_position=0),\n        Completion(text='time_zone_transition_type', start_position=0),\n    ]\n\n\n# todo: this test belongs more logically in test_naive_completion.py, but it didn't work there:\n#       multiple completion candidates were not suggested.\ndef test_deleted_keyword_completion(completer, complete_event):\n    text = \"exi\"\n    position = len(\"exi\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text=\"exit\", start_position=-3),\n        Completion(text='exists', start_position=-3),\n        Completion(text='explain', start_position=-3),\n        Completion(text='expire', start_position=-3),\n    ]\n\n\ndef test_numbers_no_completion(completer, complete_event):\n    text = \"SELECT COUNT(1) FROM time_zone WHERE Time_zone_id = 1\"\n    position = len(\"SELECT COUNT(1) FROM time_zone WHERE Time_zone_id = 1\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []  # ie not INT1\n\n\ndef dummy_list_path(dir_name):\n    dirs = {\n        \"/\": [\n            \"dir1\",\n            \"file1.sql\",\n            \"file2.sql\",\n        ],\n        \"/dir1\": [\n            \"subdir1\",\n            \"subfile1.sql\",\n            \"subfile2.sql\",\n        ],\n        \"/dir1/subdir1\": [\n            \"lastfile.sql\",\n        ],\n    }\n    return dirs.get(dir_name, [])\n\n\n@patch(\"mycli.packages.filepaths.list_path\", new=dummy_list_path)\n@pytest.mark.parametrize(\n    \"text,expected\",\n    [\n        ('source ', [('/', 0), ('~', 0), ('.', 0), ('..', 0)]),\n        (\"source /\", [(\"dir1\", 0), (\"file1.sql\", 0), (\"file2.sql\", 0)]),\n        (\"source /dir1/\", [(\"subdir1\", 0), (\"subfile1.sql\", 0), (\"subfile2.sql\", 0)]),\n        (\"source /dir1/subdir1/\", [(\"lastfile.sql\", 0)]),\n    ],\n)\ndef test_file_name_completion(completer, complete_event, text, expected):\n    position = len(text)\n    special.register_special_command(..., 'source', '\\\\. <filename>', 'Execute commands from file.', aliases=['\\\\.'])\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    expected = [Completion(txt, pos) for txt, pos in expected]\n    assert result == expected\n\n\ndef test_auto_case_heuristic(completer, complete_event):\n    text = \"select json_v\"\n    position = len(\"select json_v\")\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert [x.text for x in result] == [\n        'json_value',\n        'json_valid',\n    ]\n\n\ndef test_create_table_like_completion(completer, complete_event):\n    text = \"CREATE TABLE foo LIKE ti\"\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert [x.text for x in result] == [\n        'time_zone',\n        'time_zone_name',\n        'time_zone_transition',\n        'time_zone_leap_second',\n        'time_zone_transition_type',\n    ]\n\n\ndef test_source_eager_completion(completer, complete_event):\n    text = \"source sc\"\n    position = len(text)\n    script_filename = 'script_for_test_suite.sql'\n    f = open(script_filename, 'w')\n    f.close()\n    special.register_special_command(..., 'source', '\\\\. <filename>', 'Execute commands from file.', aliases=['\\\\.'])\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    success = True\n    error = 'unknown'\n    try:\n        assert [x.text for x in result] == [\n            script_filename,\n            'screenshots/',\n        ]\n    except AssertionError as e:\n        success = False\n        error = e\n    if os.path.exists(script_filename):\n        os.remove(script_filename)\n    if not success:\n        raise AssertionError(error)\n\n\ndef test_source_leading_dot_suggestions_completion(completer, complete_event):\n    text = \"source ./sc\"\n    position = len(text)\n    script_filename = 'script_for_test_suite.sql'\n    f = open(script_filename, 'w')\n    f.close()\n    special.register_special_command(..., 'source', '\\\\. <filename>', 'Execute commands from file.', aliases=['\\\\.'])\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    success = True\n    error = 'unknown'\n    try:\n        assert [x.text for x in result] == [\n            script_filename,\n            'screenshots/',\n        ]\n    except AssertionError as e:\n        success = False\n        error = e\n    if os.path.exists(script_filename):\n        os.remove(script_filename)\n    if not success:\n        raise AssertionError(error)\n\n\ndef test_string_no_completion(completer, complete_event):\n    text = 'select \"json'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []\n\n\ndef test_string_no_completion_single_quote(completer, complete_event):\n    text = \"select 'json\"\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []\n\n\ndef test_string_no_completion_spaces(completer, complete_event):\n    text = 'select \"nocomplete json'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []\n\n\ndef test_string_no_completion_spaces_inner_1(completer, complete_event):\n    text = 'select \"json nocomplete'\n    position = len('select \"json')\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []\n\n\ndef test_string_no_completion_spaces_inner_2(completer, complete_event):\n    text = 'select \"json nocomplete'\n    position = len('select \"json ')\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []\n\n\ndef test_backticked_column_completion(completer, complete_event):\n    text = 'select `Tim'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        # todo it would be nicer if the column names sorted to the top\n        Completion(text='`time`', start_position=-4),\n        Completion(text='`timediff`', start_position=-4),\n        Completion(text='`timestamp`', start_position=-4),\n        Completion(text='`time_format`', start_position=-4),\n        Completion(text='`time_to_sec`', start_position=-4),\n        Completion(text='`Time_zone_id`', start_position=-4),\n        Completion(text='`timestampadd`', start_position=-4),\n        Completion(text='`timestampdiff`', start_position=-4),\n        Completion(text='`datetime`', start_position=-4),\n        Completion(text='`optimize`', start_position=-4),\n        Completion(text='`optimizer_costs`', start_position=-4),\n        Completion(text='`utc_time`', start_position=-4),\n        Completion(text='`utc_timestamp`', start_position=-4),\n        Completion(text='`current_time`', start_position=-4),\n        Completion(text='`current_timestamp`', start_position=-4),\n        Completion(text='`localtime`', start_position=-4),\n        Completion(text='`localtimestamp`', start_position=-4),\n        Completion(text='`password_lock_time`', start_position=-4),\n    ]\n\n\ndef test_backticked_column_completion_component(completer, complete_event):\n    text = 'select `com'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        # todo it would be nicer if \"comment\" sorted to the top because it is a column name,\n        #      and because it is a reserved word\n        Completion(text='`commit`', start_position=-4),\n        Completion(text='`comment`', start_position=-4),\n        Completion(text='`compact`', start_position=-4),\n        Completion(text='`compress`', start_position=-4),\n        Completion(text='`committed`', start_position=-4),\n        Completion(text='`component`', start_position=-4),\n        Completion(text='`completion`', start_position=-4),\n        Completion(text='`compressed`', start_position=-4),\n        Completion(text='`compression`', start_position=-4),\n        Completion(text='`column`', start_position=-4),\n        Completion(text='`column_format`', start_position=-4),\n        Completion(text='`column_name`', start_position=-4),\n        Completion(text='`columns`', start_position=-4),\n        Completion(text='`second_microsecond`', start_position=-4),\n        Completion(text='`uncommitted`', start_position=-4),\n    ]\n\n\ndef test_backticked_column_completion_two_character(completer, complete_event):\n    text = 'select `f'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        # todo it would be nicer if the column name \"first_name\" sorted to the top\n        Completion(text='`for`', start_position=-2),\n        Completion(text='`from`', start_position=-2),\n        Completion(text='`fast`', start_position=-2),\n        Completion(text='`file`', start_position=-2),\n        Completion(text='`full`', start_position=-2),\n        Completion(text='`floor`', start_position=-2),\n        Completion(text='`false`', start_position=-2),\n        Completion(text='`field`', start_position=-2),\n        Completion(text='`fixed`', start_position=-2),\n        Completion(text='`float`', start_position=-2),\n        Completion(text='`fetch`', start_position=-2),\n        Completion(text='`first`', start_position=-2),\n        Completion(text='`flush`', start_position=-2),\n        Completion(text='`force`', start_position=-2),\n        Completion(text='`found`', start_position=-2),\n        Completion(text='`format`', start_position=-2),\n        Completion(text='`float4`', start_position=-2),\n        Completion(text='`float8`', start_position=-2),\n        Completion(text='`factor`', start_position=-2),\n        Completion(text='`faults`', start_position=-2),\n        Completion(text='`fields`', start_position=-2),\n        Completion(text='`filter`', start_position=-2),\n        Completion(text='`finish`', start_position=-2),\n        Completion(text='`follows`', start_position=-2),\n        Completion(text='`foreign`', start_position=-2),\n        Completion(text='`fulltext`', start_position=-2),\n        Completion(text='`function`', start_position=-2),\n        Completion(text='`from_days`', start_position=-2),\n        Completion(text='`following`', start_position=-2),\n        Completion(text='`first_name`', start_position=-2),\n        Completion(text='`found_rows`', start_position=-2),\n        Completion(text='`find_in_set`', start_position=-2),\n        Completion(text='`first_value`', start_position=-2),\n        Completion(text='`from_base64`', start_position=-2),\n        Completion(text='`foreign key`', start_position=-2),\n        Completion(text='`format_bytes`', start_position=-2),\n        Completion(text='`from_unixtime`', start_position=-2),\n        Completion(text='`file_block_size`', start_position=-2),\n        Completion(text='`format_pico_time`', start_position=-2),\n        Completion(text='`failed_login_attempts`', start_position=-2),\n        Completion(text='`left join`', start_position=-2),\n        Completion(text='`after`', start_position=-2),\n        Completion(text='`before`', start_position=-2),\n        Completion(text='`default`', start_position=-2),\n        Completion(text='`default_auth`', start_position=-2),\n        Completion(text='`definer`', start_position=-2),\n        Completion(text='`definition`', start_position=-2),\n        Completion(text='`enforced`', start_position=-2),\n        Completion(text='`if`', start_position=-2),\n        Completion(text='`infile`', start_position=-2),\n        Completion(text='`left`', start_position=-2),\n        Completion(text='`logfile`', start_position=-2),\n        Completion(text='`of`', start_position=-2),\n        Completion(text='`off`', start_position=-2),\n        Completion(text='`offset`', start_position=-2),\n        Completion(text='`outfile`', start_position=-2),\n        Completion(text='`profile`', start_position=-2),\n        Completion(text='`profiles`', start_position=-2),\n        Completion(text='`reference`', start_position=-2),\n        Completion(text='`references`', start_position=-2),\n    ]\n\n\ndef test_backticked_column_completion_three_character(completer, complete_event):\n    text = 'select `fi'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        # todo it would be nicer if the column name \"first_name\" sorted to the top\n        Completion(text='`file`', start_position=-3),\n        Completion(text='`field`', start_position=-3),\n        Completion(text='`fixed`', start_position=-3),\n        Completion(text='`first`', start_position=-3),\n        Completion(text='`fields`', start_position=-3),\n        Completion(text='`filter`', start_position=-3),\n        Completion(text='`finish`', start_position=-3),\n        Completion(text='`first_name`', start_position=-3),\n        Completion(text='`find_in_set`', start_position=-3),\n        Completion(text='`first_value`', start_position=-3),\n        Completion(text='`file_block_size`', start_position=-3),\n        Completion(text='`definer`', start_position=-3),\n        Completion(text='`definition`', start_position=-3),\n        Completion(text='`failed_login_attempts`', start_position=-3),\n        Completion(text='`foreign`', start_position=-3),\n        Completion(text='`infile`', start_position=-3),\n        Completion(text='`logfile`', start_position=-3),\n        Completion(text='`outfile`', start_position=-3),\n        Completion(text='`profile`', start_position=-3),\n        Completion(text='`profiles`', start_position=-3),\n        Completion(text='`foreign key`', start_position=-3),\n    ]\n\n\ndef test_backticked_column_completion_four_character(completer, complete_event):\n    text = 'select `fir'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        # todo it would be nicer if the column name \"first_name\" sorted to the top\n        Completion(text='`first`', start_position=-4),\n        Completion(text='`first_name`', start_position=-4),\n        Completion(text='`first_value`', start_position=-4),\n        Completion(text='`definer`', start_position=-4),\n        Completion(text='`filter`', start_position=-4),\n    ]\n\n\ndef test_backticked_table_completion_required(completer, complete_event):\n    text = 'select ABC from `rév'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text='`réveillé`', start_position=-4),\n    ]\n\n\ndef test_backticked_table_completion_not_required(completer, complete_event):\n    text = 'select * from `t'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == [\n        Completion(text='`test`', start_position=-2),\n        Completion(text='`test 2`', start_position=-2),\n        Completion(text='`time_zone`', start_position=-2),\n        Completion(text='`time_zone_name`', start_position=-2),\n        Completion(text='`time_zone_transition`', start_position=-2),\n        Completion(text='`time_zone_leap_second`', start_position=-2),\n        Completion(text='`time_zone_transition_type`', start_position=-2),\n    ]\n\n\ndef test_string_no_completion_backtick(completer, complete_event):\n    text = 'select * from \"`t'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []\n\n\n# todo this shouldn't suggest anything but the space resets the logic\n# and it completes on \"bar\" alone\n@pytest.mark.xfail\ndef test_backticked_no_completion_spaces(completer, complete_event):\n    text = 'select * from `nocomplete bar'\n    position = len(text)\n    result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event))\n    assert result == []\n"
  },
  {
    "path": "test/test_special_iocommands.py",
    "content": "# type: ignore\n\nimport os\nimport stat\nimport tempfile\nfrom time import time\nfrom unittest.mock import patch\n\nfrom pymysql import ProgrammingError\nimport pytest\n\nimport mycli.packages.special\nfrom test.utils import TEMPFILE_PREFIX, db_connection, dbtest, send_ctrl_c\n\n\ndef test_set_get_pager(monkeypatch):\n    monkeypatch.setenv('PAGER', '')\n    mycli.packages.special.set_pager_enabled(True)\n    assert mycli.packages.special.is_pager_enabled()\n    mycli.packages.special.set_pager_enabled(False)\n    assert not mycli.packages.special.is_pager_enabled()\n    mycli.packages.special.set_pager(\"less\")\n    assert os.environ[\"PAGER\"] == \"less\"\n    mycli.packages.special.set_pager(False)\n    assert os.environ[\"PAGER\"] == \"less\"\n    del os.environ[\"PAGER\"]\n    mycli.packages.special.set_pager(False)\n    mycli.packages.special.disable_pager()\n    assert not mycli.packages.special.is_pager_enabled()\n\n\ndef test_set_get_timing():\n    mycli.packages.special.set_timing_enabled(True)\n    assert mycli.packages.special.is_timing_enabled()\n    mycli.packages.special.set_timing_enabled(False)\n    assert not mycli.packages.special.is_timing_enabled()\n\n\ndef test_set_get_expanded_output():\n    mycli.packages.special.set_expanded_output(True)\n    assert mycli.packages.special.is_expanded_output()\n    mycli.packages.special.set_expanded_output(False)\n    assert not mycli.packages.special.is_expanded_output()\n\n\ndef test_editor_command(monkeypatch):\n    monkeypatch.setenv('EDITOR', 'true')\n    monkeypatch.setenv('VISUAL', 'true')\n\n    assert mycli.packages.special.editor_command(r\"hello\\e\")\n    assert mycli.packages.special.editor_command(r\"hello\\edit\")\n    assert mycli.packages.special.editor_command(r\"\\e hello\")\n    assert mycli.packages.special.editor_command(r\"\\edit hello\")\n\n    assert not mycli.packages.special.editor_command(r\"hello\")\n    assert not mycli.packages.special.editor_command(r\"\\ehello\")\n    assert not mycli.packages.special.editor_command(r\"\\edithello\")\n\n    assert mycli.packages.special.get_filename(r\"\\e filename\") == \"filename\"\n\n    if os.name != \"nt\":\n        assert mycli.packages.special.open_external_editor(sql=r\"select 1\") == ('select 1', None)\n    else:\n        pytest.skip(\"Skipping on Windows platform.\")\n\n\ndef test_tee_command():\n    mycli.packages.special.write_tee(\"hello world\")  # write without file set\n    # keep Windows from locking the file with delete=False\n    with tempfile.NamedTemporaryFile(prefix=TEMPFILE_PREFIX, delete=False) as f:\n        mycli.packages.special.execute(None, \"tee \" + f.name)\n        mycli.packages.special.write_tee(\"hello world\")\n        if os.name == \"nt\":\n            assert f.read() == b\"hello world\\r\\n\"\n        else:\n            assert f.read() == b\"hello world\\n\"\n\n        mycli.packages.special.execute(None, \"tee -o \" + f.name)\n        mycli.packages.special.write_tee(\"hello world\")\n        f.seek(0)\n        if os.name == \"nt\":\n            assert f.read() == b\"hello world\\r\\n\"\n        else:\n            assert f.read() == b\"hello world\\n\"\n\n        mycli.packages.special.execute(None, \"notee\")\n        mycli.packages.special.write_tee(\"hello world\")\n        f.seek(0)\n        if os.name == \"nt\":\n            assert f.read() == b\"hello world\\r\\n\"\n        else:\n            assert f.read() == b\"hello world\\n\"\n\n    # remove temp file\n    # delete=False means we should try to clean up\n    try:\n        if os.path.exists(f.name):\n            os.remove(f.name)\n    except Exception as e:\n        print(f\"An error occurred while attempting to delete the file: {e}\")\n\n\ndef test_tee_command_error():\n    with pytest.raises(TypeError):\n        mycli.packages.special.execute(None, \"tee\")\n\n    with pytest.raises(OSError):\n        with tempfile.NamedTemporaryFile(prefix=TEMPFILE_PREFIX) as f:\n            os.chmod(f.name, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)\n            mycli.packages.special.execute(None, f\"tee {f.name}\")\n\n\n@dbtest\n@pytest.mark.skipif(os.name == \"nt\", reason=\"Bug: fails on Windows, needs fixing, singleton of FQ not working right\")\ndef test_favorite_query():\n    with db_connection().cursor() as cur:\n        query = 'select \"✔\"'\n        mycli.packages.special.execute(cur, f\"\\\\fs check {query}\")\n        assert next(mycli.packages.special.execute(cur, \"\\\\f check\")).preamble == \"> \" + query\n\n\n@dbtest\n@pytest.mark.skipif(os.name == \"nt\", reason=\"Bug: fails on Windows, needs fixing, singleton of FQ not working right\")\ndef test_special_favorite_query():\n    with db_connection().cursor() as cur:\n        query = r'\\?'\n        mycli.packages.special.execute(cur, rf\"\\fs special {query}\")\n        assert (r'\\G', None, r'<query>\\G', 'Display query results vertically.') in next(\n            mycli.packages.special.execute(cur, r'\\f special')\n        ).rows\n\n\ndef test_once_command():\n    with pytest.raises(TypeError):\n        mycli.packages.special.execute(None, \"\\\\once\")\n\n    with pytest.raises(OSError):\n        mycli.packages.special.execute(None, \"\\\\once /proc/access-denied\")\n\n    mycli.packages.special.write_once(\"hello world\")  # write without file set\n    # keep Windows from locking the file with delete=False\n    with tempfile.NamedTemporaryFile(prefix=TEMPFILE_PREFIX, delete=False) as f:\n        mycli.packages.special.execute(None, \"\\\\once \" + f.name)\n        mycli.packages.special.write_once(\"hello world\")\n        if os.name == \"nt\":\n            assert f.read() == b\"hello world\\r\\n\"\n        else:\n            assert f.read() == b\"hello world\\n\"\n\n        mycli.packages.special.execute(None, \"\\\\once -o \" + f.name)\n        mycli.packages.special.write_once(\"hello world line 1\")\n        mycli.packages.special.write_once(\"hello world line 2\")\n        f.seek(0)\n        if os.name == \"nt\":\n            assert f.read() == b\"hello world line 1\\r\\nhello world line 2\\r\\n\"\n        else:\n            assert f.read() == b\"hello world line 1\\nhello world line 2\\n\"\n    # delete=False means we should try to clean up\n    try:\n        if os.path.exists(f.name):\n            os.remove(f.name)\n    except Exception as e:\n        print(f\"An error occurred while attempting to delete the file: {e}\")\n\n\ndef test_pipe_once_command():\n    with pytest.raises(IOError):\n        mycli.packages.special.execute(None, \"\\\\pipe_once\")\n\n    with pytest.raises(OSError):\n        mycli.packages.special.execute(None, \"\\\\pipe_once /proc/access-denied\")\n        mycli.packages.special.write_pipe_once(\"select 1\")\n        mycli.packages.special.flush_pipe_once_if_written(None)\n\n    if os.name == \"nt\":\n        mycli.packages.special.execute(None, '\\\\pipe_once python -c \"import sys; print(len(sys.stdin.read().strip()))\"')\n        mycli.packages.special.write_once(\"hello world\")\n        mycli.packages.special.flush_pipe_once_if_written(None)\n    else:\n        with tempfile.NamedTemporaryFile(prefix=TEMPFILE_PREFIX) as f:\n            mycli.packages.special.execute(None, \"\\\\pipe_once tee \" + f.name)\n            mycli.packages.special.write_pipe_once(\"hello world\")\n            mycli.packages.special.flush_pipe_once_if_written(None)\n            f.seek(0)\n            assert f.read() == b\"hello world\\n\"\n\n\ndef test_parseargfile():\n    \"\"\"Test that parseargfile expands the user directory.\"\"\"\n    expected = (os.path.join(os.path.expanduser(\"~\"), \"filename\"), \"a\")\n\n    if os.name == \"nt\":\n        assert expected == mycli.packages.special.iocommands.parseargfile(\"~\\\\filename\")\n    else:\n        assert expected == mycli.packages.special.iocommands.parseargfile(\"~/filename\")\n\n    expected = (os.path.join(os.path.expanduser(\"~\"), \"filename\"), \"w\")\n    if os.name == \"nt\":\n        assert expected == mycli.packages.special.iocommands.parseargfile(\"-o ~\\\\filename\")\n    else:\n        assert expected == mycli.packages.special.iocommands.parseargfile(\"-o ~/filename\")\n\n\ndef test_parseargfile_no_file():\n    \"\"\"Test that parseargfile raises a TypeError if there is no filename.\"\"\"\n    with pytest.raises(TypeError):\n        mycli.packages.special.iocommands.parseargfile(\"\")\n\n    with pytest.raises(TypeError):\n        mycli.packages.special.iocommands.parseargfile(\"-o \")\n\n\n@dbtest\ndef test_watch_query_iteration():\n    \"\"\"Test that a single iteration of the result of `watch_query` executes\n    the desired query and returns the given results.\"\"\"\n    expected_value = \"1\"\n    query = f\"SELECT {expected_value}\"\n    expected_preamble = f\"> {query}\"\n    with db_connection().cursor() as cur:\n        result = next(mycli.packages.special.iocommands.watch_query(arg=query, cur=cur))\n    assert result.preamble == expected_preamble\n    assert result.header[0] == expected_value\n\n\n@dbtest\n@pytest.mark.skipif(os.name == \"nt\", reason=\"Bug: Win handles this differently.  May need to refactor watch_query to work for Win\")\ndef test_watch_query_full():\n    \"\"\"Test that `watch_query`:\n\n    * Returns the expected results.\n    * Executes the defined times inside the given interval, in this case with\n      a 0.3 seconds wait, it should execute 4 times inside a 1 seconds\n      interval.\n    * Stops at Ctrl-C\n\n    \"\"\"\n    watch_seconds = 0.3\n    wait_interval = 1\n    expected_value = \"1\"\n    query = f\"SELECT {expected_value}\"\n    expected_preamble = f\"> {query}\"\n    expected_results = [4, 5, 6, 7]  # Python 3.14 is skipping ahead to 6 or 7\n    ctrl_c_process = send_ctrl_c(wait_interval)\n    with db_connection().cursor() as cur:\n        results = list(mycli.packages.special.iocommands.watch_query(arg=f\"{watch_seconds} {query}\", cur=cur))\n    ctrl_c_process.join(1)\n    assert len(results) in expected_results\n    for result in results:\n        assert result.preamble == expected_preamble\n        assert result.header[0] == expected_value\n\n\n@dbtest\n@patch(\"click.clear\")\ndef test_watch_query_clear(clear_mock):\n    \"\"\"Test that the screen is cleared with the -c flag of `watch` command\n    before execute the query.\"\"\"\n    with db_connection().cursor() as cur:\n        watch_gen = mycli.packages.special.iocommands.watch_query(arg=\"0.1 -c select 1;\", cur=cur)\n        assert not clear_mock.called\n        next(watch_gen)\n        assert clear_mock.called\n        clear_mock.reset_mock()\n        next(watch_gen)\n        assert clear_mock.called\n        clear_mock.reset_mock()\n\n\n@dbtest\ndef test_watch_query_bad_arguments():\n    \"\"\"Test different incorrect combinations of arguments for `watch`\n    command.\"\"\"\n    watch_query = mycli.packages.special.iocommands.watch_query\n    with db_connection().cursor() as cur:\n        with pytest.raises(ProgrammingError):\n            next(watch_query(\"a select 1;\", cur=cur))\n        with pytest.raises(ProgrammingError):\n            next(watch_query(\"-a select 1;\", cur=cur))\n        with pytest.raises(ProgrammingError):\n            next(watch_query(\"1 -a select 1;\", cur=cur))\n        with pytest.raises(ProgrammingError):\n            next(watch_query(\"-c -a select 1;\", cur=cur))\n\n\n@dbtest\n@patch(\"click.clear\")\ndef test_watch_query_interval_clear(clear_mock):\n    \"\"\"Test `watch` command with interval and clear flag.\"\"\"\n\n    def test_asserts(gen):\n        clear_mock.reset_mock()\n        start = time()\n        next(gen)\n        assert clear_mock.called\n        next(gen)\n        exec_time = time() - start\n        assert exec_time > seconds and exec_time < (seconds + seconds)\n\n    seconds = 1.0\n    watch_query = mycli.packages.special.iocommands.watch_query\n    with db_connection().cursor() as cur:\n        test_asserts(watch_query(f\"{seconds} -c select 1;\", cur=cur))\n        test_asserts(watch_query(f\"-c {seconds} select 1;\", cur=cur))\n\n\ndef test_split_sql_by_delimiter():\n    for delimiter_str in (\";\", \"$\", \"😀\"):\n        mycli.packages.special.set_delimiter(delimiter_str)\n        sql_input = f\"select 1{delimiter_str} select \\ufffc2\"\n        queries = (\"select 1\", \"select \\ufffc2\")\n        for query, parsed_query in zip(queries, mycli.packages.special.split_queries(sql_input), strict=True):\n            assert query == parsed_query\n\n\ndef test_switch_delimiter_within_query():\n    mycli.packages.special.set_delimiter(\";\")\n    sql_input = \"select 1; delimiter $$ select 2 $$ select 3 $$\"\n    queries = (\"select 1\", \"delimiter $$ select 2 $$ select 3 $$\")\n    for query, parsed_query in zip(queries, mycli.packages.special.split_queries(sql_input), strict=True):\n        assert query == parsed_query\n\n\ndef test_set_delimiter():\n    for delim in (\"foo\", \"bar\"):\n        mycli.packages.special.set_delimiter(delim)\n        assert mycli.packages.special.get_current_delimiter() == delim\n\n\ndef teardown_function():\n    mycli.packages.special.set_delimiter(\";\")\n"
  },
  {
    "path": "test/test_sqlexecute.py",
    "content": "# type: ignore\n\nfrom datetime import time\nimport os\n\nfrom prompt_toolkit.formatted_text import FormattedText\nimport pymysql\nimport pytest\n\nfrom mycli.constants import TEST_DATABASE\nfrom mycli.sqlexecute import ServerInfo, ServerSpecies\nfrom test.utils import dbtest, is_expanded_output, run, set_expanded_output\n\n\ndef assert_result_equal(\n    result,\n    preamble=None,\n    header=None,\n    rows=None,\n    status=None,\n    status_plain=None,\n    postamble=None,\n    auto_status=True,\n    assert_contains=False,\n):\n    \"\"\"Assert that an sqlexecute.run() result matches the expected values.\"\"\"\n    if status_plain is None and auto_status and rows:\n        status_plain = f\"{len(rows)} row{'s' if len(rows) > 1 else ''} in set\"\n        status = FormattedText([('', status_plain)])\n    fields = {\n        \"preamble\": preamble,\n        \"header\": header,\n        \"rows\": rows,\n        \"postamble\": postamble,\n        \"status\": status,\n        \"status_plain\": status_plain,\n    }\n\n    if assert_contains:\n        # Do a loose match on the results using the *in* operator.\n        for key, field in fields.items():\n            if field:\n                assert field in result[0][key]\n    else:\n        # Do an exact match on the fields.\n        assert result == [fields]\n\n\n@dbtest\ndef test_timediff_negative_value(executor):\n    sql = \"select timediff('2020-11-11 01:01:01', '2020-11-11 01:02:01')\"\n    result = run(executor, sql)\n    # negative value comes back as str\n    assert result[0][\"rows\"][0][0] == \"-00:01:00\"\n\n\n@dbtest\ndef test_timediff_positive_value(executor):\n    sql = \"select timediff('2020-11-11 01:02:01', '2020-11-11 01:01:01')\"\n    result = run(executor, sql)\n    # positive value comes back as datetime.time\n    assert result[0][\"rows\"][0][0] == time(0, 1)\n\n\n@dbtest\ndef test_get_result_status_without_warning(executor):\n    sql = \"select 1\"\n    result = run(executor, sql)\n    assert result[0][\"status_plain\"] == \"1 row in set\"\n\n\n@dbtest\ndef test_get_result_status_with_warning(executor):\n    sql = \"SELECT 1 + '0 foo'\"\n    result = run(executor, sql)\n    assert result[0][\"status\"] == FormattedText([\n        ('', '1 row in set'),\n        ('', ', '),\n        ('class:output.status.warning-count', '1 warning'),\n    ])\n    assert result[0][\"status_plain\"] == \"1 row in set, 1 warning\"\n\n\n@dbtest\ndef test_conn(executor):\n    run(executor, \"\"\"create table test(a text)\"\"\")\n    run(executor, \"\"\"insert into test values('abc')\"\"\")\n    results = run(executor, \"\"\"select * from test\"\"\")\n\n    assert_result_equal(results, header=[\"a\"], rows=[(\"abc\",)])\n\n\n@dbtest\ndef test_bools(executor):\n    run(executor, \"\"\"create table test(a boolean)\"\"\")\n    run(executor, \"\"\"insert into test values(True)\"\"\")\n    results = run(executor, \"\"\"select * from test\"\"\")\n\n    assert_result_equal(results, header=[\"a\"], rows=[(1,)])\n\n\n@dbtest\ndef test_binary(executor):\n    run(executor, \"\"\"create table bt(geom linestring NOT NULL)\"\"\")\n    run(executor, \"INSERT INTO bt VALUES (ST_GeomFromText('LINESTRING(116.37604 39.73979,116.375 39.73965)'));\")\n    results = run(executor, \"\"\"select * from bt\"\"\")\n\n    geom = (\n        b\"\\x00\\x00\\x00\\x00\\x01\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x009\\x7f\\x13\\n\"\n        b\"\\x11\\x18]@4\\xf4Op\\xb1\\xdeC@\\x00\\x00\\x00\\x00\\x00\\x18]@B>\\xe8\\xd9\"\n        b\"\\xac\\xdeC@\"\n    )\n\n    assert_result_equal(results, header=[\"geom\"], rows=[(geom,)])\n\n\n@dbtest\ndef test_table_and_columns_query(executor):\n    run(executor, \"create table a(x text, y text)\")\n    run(executor, \"create table b(z text)\")\n\n    assert set(executor.tables()) == {(\"a\",), (\"b\",)}\n    assert set(executor.table_columns()) == {(\"a\", \"x\"), (\"a\", \"y\"), (\"b\", \"z\")}\n\n\n@dbtest\ndef test_database_list(executor):\n    databases = executor.databases()\n    assert TEST_DATABASE in databases\n\n\n@dbtest\ndef test_invalid_syntax(executor):\n    with pytest.raises(pymysql.ProgrammingError) as excinfo:\n        run(executor, \"invalid syntax!\")\n    assert \"You have an error in your SQL syntax;\" in str(excinfo.value)\n\n\n@dbtest\ndef test_invalid_column_name(executor):\n    with pytest.raises(pymysql.err.OperationalError) as excinfo:\n        run(executor, \"select invalid command\")\n    assert \"Unknown column 'invalid' in 'field list'\" in str(excinfo.value)\n\n\n@dbtest\ndef test_unicode_support_in_output(executor):\n    run(executor, \"create table unicodechars(t text)\")\n    run(executor, \"insert into unicodechars (t) values ('é')\")\n\n    # See issue #24, this raises an exception without proper handling\n    results = run(executor, \"select * from unicodechars\")\n    assert_result_equal(results, header=[\"t\"], rows=[(\"é\",)])\n\n\n@dbtest\ndef test_multiple_queries_same_line(executor):\n    results = run(executor, \"select 'foo'; select 'bar'\")\n\n    expected = [\n        {\n            \"preamble\": None,\n            \"header\": [\"foo\"],\n            \"rows\": [(\"foo\",)],\n            \"postamble\": None,\n            \"status_plain\": \"1 row in set\",\n            'status': FormattedText([('', '1 row in set')]),\n        },\n        {\n            \"preamble\": None,\n            \"header\": [\"bar\"],\n            \"rows\": [(\"bar\",)],\n            \"postamble\": None,\n            \"status_plain\": \"1 row in set\",\n            'status': FormattedText([('', '1 row in set')]),\n        },\n    ]\n    assert expected == results\n\n\n@dbtest\ndef test_multiple_queries_same_line_syntaxerror(executor):\n    with pytest.raises(pymysql.ProgrammingError) as excinfo:\n        run(executor, \"select 'foo'; invalid syntax\")\n    assert \"You have an error in your SQL syntax;\" in str(excinfo.value)\n\n\n@dbtest\n@pytest.mark.skipif(os.name == \"nt\", reason=\"Bug: fails on Windows, needs fixing, singleton of FQ not working right\")\ndef test_favorite_query(executor):\n    set_expanded_output(False)\n    run(executor, \"create table test(a text)\")\n    run(executor, \"insert into test values('abc')\")\n    run(executor, \"insert into test values('def')\")\n\n    results = run(executor, \"\\\\fs test-a select * from test where a like 'a%'\")\n    assert_result_equal(results, status=\"Saved.\", status_plain=\"Saved.\")\n\n    results = run(executor, \"\\\\f test-a\")\n    assert_result_equal(results, preamble=\"> select * from test where a like 'a%'\", header=[\"a\"], rows=[(\"abc\",)], auto_status=False)\n\n    results = run(executor, \"\\\\fd test-a\")\n    assert_result_equal(results, status=\"test-a: Deleted.\", status_plain=\"test-a: Deleted.\")\n\n\n@dbtest\n@pytest.mark.skipif(os.name == \"nt\", reason=\"Bug: fails on Windows, needs fixing, singleton of FQ not working right\")\ndef test_favorite_query_multiple_statement(executor):\n    set_expanded_output(False)\n    run(executor, \"create table test(a text)\")\n    run(executor, \"insert into test values('abc')\")\n    run(executor, \"insert into test values('def')\")\n\n    results = run(executor, \"\\\\fs test-ad select * from test where a like 'a%'; select * from test where a like 'd%'\")\n    assert_result_equal(results, status=\"Saved.\", status_plain=\"Saved.\")\n\n    results = run(executor, \"\\\\f test-ad\")\n    expected = [\n        {\n            \"preamble\": \"> select * from test where a like 'a%'\",\n            \"header\": [\"a\"],\n            \"rows\": [(\"abc\",)],\n            \"postamble\": None,\n            \"status\": None,\n            \"status_plain\": None,\n        },\n        {\n            \"preamble\": \"> select * from test where a like 'd%'\",\n            \"header\": [\"a\"],\n            \"rows\": [(\"def\",)],\n            \"postamble\": None,\n            \"status\": None,\n            \"status_plain\": None,\n        },\n    ]\n    assert expected == results\n\n    results = run(executor, \"\\\\fd test-ad\")\n    assert_result_equal(results, status=\"test-ad: Deleted.\", status_plain=\"test-ad: Deleted.\")\n\n\n@dbtest\n@pytest.mark.skipif(os.name == \"nt\", reason=\"Bug: fails on Windows, needs fixing, singleton of FQ not working right\")\ndef test_favorite_query_expanded_output(executor):\n    set_expanded_output(False)\n    run(executor, \"\"\"create table test(a text)\"\"\")\n    run(executor, \"\"\"insert into test values('abc')\"\"\")\n\n    results = run(executor, \"\\\\fs test-ae select * from test\")\n    assert_result_equal(results, status=\"Saved.\", status_plain=\"Saved.\")\n\n    results = run(executor, \"\\\\f test-ae \\\\G\")\n    assert is_expanded_output() is True\n    assert_result_equal(results, preamble=\"> select * from test\", header=[\"a\"], rows=[(\"abc\",)], auto_status=False)\n\n    set_expanded_output(False)\n\n    results = run(executor, \"\\\\fd test-ae\")\n    assert_result_equal(results, status=\"test-ae: Deleted.\", status_plain=\"test-ae: Deleted.\")\n\n\n@dbtest\ndef test_collapsed_output_special_command(executor):\n    set_expanded_output(True)\n    run(executor, \"select 1\\\\g\")\n    assert is_expanded_output() is False\n\n\n@dbtest\ndef test_special_command(executor):\n    results = run(executor, \"\\\\?\")\n    assert_result_equal(results, rows=(\"quit\", \"\\\\q\", \"quit\", \"Quit.\"), header=\"Command\", assert_contains=True, auto_status=False)\n\n\n@dbtest\ndef test_cd_command_without_a_folder_name(executor):\n    results = run(executor, \"system cd\")\n    assert_result_equal(\n        results, status=\"Exactly one directory name must be provided.\", status_plain=\"Exactly one directory name must be provided.\"\n    )\n\n\n@dbtest\ndef test_cd_command_with_one_nonexistent_folder_name(executor):\n    results = run(executor, 'system cd nonexistent_folder_name')\n    assert_result_equal(results, status='No such file or directory', status_plain='No such file or directory')\n\n\n@dbtest\ndef test_cd_command_with_one_real_folder_name(executor):\n    results = run(executor, 'system cd screenshots')\n    # todo would be better to capture stderr but there was a problem with capsys\n    assert results[0]['status_plain'] is None\n\n\n@dbtest\ndef test_cd_command_with_two_folder_names(executor):\n    results = run(executor, \"system cd one two\")\n    assert_result_equal(\n        results, status='Exactly one directory name must be provided.', status_plain='Exactly one directory name must be provided.'\n    )\n\n\n@dbtest\ndef test_cd_command_unbalanced(executor):\n    results = run(executor, \"system cd 'one\")\n    assert_result_equal(\n        results,\n        status='Cannot parse system command: No closing quotation',\n        status_plain='Cannot parse system command: No closing quotation',\n    )\n\n\n@dbtest\ndef test_system_command_not_found(executor):\n    results = run(executor, \"system xyz\")\n    if os.name == \"nt\":\n        assert_result_equal(results, status_plain=\"OSError: The system cannot find the file specified\", assert_contains=True)\n    else:\n        assert_result_equal(results, status_plain=\"OSError: No such file or directory\", assert_contains=True)\n\n\n@dbtest\ndef test_system_command_output(executor):\n    eol = os.linesep\n    test_dir = os.path.abspath(os.path.dirname(__file__))\n    test_file_path = os.path.join(test_dir, \"test.txt\")\n    results = run(executor, f\"system cat {test_file_path}\")\n    assert_result_equal(results, preamble=f\"mycli rocks!{eol}\")\n\n\n@dbtest\ndef test_cd_command_current_dir(executor):\n    test_path = os.path.abspath(os.path.dirname(__file__))\n    run(executor, f\"system cd {test_path}\")\n    assert os.getcwd() == test_path\n\n\n@dbtest\ndef test_unicode_support(executor):\n    results = run(executor, \"SELECT '日本語' AS japanese;\")\n    assert_result_equal(results, header=[\"japanese\"], rows=[(\"日本語\",)])\n\n\n@dbtest\ndef test_timestamp_null(executor):\n    run(executor, \"\"\"create table ts_null(a timestamp null)\"\"\")\n    run(executor, \"\"\"insert into ts_null values(null)\"\"\")\n    results = run(executor, \"\"\"select * from ts_null\"\"\")\n    assert_result_equal(results, header=[\"a\"], rows=[(None,)])\n\n\n@dbtest\ndef test_datetime_null(executor):\n    run(executor, \"\"\"create table dt_null(a datetime null)\"\"\")\n    run(executor, \"\"\"insert into dt_null values(null)\"\"\")\n    results = run(executor, \"\"\"select * from dt_null\"\"\")\n    assert_result_equal(results, header=[\"a\"], rows=[(None,)])\n\n\n@dbtest\ndef test_date_null(executor):\n    run(executor, \"\"\"create table date_null(a date null)\"\"\")\n    run(executor, \"\"\"insert into date_null values(null)\"\"\")\n    results = run(executor, \"\"\"select * from date_null\"\"\")\n    assert_result_equal(results, header=[\"a\"], rows=[(None,)])\n\n\n@dbtest\ndef test_time_null(executor):\n    run(executor, \"\"\"create table time_null(a time null)\"\"\")\n    run(executor, \"\"\"insert into time_null values(null)\"\"\")\n    results = run(executor, \"\"\"select * from time_null\"\"\")\n    assert_result_equal(results, header=[\"a\"], rows=[(None,)])\n\n\n@dbtest\ndef test_multiple_results(executor):\n    query = \"\"\"CREATE PROCEDURE dmtest()\n        BEGIN\n          SELECT 1;\n          SELECT 2;\n        END\"\"\"\n    executor.conn.cursor().execute(query)\n\n    results = run(executor, \"call dmtest;\")\n    expected = [\n        {\n            \"preamble\": None,\n            \"header\": [\"1\"],\n            \"rows\": [(1,)],\n            \"postamble\": None,\n            \"status_plain\": \"1 row in set\",\n            'status': FormattedText([('', '1 row in set')]),\n        },\n        {\n            \"preamble\": None,\n            \"header\": [\"2\"],\n            \"rows\": [(2,)],\n            \"postamble\": None,\n            \"status_plain\": \"1 row in set\",\n            'status': FormattedText([('', '1 row in set')]),\n        },\n    ]\n    assert results == expected\n\n\n@pytest.mark.parametrize(\n    \"version_string, species, parsed_version_string, version\",\n    (\n        (\"5.7.25-TiDB-v6.1.0\", \"TiDB\", \"6.1.0\", 60100),\n        (\"8.0.11-TiDB-v7.2.0-alpha-69-g96e9e68daa\", \"TiDB\", \"7.2.0\", 70200),\n        (\"5.7.32-35\", \"Percona\", \"5.7.32\", 50732),\n        (\"5.7.32-0ubuntu0.18.04.1\", \"MySQL\", \"5.7.32\", 50732),\n        (\"10.5.8-MariaDB-1:10.5.8+maria~focal\", \"MariaDB\", \"10.5.8\", 100508),\n        (\"5.5.5-10.5.8-MariaDB-1:10.5.8+maria~focal\", \"MariaDB\", \"10.5.8\", 100508),\n        (\"5.0.16-pro-nt-log\", \"MySQL\", \"5.0.16\", 50016),\n        (\"5.1.5a-alpha\", \"MySQL\", \"5.1.5\", 50105),\n        (\"unexpected version string\", None, \"\", 0),\n        (\"\", None, \"\", 0),\n        (None, None, \"\", 0),\n    ),\n)\ndef test_version_parsing(version_string, species, parsed_version_string, version):\n    server_info = ServerInfo.from_version_string(version_string)\n    assert (server_info.species and server_info.species.name) == species or ServerSpecies.MySQL\n    assert server_info.version_str == parsed_version_string\n    assert server_info.version == version\n"
  },
  {
    "path": "test/test_tabular_output.py",
    "content": "# type: ignore\n\n\"\"\"Test the sql output adapter.\"\"\"\n\nfrom textwrap import dedent\n\nfrom pymysql.constants import FIELD_TYPE\nimport pytest\n\nfrom mycli.main import MyCli\nfrom mycli.packages.sqlresult import SQLResult\nfrom test.utils import HOST, PASSWORD, PORT, USER, dbtest\n\n\n@pytest.fixture\ndef mycli():\n    cli = MyCli()\n    cli.connect(None, USER, PASSWORD, HOST, PORT, None, init_command=None)\n    yield cli\n    cli.sqlexecute.conn.close()\n\n\n@dbtest\ndef test_sql_output(mycli):\n    \"\"\"Test the sql output adapter.\"\"\"\n    header = [\"letters\", \"number\", \"optional\", \"float\", \"binary\"]\n\n    class FakeCursor:\n        def __init__(self):\n            self.data = [(\"abc\", 1, None, 10.0, b\"\\xaa\"), (\"d\", 456, \"1\", 0.5, b\"\\xaa\\xbb\")]\n            self.description = [\n                (None, FIELD_TYPE.VARCHAR),\n                (None, FIELD_TYPE.LONG),\n                (None, FIELD_TYPE.LONG),\n                (None, FIELD_TYPE.FLOAT),\n                (None, FIELD_TYPE.BLOB),\n            ]\n\n        def __iter__(self):\n            return self\n\n        def __next__(self):\n            if self.data:\n                return self.data.pop(0)\n            else:\n                raise StopIteration()\n\n        def description(self):\n            return self.description\n\n    # Test sql-update output format\n    assert list(mycli.change_table_format(\"sql-update\")) == [SQLResult(status=\"Changed table format to sql-update\")]\n    mycli.main_formatter.query = \"\"\n    mycli.redirect_formatter.query = \"\"\n    output = mycli.format_sqlresult(SQLResult(header=header, rows=FakeCursor()))\n    actual = \"\\n\".join(output)\n    assert actual == dedent(\"\"\"\\\n            UPDATE `DUAL` SET\n              `number` = 1\n            , `optional` = NULL\n            , `float` = 10.0e0\n            , `binary` = 0xaa\n            WHERE `letters` = 'abc';\n            UPDATE `DUAL` SET\n              `number` = 456\n            , `optional` = '1'\n            , `float` = 0.5e0\n            , `binary` = 0xaabb\n            WHERE `letters` = 'd';\"\"\")\n    # Test sql-update-2 output format\n    assert list(mycli.change_table_format(\"sql-update-2\")) == [SQLResult(status=\"Changed table format to sql-update-2\")]\n    mycli.main_formatter.query = \"\"\n    mycli.redirect_formatter.query = \"\"\n    output = mycli.format_sqlresult(SQLResult(header=header, rows=FakeCursor()))\n    assert \"\\n\".join(output) == dedent(\"\"\"\\\n            UPDATE `DUAL` SET\n              `optional` = NULL\n            , `float` = 10.0e0\n            , `binary` = 0xaa\n            WHERE `letters` = 'abc' AND `number` = 1;\n            UPDATE `DUAL` SET\n              `optional` = '1'\n            , `float` = 0.5e0\n            , `binary` = 0xaabb\n            WHERE `letters` = 'd' AND `number` = 456;\"\"\")\n    # Test sql-insert output format (without table name)\n    assert list(mycli.change_table_format(\"sql-insert\")) == [SQLResult(status=\"Changed table format to sql-insert\")]\n    mycli.main_formatter.query = \"\"\n    mycli.redirect_formatter.query = \"\"\n    output = mycli.format_sqlresult(SQLResult(header=header, rows=FakeCursor()))\n    assert \"\\n\".join(output) == dedent(\"\"\"\\\n            INSERT INTO `DUAL` (`letters`, `number`, `optional`, `float`, `binary`) VALUES\n              ('abc', 1, NULL, 10.0e0, 0xaa)\n            , ('d', 456, '1', 0.5e0, 0xaabb)\n            ;\"\"\")\n    # Test sql-insert output format (with table name)\n    assert list(mycli.change_table_format(\"sql-insert\")) == [SQLResult(status=\"Changed table format to sql-insert\")]\n    mycli.main_formatter.query = \"SELECT * FROM `table`\"\n    mycli.redirect_formatter.query = \"SELECT * FROM `table`\"\n    output = mycli.format_sqlresult(SQLResult(header=header, rows=FakeCursor()))\n    assert \"\\n\".join(output) == dedent(\"\"\"\\\n            INSERT INTO table (`letters`, `number`, `optional`, `float`, `binary`) VALUES\n              ('abc', 1, NULL, 10.0e0, 0xaa)\n            , ('d', 456, '1', 0.5e0, 0xaabb)\n            ;\"\"\")\n    # Test sql-insert output format (with database + table name)\n    assert list(mycli.change_table_format(\"sql-insert\")) == [SQLResult(status=\"Changed table format to sql-insert\")]\n    mycli.main_formatter.query = \"SELECT * FROM `database`.`table`\"\n    mycli.redirect_formatter.query = \"SELECT * FROM `database`.`table`\"\n    output = mycli.format_sqlresult(SQLResult(header=header, rows=FakeCursor()))\n    assert \"\\n\".join(output) == dedent(\"\"\"\\\n            INSERT INTO database.table (`letters`, `number`, `optional`, `float`, `binary`) VALUES\n              ('abc', 1, NULL, 10.0e0, 0xaa)\n            , ('d', 456, '1', 0.5e0, 0xaabb)\n            ;\"\"\")\n    # Test binary output format is a hex string\n    assert list(mycli.change_table_format(\"psql\")) == [SQLResult(status=\"Changed table format to psql\")]\n    output = mycli.format_sqlresult(SQLResult(header=header, rows=FakeCursor()))\n    assert '0xaabb' in '\\n'.join(output)\n\n\n@dbtest\ndef test_postamble_output(mycli):\n    \"\"\"Test the postamble output property.\"\"\"\n    header = ['letters', 'number', 'optional', 'float']\n\n    class FakeCursor:\n        def __init__(self):\n            self.data = [('abc', 1, None, 10.0)]\n            self.description = [\n                (None, FIELD_TYPE.VARCHAR),\n                (None, FIELD_TYPE.LONG),\n                (None, FIELD_TYPE.LONG),\n                (None, FIELD_TYPE.FLOAT),\n            ]\n\n        def __iter__(self):\n            return self\n\n        def __next__(self):\n            if self.data:\n                return self.data.pop(0)\n            else:\n                raise StopIteration()\n\n        def description(self):\n            return self.description\n\n    postamble = 'postamble:\\nfooter content'\n    mycli.change_table_format('ascii')\n    mycli.main_formatter.query = ''\n    output = mycli.format_sqlresult(SQLResult(header=header, rows=FakeCursor(), postamble=postamble))\n    actual = \"\\n\".join(output)\n    assert actual.endswith(postamble)\n"
  },
  {
    "path": "test/utils.py",
    "content": "# type: ignore\n\nimport multiprocessing\nimport os\nimport platform\nimport signal\nimport time\n\nimport pymysql\nimport pytest\n\nfrom mycli.constants import (\n    DEFAULT_CHARSET,\n    DEFAULT_HOST,\n    DEFAULT_PORT,\n    DEFAULT_USER,\n    TEST_DATABASE,\n)\nfrom mycli.main import special\n\nDATABASE = TEST_DATABASE\nPASSWORD = os.getenv(\"PYTEST_PASSWORD\")\nUSER = os.getenv(\"PYTEST_USER\", DEFAULT_USER)\nHOST = os.getenv(\"PYTEST_HOST\", DEFAULT_HOST)\nPORT = int(os.getenv(\"PYTEST_PORT\", DEFAULT_PORT))\nCHARACTER_SET = os.getenv(\"PYTEST_CHARSET\", DEFAULT_CHARSET)\nSSH_USER = os.getenv(\"PYTEST_SSH_USER\", None)\nSSH_HOST = os.getenv(\"PYTEST_SSH_HOST\", None)\nSSH_PORT = int(os.getenv(\"PYTEST_SSH_PORT\", \"22\"))\nTEMPFILE_PREFIX = 'mycli_test_suite_'\n\n\ndef db_connection(dbname=None):\n    conn = pymysql.connect(user=USER, host=HOST, port=PORT, database=dbname, password=PASSWORD, charset=CHARACTER_SET, local_infile=False)\n    conn.autocommit = True\n    return conn\n\n\ntry:\n    db_connection()\n    CAN_CONNECT_TO_DB = True\nexcept Exception:\n    CAN_CONNECT_TO_DB = False\n\ndbtest = pytest.mark.skipif(not CAN_CONNECT_TO_DB, reason=f\"Need a mysql instance at {DEFAULT_HOST} accessible by user '{DEFAULT_USER}'\")\n\n\ndef create_db(dbname):\n    with db_connection().cursor() as cur:\n        try:\n            cur.execute(f\"DROP DATABASE IF EXISTS {TEST_DATABASE}\")\n            cur.execute(f\"CREATE DATABASE {TEST_DATABASE}\")\n        except Exception:\n            pass\n\n\ndef run(executor, sql, rows_as_list=True):\n    \"\"\"Return string output for the sql to be run.\"\"\"\n    results = []\n\n    for result in executor.run(sql):\n        rows = list(result.rows) if (rows_as_list and result.rows) else result.rows\n        results.append({\n            \"preamble\": result.preamble,\n            \"header\": result.header,\n            \"rows\": rows,\n            \"postamble\": result.postamble,\n            \"status\": result.status,\n            \"status_plain\": result.status_plain,\n        })\n\n    return results\n\n\ndef set_expanded_output(is_expanded):\n    \"\"\"Pass-through for the tests.\"\"\"\n    return special.set_expanded_output(is_expanded)\n\n\ndef is_expanded_output():\n    \"\"\"Pass-through for the tests.\"\"\"\n    return special.is_expanded_output()\n\n\ndef send_ctrl_c_to_pid(pid, wait_seconds):\n    \"\"\"Sends a Ctrl-C like signal to the given `pid` after `wait_seconds`\n    seconds.\"\"\"\n    time.sleep(wait_seconds)\n    system_name = platform.system()\n    if system_name == \"Windows\":\n        os.kill(pid, signal.CTRL_C_EVENT)\n    else:\n        os.kill(pid, signal.SIGINT)\n\n\ndef send_ctrl_c(wait_seconds):\n    \"\"\"Create a process that sends a Ctrl-C like signal to the current process\n    after `wait_seconds` seconds.\n\n    Returns the `multiprocessing.Process` created.\n\n    \"\"\"\n    ctrl_c_process = multiprocessing.Process(target=send_ctrl_c_to_pid, args=(os.getpid(), wait_seconds))\n    ctrl_c_process.start()\n    return ctrl_c_process\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py\n\n[testenv]\nskip_install = true\ndeps = uv\npassenv = PYTEST_HOST\n    PYTEST_USER\n    PYTEST_PASSWORD\n    PYTEST_PORT\n    PYTEST_CHARSET\ncommands = uv pip install -e .[dev,ssh,llm]\n        coverage run -m pytest -v test\n        coverage report -m\n        behave test/features\n\n[testenv:style]\nskip_install = true\ndeps = ruff\ncommands = ruff check\n           ruff format --diff\n"
  }
]