Adding upstream version 1.1.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
9c1e264be3
commit
f19547bec9
22 changed files with 4449 additions and 0 deletions
47
.github/workflows/publish.yml
vendored
Normal file
47
.github/workflows/publish.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
name: Build and Publish Package
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
publish-package:
|
||||
if: ${{ github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out harlequin-postgres main branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: 1.6.1
|
||||
- name: Configure poetry
|
||||
run: poetry config --no-interaction pypi-token.pypi ${{ secrets.HARLEQUIN_PG_PYPI_TOKEN }}
|
||||
- name: Get harlequin-postgres Version
|
||||
id: harlequin_pg_version
|
||||
run: echo "harlequin_pg_version=$(poetry version --short)" >> $GITHUB_OUTPUT
|
||||
- name: Build package
|
||||
run: poetry build --no-interaction
|
||||
- name: Publish package to PyPI
|
||||
run: poetry publish --no-interaction
|
||||
- name: Create a Github Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: v${{ steps.harlequin_pg_version.outputs.harlequin_pg_version }}
|
||||
target_commitish: main
|
||||
token: ${{ secrets.HARLEQUIN_PG_RELEASE_TOKEN }}
|
||||
body_path: CHANGELOG.md
|
||||
files: |
|
||||
LICENSE
|
||||
dist/*harlequin*.whl
|
||||
dist/*harlequin*.tar.gz
|
57
.github/workflows/release.yml
vendored
Normal file
57
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
name: Create Release Branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
newVersion:
|
||||
description: A version number for this release (e.g., "0.1.0")
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out harlequin-postgres main branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: 1.6.1
|
||||
- name: Create release branch
|
||||
run: |
|
||||
git checkout -b release/v${{ github.event.inputs.newVersion }}
|
||||
git push --set-upstream origin release/v${{ github.event.inputs.newVersion }}
|
||||
- name: Bump version
|
||||
run: poetry version ${{ github.event.inputs.newVersion }} --no-interaction
|
||||
- name: Ensure package can be built
|
||||
run: poetry build --no-interaction
|
||||
- name: Update CHANGELOG
|
||||
uses: thomaseizinger/keep-a-changelog-new-release@v1
|
||||
with:
|
||||
version: ${{ github.event.inputs.newVersion }}
|
||||
- name: Commit Changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Bumps version to ${{ github.event.inputs.newVersion }}
|
||||
- name: Create pull request into main
|
||||
uses: thomaseizinger/create-pull-request@1.3.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
head: release/v${{ github.event.inputs.newVersion }}
|
||||
base: main
|
||||
title: v${{ github.event.inputs.newVersion }}
|
||||
body: >
|
||||
This PR was automatically generated. It bumps the version number
|
||||
in pyproject.toml and updates CHANGELOG.md. You may have to close
|
||||
this PR and reopen it to get the required checks to run.
|
163
.gitignore
vendored
Normal file
163
.gitignore
vendored
Normal file
|
@ -0,0 +1,163 @@
|
|||
profile.html
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
Pipfile
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
95
CHANGELOG.md
Normal file
95
CHANGELOG.md
Normal file
|
@ -0,0 +1,95 @@
|
|||
# Harlequin-Postgres CHANGELOG
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.1.1] - 2025-02-05
|
||||
|
||||
- This adapter now supports `infinity` and `-infinity` dates and timestamps by loading their values as `date[time].max` or `date[time].min` ([tconbeer/harlequin#690](https://github.com/tconbeer/harlequin/issues/690)).
|
||||
|
||||
## [1.1.0] - 2025-01-27
|
||||
|
||||
- This adapter now lazy-loads the catalog, which will dramatically improve the catalog performance for large databases with thousands of objects.
|
||||
- This adapter now implements interactions for catalog items, like dropping tables, setting the search path, etc.
|
||||
|
||||
## [1.0.0] - 2025-01-07
|
||||
|
||||
- Drops support for Python 3.8
|
||||
- Adds support for Python 3.13
|
||||
- Adds support for Harlequin 2.X
|
||||
|
||||
## [0.4.0] - 2024-08-20
|
||||
|
||||
- Upgrades client library to `psycopg3` (from `psycopg2`).
|
||||
- Adds an implementation of `connection_id` to improve catalog and history persistence.
|
||||
- Implements `cancel()` to interrupt in-flight queries.
|
||||
|
||||
## [0.3.0] - 2024-07-22
|
||||
|
||||
- Adds an implementation of `close` to gracefully close the connection pool on Harlequin shut-down.
|
||||
- Adds support for Harlequin Transaction Modes and manual transactions.
|
||||
|
||||
## [0.2.2] - 2024-01-09
|
||||
|
||||
- Sorts databases, schemas, and relations alphabetically; sorts columns ordinally. ([#10](https://github.com/tconbeer/harlequin-postgres/issues/10) - thank you [@frankbreetz](https://github.com/frankbreetz)!)
|
||||
|
||||
## [0.2.1] - 2023-12-14
|
||||
|
||||
- Lowercases inserted values for keyword completions.
|
||||
|
||||
## [0.2.0] - 2023-12-14
|
||||
|
||||
### Features
|
||||
|
||||
- Implements get_completions for keywords, functions, and settings.
|
||||
|
||||
## [0.1.3] - 2023-11-28
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Implements connection pools instead of sharing a connection across threads.
|
||||
|
||||
## [0.1.2] - 2023-11-27
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fixes issues with package metadata.
|
||||
|
||||
## [0.1.1] - 2023-11-27
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fixes typo in release script.
|
||||
|
||||
## [0.1.0] - 2023-11-27
|
||||
|
||||
### Features
|
||||
|
||||
- Adds a basic Postgres adapter with most common connection options.
|
||||
|
||||
[Unreleased]: https://github.com/tconbeer/harlequin-postgres/compare/1.1.1...HEAD
|
||||
|
||||
[1.1.1]: https://github.com/tconbeer/harlequin-postgres/compare/1.1.0...1.1.1
|
||||
|
||||
[1.1.0]: https://github.com/tconbeer/harlequin-postgres/compare/1.0.0...1.1.0
|
||||
|
||||
[1.0.0]: https://github.com/tconbeer/harlequin-postgres/compare/0.4.0...1.0.0
|
||||
|
||||
[0.4.0]: https://github.com/tconbeer/harlequin-postgres/compare/0.3.0...0.4.0
|
||||
|
||||
[0.3.0]: https://github.com/tconbeer/harlequin-postgres/compare/0.2.2...0.3.0
|
||||
|
||||
[0.2.2]: https://github.com/tconbeer/harlequin-postgres/compare/0.2.1...0.2.2
|
||||
|
||||
[0.2.1]: https://github.com/tconbeer/harlequin-postgres/compare/0.2.0...0.2.1
|
||||
|
||||
[0.2.0]: https://github.com/tconbeer/harlequin-postgres/compare/0.1.3...0.2.0
|
||||
|
||||
[0.1.3]: https://github.com/tconbeer/harlequin-postgres/compare/0.1.2...0.1.3
|
||||
|
||||
[0.1.2]: https://github.com/tconbeer/harlequin-postgres/compare/0.1.1...0.1.2
|
||||
|
||||
[0.1.1]: https://github.com/tconbeer/harlequin-postgres/compare/0.1.0...0.1.1
|
||||
|
||||
[0.1.0]: https://github.com/tconbeer/harlequin-postgres/compare/8611e628dc9d28b6a24817c761cd8a6da11a87ad...0.1.0
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Ted Conbeer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
25
Makefile
Normal file
25
Makefile
Normal file
|
@ -0,0 +1,25 @@
|
|||
.PHONY: check
|
||||
check:
|
||||
ruff format .
|
||||
ruff check . --fix
|
||||
mypy
|
||||
pytest
|
||||
|
||||
.PHONY: init
|
||||
init:
|
||||
docker-compose up -d
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
docker-compose down
|
||||
|
||||
.PHONY: serve
|
||||
serve:
|
||||
harlequin -P None -a postgres "postgresql://postgres:for-testing@localhost:5432/postgres"
|
||||
|
||||
.PHONY: psql
|
||||
psql:
|
||||
PGPASSWORD=for-testing psql -h localhost -p 5432 -U postgres
|
||||
|
||||
profile.html: $(wildcard src/**/*.py)
|
||||
pyinstrument -r html -o profile.html --from-path harlequin -a postgres "postgresql://postgres:for-testing@localhost:5432/postgres"
|
77
README.md
Normal file
77
README.md
Normal file
|
@ -0,0 +1,77 @@
|
|||
# harlequin-postgres
|
||||
|
||||
This repo provides the Harlequin adapter for Postgres.
|
||||
|
||||
## Installation
|
||||
|
||||
`harlequin-postgres` depends on `harlequin`, so installing this package will also install Harlequin.
|
||||
|
||||
### Using pip
|
||||
|
||||
To install this adapter into an activated virtual environment:
|
||||
```bash
|
||||
pip install harlequin-postgres
|
||||
```
|
||||
|
||||
### Using poetry
|
||||
|
||||
```bash
|
||||
poetry add harlequin-postgres
|
||||
```
|
||||
|
||||
### Using pipx
|
||||
|
||||
If you do not already have Harlequin installed:
|
||||
|
||||
```bash
|
||||
pip install harlequin-postgres
|
||||
```
|
||||
|
||||
If you would like to add the Postgres adapter to an existing Harlequin installation:
|
||||
|
||||
```bash
|
||||
pipx inject harlequin harlequin-postgres
|
||||
```
|
||||
|
||||
### As an Extra
|
||||
Alternatively, you can install Harlequin with the `postgres` extra:
|
||||
|
||||
```bash
|
||||
pip install harlequin[postgres]
|
||||
```
|
||||
|
||||
```bash
|
||||
poetry add harlequin[postgres]
|
||||
```
|
||||
|
||||
```bash
|
||||
pipx install harlequin[postgres]
|
||||
```
|
||||
|
||||
## Usage and Configuration
|
||||
|
||||
You can open Harlequin with the Postgres adapter by selecting it with the `-a` option and passing a [Posgres DSN](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING):
|
||||
|
||||
```bash
|
||||
harlequin -a postgres "postgres://my-user:my-pass@localhost:5432/my-database"
|
||||
```
|
||||
|
||||
You can also pass all or parts of the connection string as separate options. The following is equivalent to the above DSN:
|
||||
|
||||
```bash
|
||||
harlequin -a postgres -h localhost -p 5432 -U my-user --password my-pass -d my-database
|
||||
```
|
||||
|
||||
Many more options are available; to see the full list, run:
|
||||
|
||||
```bash
|
||||
harlequin --help
|
||||
```
|
||||
|
||||
## Manual Transactions
|
||||
|
||||
To use Manual transaction mode, click on the label in the Run Query Bar to toggle the transaction mode from Auto to Manual.
|
||||
|
||||
## Further Documentation
|
||||
|
||||
For more information, see the [Harlequin Docs](https://harlequin.sh/docs/postgres/index).
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
services:
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: for-testing
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
volumes:
|
||||
pgdata:
|
1561
poetry.lock
generated
Normal file
1561
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
68
pyproject.toml
Normal file
68
pyproject.toml
Normal file
|
@ -0,0 +1,68 @@
|
|||
[tool.poetry]
|
||||
name = "harlequin-postgres"
|
||||
version = "1.1.1"
|
||||
description = "A Harlequin adapter for Postgres."
|
||||
authors = ["Ted Conbeer <tconbeer@users.noreply.github.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
packages = [
|
||||
{ include = "harlequin_postgres", from = "src" },
|
||||
]
|
||||
|
||||
[tool.poetry.plugins."harlequin.adapter"]
|
||||
postgres = "harlequin_postgres:HarlequinPostgresAdapter"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.14"
|
||||
harlequin = ">=1.25,<3"
|
||||
psycopg = { version = "^3.2", extras = ["binary", "pool"]}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.5"
|
||||
pytest = "^7.4.3"
|
||||
mypy = "^1.10.0"
|
||||
pre-commit = "^3.5.0"
|
||||
importlib_metadata = { version = ">=4.6.0", python = "<3.10.0" }
|
||||
pyinstrument = "^4.6.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py39"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["A", "B", "E", "F", "I"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
files = [
|
||||
"src/**/*.py",
|
||||
"tests/**/*.py",
|
||||
]
|
||||
mypy_path = "src:stubs"
|
||||
|
||||
show_column_numbers = true
|
||||
|
||||
# show error messages from unrelated files
|
||||
follow_imports = "normal"
|
||||
|
||||
# be strict
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
check_untyped_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
strict_optional = true
|
||||
|
||||
warn_return_any = true
|
||||
warn_no_return = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_unused_configs = true
|
||||
|
||||
no_implicit_reexport = true
|
||||
strict_equality = true
|
3
src/harlequin_postgres/__init__.py
Normal file
3
src/harlequin_postgres/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from harlequin_postgres.adapter import HarlequinPostgresAdapter
|
||||
|
||||
__all__ = ["HarlequinPostgresAdapter"]
|
453
src/harlequin_postgres/adapter.py
Normal file
453
src/harlequin_postgres/adapter.py
Normal file
|
@ -0,0 +1,453 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from itertools import cycle
|
||||
from typing import Any, Sequence
|
||||
|
||||
from harlequin import (
|
||||
HarlequinAdapter,
|
||||
HarlequinCompletion,
|
||||
HarlequinConnection,
|
||||
HarlequinCursor,
|
||||
HarlequinTransactionMode,
|
||||
)
|
||||
from harlequin.catalog import Catalog, CatalogItem
|
||||
from harlequin.exception import HarlequinConnectionError, HarlequinQueryError
|
||||
from psycopg import Connection, Cursor, conninfo
|
||||
from psycopg.errors import QueryCanceled
|
||||
from psycopg.pq import TransactionStatus
|
||||
from psycopg_pool import ConnectionPool
|
||||
from textual_fastdatatable.backend import AutoBackendType
|
||||
|
||||
from harlequin_postgres.catalog import DatabaseCatalogItem
|
||||
from harlequin_postgres.cli_options import POSTGRES_OPTIONS
|
||||
from harlequin_postgres.completions import _get_completions
|
||||
from harlequin_postgres.loaders import register_inf_loaders
|
||||
|
||||
|
||||
class HarlequinPostgresCursor(HarlequinCursor):
|
||||
def __init__(self, conn: HarlequinPostgresConnection, cur: Cursor) -> None:
|
||||
self.conn = conn
|
||||
self.cur = cur
|
||||
# we need to copy the description from the cursor in case the results are
|
||||
# fetched and the cursor is closed before columns() is called.
|
||||
assert cur.description is not None
|
||||
self.description = cur.description.copy()
|
||||
self._limit: int | None = None
|
||||
|
||||
def columns(self) -> list[tuple[str, str]]:
|
||||
return [
|
||||
(col.name, self.conn._short_column_type_from_oid(col.type_code))
|
||||
for col in self.description
|
||||
]
|
||||
|
||||
def set_limit(self, limit: int) -> HarlequinPostgresCursor:
|
||||
self._limit = limit
|
||||
return self
|
||||
|
||||
def fetchall(self) -> AutoBackendType:
|
||||
try:
|
||||
if self._limit is None:
|
||||
return self.cur.fetchall()
|
||||
else:
|
||||
return self.cur.fetchmany(self._limit)
|
||||
except QueryCanceled:
|
||||
return []
|
||||
except Exception as e:
|
||||
raise HarlequinQueryError(
|
||||
msg=f"{e.__class__.__name__}: {e}",
|
||||
title="Harlequin encountered an error while executing your query.",
|
||||
) from e
|
||||
finally:
|
||||
self.cur.close()
|
||||
|
||||
|
||||
class HarlequinPostgresConnection(HarlequinConnection):
|
||||
def __init__(
|
||||
self,
|
||||
conn_str: Sequence[str],
|
||||
*_: Any,
|
||||
init_message: str = "",
|
||||
options: dict[str, Any],
|
||||
) -> None:
|
||||
self.init_message = init_message
|
||||
try:
|
||||
self.conn_info = conninfo.conninfo_to_dict(
|
||||
conninfo=conn_str[0] if conn_str else "", **options
|
||||
)
|
||||
except Exception as e:
|
||||
raise HarlequinConnectionError(
|
||||
msg=str(e),
|
||||
title=(
|
||||
"Harlequin could not connect to Postgres. "
|
||||
"Invalid connection string."
|
||||
),
|
||||
) from e
|
||||
try:
|
||||
raw_timeout = self.conn_info.get("connect_timeout")
|
||||
timeout = float(raw_timeout) if raw_timeout is not None else 30.0
|
||||
except (TypeError, ValueError) as e:
|
||||
raise HarlequinConnectionError(
|
||||
msg=str(e),
|
||||
title=(
|
||||
"Harlequin could not connect to Postgres. "
|
||||
"Invalid value for connection_timeout."
|
||||
),
|
||||
) from e
|
||||
try:
|
||||
self.pool: ConnectionPool = ConnectionPool(
|
||||
conninfo=conn_str[0] if conn_str and conn_str[0] else "",
|
||||
min_size=2,
|
||||
max_size=5,
|
||||
kwargs=options,
|
||||
open=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
self._main_conn: Connection = self.pool.getconn()
|
||||
except Exception as e:
|
||||
raise HarlequinConnectionError(
|
||||
msg=str(e), title="Harlequin could not connect to Postgres."
|
||||
) from e
|
||||
|
||||
self._transaction_modes = cycle(
|
||||
[
|
||||
HarlequinTransactionMode(label="Auto"),
|
||||
HarlequinTransactionMode(
|
||||
label="Manual",
|
||||
commit=self.commit,
|
||||
rollback=self.rollback,
|
||||
),
|
||||
]
|
||||
)
|
||||
self.toggle_transaction_mode()
|
||||
|
||||
def execute(self, query: str) -> HarlequinCursor | None:
|
||||
if (
|
||||
self.transaction_mode.label != "Auto"
|
||||
and self._main_conn.info.transaction_status == TransactionStatus.IDLE
|
||||
):
|
||||
cur = self._main_conn.cursor()
|
||||
cur.execute(query="begin;")
|
||||
cur.close()
|
||||
|
||||
try:
|
||||
cur = self._main_conn.cursor()
|
||||
cur.execute(query=query)
|
||||
except QueryCanceled:
|
||||
cur.close()
|
||||
return None
|
||||
except Exception as e:
|
||||
cur.close()
|
||||
self.rollback()
|
||||
raise HarlequinQueryError(
|
||||
msg=str(e),
|
||||
title="Harlequin encountered an error while executing your query.",
|
||||
) from e
|
||||
else:
|
||||
if cur.description is not None:
|
||||
return HarlequinPostgresCursor(self, cur)
|
||||
else:
|
||||
cur.close()
|
||||
return None
|
||||
|
||||
def cancel(self) -> None:
|
||||
self._main_conn.cancel_safe()
|
||||
|
||||
def commit(self) -> None:
|
||||
self._main_conn.commit()
|
||||
|
||||
def rollback(self) -> None:
|
||||
self._main_conn.rollback()
|
||||
|
||||
def get_catalog(self) -> Catalog:
|
||||
databases = self._get_databases()
|
||||
db_items: list[CatalogItem] = [
|
||||
DatabaseCatalogItem.from_label(label=db, connection=self)
|
||||
for (db,) in databases
|
||||
]
|
||||
return Catalog(items=db_items)
|
||||
|
||||
def get_completions(self) -> list[HarlequinCompletion]:
|
||||
conn: Connection = self.pool.getconn()
|
||||
completions = _get_completions(conn)
|
||||
self.pool.putconn(conn)
|
||||
return completions
|
||||
|
||||
def close(self) -> None:
|
||||
self.pool.putconn(self._main_conn)
|
||||
self.pool.close()
|
||||
|
||||
@property
|
||||
def transaction_mode(self) -> HarlequinTransactionMode:
|
||||
return self._transaction_mode
|
||||
|
||||
def toggle_transaction_mode(self) -> HarlequinTransactionMode:
|
||||
self._transaction_mode = next(self._transaction_modes)
|
||||
self._sync_transaction_mode()
|
||||
return self._transaction_mode
|
||||
|
||||
def _sync_transaction_mode(self) -> None:
|
||||
"""
|
||||
Sync this class's transaction mode with the main connection
|
||||
"""
|
||||
conn = self._main_conn
|
||||
if self.transaction_mode.label == "Auto":
|
||||
conn.autocommit = True
|
||||
conn.commit()
|
||||
else:
|
||||
conn.autocommit = False
|
||||
|
||||
def _get_databases(self) -> list[tuple[str]]:
|
||||
conn: Connection = self.pool.getconn()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
select datname
|
||||
from pg_database
|
||||
where
|
||||
datistemplate is false
|
||||
and datallowconn is true
|
||||
order by datname asc
|
||||
;"""
|
||||
)
|
||||
results: list[tuple[str]] = cur.fetchall()
|
||||
self.pool.putconn(conn)
|
||||
return results
|
||||
|
||||
def _get_schemas(self, dbname: str) -> list[tuple[str]]:
|
||||
conn: Connection = self.pool.getconn()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
select schema_name
|
||||
from information_schema.schemata
|
||||
where
|
||||
catalog_name = '{dbname}'
|
||||
and schema_name != 'information_schema'
|
||||
and schema_name not like 'pg_%'
|
||||
order by schema_name asc
|
||||
;"""
|
||||
)
|
||||
results: list[tuple[str]] = cur.fetchall()
|
||||
self.pool.putconn(conn)
|
||||
return results
|
||||
|
||||
def _get_relations(self, dbname: str, schema: str) -> list[tuple[str, str]]:
|
||||
conn: Connection = self.pool.getconn()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
select table_name, table_type
|
||||
from information_schema.tables
|
||||
where
|
||||
table_catalog = '{dbname}'
|
||||
and table_schema = '{schema}'
|
||||
order by table_name asc
|
||||
;"""
|
||||
)
|
||||
results: list[tuple[str, str]] = cur.fetchall()
|
||||
self.pool.putconn(conn)
|
||||
return results
|
||||
|
||||
def _get_columns(
|
||||
self, dbname: str, schema: str, relation: str
|
||||
) -> list[tuple[str, str]]:
|
||||
conn: Connection = self.pool.getconn()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
select column_name, data_type
|
||||
from information_schema.columns
|
||||
where
|
||||
table_catalog = '{dbname}'
|
||||
and table_schema = '{schema}'
|
||||
and table_name = '{relation}'
|
||||
order by ordinal_position asc
|
||||
;"""
|
||||
)
|
||||
results: list[tuple[str, str]] = cur.fetchall()
|
||||
self.pool.putconn(conn)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def _short_column_type(type_name: str) -> str:
|
||||
MAPPING = {
|
||||
"bigint": "##",
|
||||
"bigserial": "##",
|
||||
"bit": "010",
|
||||
"boolean": "t/f",
|
||||
"box": "□",
|
||||
"bytea": "b",
|
||||
"character": "s",
|
||||
"cidr": "ip",
|
||||
"circle": "○",
|
||||
"date": "d",
|
||||
"double": "#.#",
|
||||
"inet": "ip",
|
||||
"integer": "#",
|
||||
"interval": "|-|",
|
||||
"json": "{}",
|
||||
"jsonb": "b{}",
|
||||
"line": "—",
|
||||
"lseg": "-",
|
||||
"macaddr": "mac",
|
||||
"macaddr8": "mac",
|
||||
"money": "$$",
|
||||
"numeric": "#.#",
|
||||
"path": "╭",
|
||||
"pg_lsn": "lsn",
|
||||
"pg_snapshot": "snp",
|
||||
"point": "•",
|
||||
"polygon": "▽",
|
||||
"real": "#.#",
|
||||
"smallint": "#",
|
||||
"smallserial": "#",
|
||||
"serial": "#",
|
||||
"text": "s",
|
||||
"time": "t",
|
||||
"timestamp": "ts",
|
||||
"tsquery": "tsq",
|
||||
"tsvector": "tsv",
|
||||
"txid_snapshot": "snp",
|
||||
"uuid": "uid",
|
||||
"xml": "xml",
|
||||
"array": "[]",
|
||||
}
|
||||
return MAPPING.get(type_name.split("(")[0].split(" ")[0], "?")
|
||||
|
||||
@staticmethod
|
||||
def _short_column_type_from_oid(oid: int) -> str:
|
||||
MAPPING = {
|
||||
16: "t/f",
|
||||
17: "b",
|
||||
18: "s",
|
||||
19: "s",
|
||||
20: "##",
|
||||
21: "#",
|
||||
22: "[#]",
|
||||
23: "#",
|
||||
25: "s",
|
||||
26: "oid",
|
||||
114: "{}",
|
||||
142: "xml",
|
||||
600: "•",
|
||||
601: "-",
|
||||
602: "╭",
|
||||
603: "□",
|
||||
604: "▽",
|
||||
628: "—",
|
||||
651: "[ip]",
|
||||
700: "#.#",
|
||||
701: "#.#",
|
||||
704: "|-|",
|
||||
718: "○",
|
||||
790: "$$",
|
||||
829: "mac",
|
||||
869: "ip",
|
||||
650: "ip",
|
||||
774: "mac",
|
||||
1000: "[t/f]",
|
||||
1001: "[b]",
|
||||
1002: "[s]",
|
||||
1003: "[s]",
|
||||
1009: "[s]",
|
||||
1013: "[oid]",
|
||||
1014: "[s]",
|
||||
1015: "[s]",
|
||||
1016: "[#]",
|
||||
1021: "[#.#]",
|
||||
1022: "[#.#]",
|
||||
1028: "[oid]",
|
||||
1040: "[mac]",
|
||||
1041: "[ip]",
|
||||
1042: "s",
|
||||
1043: "s",
|
||||
1082: "d",
|
||||
1083: "t",
|
||||
1114: "ts",
|
||||
1115: "[ts]",
|
||||
1182: "[d]",
|
||||
1183: "[t]",
|
||||
1184: "ts",
|
||||
1185: "[ts]",
|
||||
1186: "|-|",
|
||||
1187: "[|-|]",
|
||||
1231: "[#.#]",
|
||||
1266: "t",
|
||||
1270: "[t]",
|
||||
1560: "010",
|
||||
1562: "010",
|
||||
1700: "#.#",
|
||||
2950: "uid",
|
||||
3614: "tsv",
|
||||
3615: "tsq",
|
||||
3802: "b{}",
|
||||
}
|
||||
return MAPPING.get(oid, "?")
|
||||
|
||||
|
||||
class HarlequinPostgresAdapter(HarlequinAdapter):
|
||||
ADAPTER_OPTIONS = POSTGRES_OPTIONS
|
||||
IMPLEMENTS_CANCEL = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conn_str: Sequence[str],
|
||||
host: str | None = None,
|
||||
port: str | None = None,
|
||||
dbname: str | None = None,
|
||||
user: str | None = None,
|
||||
password: str | None = None,
|
||||
passfile: str | None = None,
|
||||
require_auth: str | None = None,
|
||||
channel_binding: str | None = None,
|
||||
connect_timeout: int | float | None = None,
|
||||
sslmode: str | None = None,
|
||||
sslcert: str | None = None,
|
||||
sslkey: str | None = None,
|
||||
**_: Any,
|
||||
) -> None:
|
||||
self.conn_str = conn_str
|
||||
self.options: dict[str, str | int | None] = {
|
||||
"host": host,
|
||||
"port": port,
|
||||
"dbname": dbname,
|
||||
"user": user,
|
||||
"password": password,
|
||||
"passfile": passfile,
|
||||
"require_auth": require_auth,
|
||||
"channel_binding": channel_binding,
|
||||
"connect_timeout": connect_timeout, # type: ignore[dict-item]
|
||||
"sslmode": sslmode,
|
||||
"sslcert": sslcert,
|
||||
"sslkey": sslkey,
|
||||
}
|
||||
|
||||
@property
|
||||
def connection_id(self) -> str | None:
|
||||
"""
|
||||
Use a simplified connection string, with only the host, port, and database
|
||||
"""
|
||||
try:
|
||||
conn_info = conninfo.conninfo_to_dict(
|
||||
conninfo=self.conn_str[0] if self.conn_str else "",
|
||||
**self.options,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
host = conn_info.get("host", "localhost")
|
||||
port = conn_info.get("port", "5432")
|
||||
dbname = conn_info.get("dbname", "postgres")
|
||||
return f"{host}:{port}/{dbname}"
|
||||
|
||||
def connect(self) -> HarlequinPostgresConnection:
|
||||
if len(self.conn_str) > 1:
|
||||
raise HarlequinConnectionError(
|
||||
"Cannot provide multiple connection strings to the Postgres adapter. "
|
||||
f"{self.conn_str}"
|
||||
)
|
||||
# before creating the connection, register updated type adapters, so
|
||||
# all subsequent connections will use those adapters
|
||||
register_inf_loaders()
|
||||
conn = HarlequinPostgresConnection(self.conn_str, options=self.options)
|
||||
return conn
|
253
src/harlequin_postgres/catalog.py
Normal file
253
src/harlequin_postgres/catalog.py
Normal file
|
@ -0,0 +1,253 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from harlequin.catalog import InteractiveCatalogItem
|
||||
|
||||
from harlequin_postgres.interactions import (
|
||||
execute_drop_database_statement,
|
||||
execute_drop_foreign_table_statement,
|
||||
execute_drop_schema_statement,
|
||||
execute_drop_table_statement,
|
||||
execute_drop_view_statement,
|
||||
execute_use_statement,
|
||||
insert_columns_at_cursor,
|
||||
show_select_star,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from harlequin_postgres.adapter import HarlequinPostgresConnection
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColumnCatalogItem(InteractiveCatalogItem["HarlequinPostgresConnection"]):
|
||||
parent: "RelationCatalogItem" | None = None
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent: "RelationCatalogItem",
|
||||
label: str,
|
||||
type_label: str,
|
||||
) -> "ColumnCatalogItem":
|
||||
column_qualified_identifier = f'{parent.qualified_identifier}."{label}"'
|
||||
column_query_name = f'"{label}"'
|
||||
return cls(
|
||||
qualified_identifier=column_qualified_identifier,
|
||||
query_name=column_query_name,
|
||||
label=label,
|
||||
type_label=type_label,
|
||||
connection=parent.connection,
|
||||
parent=parent,
|
||||
loaded=True,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RelationCatalogItem(InteractiveCatalogItem["HarlequinPostgresConnection"]):
|
||||
INTERACTIONS = [
|
||||
("Insert Columns at Cursor", insert_columns_at_cursor),
|
||||
("Preview Data", show_select_star),
|
||||
]
|
||||
parent: "SchemaCatalogItem" | None = None
|
||||
|
||||
def fetch_children(self) -> list[ColumnCatalogItem]:
|
||||
if self.parent is None or self.parent.parent is None or self.connection is None:
|
||||
return []
|
||||
result = self.connection._get_columns(
|
||||
self.parent.parent.label, self.parent.label, self.label
|
||||
)
|
||||
return [
|
||||
ColumnCatalogItem.from_parent(
|
||||
parent=self,
|
||||
label=column_name,
|
||||
type_label=self.connection._short_column_type(column_type),
|
||||
)
|
||||
for column_name, column_type in result
|
||||
]
|
||||
|
||||
|
||||
class ViewCatalogItem(RelationCatalogItem):
|
||||
INTERACTIONS = RelationCatalogItem.INTERACTIONS + [
|
||||
("Drop View", execute_drop_view_statement),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent: "SchemaCatalogItem",
|
||||
label: str,
|
||||
) -> "ViewCatalogItem":
|
||||
relation_query_name = f'"{parent.label}"."{label}"'
|
||||
relation_qualified_identifier = f'{parent.qualified_identifier}."{label}"'
|
||||
return cls(
|
||||
qualified_identifier=relation_qualified_identifier,
|
||||
query_name=relation_query_name,
|
||||
label=label,
|
||||
type_label="v",
|
||||
connection=parent.connection,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
|
||||
class TableCatalogItem(RelationCatalogItem):
|
||||
INTERACTIONS = RelationCatalogItem.INTERACTIONS + [
|
||||
("Drop Table", execute_drop_table_statement),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent: "SchemaCatalogItem",
|
||||
label: str,
|
||||
) -> "TableCatalogItem":
|
||||
relation_query_name = f'"{parent.label}"."{label}"'
|
||||
relation_qualified_identifier = f'{parent.qualified_identifier}."{label}"'
|
||||
return cls(
|
||||
qualified_identifier=relation_qualified_identifier,
|
||||
query_name=relation_query_name,
|
||||
label=label,
|
||||
type_label="t",
|
||||
connection=parent.connection,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
|
||||
class TempTableCatalogItem(TableCatalogItem):
|
||||
INTERACTIONS = RelationCatalogItem.INTERACTIONS + [
|
||||
("Drop Table", execute_drop_table_statement),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent: "SchemaCatalogItem",
|
||||
label: str,
|
||||
) -> "TempTableCatalogItem":
|
||||
relation_query_name = f'"{parent.label}"."{label}"'
|
||||
relation_qualified_identifier = f'{parent.qualified_identifier}."{label}"'
|
||||
return cls(
|
||||
qualified_identifier=relation_qualified_identifier,
|
||||
query_name=relation_query_name,
|
||||
label=label,
|
||||
type_label="tmp",
|
||||
connection=parent.connection,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
|
||||
class ForeignCatalogItem(TableCatalogItem):
|
||||
INTERACTIONS = RelationCatalogItem.INTERACTIONS + [
|
||||
("Drop Table", execute_drop_foreign_table_statement),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent: "SchemaCatalogItem",
|
||||
label: str,
|
||||
) -> "ForeignCatalogItem":
|
||||
relation_query_name = f'"{parent.label}"."{label}"'
|
||||
relation_qualified_identifier = f'{parent.qualified_identifier}."{label}"'
|
||||
return cls(
|
||||
qualified_identifier=relation_qualified_identifier,
|
||||
query_name=relation_query_name,
|
||||
label=label,
|
||||
type_label="f",
|
||||
connection=parent.connection,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SchemaCatalogItem(InteractiveCatalogItem["HarlequinPostgresConnection"]):
|
||||
INTERACTIONS = [
|
||||
("Set Search Path", execute_use_statement),
|
||||
("Drop Schema", execute_drop_schema_statement),
|
||||
]
|
||||
parent: "DatabaseCatalogItem" | None = None
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent: "DatabaseCatalogItem",
|
||||
label: str,
|
||||
) -> "SchemaCatalogItem":
|
||||
schema_identifier = f'"{label}"'
|
||||
return cls(
|
||||
qualified_identifier=schema_identifier,
|
||||
query_name=schema_identifier,
|
||||
label=label,
|
||||
type_label="sch",
|
||||
connection=parent.connection,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
def fetch_children(self) -> list[RelationCatalogItem]:
|
||||
if self.parent is None or self.connection is None:
|
||||
return []
|
||||
children: list[RelationCatalogItem] = []
|
||||
result = self.connection._get_relations(self.parent.label, self.label)
|
||||
for table_label, table_type in result:
|
||||
if table_type == "VIEW":
|
||||
children.append(
|
||||
ViewCatalogItem.from_parent(
|
||||
parent=self,
|
||||
label=table_label,
|
||||
)
|
||||
)
|
||||
elif table_type == "LOCAL TEMPORARY":
|
||||
children.append(
|
||||
TempTableCatalogItem.from_parent(
|
||||
parent=self,
|
||||
label=table_label,
|
||||
)
|
||||
)
|
||||
elif table_type == "FOREIGN":
|
||||
children.append(
|
||||
ForeignCatalogItem.from_parent(
|
||||
parent=self,
|
||||
label=table_label,
|
||||
)
|
||||
)
|
||||
else:
|
||||
children.append(
|
||||
TableCatalogItem.from_parent(
|
||||
parent=self,
|
||||
label=table_label,
|
||||
)
|
||||
)
|
||||
|
||||
return children
|
||||
|
||||
|
||||
class DatabaseCatalogItem(InteractiveCatalogItem["HarlequinPostgresConnection"]):
|
||||
INTERACTIONS = [
|
||||
("Drop Database", execute_drop_database_statement),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_label(
|
||||
cls, label: str, connection: "HarlequinPostgresConnection"
|
||||
) -> "DatabaseCatalogItem":
|
||||
database_identifier = f'"{label}"'
|
||||
return cls(
|
||||
qualified_identifier=database_identifier,
|
||||
query_name=database_identifier,
|
||||
label=label,
|
||||
type_label="db",
|
||||
connection=connection,
|
||||
)
|
||||
|
||||
def fetch_children(self) -> list[SchemaCatalogItem]:
|
||||
if self.connection is None:
|
||||
return []
|
||||
schemas = self.connection._get_schemas(self.label)
|
||||
return [
|
||||
SchemaCatalogItem.from_parent(
|
||||
parent=self,
|
||||
label=schema_label,
|
||||
)
|
||||
for (schema_label,) in schemas
|
||||
]
|
157
src/harlequin_postgres/cli_options.py
Normal file
157
src/harlequin_postgres/cli_options.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from harlequin.options import (
|
||||
FlagOption, # noqa
|
||||
ListOption, # noqa
|
||||
PathOption, # noqa
|
||||
SelectOption, # noqa
|
||||
TextOption,
|
||||
)
|
||||
|
||||
host = TextOption(
|
||||
name="host",
|
||||
description=(
|
||||
"Specifies the host name of the machine on which the server is running. "
|
||||
"If the value begins with a slash, it is used as the directory for the "
|
||||
"Unix-domain socket."
|
||||
),
|
||||
short_decls=["-h"],
|
||||
default="localhost",
|
||||
)
|
||||
|
||||
|
||||
port = TextOption(
|
||||
name="port",
|
||||
description=(
|
||||
"Port number to connect to at the server host, or socket file name extension "
|
||||
"for Unix-domain connections."
|
||||
),
|
||||
short_decls=["-p"],
|
||||
default="5432",
|
||||
)
|
||||
|
||||
|
||||
dbname = TextOption(
|
||||
name="dbname",
|
||||
description=("The database name to use when connecting with the Postgres server."),
|
||||
short_decls=["-d"],
|
||||
default="postgres",
|
||||
)
|
||||
|
||||
|
||||
user = TextOption(
|
||||
name="user",
|
||||
description=("PostgreSQL user name to connect as."),
|
||||
short_decls=["-u", "--username", "-U"],
|
||||
)
|
||||
|
||||
|
||||
password = TextOption(
|
||||
name="password",
|
||||
description=("Password to be used if the server demands password authentication."),
|
||||
)
|
||||
|
||||
|
||||
passfile = PathOption(
|
||||
name="passfile",
|
||||
description=(
|
||||
"Specifies the name of the file used to store passwords. Defaults to "
|
||||
r"~/.pgpass, or %APPDATA%\postgresql\pgpass.conf on Windows. (No error is "
|
||||
"reported if this file does not exist.)"
|
||||
),
|
||||
resolve_path=True,
|
||||
exists=False,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
)
|
||||
|
||||
require_auth = SelectOption(
|
||||
name="require_auth",
|
||||
description=(
|
||||
"Specifies the authentication method that the client requires from the server. "
|
||||
"If the server does not use the required method to authenticate the client, or "
|
||||
"if the authentication handshake is not fully completed by the server, the "
|
||||
"connection will fail."
|
||||
),
|
||||
choices=["password", "md5", "gss", "sspi", "scram-sha-256", "none"],
|
||||
)
|
||||
|
||||
channel_binding = SelectOption(
|
||||
name="channel_binding",
|
||||
description=(
|
||||
"This option controls the client's use of channel binding. A setting of "
|
||||
"require means that the connection must employ channel binding, prefer "
|
||||
"means that the client will choose channel binding if available, and "
|
||||
"disable prevents the use of channel binding. The default is prefer if "
|
||||
"PostgreSQL is compiled with SSL support; otherwise the default is disable."
|
||||
),
|
||||
choices=["require", "prefer", "disable"],
|
||||
)
|
||||
|
||||
|
||||
def _int_validator(s: str | None) -> tuple[bool, str]:
|
||||
if s is None:
|
||||
return True, ""
|
||||
try:
|
||||
_ = int(s)
|
||||
except ValueError:
|
||||
return False, f"Cannot convert {s} to an int!"
|
||||
else:
|
||||
return True, ""
|
||||
|
||||
|
||||
connect_timeout = TextOption(
|
||||
name="connect_timeout",
|
||||
description=(
|
||||
"Maximum time to wait while connecting, in seconds (write as an integer, "
|
||||
"e.g., 10)."
|
||||
),
|
||||
validator=_int_validator,
|
||||
)
|
||||
|
||||
sslmode = SelectOption(
|
||||
name="sslmode",
|
||||
description=(
|
||||
"Determines whether or with what priority a secure SSL TCP/IP connection will "
|
||||
"be negotiated with the server."
|
||||
),
|
||||
choices=["disable", "allow", "prefer", "require", "verify-ca", "verify-full"],
|
||||
default="prefer",
|
||||
)
|
||||
|
||||
sslcert = PathOption(
|
||||
name="sslcert",
|
||||
description=(
|
||||
"Specifies the file name of the client SSL certificate. "
|
||||
"Ignored if an SSL connection is not made."
|
||||
),
|
||||
default="~/.postgresql/postgresql.crt",
|
||||
)
|
||||
|
||||
sslkey = TextOption(
|
||||
name="sslkey",
|
||||
description=(
|
||||
"Specifies the location for the secret key used for the client certificate. "
|
||||
"It can either specify a file name that will be used instead of the default "
|
||||
"~/.postgresql/postgresql.key, or it can specify a key obtained from an "
|
||||
"external engine. An external engine specification should consist of a "
|
||||
"colon-separated engine name and an engine-specific key identifier. This "
|
||||
"parameter is ignored if an SSL connection is not made."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
POSTGRES_OPTIONS = [
|
||||
host,
|
||||
port,
|
||||
dbname,
|
||||
user,
|
||||
password,
|
||||
passfile,
|
||||
require_auth,
|
||||
channel_binding,
|
||||
connect_timeout,
|
||||
sslmode,
|
||||
sslcert,
|
||||
sslkey,
|
||||
]
|
74
src/harlequin_postgres/completions.py
Normal file
74
src/harlequin_postgres/completions.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
from harlequin import HarlequinCompletion
|
||||
from psycopg import Connection
|
||||
|
||||
|
||||
def _get_completions(conn: Connection) -> list[HarlequinCompletion]:
|
||||
completions: list[HarlequinCompletion] = []
|
||||
|
||||
# source: https://www.postgresql.org/docs/current/sql-keywords-appendix.html
|
||||
keyword_path = Path(__file__).parent / "keywords.tsv"
|
||||
with keyword_path.open("r") as f:
|
||||
keyword_reader = csv.reader(
|
||||
f,
|
||||
delimiter="\t",
|
||||
)
|
||||
_header = next(keyword_reader)
|
||||
for keyword, kind, _, _, _ in keyword_reader:
|
||||
completions.append(
|
||||
HarlequinCompletion(
|
||||
label=keyword.lower(),
|
||||
type_label="kw",
|
||||
value=keyword.lower(),
|
||||
priority=100 if kind.startswith("reserved") else 1000,
|
||||
context=None,
|
||||
)
|
||||
)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
r"""
|
||||
select distinct
|
||||
routine_name as label,
|
||||
case when routine_type is null then 'agg' else 'fn' end as type_label,
|
||||
case
|
||||
when routine_schema = 'pg_catalog' --
|
||||
then null
|
||||
else routine_schema
|
||||
end as context
|
||||
from information_schema.routines
|
||||
where
|
||||
length(routine_name) < 37
|
||||
and routine_name not ilike '\_%'
|
||||
and routine_name not ilike 'pg\_%'
|
||||
and routine_name not ilike 'binary\_upgrade\_%'
|
||||
|
||||
;"""
|
||||
)
|
||||
results = cur.fetchall()
|
||||
for label, type_label, context in results:
|
||||
completions.append(
|
||||
HarlequinCompletion(
|
||||
label=label,
|
||||
type_label=type_label,
|
||||
value=label,
|
||||
priority=1000,
|
||||
context=context,
|
||||
)
|
||||
)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""select distinct name as label from pg_settings""")
|
||||
results = cur.fetchall()
|
||||
for (label,) in results:
|
||||
completions.append(
|
||||
HarlequinCompletion(
|
||||
label=label, type_label="set", value=label, priority=2000, context=None
|
||||
)
|
||||
)
|
||||
|
||||
return sorted(completions)
|
143
src/harlequin_postgres/interactions.py
Normal file
143
src/harlequin_postgres/interactions.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Literal, Sequence
|
||||
|
||||
from harlequin.catalog import CatalogItem
|
||||
from harlequin.exception import HarlequinQueryError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from harlequin.driver import HarlequinDriver
|
||||
from harlequin_duckdb.catalog import (
|
||||
ColumnCatalogItem,
|
||||
DatabaseCatalogItem,
|
||||
RelationCatalogItem,
|
||||
SchemaCatalogItem,
|
||||
)
|
||||
|
||||
|
||||
def execute_use_statement(
|
||||
item: "SchemaCatalogItem",
|
||||
driver: "HarlequinDriver",
|
||||
) -> None:
|
||||
if item.connection is None:
|
||||
return
|
||||
try:
|
||||
item.connection.execute(f"set search_path to {item.qualified_identifier}")
|
||||
except HarlequinQueryError:
|
||||
driver.notify("Could not switch context", severity="error")
|
||||
raise
|
||||
else:
|
||||
driver.notify(f"Editor context switched to {item.label}")
|
||||
|
||||
|
||||
def execute_drop_schema_statement(
|
||||
item: "SchemaCatalogItem",
|
||||
driver: "HarlequinDriver",
|
||||
) -> None:
|
||||
def _drop_schema() -> None:
|
||||
if item.connection is None:
|
||||
return
|
||||
try:
|
||||
item.connection.execute(f"drop schema {item.qualified_identifier} cascade")
|
||||
except HarlequinQueryError:
|
||||
driver.notify(f"Could not drop schema {item.label}", severity="error")
|
||||
raise
|
||||
else:
|
||||
driver.notify(f"Dropped schema {item.label}")
|
||||
driver.refresh_catalog()
|
||||
|
||||
if item.children or item.fetch_children():
|
||||
driver.confirm_and_execute(callback=_drop_schema)
|
||||
else:
|
||||
_drop_schema()
|
||||
|
||||
|
||||
def execute_drop_database_statement(
|
||||
item: "DatabaseCatalogItem",
|
||||
driver: "HarlequinDriver",
|
||||
) -> None:
|
||||
def _drop_database() -> None:
|
||||
if item.connection is None:
|
||||
return
|
||||
try:
|
||||
item.connection.execute(f"drop database {item.qualified_identifier}")
|
||||
except HarlequinQueryError:
|
||||
driver.notify(f"Could not drop database {item.label}", severity="error")
|
||||
raise
|
||||
else:
|
||||
driver.notify(f"Dropped database {item.label}")
|
||||
driver.refresh_catalog()
|
||||
|
||||
if item.children or item.fetch_children():
|
||||
driver.confirm_and_execute(callback=_drop_database)
|
||||
else:
|
||||
_drop_database()
|
||||
|
||||
|
||||
def execute_drop_relation_statement(
|
||||
item: "RelationCatalogItem",
|
||||
driver: "HarlequinDriver",
|
||||
relation_type: Literal["view", "table", "foreign table"],
|
||||
) -> None:
|
||||
def _drop_relation() -> None:
|
||||
if item.connection is None:
|
||||
return
|
||||
try:
|
||||
item.connection.execute(f"drop {relation_type} {item.qualified_identifier}")
|
||||
except HarlequinQueryError:
|
||||
driver.notify(
|
||||
f"Could not drop {relation_type} {item.label}", severity="error"
|
||||
)
|
||||
raise
|
||||
else:
|
||||
driver.notify(f"Dropped {relation_type} {item.label}")
|
||||
driver.refresh_catalog()
|
||||
|
||||
driver.confirm_and_execute(callback=_drop_relation)
|
||||
|
||||
|
||||
def execute_drop_table_statement(
|
||||
item: "RelationCatalogItem", driver: "HarlequinDriver"
|
||||
) -> None:
|
||||
execute_drop_relation_statement(item=item, driver=driver, relation_type="table")
|
||||
|
||||
|
||||
def execute_drop_foreign_table_statement(
|
||||
item: "RelationCatalogItem", driver: "HarlequinDriver"
|
||||
) -> None:
|
||||
execute_drop_relation_statement(
|
||||
item=item, driver=driver, relation_type="foreign table"
|
||||
)
|
||||
|
||||
|
||||
def execute_drop_view_statement(
|
||||
item: "RelationCatalogItem", driver: "HarlequinDriver"
|
||||
) -> None:
|
||||
execute_drop_relation_statement(item=item, driver=driver, relation_type="view")
|
||||
|
||||
|
||||
def show_select_star(
|
||||
item: "RelationCatalogItem",
|
||||
driver: "HarlequinDriver",
|
||||
) -> None:
|
||||
driver.insert_text_in_new_buffer(
|
||||
dedent(
|
||||
f"""
|
||||
select *
|
||||
from {item.qualified_identifier}
|
||||
limit 100
|
||||
""".strip("\n")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def insert_columns_at_cursor(
|
||||
item: "RelationCatalogItem",
|
||||
driver: "HarlequinDriver",
|
||||
) -> None:
|
||||
if item.loaded:
|
||||
cols: Sequence["CatalogItem" | "ColumnCatalogItem"] = item.children
|
||||
else:
|
||||
cols = item.fetch_children()
|
||||
driver.insert_text_at_selection(text=",\n".join(c.query_name for c in cols))
|
842
src/harlequin_postgres/keywords.tsv
Normal file
842
src/harlequin_postgres/keywords.tsv
Normal file
|
@ -0,0 +1,842 @@
|
|||
Key Word PostgreSQL SQL:2023 SQL:2016 SQL-92
|
||||
A non-reserved non-reserved
|
||||
ABORT non-reserved
|
||||
ABS reserved reserved
|
||||
ABSENT non-reserved reserved reserved
|
||||
ABSOLUTE non-reserved non-reserved non-reserved reserved
|
||||
ACCESS non-reserved
|
||||
ACCORDING non-reserved non-reserved
|
||||
ACOS reserved reserved
|
||||
ACTION non-reserved non-reserved non-reserved reserved
|
||||
ADA non-reserved non-reserved non-reserved
|
||||
ADD non-reserved non-reserved non-reserved reserved
|
||||
ADMIN non-reserved non-reserved non-reserved
|
||||
AFTER non-reserved non-reserved non-reserved
|
||||
AGGREGATE non-reserved
|
||||
ALL reserved reserved reserved reserved
|
||||
ALLOCATE reserved reserved reserved
|
||||
ALSO non-reserved
|
||||
ALTER non-reserved reserved reserved reserved
|
||||
ALWAYS non-reserved non-reserved non-reserved
|
||||
ANALYSE reserved
|
||||
ANALYZE reserved
|
||||
AND reserved reserved reserved reserved
|
||||
ANY reserved reserved reserved reserved
|
||||
ANY_VALUE reserved
|
||||
ARE reserved reserved reserved
|
||||
ARRAY reserved, requires AS reserved reserved
|
||||
ARRAY_AGG reserved reserved
|
||||
ARRAY_MAX_CARDINALITY reserved reserved
|
||||
AS reserved, requires AS reserved reserved reserved
|
||||
ASC reserved non-reserved non-reserved reserved
|
||||
ASENSITIVE non-reserved reserved reserved
|
||||
ASIN reserved reserved
|
||||
ASSERTION non-reserved non-reserved non-reserved reserved
|
||||
ASSIGNMENT non-reserved non-reserved non-reserved
|
||||
ASYMMETRIC reserved reserved reserved
|
||||
AT non-reserved reserved reserved reserved
|
||||
ATAN reserved reserved
|
||||
ATOMIC non-reserved reserved reserved
|
||||
ATTACH non-reserved
|
||||
ATTRIBUTE non-reserved non-reserved non-reserved
|
||||
ATTRIBUTES non-reserved non-reserved
|
||||
AUTHORIZATION reserved (can be function or type) reserved reserved reserved
|
||||
AVG reserved reserved reserved
|
||||
BACKWARD non-reserved
|
||||
BASE64 non-reserved non-reserved
|
||||
BEFORE non-reserved non-reserved non-reserved
|
||||
BEGIN non-reserved reserved reserved reserved
|
||||
BEGIN_FRAME reserved reserved
|
||||
BEGIN_PARTITION reserved reserved
|
||||
BERNOULLI non-reserved non-reserved
|
||||
BETWEEN non-reserved (cannot be function or type) reserved reserved reserved
|
||||
BIGINT non-reserved (cannot be function or type) reserved reserved
|
||||
BINARY reserved (can be function or type) reserved reserved
|
||||
BIT non-reserved (cannot be function or type) reserved
|
||||
BIT_LENGTH reserved
|
||||
BLOB reserved reserved
|
||||
BLOCKED non-reserved non-reserved
|
||||
BOM non-reserved non-reserved
|
||||
BOOLEAN non-reserved (cannot be function or type) reserved reserved
|
||||
BOTH reserved reserved reserved reserved
|
||||
BREADTH non-reserved non-reserved non-reserved
|
||||
BTRIM reserved
|
||||
BY non-reserved reserved reserved reserved
|
||||
C non-reserved non-reserved non-reserved
|
||||
CACHE non-reserved
|
||||
CALL non-reserved reserved reserved
|
||||
CALLED non-reserved reserved reserved
|
||||
CARDINALITY reserved reserved
|
||||
CASCADE non-reserved non-reserved non-reserved reserved
|
||||
CASCADED non-reserved reserved reserved reserved
|
||||
CASE reserved reserved reserved reserved
|
||||
CAST reserved reserved reserved reserved
|
||||
CATALOG non-reserved non-reserved non-reserved reserved
|
||||
CATALOG_NAME non-reserved non-reserved non-reserved
|
||||
CEIL reserved reserved
|
||||
CEILING reserved reserved
|
||||
CHAIN non-reserved non-reserved non-reserved
|
||||
CHAINING non-reserved non-reserved
|
||||
CHAR non-reserved (cannot be function or type), requires AS reserved reserved reserved
|
||||
CHARACTER non-reserved (cannot be function or type), requires AS reserved reserved reserved
|
||||
CHARACTERISTICS non-reserved non-reserved non-reserved
|
||||
CHARACTERS non-reserved non-reserved
|
||||
CHARACTER_LENGTH reserved reserved reserved
|
||||
CHARACTER_SET_CATALOG non-reserved non-reserved non-reserved
|
||||
CHARACTER_SET_NAME non-reserved non-reserved non-reserved
|
||||
CHARACTER_SET_SCHEMA non-reserved non-reserved non-reserved
|
||||
CHAR_LENGTH reserved reserved reserved
|
||||
CHECK reserved reserved reserved reserved
|
||||
CHECKPOINT non-reserved
|
||||
CLASS non-reserved
|
||||
CLASSIFIER reserved reserved
|
||||
CLASS_ORIGIN non-reserved non-reserved non-reserved
|
||||
CLOB reserved reserved
|
||||
CLOSE non-reserved reserved reserved reserved
|
||||
CLUSTER non-reserved
|
||||
COALESCE non-reserved (cannot be function or type) reserved reserved reserved
|
||||
COBOL non-reserved non-reserved non-reserved
|
||||
COLLATE reserved reserved reserved reserved
|
||||
COLLATION reserved (can be function or type) non-reserved non-reserved reserved
|
||||
COLLATION_CATALOG non-reserved non-reserved non-reserved
|
||||
COLLATION_NAME non-reserved non-reserved non-reserved
|
||||
COLLATION_SCHEMA non-reserved non-reserved non-reserved
|
||||
COLLECT reserved reserved
|
||||
COLUMN reserved reserved reserved reserved
|
||||
COLUMNS non-reserved non-reserved non-reserved
|
||||
COLUMN_NAME non-reserved non-reserved non-reserved
|
||||
COMMAND_FUNCTION non-reserved non-reserved non-reserved
|
||||
COMMAND_FUNCTION_CODE non-reserved non-reserved
|
||||
COMMENT non-reserved
|
||||
COMMENTS non-reserved
|
||||
COMMIT non-reserved reserved reserved reserved
|
||||
COMMITTED non-reserved non-reserved non-reserved non-reserved
|
||||
COMPRESSION non-reserved
|
||||
CONCURRENTLY reserved (can be function or type)
|
||||
CONDITION reserved reserved
|
||||
CONDITIONAL non-reserved non-reserved
|
||||
CONDITION_NUMBER non-reserved non-reserved non-reserved
|
||||
CONFIGURATION non-reserved
|
||||
CONFLICT non-reserved
|
||||
CONNECT reserved reserved reserved
|
||||
CONNECTION non-reserved non-reserved non-reserved reserved
|
||||
CONNECTION_NAME non-reserved non-reserved non-reserved
|
||||
CONSTRAINT reserved reserved reserved reserved
|
||||
CONSTRAINTS non-reserved non-reserved non-reserved reserved
|
||||
CONSTRAINT_CATALOG non-reserved non-reserved non-reserved
|
||||
CONSTRAINT_NAME non-reserved non-reserved non-reserved
|
||||
CONSTRAINT_SCHEMA non-reserved non-reserved non-reserved
|
||||
CONSTRUCTOR non-reserved non-reserved
|
||||
CONTAINS reserved reserved
|
||||
CONTENT non-reserved non-reserved non-reserved
|
||||
CONTINUE non-reserved non-reserved non-reserved reserved
|
||||
CONTROL non-reserved non-reserved
|
||||
CONVERSION non-reserved
|
||||
CONVERT reserved reserved reserved
|
||||
COPARTITION non-reserved
|
||||
COPY non-reserved reserved reserved
|
||||
CORR reserved reserved
|
||||
CORRESPONDING reserved reserved reserved
|
||||
COS reserved reserved
|
||||
COSH reserved reserved
|
||||
COST non-reserved
|
||||
COUNT reserved reserved reserved
|
||||
COVAR_POP reserved reserved
|
||||
COVAR_SAMP reserved reserved
|
||||
CREATE reserved, requires AS reserved reserved reserved
|
||||
CROSS reserved (can be function or type) reserved reserved reserved
|
||||
CSV non-reserved
|
||||
CUBE non-reserved reserved reserved
|
||||
CUME_DIST reserved reserved
|
||||
CURRENT non-reserved reserved reserved reserved
|
||||
CURRENT_CATALOG reserved reserved reserved
|
||||
CURRENT_DATE reserved reserved reserved reserved
|
||||
CURRENT_DEFAULT_TRANSFORM_GROUP reserved reserved
|
||||
CURRENT_PATH reserved reserved
|
||||
CURRENT_ROLE reserved reserved reserved
|
||||
CURRENT_ROW reserved reserved
|
||||
CURRENT_SCHEMA reserved (can be function or type) reserved reserved
|
||||
CURRENT_TIME reserved reserved reserved reserved
|
||||
CURRENT_TIMESTAMP reserved reserved reserved reserved
|
||||
CURRENT_TRANSFORM_GROUP_FOR_TYPE reserved reserved
|
||||
CURRENT_USER reserved reserved reserved reserved
|
||||
CURSOR non-reserved reserved reserved reserved
|
||||
CURSOR_NAME non-reserved non-reserved non-reserved
|
||||
CYCLE non-reserved reserved reserved
|
||||
DATA non-reserved non-reserved non-reserved non-reserved
|
||||
DATABASE non-reserved
|
||||
DATALINK reserved reserved
|
||||
DATE reserved reserved reserved
|
||||
DATETIME_INTERVAL_CODE non-reserved non-reserved non-reserved
|
||||
DATETIME_INTERVAL_PRECISION non-reserved non-reserved non-reserved
|
||||
DAY non-reserved, requires AS reserved reserved reserved
|
||||
DB non-reserved non-reserved
|
||||
DEALLOCATE non-reserved reserved reserved reserved
|
||||
DEC non-reserved (cannot be function or type) reserved reserved reserved
|
||||
DECFLOAT reserved reserved
|
||||
DECIMAL non-reserved (cannot be function or type) reserved reserved reserved
|
||||
DECLARE non-reserved reserved reserved reserved
|
||||
DEFAULT reserved reserved reserved reserved
|
||||
DEFAULTS non-reserved non-reserved non-reserved
|
||||
DEFERRABLE reserved non-reserved non-reserved reserved
|
||||
DEFERRED non-reserved non-reserved non-reserved reserved
|
||||
DEFINE reserved reserved
|
||||
DEFINED non-reserved non-reserved
|
||||
DEFINER non-reserved non-reserved non-reserved
|
||||
DEGREE non-reserved non-reserved
|
||||
DELETE non-reserved reserved reserved reserved
|
||||
DELIMITER non-reserved
|
||||
DELIMITERS non-reserved
|
||||
DENSE_RANK reserved reserved
|
||||
DEPENDS non-reserved
|
||||
DEPTH non-reserved non-reserved non-reserved
|
||||
DEREF reserved reserved
|
||||
DERIVED non-reserved non-reserved
|
||||
DESC reserved non-reserved non-reserved reserved
|
||||
DESCRIBE reserved reserved reserved
|
||||
DESCRIPTOR non-reserved non-reserved reserved
|
||||
DETACH non-reserved
|
||||
DETERMINISTIC reserved reserved
|
||||
DIAGNOSTICS non-reserved non-reserved reserved
|
||||
DICTIONARY non-reserved
|
||||
DISABLE non-reserved
|
||||
DISCARD non-reserved
|
||||
DISCONNECT reserved reserved reserved
|
||||
DISPATCH non-reserved non-reserved
|
||||
DISTINCT reserved reserved reserved reserved
|
||||
DLNEWCOPY reserved reserved
|
||||
DLPREVIOUSCOPY reserved reserved
|
||||
DLURLCOMPLETE reserved reserved
|
||||
DLURLCOMPLETEONLY reserved reserved
|
||||
DLURLCOMPLETEWRITE reserved reserved
|
||||
DLURLPATH reserved reserved
|
||||
DLURLPATHONLY reserved reserved
|
||||
DLURLPATHWRITE reserved reserved
|
||||
DLURLSCHEME reserved reserved
|
||||
DLURLSERVER reserved reserved
|
||||
DLVALUE reserved reserved
|
||||
DO reserved
|
||||
DOCUMENT non-reserved non-reserved non-reserved
|
||||
DOMAIN non-reserved non-reserved non-reserved reserved
|
||||
DOUBLE non-reserved reserved reserved reserved
|
||||
DROP non-reserved reserved reserved reserved
|
||||
DYNAMIC reserved reserved
|
||||
DYNAMIC_FUNCTION non-reserved non-reserved non-reserved
|
||||
DYNAMIC_FUNCTION_CODE non-reserved non-reserved
|
||||
EACH non-reserved reserved reserved
|
||||
ELEMENT reserved reserved
|
||||
ELSE reserved reserved reserved reserved
|
||||
EMPTY reserved reserved
|
||||
ENABLE non-reserved
|
||||
ENCODING non-reserved non-reserved non-reserved
|
||||
ENCRYPTED non-reserved
|
||||
END reserved reserved reserved reserved
|
||||
END-EXEC reserved reserved reserved
|
||||
END_FRAME reserved reserved
|
||||
END_PARTITION reserved reserved
|
||||
ENFORCED non-reserved non-reserved
|
||||
ENUM non-reserved
|
||||
EQUALS reserved reserved
|
||||
ERROR non-reserved non-reserved
|
||||
ESCAPE non-reserved reserved reserved reserved
|
||||
EVENT non-reserved
|
||||
EVERY reserved reserved
|
||||
EXCEPT reserved, requires AS reserved reserved reserved
|
||||
EXCEPTION reserved
|
||||
EXCLUDE non-reserved non-reserved non-reserved
|
||||
EXCLUDING non-reserved non-reserved non-reserved
|
||||
EXCLUSIVE non-reserved
|
||||
EXEC reserved reserved reserved
|
||||
EXECUTE non-reserved reserved reserved reserved
|
||||
EXISTS non-reserved (cannot be function or type) reserved reserved reserved
|
||||
EXP reserved reserved
|
||||
EXPLAIN non-reserved
|
||||
EXPRESSION non-reserved non-reserved non-reserved
|
||||
EXTENSION non-reserved
|
||||
EXTERNAL non-reserved reserved reserved reserved
|
||||
EXTRACT non-reserved (cannot be function or type) reserved reserved reserved
|
||||
FALSE reserved reserved reserved reserved
|
||||
FAMILY non-reserved
|
||||
FETCH reserved, requires AS reserved reserved reserved
|
||||
FILE non-reserved non-reserved
|
||||
FILTER non-reserved, requires AS reserved reserved
|
||||
FINAL non-reserved non-reserved
|
||||
FINALIZE non-reserved
|
||||
FINISH non-reserved non-reserved
|
||||
FIRST non-reserved non-reserved non-reserved reserved
|
||||
FIRST_VALUE reserved reserved
|
||||
FLAG non-reserved non-reserved
|
||||
FLOAT non-reserved (cannot be function or type) reserved reserved reserved
|
||||
FLOOR reserved reserved
|
||||
FOLLOWING non-reserved non-reserved non-reserved
|
||||
FOR reserved, requires AS reserved reserved reserved
|
||||
FORCE non-reserved
|
||||
FOREIGN reserved reserved reserved reserved
|
||||
FORMAT non-reserved non-reserved non-reserved
|
||||
FORTRAN non-reserved non-reserved non-reserved
|
||||
FORWARD non-reserved
|
||||
FOUND non-reserved non-reserved reserved
|
||||
FRAME_ROW reserved reserved
|
||||
FREE reserved reserved
|
||||
FREEZE reserved (can be function or type)
|
||||
FROM reserved, requires AS reserved reserved reserved
|
||||
FS non-reserved non-reserved
|
||||
FULFILL non-reserved non-reserved
|
||||
FULL reserved (can be function or type) reserved reserved reserved
|
||||
FUNCTION non-reserved reserved reserved
|
||||
FUNCTIONS non-reserved
|
||||
FUSION reserved reserved
|
||||
G non-reserved non-reserved
|
||||
GENERAL non-reserved non-reserved
|
||||
GENERATED non-reserved non-reserved non-reserved
|
||||
GET reserved reserved reserved
|
||||
GLOBAL non-reserved reserved reserved reserved
|
||||
GO non-reserved non-reserved reserved
|
||||
GOTO non-reserved non-reserved reserved
|
||||
GRANT reserved, requires AS reserved reserved reserved
|
||||
GRANTED non-reserved non-reserved non-reserved
|
||||
GREATEST non-reserved (cannot be function or type) reserved
|
||||
GROUP reserved, requires AS reserved reserved reserved
|
||||
GROUPING non-reserved (cannot be function or type) reserved reserved
|
||||
GROUPS non-reserved reserved reserved
|
||||
HANDLER non-reserved
|
||||
HAVING reserved, requires AS reserved reserved reserved
|
||||
HEADER non-reserved
|
||||
HEX non-reserved non-reserved
|
||||
HIERARCHY non-reserved non-reserved
|
||||
HOLD non-reserved reserved reserved
|
||||
HOUR non-reserved, requires AS reserved reserved reserved
|
||||
ID non-reserved non-reserved
|
||||
IDENTITY non-reserved reserved reserved reserved
|
||||
IF non-reserved
|
||||
IGNORE non-reserved non-reserved
|
||||
ILIKE reserved (can be function or type)
|
||||
IMMEDIATE non-reserved non-reserved non-reserved reserved
|
||||
IMMEDIATELY non-reserved non-reserved
|
||||
IMMUTABLE non-reserved
|
||||
IMPLEMENTATION non-reserved non-reserved
|
||||
IMPLICIT non-reserved
|
||||
IMPORT non-reserved reserved reserved
|
||||
IN reserved reserved reserved reserved
|
||||
INCLUDE non-reserved
|
||||
INCLUDING non-reserved non-reserved non-reserved
|
||||
INCREMENT non-reserved non-reserved non-reserved
|
||||
INDENT non-reserved non-reserved non-reserved
|
||||
INDEX non-reserved
|
||||
INDEXES non-reserved
|
||||
INDICATOR reserved reserved reserved
|
||||
INHERIT non-reserved
|
||||
INHERITS non-reserved
|
||||
INITIAL reserved reserved
|
||||
INITIALLY reserved non-reserved non-reserved reserved
|
||||
INLINE non-reserved
|
||||
INNER reserved (can be function or type) reserved reserved reserved
|
||||
INOUT non-reserved (cannot be function or type) reserved reserved
|
||||
INPUT non-reserved non-reserved non-reserved reserved
|
||||
INSENSITIVE non-reserved reserved reserved reserved
|
||||
INSERT non-reserved reserved reserved reserved
|
||||
INSTANCE non-reserved non-reserved
|
||||
INSTANTIABLE non-reserved non-reserved
|
||||
INSTEAD non-reserved non-reserved non-reserved
|
||||
INT non-reserved (cannot be function or type) reserved reserved reserved
|
||||
INTEGER non-reserved (cannot be function or type) reserved reserved reserved
|
||||
INTEGRITY non-reserved non-reserved
|
||||
INTERSECT reserved, requires AS reserved reserved reserved
|
||||
INTERSECTION reserved reserved
|
||||
INTERVAL non-reserved (cannot be function or type) reserved reserved reserved
|
||||
INTO reserved, requires AS reserved reserved reserved
|
||||
INVOKER non-reserved non-reserved non-reserved
|
||||
IS reserved (can be function or type) reserved reserved reserved
|
||||
ISNULL reserved (can be function or type), requires AS
|
||||
ISOLATION non-reserved non-reserved non-reserved reserved
|
||||
JOIN reserved (can be function or type) reserved reserved reserved
|
||||
JSON non-reserved reserved
|
||||
JSON_ARRAY non-reserved (cannot be function or type) reserved reserved
|
||||
JSON_ARRAYAGG non-reserved (cannot be function or type) reserved reserved
|
||||
JSON_EXISTS reserved reserved
|
||||
JSON_OBJECT non-reserved (cannot be function or type) reserved reserved
|
||||
JSON_OBJECTAGG non-reserved (cannot be function or type) reserved reserved
|
||||
JSON_QUERY reserved reserved
|
||||
JSON_SCALAR reserved
|
||||
JSON_SERIALIZE reserved
|
||||
JSON_TABLE reserved reserved
|
||||
JSON_TABLE_PRIMITIVE reserved reserved
|
||||
JSON_VALUE reserved reserved
|
||||
K non-reserved non-reserved
|
||||
KEEP non-reserved non-reserved
|
||||
KEY non-reserved non-reserved non-reserved reserved
|
||||
KEYS non-reserved non-reserved non-reserved
|
||||
KEY_MEMBER non-reserved non-reserved
|
||||
KEY_TYPE non-reserved non-reserved
|
||||
LABEL non-reserved
|
||||
LAG reserved reserved
|
||||
LANGUAGE non-reserved reserved reserved reserved
|
||||
LARGE non-reserved reserved reserved
|
||||
LAST non-reserved non-reserved non-reserved reserved
|
||||
LAST_VALUE reserved reserved
|
||||
LATERAL reserved reserved reserved
|
||||
LEAD reserved reserved
|
||||
LEADING reserved reserved reserved reserved
|
||||
LEAKPROOF non-reserved
|
||||
LEAST non-reserved (cannot be function or type) reserved
|
||||
LEFT reserved (can be function or type) reserved reserved reserved
|
||||
LENGTH non-reserved non-reserved non-reserved
|
||||
LEVEL non-reserved non-reserved non-reserved reserved
|
||||
LIBRARY non-reserved non-reserved
|
||||
LIKE reserved (can be function or type) reserved reserved reserved
|
||||
LIKE_REGEX reserved reserved
|
||||
LIMIT reserved, requires AS non-reserved non-reserved
|
||||
LINK non-reserved non-reserved
|
||||
LISTAGG reserved reserved
|
||||
LISTEN non-reserved
|
||||
LN reserved reserved
|
||||
LOAD non-reserved
|
||||
LOCAL non-reserved reserved reserved reserved
|
||||
LOCALTIME reserved reserved reserved
|
||||
LOCALTIMESTAMP reserved reserved reserved
|
||||
LOCATION non-reserved non-reserved non-reserved
|
||||
LOCATOR non-reserved non-reserved
|
||||
LOCK non-reserved
|
||||
LOCKED non-reserved
|
||||
LOG reserved reserved
|
||||
LOG10 reserved reserved
|
||||
LOGGED non-reserved
|
||||
LOWER reserved reserved reserved
|
||||
LPAD reserved
|
||||
LTRIM reserved
|
||||
M non-reserved non-reserved
|
||||
MAP non-reserved non-reserved
|
||||
MAPPING non-reserved non-reserved non-reserved
|
||||
MATCH non-reserved reserved reserved reserved
|
||||
MATCHED non-reserved non-reserved non-reserved
|
||||
MATCHES reserved reserved
|
||||
MATCH_NUMBER reserved reserved
|
||||
MATCH_RECOGNIZE reserved reserved
|
||||
MATERIALIZED non-reserved
|
||||
MAX reserved reserved reserved
|
||||
MAXVALUE non-reserved non-reserved non-reserved
|
||||
MEASURES non-reserved non-reserved
|
||||
MEMBER reserved reserved
|
||||
MERGE non-reserved reserved reserved
|
||||
MESSAGE_LENGTH non-reserved non-reserved non-reserved
|
||||
MESSAGE_OCTET_LENGTH non-reserved non-reserved non-reserved
|
||||
MESSAGE_TEXT non-reserved non-reserved non-reserved
|
||||
METHOD non-reserved reserved reserved
|
||||
MIN reserved reserved reserved
|
||||
MINUTE non-reserved, requires AS reserved reserved reserved
|
||||
MINVALUE non-reserved non-reserved non-reserved
|
||||
MOD reserved reserved
|
||||
MODE non-reserved
|
||||
MODIFIES reserved reserved
|
||||
MODULE reserved reserved reserved
|
||||
MONTH non-reserved, requires AS reserved reserved reserved
|
||||
MORE non-reserved non-reserved non-reserved
|
||||
MOVE non-reserved
|
||||
MULTISET reserved reserved
|
||||
MUMPS non-reserved non-reserved non-reserved
|
||||
NAME non-reserved non-reserved non-reserved non-reserved
|
||||
NAMES non-reserved non-reserved non-reserved reserved
|
||||
NAMESPACE non-reserved non-reserved
|
||||
NATIONAL non-reserved (cannot be function or type) reserved reserved reserved
|
||||
NATURAL reserved (can be function or type) reserved reserved reserved
|
||||
NCHAR non-reserved (cannot be function or type) reserved reserved reserved
|
||||
NCLOB reserved reserved
|
||||
NESTED non-reserved non-reserved
|
||||
NESTING non-reserved non-reserved
|
||||
NEW non-reserved reserved reserved
|
||||
NEXT non-reserved non-reserved non-reserved reserved
|
||||
NFC non-reserved non-reserved non-reserved
|
||||
NFD non-reserved non-reserved non-reserved
|
||||
NFKC non-reserved non-reserved non-reserved
|
||||
NFKD non-reserved non-reserved non-reserved
|
||||
NIL non-reserved non-reserved
|
||||
NO non-reserved reserved reserved reserved
|
||||
NONE non-reserved (cannot be function or type) reserved reserved
|
||||
NORMALIZE non-reserved (cannot be function or type) reserved reserved
|
||||
NORMALIZED non-reserved non-reserved non-reserved
|
||||
NOT reserved reserved reserved reserved
|
||||
NOTHING non-reserved
|
||||
NOTIFY non-reserved
|
||||
NOTNULL reserved (can be function or type), requires AS
|
||||
NOWAIT non-reserved
|
||||
NTH_VALUE reserved reserved
|
||||
NTILE reserved reserved
|
||||
NULL reserved reserved reserved reserved
|
||||
NULLABLE non-reserved non-reserved non-reserved
|
||||
NULLIF non-reserved (cannot be function or type) reserved reserved reserved
|
||||
NULLS non-reserved non-reserved non-reserved
|
||||
NULL_ORDERING non-reserved non-reserved
|
||||
NUMBER non-reserved non-reserved non-reserved
|
||||
NUMERIC non-reserved (cannot be function or type) reserved reserved reserved
|
||||
OBJECT non-reserved non-reserved non-reserved
|
||||
OCCURRENCE non-reserved non-reserved
|
||||
OCCURRENCES_REGEX reserved reserved
|
||||
OCTETS non-reserved non-reserved
|
||||
OCTET_LENGTH reserved reserved reserved
|
||||
OF non-reserved reserved reserved reserved
|
||||
OFF non-reserved non-reserved non-reserved
|
||||
OFFSET reserved, requires AS reserved reserved
|
||||
OIDS non-reserved
|
||||
OLD non-reserved reserved reserved
|
||||
OMIT reserved reserved
|
||||
ON reserved, requires AS reserved reserved reserved
|
||||
ONE reserved reserved
|
||||
ONLY reserved reserved reserved reserved
|
||||
OPEN reserved reserved reserved
|
||||
OPERATOR non-reserved
|
||||
OPTION non-reserved non-reserved non-reserved reserved
|
||||
OPTIONS non-reserved non-reserved non-reserved
|
||||
OR reserved reserved reserved reserved
|
||||
ORDER reserved, requires AS reserved reserved reserved
|
||||
ORDERING non-reserved non-reserved
|
||||
ORDINALITY non-reserved non-reserved non-reserved
|
||||
OTHERS non-reserved non-reserved non-reserved
|
||||
OUT non-reserved (cannot be function or type) reserved reserved
|
||||
OUTER reserved (can be function or type) reserved reserved reserved
|
||||
OUTPUT non-reserved non-reserved reserved
|
||||
OVER non-reserved, requires AS reserved reserved
|
||||
OVERFLOW non-reserved non-reserved
|
||||
OVERLAPS reserved (can be function or type), requires AS reserved reserved reserved
|
||||
OVERLAY non-reserved (cannot be function or type) reserved reserved
|
||||
OVERRIDING non-reserved non-reserved non-reserved
|
||||
OWNED non-reserved
|
||||
OWNER non-reserved
|
||||
P non-reserved non-reserved
|
||||
PAD non-reserved non-reserved reserved
|
||||
PARALLEL non-reserved
|
||||
PARAMETER non-reserved reserved reserved
|
||||
PARAMETER_MODE non-reserved non-reserved
|
||||
PARAMETER_NAME non-reserved non-reserved
|
||||
PARAMETER_ORDINAL_POSITION non-reserved non-reserved
|
||||
PARAMETER_SPECIFIC_CATALOG non-reserved non-reserved
|
||||
PARAMETER_SPECIFIC_NAME non-reserved non-reserved
|
||||
PARAMETER_SPECIFIC_SCHEMA non-reserved non-reserved
|
||||
PARSER non-reserved
|
||||
PARTIAL non-reserved non-reserved non-reserved reserved
|
||||
PARTITION non-reserved reserved reserved
|
||||
PASCAL non-reserved non-reserved non-reserved
|
||||
PASS non-reserved non-reserved
|
||||
PASSING non-reserved non-reserved non-reserved
|
||||
PASSTHROUGH non-reserved non-reserved
|
||||
PASSWORD non-reserved
|
||||
PAST non-reserved non-reserved
|
||||
PATH non-reserved non-reserved
|
||||
PATTERN reserved reserved
|
||||
PER reserved reserved
|
||||
PERCENT reserved reserved
|
||||
PERCENTILE_CONT reserved reserved
|
||||
PERCENTILE_DISC reserved reserved
|
||||
PERCENT_RANK reserved reserved
|
||||
PERIOD reserved reserved
|
||||
PERMISSION non-reserved non-reserved
|
||||
PERMUTE non-reserved non-reserved
|
||||
PIPE non-reserved non-reserved
|
||||
PLACING reserved non-reserved non-reserved
|
||||
PLAN non-reserved non-reserved
|
||||
PLANS non-reserved
|
||||
PLI non-reserved non-reserved non-reserved
|
||||
POLICY non-reserved
|
||||
PORTION reserved reserved
|
||||
POSITION non-reserved (cannot be function or type) reserved reserved reserved
|
||||
POSITION_REGEX reserved reserved
|
||||
POWER reserved reserved
|
||||
PRECEDES reserved reserved
|
||||
PRECEDING non-reserved non-reserved non-reserved
|
||||
PRECISION non-reserved (cannot be function or type), requires AS reserved reserved reserved
|
||||
PREPARE non-reserved reserved reserved reserved
|
||||
PREPARED non-reserved
|
||||
PRESERVE non-reserved non-reserved non-reserved reserved
|
||||
PREV non-reserved non-reserved
|
||||
PRIMARY reserved reserved reserved reserved
|
||||
PRIOR non-reserved non-reserved non-reserved reserved
|
||||
PRIVATE non-reserved non-reserved
|
||||
PRIVILEGES non-reserved non-reserved non-reserved reserved
|
||||
PROCEDURAL non-reserved
|
||||
PROCEDURE non-reserved reserved reserved reserved
|
||||
PROCEDURES non-reserved
|
||||
PROGRAM non-reserved
|
||||
PRUNE non-reserved non-reserved
|
||||
PTF reserved reserved
|
||||
PUBLIC non-reserved non-reserved reserved
|
||||
PUBLICATION non-reserved
|
||||
QUOTE non-reserved
|
||||
QUOTES non-reserved non-reserved
|
||||
RANGE non-reserved reserved reserved
|
||||
RANK reserved reserved
|
||||
READ non-reserved non-reserved non-reserved reserved
|
||||
READS reserved reserved
|
||||
REAL non-reserved (cannot be function or type) reserved reserved reserved
|
||||
REASSIGN non-reserved
|
||||
RECHECK non-reserved
|
||||
RECOVERY non-reserved non-reserved
|
||||
RECURSIVE non-reserved reserved reserved
|
||||
REF non-reserved reserved reserved
|
||||
REFERENCES reserved reserved reserved reserved
|
||||
REFERENCING non-reserved reserved reserved
|
||||
REFRESH non-reserved
|
||||
REGR_AVGX reserved reserved
|
||||
REGR_AVGY reserved reserved
|
||||
REGR_COUNT reserved reserved
|
||||
REGR_INTERCEPT reserved reserved
|
||||
REGR_R2 reserved reserved
|
||||
REGR_SLOPE reserved reserved
|
||||
REGR_SXX reserved reserved
|
||||
REGR_SXY reserved reserved
|
||||
REGR_SYY reserved reserved
|
||||
REINDEX non-reserved
|
||||
RELATIVE non-reserved non-reserved non-reserved reserved
|
||||
RELEASE non-reserved reserved reserved
|
||||
RENAME non-reserved
|
||||
REPEATABLE non-reserved non-reserved non-reserved non-reserved
|
||||
REPLACE non-reserved
|
||||
REPLICA non-reserved
|
||||
REQUIRING non-reserved non-reserved
|
||||
RESET non-reserved
|
||||
RESPECT non-reserved non-reserved
|
||||
RESTART non-reserved non-reserved non-reserved
|
||||
RESTORE non-reserved non-reserved
|
||||
RESTRICT non-reserved non-reserved non-reserved reserved
|
||||
RESULT reserved reserved
|
||||
RETURN non-reserved reserved reserved
|
||||
RETURNED_CARDINALITY non-reserved non-reserved
|
||||
RETURNED_LENGTH non-reserved non-reserved non-reserved
|
||||
RETURNED_OCTET_LENGTH non-reserved non-reserved non-reserved
|
||||
RETURNED_SQLSTATE non-reserved non-reserved non-reserved
|
||||
RETURNING reserved, requires AS non-reserved non-reserved
|
||||
RETURNS non-reserved reserved reserved
|
||||
REVOKE non-reserved reserved reserved reserved
|
||||
RIGHT reserved (can be function or type) reserved reserved reserved
|
||||
ROLE non-reserved non-reserved non-reserved
|
||||
ROLLBACK non-reserved reserved reserved reserved
|
||||
ROLLUP non-reserved reserved reserved
|
||||
ROUTINE non-reserved non-reserved non-reserved
|
||||
ROUTINES non-reserved
|
||||
ROUTINE_CATALOG non-reserved non-reserved
|
||||
ROUTINE_NAME non-reserved non-reserved
|
||||
ROUTINE_SCHEMA non-reserved non-reserved
|
||||
ROW non-reserved (cannot be function or type) reserved reserved
|
||||
ROWS non-reserved reserved reserved reserved
|
||||
ROW_COUNT non-reserved non-reserved non-reserved
|
||||
ROW_NUMBER reserved reserved
|
||||
RPAD reserved
|
||||
RTRIM reserved
|
||||
RULE non-reserved
|
||||
RUNNING reserved reserved
|
||||
SAVEPOINT non-reserved reserved reserved
|
||||
SCALAR non-reserved non-reserved non-reserved
|
||||
SCALE non-reserved non-reserved non-reserved
|
||||
SCHEMA non-reserved non-reserved non-reserved reserved
|
||||
SCHEMAS non-reserved
|
||||
SCHEMA_NAME non-reserved non-reserved non-reserved
|
||||
SCOPE reserved reserved
|
||||
SCOPE_CATALOG non-reserved non-reserved
|
||||
SCOPE_NAME non-reserved non-reserved
|
||||
SCOPE_SCHEMA non-reserved non-reserved
|
||||
SCROLL non-reserved reserved reserved reserved
|
||||
SEARCH non-reserved reserved reserved
|
||||
SECOND non-reserved, requires AS reserved reserved reserved
|
||||
SECTION non-reserved non-reserved reserved
|
||||
SECURITY non-reserved non-reserved non-reserved
|
||||
SEEK reserved reserved
|
||||
SELECT reserved reserved reserved reserved
|
||||
SELECTIVE non-reserved non-reserved
|
||||
SELF non-reserved non-reserved
|
||||
SEMANTICS non-reserved non-reserved
|
||||
SENSITIVE reserved reserved
|
||||
SEQUENCE non-reserved non-reserved non-reserved
|
||||
SEQUENCES non-reserved
|
||||
SERIALIZABLE non-reserved non-reserved non-reserved non-reserved
|
||||
SERVER non-reserved non-reserved non-reserved
|
||||
SERVER_NAME non-reserved non-reserved non-reserved
|
||||
SESSION non-reserved non-reserved non-reserved reserved
|
||||
SESSION_USER reserved reserved reserved reserved
|
||||
SET non-reserved reserved reserved reserved
|
||||
SETOF non-reserved (cannot be function or type)
|
||||
SETS non-reserved non-reserved non-reserved
|
||||
SHARE non-reserved
|
||||
SHOW non-reserved reserved reserved
|
||||
SIMILAR reserved (can be function or type) reserved reserved
|
||||
SIMPLE non-reserved non-reserved non-reserved
|
||||
SIN reserved reserved
|
||||
SINH reserved reserved
|
||||
SIZE non-reserved non-reserved reserved
|
||||
SKIP non-reserved reserved reserved
|
||||
SMALLINT non-reserved (cannot be function or type) reserved reserved reserved
|
||||
SNAPSHOT non-reserved
|
||||
SOME reserved reserved reserved reserved
|
||||
SORT_DIRECTION non-reserved non-reserved
|
||||
SOURCE non-reserved non-reserved
|
||||
SPACE non-reserved non-reserved reserved
|
||||
SPECIFIC reserved reserved
|
||||
SPECIFICTYPE reserved reserved
|
||||
SPECIFIC_NAME non-reserved non-reserved
|
||||
SQL non-reserved reserved reserved reserved
|
||||
SQLCODE reserved
|
||||
SQLERROR reserved
|
||||
SQLEXCEPTION reserved reserved
|
||||
SQLSTATE reserved reserved reserved
|
||||
SQLWARNING reserved reserved
|
||||
SQRT reserved reserved
|
||||
STABLE non-reserved
|
||||
STANDALONE non-reserved non-reserved non-reserved
|
||||
START non-reserved reserved reserved
|
||||
STATE non-reserved non-reserved
|
||||
STATEMENT non-reserved non-reserved non-reserved
|
||||
STATIC reserved reserved
|
||||
STATISTICS non-reserved
|
||||
STDDEV_POP reserved reserved
|
||||
STDDEV_SAMP reserved reserved
|
||||
STDIN non-reserved
|
||||
STDOUT non-reserved
|
||||
STORAGE non-reserved
|
||||
STORED non-reserved
|
||||
STRICT non-reserved
|
||||
STRING non-reserved non-reserved
|
||||
STRIP non-reserved non-reserved non-reserved
|
||||
STRUCTURE non-reserved non-reserved
|
||||
STYLE non-reserved non-reserved
|
||||
SUBCLASS_ORIGIN non-reserved non-reserved non-reserved
|
||||
SUBMULTISET reserved reserved
|
||||
SUBSCRIPTION non-reserved
|
||||
SUBSET reserved reserved
|
||||
SUBSTRING non-reserved (cannot be function or type) reserved reserved reserved
|
||||
SUBSTRING_REGEX reserved reserved
|
||||
SUCCEEDS reserved reserved
|
||||
SUM reserved reserved reserved
|
||||
SUPPORT non-reserved
|
||||
SYMMETRIC reserved reserved reserved
|
||||
SYSID non-reserved
|
||||
SYSTEM non-reserved reserved reserved
|
||||
SYSTEM_TIME reserved reserved
|
||||
SYSTEM_USER reserved reserved reserved reserved
|
||||
T non-reserved non-reserved
|
||||
TABLE reserved reserved reserved reserved
|
||||
TABLES non-reserved
|
||||
TABLESAMPLE reserved (can be function or type) reserved reserved
|
||||
TABLESPACE non-reserved
|
||||
TABLE_NAME non-reserved non-reserved non-reserved
|
||||
TAN reserved reserved
|
||||
TANH reserved reserved
|
||||
TEMP non-reserved
|
||||
TEMPLATE non-reserved
|
||||
TEMPORARY non-reserved non-reserved non-reserved reserved
|
||||
TEXT non-reserved
|
||||
THEN reserved reserved reserved reserved
|
||||
THROUGH non-reserved non-reserved
|
||||
TIES non-reserved non-reserved non-reserved
|
||||
TIME non-reserved (cannot be function or type) reserved reserved reserved
|
||||
TIMESTAMP non-reserved (cannot be function or type) reserved reserved reserved
|
||||
TIMEZONE_HOUR reserved reserved reserved
|
||||
TIMEZONE_MINUTE reserved reserved reserved
|
||||
TO reserved, requires AS reserved reserved reserved
|
||||
TOKEN non-reserved non-reserved
|
||||
TOP_LEVEL_COUNT non-reserved non-reserved
|
||||
TRAILING reserved reserved reserved reserved
|
||||
TRANSACTION non-reserved non-reserved non-reserved reserved
|
||||
TRANSACTIONS_COMMITTED non-reserved non-reserved
|
||||
TRANSACTIONS_ROLLED_BACK non-reserved non-reserved
|
||||
TRANSACTION_ACTIVE non-reserved non-reserved
|
||||
TRANSFORM non-reserved non-reserved non-reserved
|
||||
TRANSFORMS non-reserved non-reserved
|
||||
TRANSLATE reserved reserved reserved
|
||||
TRANSLATE_REGEX reserved reserved
|
||||
TRANSLATION reserved reserved reserved
|
||||
TREAT non-reserved (cannot be function or type) reserved reserved
|
||||
TRIGGER non-reserved reserved reserved
|
||||
TRIGGER_CATALOG non-reserved non-reserved
|
||||
TRIGGER_NAME non-reserved non-reserved
|
||||
TRIGGER_SCHEMA non-reserved non-reserved
|
||||
TRIM non-reserved (cannot be function or type) reserved reserved reserved
|
||||
TRIM_ARRAY reserved reserved
|
||||
TRUE reserved reserved reserved reserved
|
||||
TRUNCATE non-reserved reserved reserved
|
||||
TRUSTED non-reserved
|
||||
TYPE non-reserved non-reserved non-reserved non-reserved
|
||||
TYPES non-reserved
|
||||
UESCAPE non-reserved reserved reserved
|
||||
UNBOUNDED non-reserved non-reserved non-reserved
|
||||
UNCOMMITTED non-reserved non-reserved non-reserved non-reserved
|
||||
UNCONDITIONAL non-reserved non-reserved
|
||||
UNDER non-reserved non-reserved
|
||||
UNENCRYPTED non-reserved
|
||||
UNION reserved, requires AS reserved reserved reserved
|
||||
UNIQUE reserved reserved reserved reserved
|
||||
UNKNOWN non-reserved reserved reserved reserved
|
||||
UNLINK non-reserved non-reserved
|
||||
UNLISTEN non-reserved
|
||||
UNLOGGED non-reserved
|
||||
UNMATCHED non-reserved non-reserved
|
||||
UNNAMED non-reserved non-reserved non-reserved
|
||||
UNNEST reserved reserved
|
||||
UNTIL non-reserved
|
||||
UNTYPED non-reserved non-reserved
|
||||
UPDATE non-reserved reserved reserved reserved
|
||||
UPPER reserved reserved reserved
|
||||
URI non-reserved non-reserved
|
||||
USAGE non-reserved non-reserved reserved
|
||||
USER reserved reserved reserved reserved
|
||||
USER_DEFINED_TYPE_CATALOG non-reserved non-reserved
|
||||
USER_DEFINED_TYPE_CODE non-reserved non-reserved
|
||||
USER_DEFINED_TYPE_NAME non-reserved non-reserved
|
||||
USER_DEFINED_TYPE_SCHEMA non-reserved non-reserved
|
||||
USING reserved reserved reserved reserved
|
||||
UTF16 non-reserved non-reserved
|
||||
UTF32 non-reserved non-reserved
|
||||
UTF8 non-reserved non-reserved
|
||||
VACUUM non-reserved
|
||||
VALID non-reserved non-reserved non-reserved
|
||||
VALIDATE non-reserved
|
||||
VALIDATOR non-reserved
|
||||
VALUE non-reserved reserved reserved reserved
|
||||
VALUES non-reserved (cannot be function or type) reserved reserved reserved
|
||||
VALUE_OF reserved reserved
|
||||
VARBINARY reserved reserved
|
||||
VARCHAR non-reserved (cannot be function or type) reserved reserved reserved
|
||||
VARIADIC reserved
|
||||
VARYING non-reserved, requires AS reserved reserved reserved
|
||||
VAR_POP reserved reserved
|
||||
VAR_SAMP reserved reserved
|
||||
VERBOSE reserved (can be function or type)
|
||||
VERSION non-reserved non-reserved non-reserved
|
||||
VERSIONING reserved reserved
|
||||
VIEW non-reserved non-reserved non-reserved reserved
|
||||
VIEWS non-reserved
|
||||
VOLATILE non-reserved
|
||||
WHEN reserved reserved reserved reserved
|
||||
WHENEVER reserved reserved reserved
|
||||
WHERE reserved, requires AS reserved reserved reserved
|
||||
WHITESPACE non-reserved non-reserved non-reserved
|
||||
WIDTH_BUCKET reserved reserved
|
||||
WINDOW reserved, requires AS reserved reserved
|
||||
WITH reserved, requires AS reserved reserved reserved
|
||||
WITHIN non-reserved, requires AS reserved reserved
|
||||
WITHOUT non-reserved, requires AS reserved reserved
|
||||
WORK non-reserved non-reserved non-reserved reserved
|
||||
WRAPPER non-reserved non-reserved non-reserved
|
||||
WRITE non-reserved non-reserved non-reserved reserved
|
||||
XML non-reserved reserved reserved
|
||||
XMLAGG reserved reserved
|
||||
XMLATTRIBUTES non-reserved (cannot be function or type) reserved reserved
|
||||
XMLBINARY reserved reserved
|
||||
XMLCAST reserved reserved
|
||||
XMLCOMMENT reserved reserved
|
||||
XMLCONCAT non-reserved (cannot be function or type) reserved reserved
|
||||
XMLDECLARATION non-reserved non-reserved
|
||||
XMLDOCUMENT reserved reserved
|
||||
XMLELEMENT non-reserved (cannot be function or type) reserved reserved
|
||||
XMLEXISTS non-reserved (cannot be function or type) reserved reserved
|
||||
XMLFOREST non-reserved (cannot be function or type) reserved reserved
|
||||
XMLITERATE reserved reserved
|
||||
XMLNAMESPACES non-reserved (cannot be function or type) reserved reserved
|
||||
XMLPARSE non-reserved (cannot be function or type) reserved reserved
|
||||
XMLPI non-reserved (cannot be function or type) reserved reserved
|
||||
XMLQUERY reserved reserved
|
||||
XMLROOT non-reserved (cannot be function or type)
|
||||
XMLSCHEMA non-reserved non-reserved
|
||||
XMLSERIALIZE non-reserved (cannot be function or type) reserved reserved
|
||||
XMLTABLE non-reserved (cannot be function or type) reserved reserved
|
||||
XMLTEXT reserved reserved
|
||||
XMLVALIDATE reserved reserved
|
||||
YEAR non-reserved, requires AS reserved reserved reserved
|
||||
YES non-reserved non-reserved non-reserved
|
||||
ZONE non-reserved non-reserved non-reserved reserved
|
|
106
src/harlequin_postgres/loaders.py
Normal file
106
src/harlequin_postgres/loaders.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import psycopg
|
||||
from psycopg.errors import DataError
|
||||
|
||||
# Subclass existing adapters so that the base case is handled normally.
|
||||
from psycopg.types.datetime import (
|
||||
DateBinaryLoader,
|
||||
DateLoader,
|
||||
TimestampBinaryLoader,
|
||||
TimestampLoader,
|
||||
TimestamptzBinaryLoader,
|
||||
TimestamptzLoader,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from psycopg.adapt import Buffer, Loader
|
||||
|
||||
|
||||
class InfDateLoader(DateLoader):
|
||||
def load(self, data: "Buffer") -> date:
|
||||
if data == b"infinity":
|
||||
return date.max
|
||||
elif data == b"-infinity":
|
||||
return date.min
|
||||
else:
|
||||
return super().load(data)
|
||||
|
||||
|
||||
class InfDateBinaryLoader(DateBinaryLoader):
|
||||
def load(self, data: "Buffer") -> date:
|
||||
try:
|
||||
return super().load(data)
|
||||
except DataError as e:
|
||||
if "date too small" in str(e):
|
||||
return date.min
|
||||
elif "date too large" in str(e):
|
||||
return date.max
|
||||
raise e
|
||||
|
||||
|
||||
class InfTimestampLoader(TimestampLoader):
|
||||
def load(self, data: "Buffer") -> datetime:
|
||||
if data == b"infinity":
|
||||
return datetime.max
|
||||
elif data == b"-infinity":
|
||||
return datetime.min
|
||||
else:
|
||||
return super().load(data)
|
||||
|
||||
|
||||
class InfTimestampBinaryLoader(TimestampBinaryLoader):
|
||||
def load(self, data: "Buffer") -> datetime:
|
||||
try:
|
||||
return super().load(data)
|
||||
except DataError as e:
|
||||
if "timestamp too small" in str(e):
|
||||
return datetime.min
|
||||
elif "timestamp too large" in str(e):
|
||||
return datetime.max
|
||||
raise e
|
||||
|
||||
|
||||
class InfTimestamptzLoader(TimestamptzLoader):
|
||||
def load(self, data: "Buffer") -> datetime:
|
||||
if data == b"infinity":
|
||||
return datetime.max
|
||||
elif data == b"-infinity":
|
||||
return datetime.min
|
||||
else:
|
||||
return super().load(data)
|
||||
|
||||
|
||||
class InfTimestamptzBinaryLoader(TimestamptzBinaryLoader):
|
||||
def load(self, data: "Buffer") -> datetime:
|
||||
try:
|
||||
return super().load(data)
|
||||
except DataError as e:
|
||||
if "timestamp too small" in str(e):
|
||||
return datetime.min
|
||||
elif "timestamp too large" in str(e):
|
||||
return datetime.max
|
||||
raise e
|
||||
|
||||
|
||||
INF_LOADERS: list[tuple[str, type["Loader"]]] = [
|
||||
("date", InfDateLoader),
|
||||
("date", InfDateBinaryLoader),
|
||||
("timestamp", InfTimestampLoader),
|
||||
("timestamp", InfTimestampBinaryLoader),
|
||||
("timestamptz", InfTimestamptzLoader),
|
||||
("timestamptz", InfTimestamptzBinaryLoader),
|
||||
]
|
||||
|
||||
|
||||
def register_inf_loaders() -> None:
|
||||
"""
|
||||
Register updated date/datetime loaders in the global types
|
||||
registry, so that any connections created afterwards will use
|
||||
the updated loaders (that allow infinity date/timestamps)
|
||||
"""
|
||||
for type_name, loader in INF_LOADERS:
|
||||
psycopg.adapters.register_loader(type_name, loader)
|
0
src/harlequin_postgres/py.typed
Normal file
0
src/harlequin_postgres/py.typed
Normal file
40
tests/conftest.py
Normal file
40
tests/conftest.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Generator
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
from harlequin_postgres.adapter import (
|
||||
HarlequinPostgresAdapter,
|
||||
HarlequinPostgresConnection,
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
TEST_DB_CONN = "postgresql://postgres:for-testing@localhost:5432"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connection() -> Generator[HarlequinPostgresConnection, None, None]:
|
||||
pgconn = psycopg.connect(conninfo=TEST_DB_CONN, dbname="postgres")
|
||||
pgconn.autocommit = True
|
||||
cur = pgconn.cursor()
|
||||
cur.execute("drop database if exists test;")
|
||||
cur.execute("create database test;")
|
||||
cur.close()
|
||||
pgconn.close()
|
||||
conn = HarlequinPostgresAdapter(
|
||||
conn_str=(f"{TEST_DB_CONN}",), dbname="test"
|
||||
).connect()
|
||||
yield conn
|
||||
conn.close()
|
||||
pgconn = psycopg.connect(conninfo=TEST_DB_CONN, dbname="postgres")
|
||||
pgconn.autocommit = True
|
||||
cur = pgconn.cursor()
|
||||
cur.execute("drop database if exists test;")
|
||||
cur.close()
|
||||
pgconn.close()
|
158
tests/test_adapter.py
Normal file
158
tests/test_adapter.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
|
||||
import pytest
|
||||
from harlequin.adapter import HarlequinAdapter, HarlequinConnection, HarlequinCursor
|
||||
from harlequin.catalog import Catalog, CatalogItem
|
||||
from harlequin.exception import HarlequinConnectionError, HarlequinQueryError
|
||||
from harlequin_postgres.adapter import (
|
||||
HarlequinPostgresAdapter,
|
||||
HarlequinPostgresConnection,
|
||||
)
|
||||
from textual_fastdatatable.backend import create_backend
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
from importlib_metadata import entry_points
|
||||
else:
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
TEST_DB_CONN = "postgresql://postgres:for-testing@localhost:5432"
|
||||
|
||||
|
||||
def test_plugin_discovery() -> None:
|
||||
PLUGIN_NAME = "postgres"
|
||||
eps = entry_points(group="harlequin.adapter")
|
||||
assert eps[PLUGIN_NAME]
|
||||
adapter_cls = eps[PLUGIN_NAME].load()
|
||||
assert issubclass(adapter_cls, HarlequinAdapter)
|
||||
assert adapter_cls == HarlequinPostgresAdapter
|
||||
|
||||
|
||||
def test_connect() -> None:
|
||||
conn = HarlequinPostgresAdapter(conn_str=(TEST_DB_CONN,)).connect()
|
||||
assert isinstance(conn, HarlequinConnection)
|
||||
|
||||
|
||||
def test_init_extra_kwargs() -> None:
|
||||
assert HarlequinPostgresAdapter(
|
||||
conn_str=(TEST_DB_CONN,), foo=1, bar="baz"
|
||||
).connect()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conn_str",
|
||||
[
|
||||
("foo",),
|
||||
("host=foo",),
|
||||
("postgresql://admin:pass@foo:5432/db",),
|
||||
],
|
||||
)
|
||||
def test_connect_raises_connection_error(conn_str: tuple[str]) -> None:
|
||||
with pytest.raises(HarlequinConnectionError):
|
||||
_ = HarlequinPostgresAdapter(conn_str=conn_str, connect_timeout=0.1).connect()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conn_str,options,expected",
|
||||
[
|
||||
(("",), {}, "localhost:5432/postgres"),
|
||||
(("host=foo",), {}, "foo:5432/postgres"),
|
||||
(("postgresql://foo",), {}, "foo:5432/postgres"),
|
||||
(("postgresql://foo",), {"port": 5431}, "foo:5431/postgres"),
|
||||
(("postgresql://foo/mydb",), {"port": 5431}, "foo:5431/mydb"),
|
||||
(("postgresql://admin:pass@foo/mydb",), {"port": 5431}, "foo:5431/mydb"),
|
||||
(("postgresql://admin:pass@foo:5431/mydb",), {}, "foo:5431/mydb"),
|
||||
],
|
||||
)
|
||||
def test_connection_id(
|
||||
conn_str: tuple[str], options: dict[str, int | float | str | None], expected: str
|
||||
) -> None:
|
||||
adapter = HarlequinPostgresAdapter(
|
||||
conn_str=conn_str,
|
||||
**options, # type: ignore[arg-type]
|
||||
)
|
||||
assert adapter.connection_id == expected
|
||||
|
||||
|
||||
def test_get_catalog(connection: HarlequinPostgresConnection) -> None:
|
||||
catalog = connection.get_catalog()
|
||||
assert isinstance(catalog, Catalog)
|
||||
assert catalog.items
|
||||
assert isinstance(catalog.items[0], CatalogItem)
|
||||
|
||||
|
||||
def test_get_completions(connection: HarlequinPostgresConnection) -> None:
|
||||
completions = connection.get_completions()
|
||||
test_labels = ["atomic", "greatest", "point_right", "autovacuum"]
|
||||
filtered = list(filter(lambda x: x.label in test_labels, completions))
|
||||
assert len(filtered) == 4
|
||||
value_filtered = list(filter(lambda x: x.value in test_labels, completions))
|
||||
assert len(value_filtered) == 4
|
||||
|
||||
|
||||
def test_execute_ddl(connection: HarlequinPostgresConnection) -> None:
|
||||
cur = connection.execute("create table foo (a int)")
|
||||
assert cur is None
|
||||
|
||||
|
||||
def test_execute_select(connection: HarlequinPostgresConnection) -> None:
|
||||
cur = connection.execute("select 1 as a")
|
||||
assert isinstance(cur, HarlequinCursor)
|
||||
assert cur.columns() == [("a", "#")]
|
||||
data = cur.fetchall()
|
||||
backend = create_backend(data)
|
||||
assert backend.column_count == 1
|
||||
assert backend.row_count == 1
|
||||
|
||||
|
||||
def test_execute_select_dupe_cols(connection: HarlequinPostgresConnection) -> None:
|
||||
cur = connection.execute("select 1 as a, 2 as a, 3 as a")
|
||||
assert isinstance(cur, HarlequinCursor)
|
||||
assert len(cur.columns()) == 3
|
||||
data = cur.fetchall()
|
||||
backend = create_backend(data)
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 1
|
||||
|
||||
|
||||
def test_set_limit(connection: HarlequinPostgresConnection) -> None:
|
||||
cur = connection.execute("select 1 as a union all select 2 union all select 3")
|
||||
assert isinstance(cur, HarlequinCursor)
|
||||
cur = cur.set_limit(2)
|
||||
assert isinstance(cur, HarlequinCursor)
|
||||
data = cur.fetchall()
|
||||
backend = create_backend(data)
|
||||
assert backend.column_count == 1
|
||||
assert backend.row_count == 2
|
||||
|
||||
|
||||
def test_execute_raises_query_error(connection: HarlequinPostgresConnection) -> None:
|
||||
with pytest.raises(HarlequinQueryError):
|
||||
_ = connection.execute("sel;")
|
||||
|
||||
|
||||
def test_inf_timestamps(connection: HarlequinPostgresConnection) -> None:
|
||||
cur = connection.execute(
|
||||
"""select
|
||||
'infinity'::date,
|
||||
'infinity'::timestamp,
|
||||
'infinity'::timestamptz,
|
||||
'-infinity'::date,
|
||||
'-infinity'::timestamp,
|
||||
'-infinity'::timestamptz
|
||||
"""
|
||||
)
|
||||
assert cur is not None
|
||||
data = cur.fetchall()
|
||||
assert data == [
|
||||
(
|
||||
date.max,
|
||||
datetime.max,
|
||||
datetime.max,
|
||||
date.min,
|
||||
datetime.min,
|
||||
datetime.min,
|
||||
)
|
||||
]
|
92
tests/test_catalog.py
Normal file
92
tests/test_catalog.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
import pytest
|
||||
from harlequin.catalog import InteractiveCatalogItem
|
||||
from harlequin_postgres.adapter import HarlequinPostgresConnection
|
||||
from harlequin_postgres.catalog import (
|
||||
ColumnCatalogItem,
|
||||
DatabaseCatalogItem,
|
||||
RelationCatalogItem,
|
||||
SchemaCatalogItem,
|
||||
TableCatalogItem,
|
||||
ViewCatalogItem,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connection_with_objects(
|
||||
connection: HarlequinPostgresConnection,
|
||||
) -> HarlequinPostgresConnection:
|
||||
connection.execute("create schema one")
|
||||
connection.execute("create table one.foo as select 1 as a, '2' as b")
|
||||
connection.execute("create table one.bar as select 1 as a, '2' as b")
|
||||
connection.execute("create table one.baz as select 1 as a, '2' as b")
|
||||
connection.execute("create schema two")
|
||||
connection.execute("create view two.qux as select * from one.foo")
|
||||
connection.execute("create schema three")
|
||||
# the original connection fixture will clean this up.
|
||||
return connection
|
||||
|
||||
|
||||
def test_catalog(connection_with_objects: HarlequinPostgresConnection) -> None:
|
||||
conn = connection_with_objects
|
||||
|
||||
catalog = conn.get_catalog()
|
||||
|
||||
# at least two databases, postgres and test
|
||||
assert len(catalog.items) >= 2
|
||||
|
||||
[test_db_item] = filter(lambda item: item.label == "test", catalog.items)
|
||||
assert isinstance(test_db_item, InteractiveCatalogItem)
|
||||
assert isinstance(test_db_item, DatabaseCatalogItem)
|
||||
assert not test_db_item.children
|
||||
assert not test_db_item.loaded
|
||||
|
||||
schema_items = test_db_item.fetch_children()
|
||||
assert all(isinstance(item, SchemaCatalogItem) for item in schema_items)
|
||||
|
||||
[schema_one_item] = filter(lambda item: item.label == "one", schema_items)
|
||||
assert isinstance(schema_one_item, SchemaCatalogItem)
|
||||
assert not schema_one_item.children
|
||||
assert not schema_one_item.loaded
|
||||
|
||||
table_items = schema_one_item.fetch_children()
|
||||
assert all(isinstance(item, RelationCatalogItem) for item in table_items)
|
||||
|
||||
[foo_item] = filter(lambda item: item.label == "foo", table_items)
|
||||
assert isinstance(foo_item, TableCatalogItem)
|
||||
assert not foo_item.children
|
||||
assert not foo_item.loaded
|
||||
|
||||
foo_column_items = foo_item.fetch_children()
|
||||
assert all(isinstance(item, ColumnCatalogItem) for item in foo_column_items)
|
||||
|
||||
[schema_two_item] = filter(lambda item: item.label == "two", schema_items)
|
||||
assert isinstance(schema_two_item, SchemaCatalogItem)
|
||||
assert not schema_two_item.children
|
||||
assert not schema_two_item.loaded
|
||||
|
||||
view_items = schema_two_item.fetch_children()
|
||||
assert all(isinstance(item, ViewCatalogItem) for item in view_items)
|
||||
|
||||
[qux_item] = filter(lambda item: item.label == "qux", view_items)
|
||||
assert isinstance(qux_item, ViewCatalogItem)
|
||||
assert not qux_item.children
|
||||
assert not qux_item.loaded
|
||||
|
||||
qux_column_items = qux_item.fetch_children()
|
||||
assert all(isinstance(item, ColumnCatalogItem) for item in qux_column_items)
|
||||
|
||||
assert [item.label for item in foo_column_items] == [
|
||||
item.label for item in qux_column_items
|
||||
]
|
||||
|
||||
# ensure calling fetch_children on cols doesn't raise
|
||||
children_items = foo_column_items[0].fetch_children()
|
||||
assert not children_items
|
||||
|
||||
[schema_three_item] = filter(lambda item: item.label == "three", schema_items)
|
||||
assert isinstance(schema_two_item, SchemaCatalogItem)
|
||||
assert not schema_two_item.children
|
||||
assert not schema_two_item.loaded
|
||||
|
||||
three_children = schema_three_item.fetch_children()
|
||||
assert not three_children
|
Loading…
Add table
Reference in a new issue