jusi-postgres 0.2.0__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.
- jusi_postgres-0.2.0/.gitignore +25 -0
- jusi_postgres-0.2.0/Dockerfile +6 -0
- jusi_postgres-0.2.0/LICENSE +21 -0
- jusi_postgres-0.2.0/PKG-INFO +126 -0
- jusi_postgres-0.2.0/README.md +90 -0
- jusi_postgres-0.2.0/docker/postgres/initdb.d/001_jusi_demo.sql +52 -0
- jusi_postgres-0.2.0/docker/postgres/initdb.d/002_jusi_blob_fixture.sql +20 -0
- jusi_postgres-0.2.0/examples/jusi.toml +8 -0
- jusi_postgres-0.2.0/fixtures/blob-readme.txt +1 -0
- jusi_postgres-0.2.0/fixtures/test-blob.zip +0 -0
- jusi_postgres-0.2.0/pyproject.toml +33 -0
- jusi_postgres-0.2.0/scripts/install-blob-fixture.sh +20 -0
- jusi_postgres-0.2.0/scripts/run-postgres.sh +56 -0
- jusi_postgres-0.2.0/src/jusi_postgres/__init__.py +5 -0
- jusi_postgres-0.2.0/src/jusi_postgres/catalog.py +19 -0
- jusi_postgres-0.2.0/src/jusi_postgres/config.py +107 -0
- jusi_postgres-0.2.0/src/jusi_postgres/constants.py +3 -0
- jusi_postgres-0.2.0/src/jusi_postgres/ipc.py +129 -0
- jusi_postgres-0.2.0/src/jusi_postgres/kernel.py +19 -0
- jusi_postgres-0.2.0/src/jusi_postgres/metadata.py +121 -0
- jusi_postgres-0.2.0/src/jusi_postgres/runner.py +587 -0
- jusi_postgres-0.2.0/src/jusi_postgres/worker.py +108 -0
- jusi_postgres-0.2.0/tests/test_plugin.py +254 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
|
|
3
|
+
# Local Python environments
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
env/
|
|
7
|
+
|
|
8
|
+
# Python bytecode and test caches
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
pytest-of-*/
|
|
17
|
+
|
|
18
|
+
# Packaging/build outputs
|
|
19
|
+
*.egg-info/
|
|
20
|
+
build/
|
|
21
|
+
dist/
|
|
22
|
+
|
|
23
|
+
# Local runtime scratch
|
|
24
|
+
.tmp/
|
|
25
|
+
*.log
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 notawhaleble
|
|
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,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jusi-postgres
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: PostgreSQL provider plugin for Jusi SQL
|
|
5
|
+
Author: Jusi contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 notawhaleble
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Requires-Dist: jusi-sql<0.3,>=0.2
|
|
30
|
+
Requires-Dist: jusi<2,>=1.0
|
|
31
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
32
|
+
Requires-Dist: visidata<4,>=3
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: pytest<9,>=8; extra == 'test'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# jusi-postgres
|
|
38
|
+
|
|
39
|
+
`jusi-postgres` is the PostgreSQL exact-provider plugin for Jusi 1.0 and the
|
|
40
|
+
shared `jusi-sql` family. It exposes the `postgres`/`postgresql` provider for
|
|
41
|
+
`%%sql` cells. The family owns target selection, magic dispatch, completion
|
|
42
|
+
ranges, metadata caching, operation routing, and common VisiData SQL commands.
|
|
43
|
+
|
|
44
|
+
Example session config:
|
|
45
|
+
|
|
46
|
+
```toml
|
|
47
|
+
[sql.targets.analytics]
|
|
48
|
+
provider = "postgres"
|
|
49
|
+
host = "localhost"
|
|
50
|
+
port = 5432
|
|
51
|
+
dbname = "analytics"
|
|
52
|
+
user = "me"
|
|
53
|
+
initial_fetch = 100
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
All psycopg connection options may be provided in the target config. The
|
|
57
|
+
additional `krb5ccname` option points at a custom Kerberos credential cache and
|
|
58
|
+
is passed to libpq through `KRB5CCNAME` while connecting.
|
|
59
|
+
|
|
60
|
+
Metadata for SQL completion is collected asynchronously on a separate
|
|
61
|
+
connection. Large catalogs should be filtered or disabled:
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[sql.targets.analytics]
|
|
65
|
+
provider = "postgres"
|
|
66
|
+
host = "db.example.com"
|
|
67
|
+
dbname = "analytics"
|
|
68
|
+
user = "me"
|
|
69
|
+
metadata_schemas = ["public", "analytics"]
|
|
70
|
+
metadata_max_rows = 1000000
|
|
71
|
+
collect_metadata = true
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use `collect_metadata = false` to skip completion metadata for a target. If
|
|
75
|
+
collection crosses `metadata_max_rows`, it stops and warns that schema
|
|
76
|
+
filtering is required. Provider options belong in the target configuration;
|
|
77
|
+
the shared Jusi 1.0 `%%sql` syntax accepts one target alias.
|
|
78
|
+
|
|
79
|
+
VisiData mappings:
|
|
80
|
+
|
|
81
|
+
- `1` to `9`: fetch that many more rows for the active result sheet.
|
|
82
|
+
- `gf`: prompt for a row count to fetch; `0` fetches the rest of the cursor.
|
|
83
|
+
- `gc`: commit the current connection.
|
|
84
|
+
- `gr`: roll back the current connection.
|
|
85
|
+
- `gb`: write the selected raw cell value to a temporary file and open it with
|
|
86
|
+
VisiData. The command prompts for an optional extension; an empty extension
|
|
87
|
+
lets VisiData infer the file type from content and filename.
|
|
88
|
+
|
|
89
|
+
Follow-up cells reuse the same VisiData application, PostgreSQL connection,
|
|
90
|
+
transaction, and server-side cursors. `JusiInterrupt` requests cancellation on
|
|
91
|
+
that connection. Closing the client closes its cursors, metadata connection,
|
|
92
|
+
main connection, private control socket, and staged launch data.
|
|
93
|
+
|
|
94
|
+
## Local development database
|
|
95
|
+
|
|
96
|
+
Start a disposable PostgreSQL fixture:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
scripts/run-postgres.sh
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The script builds `jusi-postgres-dev`, runs a local container on
|
|
103
|
+
`127.0.0.1:55432`, waits for readiness, and prints this config:
|
|
104
|
+
|
|
105
|
+
```toml
|
|
106
|
+
[sql.targets.local_postgres]
|
|
107
|
+
provider = "postgres"
|
|
108
|
+
host = "127.0.0.1"
|
|
109
|
+
port = 55432
|
|
110
|
+
dbname = "jusi"
|
|
111
|
+
user = "jusi"
|
|
112
|
+
password = "jusi"
|
|
113
|
+
initial_fetch = 25
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The fixture creates `demo.accounts`, `demo.events`, `demo.account_summary`,
|
|
117
|
+
`demo.account_event_count(bigint)`, and `demo.blob_files` for result browsing,
|
|
118
|
+
completion checks, and raw bytea/blob handling.
|
|
119
|
+
|
|
120
|
+
For an already-running development container, install or refresh the blob table:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
scripts/install-blob-fixture.sh
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The source zip fixture is stored at `fixtures/test-blob.zip`.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# jusi-postgres
|
|
2
|
+
|
|
3
|
+
`jusi-postgres` is the PostgreSQL exact-provider plugin for Jusi 1.0 and the
|
|
4
|
+
shared `jusi-sql` family. It exposes the `postgres`/`postgresql` provider for
|
|
5
|
+
`%%sql` cells. The family owns target selection, magic dispatch, completion
|
|
6
|
+
ranges, metadata caching, operation routing, and common VisiData SQL commands.
|
|
7
|
+
|
|
8
|
+
Example session config:
|
|
9
|
+
|
|
10
|
+
```toml
|
|
11
|
+
[sql.targets.analytics]
|
|
12
|
+
provider = "postgres"
|
|
13
|
+
host = "localhost"
|
|
14
|
+
port = 5432
|
|
15
|
+
dbname = "analytics"
|
|
16
|
+
user = "me"
|
|
17
|
+
initial_fetch = 100
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
All psycopg connection options may be provided in the target config. The
|
|
21
|
+
additional `krb5ccname` option points at a custom Kerberos credential cache and
|
|
22
|
+
is passed to libpq through `KRB5CCNAME` while connecting.
|
|
23
|
+
|
|
24
|
+
Metadata for SQL completion is collected asynchronously on a separate
|
|
25
|
+
connection. Large catalogs should be filtered or disabled:
|
|
26
|
+
|
|
27
|
+
```toml
|
|
28
|
+
[sql.targets.analytics]
|
|
29
|
+
provider = "postgres"
|
|
30
|
+
host = "db.example.com"
|
|
31
|
+
dbname = "analytics"
|
|
32
|
+
user = "me"
|
|
33
|
+
metadata_schemas = ["public", "analytics"]
|
|
34
|
+
metadata_max_rows = 1000000
|
|
35
|
+
collect_metadata = true
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use `collect_metadata = false` to skip completion metadata for a target. If
|
|
39
|
+
collection crosses `metadata_max_rows`, it stops and warns that schema
|
|
40
|
+
filtering is required. Provider options belong in the target configuration;
|
|
41
|
+
the shared Jusi 1.0 `%%sql` syntax accepts one target alias.
|
|
42
|
+
|
|
43
|
+
VisiData mappings:
|
|
44
|
+
|
|
45
|
+
- `1` to `9`: fetch that many more rows for the active result sheet.
|
|
46
|
+
- `gf`: prompt for a row count to fetch; `0` fetches the rest of the cursor.
|
|
47
|
+
- `gc`: commit the current connection.
|
|
48
|
+
- `gr`: roll back the current connection.
|
|
49
|
+
- `gb`: write the selected raw cell value to a temporary file and open it with
|
|
50
|
+
VisiData. The command prompts for an optional extension; an empty extension
|
|
51
|
+
lets VisiData infer the file type from content and filename.
|
|
52
|
+
|
|
53
|
+
Follow-up cells reuse the same VisiData application, PostgreSQL connection,
|
|
54
|
+
transaction, and server-side cursors. `JusiInterrupt` requests cancellation on
|
|
55
|
+
that connection. Closing the client closes its cursors, metadata connection,
|
|
56
|
+
main connection, private control socket, and staged launch data.
|
|
57
|
+
|
|
58
|
+
## Local development database
|
|
59
|
+
|
|
60
|
+
Start a disposable PostgreSQL fixture:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
scripts/run-postgres.sh
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The script builds `jusi-postgres-dev`, runs a local container on
|
|
67
|
+
`127.0.0.1:55432`, waits for readiness, and prints this config:
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[sql.targets.local_postgres]
|
|
71
|
+
provider = "postgres"
|
|
72
|
+
host = "127.0.0.1"
|
|
73
|
+
port = 55432
|
|
74
|
+
dbname = "jusi"
|
|
75
|
+
user = "jusi"
|
|
76
|
+
password = "jusi"
|
|
77
|
+
initial_fetch = 25
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The fixture creates `demo.accounts`, `demo.events`, `demo.account_summary`,
|
|
81
|
+
`demo.account_event_count(bigint)`, and `demo.blob_files` for result browsing,
|
|
82
|
+
completion checks, and raw bytea/blob handling.
|
|
83
|
+
|
|
84
|
+
For an already-running development container, install or refresh the blob table:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
scripts/install-blob-fixture.sh
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The source zip fixture is stored at `fixtures/test-blob.zip`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
CREATE SCHEMA IF NOT EXISTS demo;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS demo.accounts (
|
|
4
|
+
id bigserial PRIMARY KEY,
|
|
5
|
+
email text NOT NULL UNIQUE,
|
|
6
|
+
display_name text NOT NULL,
|
|
7
|
+
active boolean NOT NULL DEFAULT true,
|
|
8
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS demo.events (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
account_id bigint NOT NULL REFERENCES demo.accounts(id),
|
|
14
|
+
event_type text NOT NULL,
|
|
15
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
16
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
INSERT INTO demo.accounts (email, display_name, active)
|
|
20
|
+
VALUES
|
|
21
|
+
('ada@example.test', 'Ada Lovelace', true),
|
|
22
|
+
('grace@example.test', 'Grace Hopper', true),
|
|
23
|
+
('alan@example.test', 'Alan Turing', false)
|
|
24
|
+
ON CONFLICT (email) DO NOTHING;
|
|
25
|
+
|
|
26
|
+
INSERT INTO demo.events (account_id, event_type, payload)
|
|
27
|
+
SELECT account_id, event_type, payload::jsonb
|
|
28
|
+
FROM (
|
|
29
|
+
VALUES
|
|
30
|
+
(1, 'login', '{"ip": "127.0.0.1"}'),
|
|
31
|
+
(1, 'query', '{"rows": 42}'),
|
|
32
|
+
(2, 'login', '{"ip": "127.0.0.2"}'),
|
|
33
|
+
(3, 'disabled_login', '{"reason": "inactive"}')
|
|
34
|
+
) AS seed(account_id, event_type, payload)
|
|
35
|
+
WHERE NOT EXISTS (SELECT 1 FROM demo.events);
|
|
36
|
+
|
|
37
|
+
CREATE OR REPLACE FUNCTION demo.account_event_count(account_id bigint)
|
|
38
|
+
RETURNS bigint
|
|
39
|
+
LANGUAGE sql
|
|
40
|
+
STABLE
|
|
41
|
+
AS $$
|
|
42
|
+
SELECT count(*) FROM demo.events e WHERE e.account_id = $1
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE VIEW demo.account_summary AS
|
|
46
|
+
SELECT
|
|
47
|
+
a.id,
|
|
48
|
+
a.email,
|
|
49
|
+
a.display_name,
|
|
50
|
+
a.active,
|
|
51
|
+
demo.account_event_count(a.id) AS event_count
|
|
52
|
+
FROM demo.accounts a;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS demo.blob_files (
|
|
2
|
+
id bigserial PRIMARY KEY,
|
|
3
|
+
filename text NOT NULL UNIQUE,
|
|
4
|
+
content_type text NOT NULL,
|
|
5
|
+
payload bytea NOT NULL,
|
|
6
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
INSERT INTO demo.blob_files (filename, content_type, payload)
|
|
10
|
+
VALUES (
|
|
11
|
+
'test-blob.zip',
|
|
12
|
+
'application/zip',
|
|
13
|
+
decode(
|
|
14
|
+
'504b03040a0000000000231ab65c2909ab311b0000001b0000000f001c00626c6f622d726561646d652e747874555409000382a00f6a82a00f6a75780b000104f501000004140000004a75736920506f73746772657320626c6f6220666978747572650a504b01021e030a0000000000231ab65c2909ab311b0000001b0000000f0018000000000001000000a48100000000626c6f622d726561646d652e747874555405000382a00f6a75780b000104f50100000414000000504b0506000000000100010055000000640000000000',
|
|
15
|
+
'hex'
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
ON CONFLICT (filename) DO UPDATE
|
|
19
|
+
SET content_type = EXCLUDED.content_type,
|
|
20
|
+
payload = EXCLUDED.payload;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Jusi Postgres blob fixture
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jusi-postgres"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "PostgreSQL provider plugin for Jusi SQL"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Jusi contributors" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"jusi>=1.0,<2",
|
|
17
|
+
"jusi-sql>=0.2,<0.3",
|
|
18
|
+
"psycopg[binary]>=3.1",
|
|
19
|
+
"visidata>=3,<4",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
test = ["pytest>=8,<9"]
|
|
24
|
+
|
|
25
|
+
[project.entry-points."jusi.plugins.v1"]
|
|
26
|
+
postgres = "jusi_postgres.catalog:catalog_entry"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/jusi_postgres"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
pythonpath = ["src"]
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
CONTAINER_NAME="${JUSI_POSTGRES_CONTAINER:-jusi-postgres-dev}"
|
|
5
|
+
POSTGRES_USER="${JUSI_POSTGRES_USER:-jusi}"
|
|
6
|
+
POSTGRES_DB="${JUSI_POSTGRES_DB:-jusi}"
|
|
7
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
SQL_PATH="${ROOT_DIR}/docker/postgres/initdb.d/002_jusi_blob_fixture.sql"
|
|
9
|
+
|
|
10
|
+
docker exec -i "${CONTAINER_NAME}" psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" < "${SQL_PATH}"
|
|
11
|
+
|
|
12
|
+
cat <<'EOF'
|
|
13
|
+
Blob fixture installed.
|
|
14
|
+
|
|
15
|
+
Try:
|
|
16
|
+
|
|
17
|
+
%%sql local_postgres
|
|
18
|
+
select id, filename, content_type, payload
|
|
19
|
+
from demo.blob_files;
|
|
20
|
+
EOF
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
IMAGE_NAME="${JUSI_POSTGRES_IMAGE:-jusi-postgres-dev}"
|
|
5
|
+
CONTAINER_NAME="${JUSI_POSTGRES_CONTAINER:-jusi-postgres-dev}"
|
|
6
|
+
HOST_PORT="${JUSI_POSTGRES_PORT:-55432}"
|
|
7
|
+
POSTGRES_USER="${JUSI_POSTGRES_USER:-jusi}"
|
|
8
|
+
POSTGRES_PASSWORD="${JUSI_POSTGRES_PASSWORD:-jusi}"
|
|
9
|
+
POSTGRES_DB="${JUSI_POSTGRES_DB:-jusi}"
|
|
10
|
+
|
|
11
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
docker build -t "${IMAGE_NAME}" "${ROOT_DIR}"
|
|
14
|
+
|
|
15
|
+
if docker ps --format '{{.Names}}' | grep -Fxq "${CONTAINER_NAME}"; then
|
|
16
|
+
echo "PostgreSQL container '${CONTAINER_NAME}' is already running on port ${HOST_PORT}."
|
|
17
|
+
else
|
|
18
|
+
if docker ps -a --format '{{.Names}}' | grep -Fxq "${CONTAINER_NAME}"; then
|
|
19
|
+
docker rm "${CONTAINER_NAME}" >/dev/null
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
docker run \
|
|
23
|
+
--detach \
|
|
24
|
+
--name "${CONTAINER_NAME}" \
|
|
25
|
+
--publish "127.0.0.1:${HOST_PORT}:5432" \
|
|
26
|
+
--env "POSTGRES_USER=${POSTGRES_USER}" \
|
|
27
|
+
--env "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" \
|
|
28
|
+
--env "POSTGRES_DB=${POSTGRES_DB}" \
|
|
29
|
+
"${IMAGE_NAME}" >/dev/null
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "Waiting for PostgreSQL to accept connections..."
|
|
33
|
+
for _ in $(seq 1 60); do
|
|
34
|
+
if docker exec "${CONTAINER_NAME}" pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" >/dev/null 2>&1; then
|
|
35
|
+
cat <<EOF
|
|
36
|
+
PostgreSQL is running.
|
|
37
|
+
|
|
38
|
+
jusi.toml:
|
|
39
|
+
|
|
40
|
+
[sql.targets.local_postgres]
|
|
41
|
+
provider = "postgres"
|
|
42
|
+
host = "127.0.0.1"
|
|
43
|
+
port = ${HOST_PORT}
|
|
44
|
+
dbname = "${POSTGRES_DB}"
|
|
45
|
+
user = "${POSTGRES_USER}"
|
|
46
|
+
password = "${POSTGRES_PASSWORD}"
|
|
47
|
+
initial_fetch = 25
|
|
48
|
+
EOF
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
sleep 1
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
echo "PostgreSQL did not become ready in time." >&2
|
|
55
|
+
docker logs "${CONTAINER_NAME}" >&2 || true
|
|
56
|
+
exit 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Lightweight Jusi 1.0 catalog provider; imports no runtime dependencies."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from jusi_sql import sql_catalog_entry
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def catalog_entry() -> dict[str, Any]:
|
|
12
|
+
return sql_catalog_entry(
|
|
13
|
+
plugin_id="postgres",
|
|
14
|
+
plugin_version=__version__,
|
|
15
|
+
distribution="jusi-postgres",
|
|
16
|
+
kernel_extension="jusi_postgres.kernel",
|
|
17
|
+
worker_entry_point="jusi_postgres.worker:create_worker",
|
|
18
|
+
provider_presentation={"syntax": "pgsql", "indent": "sql"},
|
|
19
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Iterator, Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PLUGIN_OPTIONS = {
|
|
10
|
+
"provider",
|
|
11
|
+
"initial_fetch",
|
|
12
|
+
"krb5ccname",
|
|
13
|
+
"collect_metadata",
|
|
14
|
+
"skip_metadata",
|
|
15
|
+
"metadata_max_rows",
|
|
16
|
+
"metadata_schemas",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
DEFAULT_METADATA_MAX_ROWS = 1_000_000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class PostgresOptions:
|
|
24
|
+
connect: dict[str, Any]
|
|
25
|
+
initial_fetch: int
|
|
26
|
+
krb5ccname: str
|
|
27
|
+
collect_metadata: bool
|
|
28
|
+
metadata_max_rows: int
|
|
29
|
+
metadata_schemas: tuple[str, ...]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_postgres_options(options: Mapping[str, Any]) -> PostgresOptions:
|
|
33
|
+
raw_initial = options.get("initial_fetch", 100)
|
|
34
|
+
try:
|
|
35
|
+
initial_fetch = int(raw_initial)
|
|
36
|
+
except (TypeError, ValueError):
|
|
37
|
+
initial_fetch = 100
|
|
38
|
+
if initial_fetch < 0:
|
|
39
|
+
initial_fetch = 0
|
|
40
|
+
krb5ccname = str(options.get("krb5ccname", "")).strip()
|
|
41
|
+
collect_metadata = _parse_bool(options.get("collect_metadata", True), default=True)
|
|
42
|
+
if _parse_bool(options.get("skip_metadata", False), default=False):
|
|
43
|
+
collect_metadata = False
|
|
44
|
+
raw_metadata_max_rows = options.get("metadata_max_rows", DEFAULT_METADATA_MAX_ROWS)
|
|
45
|
+
try:
|
|
46
|
+
metadata_max_rows = int(raw_metadata_max_rows)
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
metadata_max_rows = DEFAULT_METADATA_MAX_ROWS
|
|
49
|
+
if metadata_max_rows < 1:
|
|
50
|
+
metadata_max_rows = DEFAULT_METADATA_MAX_ROWS
|
|
51
|
+
metadata_schemas = _parse_metadata_schemas(options.get("metadata_schemas", ()))
|
|
52
|
+
connect = {str(key): value for key, value in options.items() if str(key) not in PLUGIN_OPTIONS}
|
|
53
|
+
return PostgresOptions(
|
|
54
|
+
connect=connect,
|
|
55
|
+
initial_fetch=initial_fetch,
|
|
56
|
+
krb5ccname=krb5ccname,
|
|
57
|
+
collect_metadata=collect_metadata,
|
|
58
|
+
metadata_max_rows=metadata_max_rows,
|
|
59
|
+
metadata_schemas=metadata_schemas,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_bool(value: Any, *, default: bool) -> bool:
|
|
64
|
+
if isinstance(value, bool):
|
|
65
|
+
return value
|
|
66
|
+
if value is None:
|
|
67
|
+
return default
|
|
68
|
+
normalized = str(value).strip().lower()
|
|
69
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
70
|
+
return True
|
|
71
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
72
|
+
return False
|
|
73
|
+
return default
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _parse_metadata_schemas(value: Any) -> tuple[str, ...]:
|
|
77
|
+
if isinstance(value, str):
|
|
78
|
+
raw_items = value.split(",")
|
|
79
|
+
elif isinstance(value, (list, tuple, set)):
|
|
80
|
+
raw_items = list(value)
|
|
81
|
+
else:
|
|
82
|
+
raw_items = []
|
|
83
|
+
schemas: list[str] = []
|
|
84
|
+
seen: set[str] = set()
|
|
85
|
+
for item in raw_items:
|
|
86
|
+
schema = str(item).strip()
|
|
87
|
+
if not schema or schema in seen:
|
|
88
|
+
continue
|
|
89
|
+
seen.add(schema)
|
|
90
|
+
schemas.append(schema)
|
|
91
|
+
return tuple(schemas)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@contextmanager
|
|
95
|
+
def kerberos_cache_env(path: str) -> Iterator[None]:
|
|
96
|
+
if not path:
|
|
97
|
+
yield
|
|
98
|
+
return
|
|
99
|
+
previous = os.environ.get("KRB5CCNAME")
|
|
100
|
+
os.environ["KRB5CCNAME"] = path
|
|
101
|
+
try:
|
|
102
|
+
yield
|
|
103
|
+
finally:
|
|
104
|
+
if previous is None:
|
|
105
|
+
os.environ.pop("KRB5CCNAME", None)
|
|
106
|
+
else:
|
|
107
|
+
os.environ["KRB5CCNAME"] = previous
|