harlequin-postgres 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harlequin_postgres-0.1.2/LICENSE +21 -0
- harlequin_postgres-0.1.2/PKG-INFO +88 -0
- harlequin_postgres-0.1.2/README.md +71 -0
- harlequin_postgres-0.1.2/pyproject.toml +67 -0
- harlequin_postgres-0.1.2/src/harlequin_postgres/__init__.py +3 -0
- harlequin_postgres-0.1.2/src/harlequin_postgres/adapter.py +352 -0
- harlequin_postgres-0.1.2/src/harlequin_postgres/cli_options.py +160 -0
- harlequin_postgres-0.1.2/src/harlequin_postgres/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Ted Conbeer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: harlequin-postgres
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A Harlequin adapter for Postgres.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Ted Conbeer
|
|
7
|
+
Author-email: tconbeer@users.noreply.github.com
|
|
8
|
+
Requires-Python: >=3.8.1,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Requires-Dist: harlequin (>=1.4,<2.0)
|
|
15
|
+
Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# harlequin-postgres
|
|
19
|
+
|
|
20
|
+
This repo provides the Harlequin adapter for Postgres.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
`harlequin-postgres` depends on `harlequin`, so installing this package will also install Harlequin.
|
|
25
|
+
|
|
26
|
+
### Using pip
|
|
27
|
+
|
|
28
|
+
To install this adapter into an activated virtual environment:
|
|
29
|
+
```bash
|
|
30
|
+
pip install harlequin-postgres
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Using poetry
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
poetry add harlequin-postgres
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using pipx
|
|
40
|
+
|
|
41
|
+
If you do not already have Harlequin installed:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install harlequin-postgres
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If you would like to add the Postgres adapter to an existing Harlequin installation:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pipx inject harlequin harlequin-postgres
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### As an Extra
|
|
54
|
+
Alternatively, you can install Harlequin with the `postgres` extra:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install harlequin[postgres]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
poetry add harlequin[postgres]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pipx install harlequin[postgres]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage and Configuration
|
|
69
|
+
|
|
70
|
+
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):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
harlequin -a postgres "postgres://my-user:my-pass@localhost:5432/my-database"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
You can also pass all or parts of the connection string as separate options. The following is equivalent to the above DSN:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
harlequin -a postgres -h localhost -p 5432 -U my-user --password my-pass -d my-database
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Many more options are available; to see the full list, run:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
harlequin --help
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For more information, see the [Harlequin Docs](https://harlequin.sh/docs/postgres/index).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# harlequin-postgres
|
|
2
|
+
|
|
3
|
+
This repo provides the Harlequin adapter for Postgres.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
`harlequin-postgres` depends on `harlequin`, so installing this package will also install Harlequin.
|
|
8
|
+
|
|
9
|
+
### Using pip
|
|
10
|
+
|
|
11
|
+
To install this adapter into an activated virtual environment:
|
|
12
|
+
```bash
|
|
13
|
+
pip install harlequin-postgres
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Using poetry
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
poetry add harlequin-postgres
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Using pipx
|
|
23
|
+
|
|
24
|
+
If you do not already have Harlequin installed:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install harlequin-postgres
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If you would like to add the Postgres adapter to an existing Harlequin installation:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pipx inject harlequin harlequin-postgres
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### As an Extra
|
|
37
|
+
Alternatively, you can install Harlequin with the `postgres` extra:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install harlequin[postgres]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
poetry add harlequin[postgres]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pipx install harlequin[postgres]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage and Configuration
|
|
52
|
+
|
|
53
|
+
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):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
harlequin -a postgres "postgres://my-user:my-pass@localhost:5432/my-database"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
You can also pass all or parts of the connection string as separate options. The following is equivalent to the above DSN:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
harlequin -a postgres -h localhost -p 5432 -U my-user --password my-pass -d my-database
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Many more options are available; to see the full list, run:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
harlequin --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For more information, see the [Harlequin Docs](https://harlequin.sh/docs/postgres/index).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "harlequin-postgres"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "A Harlequin adapter for Postgres."
|
|
5
|
+
authors = ["Ted Conbeer <tconbeer@users.noreply.github.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [
|
|
9
|
+
{ include = "harlequin_postgres", from = "src" },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.plugins."harlequin.adapter"]
|
|
13
|
+
postgres = "harlequin_postgres:HarlequinPostgresAdapter"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dependencies]
|
|
16
|
+
python = ">=3.8.1,<4.0"
|
|
17
|
+
harlequin = "^1.4"
|
|
18
|
+
psycopg2-binary = "^2.9.9"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
|
21
|
+
ruff = "^0.1.6"
|
|
22
|
+
pytest = "^7.4.3"
|
|
23
|
+
mypy = "^1.7.0"
|
|
24
|
+
pre-commit = "^3.5.0"
|
|
25
|
+
importlib_metadata = { version = ">=4.6.0", python = "<3.10.0" }
|
|
26
|
+
types-psycopg2 = "^2.9.21.16"
|
|
27
|
+
pyinstrument = "^4.6.1"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["poetry-core"]
|
|
31
|
+
build-backend = "poetry.core.masonry.api"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
select = ["A", "B", "E", "F", "I"]
|
|
36
|
+
target-version = "py38"
|
|
37
|
+
|
|
38
|
+
[tool.mypy]
|
|
39
|
+
python_version = "3.8"
|
|
40
|
+
files = [
|
|
41
|
+
"src/**/*.py",
|
|
42
|
+
"tests/**/*.py",
|
|
43
|
+
]
|
|
44
|
+
mypy_path = "src:stubs"
|
|
45
|
+
|
|
46
|
+
show_column_numbers = true
|
|
47
|
+
|
|
48
|
+
# show error messages from unrelated files
|
|
49
|
+
follow_imports = "normal"
|
|
50
|
+
|
|
51
|
+
# be strict
|
|
52
|
+
disallow_untyped_calls = true
|
|
53
|
+
disallow_untyped_defs = true
|
|
54
|
+
check_untyped_defs = true
|
|
55
|
+
disallow_untyped_decorators = true
|
|
56
|
+
disallow_incomplete_defs = true
|
|
57
|
+
disallow_subclassing_any = true
|
|
58
|
+
strict_optional = true
|
|
59
|
+
|
|
60
|
+
warn_return_any = true
|
|
61
|
+
warn_no_return = true
|
|
62
|
+
warn_redundant_casts = true
|
|
63
|
+
warn_unused_ignores = true
|
|
64
|
+
warn_unused_configs = true
|
|
65
|
+
|
|
66
|
+
no_implicit_reexport = true
|
|
67
|
+
strict_equality = true
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Sequence
|
|
4
|
+
|
|
5
|
+
from harlequin.adapter import HarlequinAdapter, HarlequinConnection, HarlequinCursor
|
|
6
|
+
from harlequin.catalog import Catalog, CatalogItem
|
|
7
|
+
from harlequin.exception import HarlequinConnectionError, HarlequinQueryError
|
|
8
|
+
from psycopg2 import connect
|
|
9
|
+
from psycopg2.extensions import connection, cursor
|
|
10
|
+
from textual_fastdatatable.backend import AutoBackendType
|
|
11
|
+
|
|
12
|
+
from harlequin_postgres.cli_options import POSTGRES_OPTIONS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HarlequinPostgresCursor(HarlequinCursor):
|
|
16
|
+
def __init__(self, conn: HarlequinPostgresConnection, cur: cursor) -> None:
|
|
17
|
+
self.conn = conn
|
|
18
|
+
self.cur = cur
|
|
19
|
+
self._limit: int | None = None
|
|
20
|
+
|
|
21
|
+
def columns(self) -> list[tuple[str, str]]:
|
|
22
|
+
assert self.cur.description is not None
|
|
23
|
+
return [
|
|
24
|
+
(col.name, self.conn._get_short_type_from_oid(col.type_code))
|
|
25
|
+
for col in self.cur.description
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def set_limit(self, limit: int) -> HarlequinPostgresCursor:
|
|
29
|
+
self._limit = limit
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def fetchall(self) -> AutoBackendType:
|
|
33
|
+
try:
|
|
34
|
+
if self._limit is None:
|
|
35
|
+
return self.cur.fetchall()
|
|
36
|
+
else:
|
|
37
|
+
return self.cur.fetchmany(self._limit)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise HarlequinQueryError(
|
|
40
|
+
msg=str(e),
|
|
41
|
+
title="Harlequin encountered an error while executing your query.",
|
|
42
|
+
) from e
|
|
43
|
+
finally:
|
|
44
|
+
self.cur.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HarlequinPostgresConnection(HarlequinConnection):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
conn_str: Sequence[str],
|
|
51
|
+
*_: Any,
|
|
52
|
+
init_message: str = "",
|
|
53
|
+
options: dict[str, Any],
|
|
54
|
+
) -> None:
|
|
55
|
+
self.init_message = init_message
|
|
56
|
+
try:
|
|
57
|
+
if conn_str and conn_str[0]:
|
|
58
|
+
self.conn: connection = connect(dsn=conn_str[0], **options)
|
|
59
|
+
else:
|
|
60
|
+
self.conn = connect(**options)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise HarlequinConnectionError(
|
|
63
|
+
msg=str(e), title="Harlequin could not connect to Postgres."
|
|
64
|
+
) from e
|
|
65
|
+
|
|
66
|
+
def execute(self, query: str) -> HarlequinCursor | None:
|
|
67
|
+
try:
|
|
68
|
+
with self.conn: # autocommit transaction
|
|
69
|
+
cur = self.conn.cursor()
|
|
70
|
+
cur.execute(query=query)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
raise HarlequinQueryError(
|
|
73
|
+
msg=str(e),
|
|
74
|
+
title="Harlequin encountered an error while executing your query.",
|
|
75
|
+
) from e
|
|
76
|
+
else:
|
|
77
|
+
if cur.description is not None:
|
|
78
|
+
return HarlequinPostgresCursor(self, cur)
|
|
79
|
+
else:
|
|
80
|
+
cur.close()
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def get_catalog(self) -> Catalog:
|
|
84
|
+
databases = self._get_databases()
|
|
85
|
+
db_items: list[CatalogItem] = []
|
|
86
|
+
for (db,) in databases:
|
|
87
|
+
schemas = self._get_schemas(db)
|
|
88
|
+
schema_items: list[CatalogItem] = []
|
|
89
|
+
for (schema,) in schemas:
|
|
90
|
+
relations = self._get_relations(db, schema)
|
|
91
|
+
rel_items: list[CatalogItem] = []
|
|
92
|
+
for rel, rel_type in relations:
|
|
93
|
+
cols = self._get_columns(db, schema, rel)
|
|
94
|
+
col_items = [
|
|
95
|
+
CatalogItem(
|
|
96
|
+
qualified_identifier=f'"{db}"."{schema}"."{rel}"."{col}"',
|
|
97
|
+
query_name=f'"{col}"',
|
|
98
|
+
label=col,
|
|
99
|
+
type_label=self._get_short_type(col_type),
|
|
100
|
+
)
|
|
101
|
+
for col, col_type in cols
|
|
102
|
+
]
|
|
103
|
+
rel_items.append(
|
|
104
|
+
CatalogItem(
|
|
105
|
+
qualified_identifier=f'"{db}"."{schema}"."{rel}"',
|
|
106
|
+
query_name=f'"{db}"."{schema}"."{rel}"',
|
|
107
|
+
label=rel,
|
|
108
|
+
type_label="v" if rel_type == "VIEW" else "t",
|
|
109
|
+
children=col_items,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
schema_items.append(
|
|
113
|
+
CatalogItem(
|
|
114
|
+
qualified_identifier=f'"{db}"."{schema}"',
|
|
115
|
+
query_name=f'"{db}"."{schema}"',
|
|
116
|
+
label=schema,
|
|
117
|
+
type_label="s",
|
|
118
|
+
children=rel_items,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
db_items.append(
|
|
122
|
+
CatalogItem(
|
|
123
|
+
qualified_identifier=f'"{db}"',
|
|
124
|
+
query_name=f'"{db}"',
|
|
125
|
+
label=db,
|
|
126
|
+
type_label="db",
|
|
127
|
+
children=schema_items,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return Catalog(items=db_items)
|
|
131
|
+
|
|
132
|
+
def _get_databases(self) -> list[tuple[str]]:
|
|
133
|
+
with self.conn.cursor() as cur:
|
|
134
|
+
cur.execute(
|
|
135
|
+
"""
|
|
136
|
+
select datname
|
|
137
|
+
from pg_database
|
|
138
|
+
where
|
|
139
|
+
datistemplate is false
|
|
140
|
+
and datallowconn is true
|
|
141
|
+
;"""
|
|
142
|
+
)
|
|
143
|
+
results = cur.fetchall()
|
|
144
|
+
return results
|
|
145
|
+
|
|
146
|
+
def _get_schemas(self, dbname: str) -> list[tuple[str]]:
|
|
147
|
+
with self.conn.cursor() as cur:
|
|
148
|
+
cur.execute(
|
|
149
|
+
f"""
|
|
150
|
+
select schema_name
|
|
151
|
+
from information_schema.schemata
|
|
152
|
+
where
|
|
153
|
+
catalog_name = '{dbname}'
|
|
154
|
+
and schema_name != 'information_schema'
|
|
155
|
+
and schema_name not like 'pg_%'
|
|
156
|
+
;"""
|
|
157
|
+
)
|
|
158
|
+
results = cur.fetchall()
|
|
159
|
+
return results
|
|
160
|
+
|
|
161
|
+
def _get_relations(self, dbname: str, schema: str) -> list[tuple[str, str]]:
|
|
162
|
+
with self.conn.cursor() as cur:
|
|
163
|
+
cur.execute(
|
|
164
|
+
f"""
|
|
165
|
+
select table_name, table_type
|
|
166
|
+
from information_schema.tables
|
|
167
|
+
where
|
|
168
|
+
table_catalog = '{dbname}'
|
|
169
|
+
and table_schema = '{schema}'
|
|
170
|
+
;"""
|
|
171
|
+
)
|
|
172
|
+
results = cur.fetchall()
|
|
173
|
+
return results
|
|
174
|
+
|
|
175
|
+
def _get_columns(
|
|
176
|
+
self, dbname: str, schema: str, relation: str
|
|
177
|
+
) -> list[tuple[str, str]]:
|
|
178
|
+
with self.conn.cursor() as cur:
|
|
179
|
+
cur.execute(
|
|
180
|
+
f"""
|
|
181
|
+
select column_name, data_type
|
|
182
|
+
from information_schema.columns
|
|
183
|
+
where
|
|
184
|
+
table_catalog = '{dbname}'
|
|
185
|
+
and table_schema = '{schema}'
|
|
186
|
+
and table_name = '{relation}'
|
|
187
|
+
;"""
|
|
188
|
+
)
|
|
189
|
+
results = cur.fetchall()
|
|
190
|
+
return results
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _get_short_type(type_name: str) -> str:
|
|
194
|
+
MAPPING = {
|
|
195
|
+
"bigint": "##",
|
|
196
|
+
"bigserial": "##",
|
|
197
|
+
"bit": "010",
|
|
198
|
+
"boolean": "t/f",
|
|
199
|
+
"box": "□",
|
|
200
|
+
"bytea": "b",
|
|
201
|
+
"character": "s",
|
|
202
|
+
"cidr": "ip",
|
|
203
|
+
"circle": "○",
|
|
204
|
+
"date": "d",
|
|
205
|
+
"double": "#.#",
|
|
206
|
+
"inet": "ip",
|
|
207
|
+
"integer": "#",
|
|
208
|
+
"interval": "|-|",
|
|
209
|
+
"json": "{}",
|
|
210
|
+
"jsonb": "b{}",
|
|
211
|
+
"line": "—",
|
|
212
|
+
"lseg": "-",
|
|
213
|
+
"macaddr": "mac",
|
|
214
|
+
"macaddr8": "mac",
|
|
215
|
+
"money": "$$",
|
|
216
|
+
"numeric": "#.#",
|
|
217
|
+
"path": "╭",
|
|
218
|
+
"pg_lsn": "lsn",
|
|
219
|
+
"pg_snapshot": "snp",
|
|
220
|
+
"point": "•",
|
|
221
|
+
"polygon": "▽",
|
|
222
|
+
"real": "#.#",
|
|
223
|
+
"smallint": "#",
|
|
224
|
+
"smallserial": "#",
|
|
225
|
+
"serial": "#",
|
|
226
|
+
"text": "s",
|
|
227
|
+
"time": "t",
|
|
228
|
+
"timestamp": "ts",
|
|
229
|
+
"tsquery": "tsq",
|
|
230
|
+
"tsvector": "tsv",
|
|
231
|
+
"txid_snapshot": "snp",
|
|
232
|
+
"uuid": "uid",
|
|
233
|
+
"xml": "xml",
|
|
234
|
+
"array": "[]",
|
|
235
|
+
}
|
|
236
|
+
return MAPPING.get(type_name.split("(")[0].split(" ")[0], "?")
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _get_short_type_from_oid(oid: int) -> str:
|
|
240
|
+
MAPPING = {
|
|
241
|
+
16: "t/f",
|
|
242
|
+
17: "b",
|
|
243
|
+
18: "s",
|
|
244
|
+
19: "s",
|
|
245
|
+
20: "##",
|
|
246
|
+
21: "#",
|
|
247
|
+
22: "[#]",
|
|
248
|
+
23: "#",
|
|
249
|
+
25: "s",
|
|
250
|
+
26: "oid",
|
|
251
|
+
114: "{}",
|
|
252
|
+
142: "xml",
|
|
253
|
+
600: "•",
|
|
254
|
+
601: "-",
|
|
255
|
+
602: "╭",
|
|
256
|
+
603: "□",
|
|
257
|
+
604: "▽",
|
|
258
|
+
628: "—",
|
|
259
|
+
651: "[ip]",
|
|
260
|
+
700: "#.#",
|
|
261
|
+
701: "#.#",
|
|
262
|
+
704: "|-|",
|
|
263
|
+
718: "○",
|
|
264
|
+
790: "$$",
|
|
265
|
+
829: "mac",
|
|
266
|
+
869: "ip",
|
|
267
|
+
650: "ip",
|
|
268
|
+
774: "mac",
|
|
269
|
+
1000: "[t/f]",
|
|
270
|
+
1001: "[b]",
|
|
271
|
+
1002: "[s]",
|
|
272
|
+
1003: "[s]",
|
|
273
|
+
1009: "[s]",
|
|
274
|
+
1013: "[oid]",
|
|
275
|
+
1014: "[s]",
|
|
276
|
+
1015: "[s]",
|
|
277
|
+
1016: "[#]",
|
|
278
|
+
1021: "[#.#]",
|
|
279
|
+
1022: "[#.#]",
|
|
280
|
+
1028: "[oid]",
|
|
281
|
+
1040: "[mac]",
|
|
282
|
+
1041: "[ip]",
|
|
283
|
+
1042: "s",
|
|
284
|
+
1043: "s",
|
|
285
|
+
1082: "d",
|
|
286
|
+
1083: "t",
|
|
287
|
+
1114: "ts",
|
|
288
|
+
1115: "[ts]",
|
|
289
|
+
1182: "[d]",
|
|
290
|
+
1183: "[t]",
|
|
291
|
+
1184: "ts",
|
|
292
|
+
1185: "[ts]",
|
|
293
|
+
1186: "|-|",
|
|
294
|
+
1187: "[|-|]",
|
|
295
|
+
1231: "[#.#]",
|
|
296
|
+
1266: "t",
|
|
297
|
+
1270: "[t]",
|
|
298
|
+
1560: "010",
|
|
299
|
+
1562: "010",
|
|
300
|
+
1700: "#.#",
|
|
301
|
+
2950: "uid",
|
|
302
|
+
3614: "tsv",
|
|
303
|
+
3615: "tsq",
|
|
304
|
+
3802: "b{}",
|
|
305
|
+
}
|
|
306
|
+
return MAPPING.get(oid, "?")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class HarlequinPostgresAdapter(HarlequinAdapter):
|
|
310
|
+
ADAPTER_OPTIONS = POSTGRES_OPTIONS
|
|
311
|
+
|
|
312
|
+
def __init__(
|
|
313
|
+
self,
|
|
314
|
+
conn_str: Sequence[str],
|
|
315
|
+
host: str | None = None,
|
|
316
|
+
port: str | None = None,
|
|
317
|
+
dbname: str | None = None,
|
|
318
|
+
user: str | None = None,
|
|
319
|
+
password: str | None = None,
|
|
320
|
+
passfile: str | None = None,
|
|
321
|
+
require_auth: str | None = None,
|
|
322
|
+
channel_binding: str | None = None,
|
|
323
|
+
connect_timeout: int | None = None,
|
|
324
|
+
sslmode: str | None = None,
|
|
325
|
+
sslcert: str | None = None,
|
|
326
|
+
sslkey: str | None = None,
|
|
327
|
+
**_: Any,
|
|
328
|
+
) -> None:
|
|
329
|
+
self.conn_str = conn_str
|
|
330
|
+
self.options = {
|
|
331
|
+
"host": host,
|
|
332
|
+
"port": port,
|
|
333
|
+
"dbname": dbname,
|
|
334
|
+
"user": user,
|
|
335
|
+
"password": password,
|
|
336
|
+
"passfile": passfile,
|
|
337
|
+
"require_auth": require_auth,
|
|
338
|
+
"channel_binding": channel_binding,
|
|
339
|
+
"connect_timeout": connect_timeout,
|
|
340
|
+
"sslmode": sslmode,
|
|
341
|
+
"sslcert": sslcert,
|
|
342
|
+
"sslkey": sslkey,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def connect(self) -> HarlequinPostgresConnection:
|
|
346
|
+
if len(self.conn_str) > 1:
|
|
347
|
+
raise HarlequinConnectionError(
|
|
348
|
+
"Cannot provide multiple connection strings to the Postgres adapter. "
|
|
349
|
+
f"{self.conn_str}"
|
|
350
|
+
)
|
|
351
|
+
conn = HarlequinPostgresConnection(self.conn_str, options=self.options)
|
|
352
|
+
return conn
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from harlequin.options import (
|
|
4
|
+
FlagOption, # noqa
|
|
5
|
+
ListOption, # noqa
|
|
6
|
+
PathOption, # noqa
|
|
7
|
+
SelectOption, # noqa
|
|
8
|
+
TextOption,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
host = TextOption(
|
|
12
|
+
name="host",
|
|
13
|
+
description=(
|
|
14
|
+
"Specifies the host name of the machine on which the server is running. "
|
|
15
|
+
"If the value begins with a slash, it is used as the directory for the "
|
|
16
|
+
"Unix-domain socket."
|
|
17
|
+
),
|
|
18
|
+
short_decls=["-h"],
|
|
19
|
+
default="localhost",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
port = TextOption(
|
|
24
|
+
name="port",
|
|
25
|
+
description=(
|
|
26
|
+
"Port number to connect to at the server host, or socket file name extension "
|
|
27
|
+
"for Unix-domain connections."
|
|
28
|
+
),
|
|
29
|
+
short_decls=["-p"],
|
|
30
|
+
default="5432",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
dbname = TextOption(
|
|
35
|
+
name="dbname",
|
|
36
|
+
description=(
|
|
37
|
+
"Port number to connect to at the server host, or socket file name extension "
|
|
38
|
+
"for Unix-domain connections."
|
|
39
|
+
),
|
|
40
|
+
short_decls=["-d"],
|
|
41
|
+
default="postgres",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
user = TextOption(
|
|
46
|
+
name="user",
|
|
47
|
+
description=("PostgreSQL user name to connect as."),
|
|
48
|
+
short_decls=["-u", "--username", "-U"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
password = TextOption(
|
|
53
|
+
name="password",
|
|
54
|
+
description=("Password to be used if the server demands password authentication."),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
passfile = PathOption(
|
|
59
|
+
name="passfile",
|
|
60
|
+
description=(
|
|
61
|
+
"Specifies the name of the file used to store passwords. Defaults to "
|
|
62
|
+
"~/.pgpass, or %APPDATA%\postgresql\pgpass.conf on Windows. (No error is "
|
|
63
|
+
"reported if this file does not exist.)"
|
|
64
|
+
),
|
|
65
|
+
resolve_path=True,
|
|
66
|
+
exists=False,
|
|
67
|
+
file_okay=True,
|
|
68
|
+
dir_okay=False,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
require_auth = SelectOption(
|
|
72
|
+
name="require_auth",
|
|
73
|
+
description=(
|
|
74
|
+
"Specifies the authentication method that the client requires from the server. "
|
|
75
|
+
"If the server does not use the required method to authenticate the client, or "
|
|
76
|
+
"if the authentication handshake is not fully completed by the server, the "
|
|
77
|
+
"connection will fail."
|
|
78
|
+
),
|
|
79
|
+
choices=["password", "md5", "gss", "sspi", "scram-sha-256", "none"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
channel_binding = SelectOption(
|
|
83
|
+
name="channel_binding",
|
|
84
|
+
description=(
|
|
85
|
+
"This option controls the client's use of channel binding. A setting of "
|
|
86
|
+
"require means that the connection must employ channel binding, prefer "
|
|
87
|
+
"means that the client will choose channel binding if available, and "
|
|
88
|
+
"disable prevents the use of channel binding. The default is prefer if "
|
|
89
|
+
"PostgreSQL is compiled with SSL support; otherwise the default is disable."
|
|
90
|
+
),
|
|
91
|
+
choices=["require", "prefer", "disable"],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _int_validator(s: str | None) -> tuple[bool, str]:
|
|
96
|
+
if s is None:
|
|
97
|
+
return True, ""
|
|
98
|
+
try:
|
|
99
|
+
_ = int(s)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return False, f"Cannot convert {s} to an int!"
|
|
102
|
+
else:
|
|
103
|
+
return True, ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
connect_timeout = TextOption(
|
|
107
|
+
name="connect_timeout",
|
|
108
|
+
description=(
|
|
109
|
+
"Maximum time to wait while connecting, in seconds (write as an integer, "
|
|
110
|
+
"e.g., 10)."
|
|
111
|
+
),
|
|
112
|
+
validator=_int_validator,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
sslmode = SelectOption(
|
|
116
|
+
name="sslmode",
|
|
117
|
+
description=(
|
|
118
|
+
"Determines whether or with what priority a secure SSL TCP/IP connection will "
|
|
119
|
+
"be negotiated with the server."
|
|
120
|
+
),
|
|
121
|
+
choices=["disable", "allow", "prefer", "require", "verify-ca", "verify-full"],
|
|
122
|
+
default="prefer",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
sslcert = PathOption(
|
|
126
|
+
name="sslcert",
|
|
127
|
+
description=(
|
|
128
|
+
"Specifies the file name of the client SSL certificate. "
|
|
129
|
+
"Ignored if an SSL connection is not made."
|
|
130
|
+
),
|
|
131
|
+
default="~/.postgresql/postgresql.crt",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
sslkey = TextOption(
|
|
135
|
+
name="sslkey",
|
|
136
|
+
description=(
|
|
137
|
+
"Specifies the location for the secret key used for the client certificate. "
|
|
138
|
+
"It can either specify a file name that will be used instead of the default "
|
|
139
|
+
"~/.postgresql/postgresql.key, or it can specify a key obtained from an "
|
|
140
|
+
"external engine. An external engine specification should consist of a "
|
|
141
|
+
"colon-separated engine name and an engine-specific key identifier. This "
|
|
142
|
+
"parameter is ignored if an SSL connection is not made."
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
POSTGRES_OPTIONS = [
|
|
148
|
+
host,
|
|
149
|
+
port,
|
|
150
|
+
dbname,
|
|
151
|
+
user,
|
|
152
|
+
password,
|
|
153
|
+
passfile,
|
|
154
|
+
require_auth,
|
|
155
|
+
channel_binding,
|
|
156
|
+
connect_timeout,
|
|
157
|
+
sslmode,
|
|
158
|
+
sslcert,
|
|
159
|
+
sslkey,
|
|
160
|
+
]
|
|
File without changes
|