pgdevkit 0.1.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.
- pgdevkit-0.1.0/.github/workflows/python-publish.yml +39 -0
- pgdevkit-0.1.0/.github/workflows/python-test.yml +36 -0
- pgdevkit-0.1.0/.gitignore +22 -0
- pgdevkit-0.1.0/.python-version +1 -0
- pgdevkit-0.1.0/PKG-INFO +140 -0
- pgdevkit-0.1.0/README.md +123 -0
- pgdevkit-0.1.0/docs/database-layout.md +145 -0
- pgdevkit-0.1.0/pgdevkit/__init__.py +0 -0
- pgdevkit-0.1.0/pgdevkit/cli.py +217 -0
- pgdevkit-0.1.0/pgdevkit/connection.py +56 -0
- pgdevkit-0.1.0/pgdevkit/db/__init__.py +37 -0
- pgdevkit-0.1.0/pgdevkit/db/connection.py +74 -0
- pgdevkit-0.1.0/pgdevkit/db/crud.py +184 -0
- pgdevkit-0.1.0/pgdevkit/db/loader.py +19 -0
- pgdevkit-0.1.0/pgdevkit/db/model.py +23 -0
- pgdevkit-0.1.0/pgdevkit/diff.py +253 -0
- pgdevkit-0.1.0/pgdevkit/fetch_missing.py +229 -0
- pgdevkit-0.1.0/pgdevkit/introspect.py +190 -0
- pgdevkit-0.1.0/pgdevkit/lakebase.py +90 -0
- pgdevkit-0.1.0/pgdevkit/models.py +102 -0
- pgdevkit-0.1.0/pgdevkit/parser.py +336 -0
- pgdevkit-0.1.0/pgdevkit/testdb/__init__.py +3 -0
- pgdevkit-0.1.0/pgdevkit/testdb/api.py +137 -0
- pgdevkit-0.1.0/pgdevkit/testdb/config.py +51 -0
- pgdevkit-0.1.0/pgdevkit/testdb/constants.py +11 -0
- pgdevkit-0.1.0/pgdevkit/testdb/container.py +95 -0
- pgdevkit-0.1.0/pgdevkit/testdb/naming.py +41 -0
- pgdevkit-0.1.0/pgdevkit/testdb/query.py +65 -0
- pgdevkit-0.1.0/pgdevkit/testdb/schema.py +188 -0
- pgdevkit-0.1.0/pyproject.toml +53 -0
- pgdevkit-0.1.0/skills/pgdevkit/SKILL.md +283 -0
- pgdevkit-0.1.0/skills/pgdevkit/references/complex_helper.py +234 -0
- pgdevkit-0.1.0/skills/pgdevkit/references/dynamic-sql.md +92 -0
- pgdevkit-0.1.0/skills/pgdevkit/references/temporal-tables.md +33 -0
- pgdevkit-0.1.0/tests/__init__.py +0 -0
- pgdevkit-0.1.0/tests/conftest.py +34 -0
- pgdevkit-0.1.0/tests/db/__init__.py +0 -0
- pgdevkit-0.1.0/tests/db/test_connection.py +58 -0
- pgdevkit-0.1.0/tests/db/test_crud.py +108 -0
- pgdevkit-0.1.0/tests/db/test_loader.py +25 -0
- pgdevkit-0.1.0/tests/fixtures/01_schema.sql +1 -0
- pgdevkit-0.1.0/tests/fixtures/02_types.sql +7 -0
- pgdevkit-0.1.0/tests/fixtures/03_tables.sql +17 -0
- pgdevkit-0.1.0/tests/fixtures/04_views.sql +2 -0
- pgdevkit-0.1.0/tests/fixtures/05_functions.sql +8 -0
- pgdevkit-0.1.0/tests/fixtures/06_indexes.sql +1 -0
- pgdevkit-0.1.0/tests/test_cli_compare.py +64 -0
- pgdevkit-0.1.0/tests/test_compare.py +119 -0
- pgdevkit-0.1.0/tests/test_connection.py +58 -0
- pgdevkit-0.1.0/tests/test_fetch_missing.py +82 -0
- pgdevkit-0.1.0/tests/test_fetch_missing_cli.py +52 -0
- pgdevkit-0.1.0/tests/test_lakebase.py +106 -0
- pgdevkit-0.1.0/tests/testdb/__init__.py +0 -0
- pgdevkit-0.1.0/tests/testdb/conftest.py +50 -0
- pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget.sql +4 -0
- pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget.test_data.json +1 -0
- pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget_part.sql +4 -0
- pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +4 -0
- pgdevkit-0.1.0/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +2 -0
- pgdevkit-0.1.0/tests/testdb/fixtures/database/app/views/b_base_view.sql +2 -0
- pgdevkit-0.1.0/tests/testdb/fixtures/database/schema/app.sql +1 -0
- pgdevkit-0.1.0/tests/testdb/test_api.py +114 -0
- pgdevkit-0.1.0/tests/testdb/test_cli.py +107 -0
- pgdevkit-0.1.0/tests/testdb/test_config.py +71 -0
- pgdevkit-0.1.0/tests/testdb/test_constants.py +39 -0
- pgdevkit-0.1.0/tests/testdb/test_container.py +89 -0
- pgdevkit-0.1.0/tests/testdb/test_naming.py +51 -0
- pgdevkit-0.1.0/tests/testdb/test_query.py +76 -0
- pgdevkit-0.1.0/tests/testdb/test_schema.py +97 -0
- pgdevkit-0.1.0/uv.lock +659 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# This workflow will upload a Python Package using Twine when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
deploy:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
environment:
|
|
20
|
+
name: pypi
|
|
21
|
+
url: https://pypi.org/p/pgdevkit
|
|
22
|
+
permissions:
|
|
23
|
+
id-token: write
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
with:
|
|
27
|
+
submodules: "recursive"
|
|
28
|
+
- name: Set up Python
|
|
29
|
+
uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.14"
|
|
32
|
+
- name: Install uv
|
|
33
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: uv sync --all-extras --all-groups
|
|
36
|
+
- name: Build package
|
|
37
|
+
run: uv build
|
|
38
|
+
- name: Publish package to PyPI
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Python Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
paths-ignore: ["README.md", "docs", ".github"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: ["main"]
|
|
9
|
+
paths-ignore: ["README.md", "docs", ".github"]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.14"]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
submodules: "recursive"
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
- name: Ensure podman is installed
|
|
28
|
+
run: command -v podman || (sudo apt-get update && sudo apt-get install -y podman)
|
|
29
|
+
- name: Install uv
|
|
30
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
31
|
+
- name: Install project dependencies
|
|
32
|
+
run: uv sync --all-extras --all-groups
|
|
33
|
+
- name: ty check
|
|
34
|
+
run: uv run ty check pgdevkit
|
|
35
|
+
- name: Test with pytest
|
|
36
|
+
run: uv run -m pytest --capture=tee-sys --maxfail=3 tests
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Local planning docs (not shipped with the package)
|
|
13
|
+
docs/superpowers/
|
|
14
|
+
|
|
15
|
+
# Local migration checklist (not shipped with the package)
|
|
16
|
+
/MIGRATION_NOTES.md
|
|
17
|
+
|
|
18
|
+
# Local follow-up tracking (not shipped with the package)
|
|
19
|
+
/FOLLOWUP.md
|
|
20
|
+
|
|
21
|
+
# Git worktrees
|
|
22
|
+
/.worktrees/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
pgdevkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pgdevkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A helper for developing with Postgres
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: psycopg[binary]>=3.2.0
|
|
7
|
+
Requires-Dist: sqlglot[c]>=30.11.0
|
|
8
|
+
Provides-Extra: azure
|
|
9
|
+
Requires-Dist: azure-identity>=1.19.0; extra == 'azure'
|
|
10
|
+
Provides-Extra: cli
|
|
11
|
+
Requires-Dist: rich>=13.0.0; extra == 'cli'
|
|
12
|
+
Requires-Dist: typer>=0.26.7; extra == 'cli'
|
|
13
|
+
Provides-Extra: db
|
|
14
|
+
Requires-Dist: psycopg-pool>=3.3.0; extra == 'db'
|
|
15
|
+
Requires-Dist: pydantic>=2.0; extra == 'db'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# pgdevkit
|
|
19
|
+
|
|
20
|
+
A helper for developing with Postgres.
|
|
21
|
+
|
|
22
|
+
## `pgdb compare`
|
|
23
|
+
|
|
24
|
+
Compare a directory of SQL scripts (see the `database-in-source` layout
|
|
25
|
+
convention) against a live database and report differences:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pgdb compare --url postgresql://user:pass@host:port/db path/to/database/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Entra ID auth (Azure Postgres / Databricks Lakebase)
|
|
32
|
+
|
|
33
|
+
Pass `--entra-user <identity>` to `pgdb compare` to authenticate with an
|
|
34
|
+
Entra ID token instead of a static password. Which token flow is used is
|
|
35
|
+
auto-detected from the database hostname:
|
|
36
|
+
|
|
37
|
+
- **Azure Database for PostgreSQL** (`*.postgres.database.azure.com`,
|
|
38
|
+
`*.postgres.cosmos.azure.com`) — the default: fetches a token via
|
|
39
|
+
`DefaultAzureCredential` and uses it directly as the password. Requires
|
|
40
|
+
the `azure` extra: `pip install pgdevkit[azure]`.
|
|
41
|
+
- **Databricks Lakebase** (`*.database.azuredatabricks.net`,
|
|
42
|
+
`*.database.cloud.databricks.com`) — fetches a Databricks-scoped Entra
|
|
43
|
+
token, then exchanges it for a short-lived Postgres credential via the
|
|
44
|
+
Databricks workspace API. Also requires `--databricks-workspace-host`
|
|
45
|
+
and `--databricks-instance`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pgdb compare --url postgresql://instance-abc.database.azuredatabricks.net:5432/databricks_postgres \
|
|
49
|
+
--entra-user alice@example.com \
|
|
50
|
+
--databricks-workspace-host https://adb-123456789.azuredatabricks.net \
|
|
51
|
+
--databricks-instance myinstance \
|
|
52
|
+
path/to/database/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
(`--url`'s own user/password, if any, are discarded and replaced — `--entra-user`
|
|
56
|
+
plus the fetched token become the connection's actual credentials.)
|
|
57
|
+
|
|
58
|
+
## `pgdb testdb`
|
|
59
|
+
|
|
60
|
+
Manages a single shared, Podman-backed Postgres container for local tests
|
|
61
|
+
across all your projects — no more one-container-per-project-per-worktree.
|
|
62
|
+
Isolation between projects and worktrees is per-database, inside one
|
|
63
|
+
container.
|
|
64
|
+
|
|
65
|
+
Add to `pyproject.toml`:
|
|
66
|
+
|
|
67
|
+
```toml
|
|
68
|
+
[tool.pgdevkit]
|
|
69
|
+
name = "myproject" # optional; defaults to the repo directory name
|
|
70
|
+
database_dir = "database" # optional; defaults to "database"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Add to `conftest.py`:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import os
|
|
77
|
+
import pytest
|
|
78
|
+
from pgdevkit.testdb import ensure_testdb
|
|
79
|
+
|
|
80
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
81
|
+
def ensure_test_postgres():
|
|
82
|
+
for k, v in ensure_testdb().items():
|
|
83
|
+
os.environ[k] = v
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
CLI: `pgdb testdb up|reset|run-sql|status|shell|clean`.
|
|
87
|
+
|
|
88
|
+
Container connection defaults (`localhost:54322`, `postgres`/`testpwd`) can
|
|
89
|
+
be overridden with `PGDEVKIT_TESTDB_HOST`, `PGDEVKIT_TESTDB_PORT`,
|
|
90
|
+
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD`. Before touching
|
|
91
|
+
podman/docker, pgdevkit first checks (with a short timeout) whether Postgres
|
|
92
|
+
is already reachable at that address and skips container management if so.
|
|
93
|
+
Set `PGDEVKIT_SKIP_CONTAINER=1` to always assume it's already there and skip
|
|
94
|
+
that check too.
|
|
95
|
+
|
|
96
|
+
## `pgdevkit.db` — helpers for application code
|
|
97
|
+
|
|
98
|
+
Install with the `db` extra: `pip install pgdevkit[db]`.
|
|
99
|
+
|
|
100
|
+
- **`PostgresTableModel`** — a `pydantic.BaseModel` base class for models
|
|
101
|
+
that map 1:1 to a table row. Implement `get_table_name()` (returns
|
|
102
|
+
`(schema, table)`) and `get_primary_key()` on each model.
|
|
103
|
+
- **`PgPool`** — an async connection pool keyed off
|
|
104
|
+
`{env_prefix}HOST/PORT/DB/USER/PASSWORD` env vars. Call `await pool.open()`
|
|
105
|
+
once at startup, then use `async with pool.connection() as con:`.
|
|
106
|
+
Pass `entra_user` to authenticate via Entra ID instead of a static
|
|
107
|
+
password — same host-based auto-detection as `pgdb compare`'s
|
|
108
|
+
`--entra-user`. For Lakebase hosts, also set the
|
|
109
|
+
`{env_prefix}DATABRICKS_WORKSPACE_HOST` and `{env_prefix}DATABRICKS_INSTANCE`
|
|
110
|
+
env vars.
|
|
111
|
+
- **CRUD functions** — `pg_retrieve`, `pg_retrieve_many`, `pg_insert`,
|
|
112
|
+
`pg_insert_many`, `pg_update`, `pg_update_dict`, `pg_upsert`,
|
|
113
|
+
`pg_upsert_dict`, `pg_upsert_many`, `pg_upsert_many_dict`, `pg_delete`,
|
|
114
|
+
`pg_delete_dict` — typed (`PostgresTableModel`-based) or dict-based CRUD
|
|
115
|
+
against a table, built on `psycopg` for safe identifier/value handling.
|
|
116
|
+
- **`SqlLoader`** — loads and caches `.sql` files from
|
|
117
|
+
`{root}/<topic>/<name>.sql`, for keeping hand-written queries out of
|
|
118
|
+
Python source.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from pgdevkit.db import PgPool, PostgresTableModel, pg_retrieve, pg_upsert
|
|
122
|
+
|
|
123
|
+
class Widget(PostgresTableModel):
|
|
124
|
+
id: int
|
|
125
|
+
name: str
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def get_table_name() -> tuple[str, str]:
|
|
129
|
+
return ("public", "widget")
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def get_primary_key() -> list[str]:
|
|
133
|
+
return ["id"]
|
|
134
|
+
|
|
135
|
+
pool = PgPool(env_prefix="POSTGRES_")
|
|
136
|
+
await pool.open()
|
|
137
|
+
async with pool.connection() as con:
|
|
138
|
+
widget = await pg_retrieve(con, Widget, {"id": 1})
|
|
139
|
+
await pg_upsert(con, Widget(id=1, name="thing"), Widget)
|
|
140
|
+
```
|
pgdevkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# pgdevkit
|
|
2
|
+
|
|
3
|
+
A helper for developing with Postgres.
|
|
4
|
+
|
|
5
|
+
## `pgdb compare`
|
|
6
|
+
|
|
7
|
+
Compare a directory of SQL scripts (see the `database-in-source` layout
|
|
8
|
+
convention) against a live database and report differences:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pgdb compare --url postgresql://user:pass@host:port/db path/to/database/
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Entra ID auth (Azure Postgres / Databricks Lakebase)
|
|
15
|
+
|
|
16
|
+
Pass `--entra-user <identity>` to `pgdb compare` to authenticate with an
|
|
17
|
+
Entra ID token instead of a static password. Which token flow is used is
|
|
18
|
+
auto-detected from the database hostname:
|
|
19
|
+
|
|
20
|
+
- **Azure Database for PostgreSQL** (`*.postgres.database.azure.com`,
|
|
21
|
+
`*.postgres.cosmos.azure.com`) — the default: fetches a token via
|
|
22
|
+
`DefaultAzureCredential` and uses it directly as the password. Requires
|
|
23
|
+
the `azure` extra: `pip install pgdevkit[azure]`.
|
|
24
|
+
- **Databricks Lakebase** (`*.database.azuredatabricks.net`,
|
|
25
|
+
`*.database.cloud.databricks.com`) — fetches a Databricks-scoped Entra
|
|
26
|
+
token, then exchanges it for a short-lived Postgres credential via the
|
|
27
|
+
Databricks workspace API. Also requires `--databricks-workspace-host`
|
|
28
|
+
and `--databricks-instance`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pgdb compare --url postgresql://instance-abc.database.azuredatabricks.net:5432/databricks_postgres \
|
|
32
|
+
--entra-user alice@example.com \
|
|
33
|
+
--databricks-workspace-host https://adb-123456789.azuredatabricks.net \
|
|
34
|
+
--databricks-instance myinstance \
|
|
35
|
+
path/to/database/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
(`--url`'s own user/password, if any, are discarded and replaced — `--entra-user`
|
|
39
|
+
plus the fetched token become the connection's actual credentials.)
|
|
40
|
+
|
|
41
|
+
## `pgdb testdb`
|
|
42
|
+
|
|
43
|
+
Manages a single shared, Podman-backed Postgres container for local tests
|
|
44
|
+
across all your projects — no more one-container-per-project-per-worktree.
|
|
45
|
+
Isolation between projects and worktrees is per-database, inside one
|
|
46
|
+
container.
|
|
47
|
+
|
|
48
|
+
Add to `pyproject.toml`:
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
[tool.pgdevkit]
|
|
52
|
+
name = "myproject" # optional; defaults to the repo directory name
|
|
53
|
+
database_dir = "database" # optional; defaults to "database"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Add to `conftest.py`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import os
|
|
60
|
+
import pytest
|
|
61
|
+
from pgdevkit.testdb import ensure_testdb
|
|
62
|
+
|
|
63
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
64
|
+
def ensure_test_postgres():
|
|
65
|
+
for k, v in ensure_testdb().items():
|
|
66
|
+
os.environ[k] = v
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
CLI: `pgdb testdb up|reset|run-sql|status|shell|clean`.
|
|
70
|
+
|
|
71
|
+
Container connection defaults (`localhost:54322`, `postgres`/`testpwd`) can
|
|
72
|
+
be overridden with `PGDEVKIT_TESTDB_HOST`, `PGDEVKIT_TESTDB_PORT`,
|
|
73
|
+
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD`. Before touching
|
|
74
|
+
podman/docker, pgdevkit first checks (with a short timeout) whether Postgres
|
|
75
|
+
is already reachable at that address and skips container management if so.
|
|
76
|
+
Set `PGDEVKIT_SKIP_CONTAINER=1` to always assume it's already there and skip
|
|
77
|
+
that check too.
|
|
78
|
+
|
|
79
|
+
## `pgdevkit.db` — helpers for application code
|
|
80
|
+
|
|
81
|
+
Install with the `db` extra: `pip install pgdevkit[db]`.
|
|
82
|
+
|
|
83
|
+
- **`PostgresTableModel`** — a `pydantic.BaseModel` base class for models
|
|
84
|
+
that map 1:1 to a table row. Implement `get_table_name()` (returns
|
|
85
|
+
`(schema, table)`) and `get_primary_key()` on each model.
|
|
86
|
+
- **`PgPool`** — an async connection pool keyed off
|
|
87
|
+
`{env_prefix}HOST/PORT/DB/USER/PASSWORD` env vars. Call `await pool.open()`
|
|
88
|
+
once at startup, then use `async with pool.connection() as con:`.
|
|
89
|
+
Pass `entra_user` to authenticate via Entra ID instead of a static
|
|
90
|
+
password — same host-based auto-detection as `pgdb compare`'s
|
|
91
|
+
`--entra-user`. For Lakebase hosts, also set the
|
|
92
|
+
`{env_prefix}DATABRICKS_WORKSPACE_HOST` and `{env_prefix}DATABRICKS_INSTANCE`
|
|
93
|
+
env vars.
|
|
94
|
+
- **CRUD functions** — `pg_retrieve`, `pg_retrieve_many`, `pg_insert`,
|
|
95
|
+
`pg_insert_many`, `pg_update`, `pg_update_dict`, `pg_upsert`,
|
|
96
|
+
`pg_upsert_dict`, `pg_upsert_many`, `pg_upsert_many_dict`, `pg_delete`,
|
|
97
|
+
`pg_delete_dict` — typed (`PostgresTableModel`-based) or dict-based CRUD
|
|
98
|
+
against a table, built on `psycopg` for safe identifier/value handling.
|
|
99
|
+
- **`SqlLoader`** — loads and caches `.sql` files from
|
|
100
|
+
`{root}/<topic>/<name>.sql`, for keeping hand-written queries out of
|
|
101
|
+
Python source.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pgdevkit.db import PgPool, PostgresTableModel, pg_retrieve, pg_upsert
|
|
105
|
+
|
|
106
|
+
class Widget(PostgresTableModel):
|
|
107
|
+
id: int
|
|
108
|
+
name: str
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def get_table_name() -> tuple[str, str]:
|
|
112
|
+
return ("public", "widget")
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def get_primary_key() -> list[str]:
|
|
116
|
+
return ["id"]
|
|
117
|
+
|
|
118
|
+
pool = PgPool(env_prefix="POSTGRES_")
|
|
119
|
+
await pool.open()
|
|
120
|
+
async with pool.connection() as con:
|
|
121
|
+
widget = await pg_retrieve(con, Widget, {"id": 1})
|
|
122
|
+
await pg_upsert(con, Widget(id=1, name="thing"), Widget)
|
|
123
|
+
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# `database/` folder layout
|
|
2
|
+
|
|
3
|
+
The Postgres schema is versioned as plain `.sql` files under a `database/`
|
|
4
|
+
folder in the repo — that folder *is* the source of truth for the schema.
|
|
5
|
+
`pgdb testdb` (see [`pgdevkit/testdb/schema.py`](../pgdevkit/testdb/schema.py))
|
|
6
|
+
applies it to a local test database; a human applies the same files to
|
|
7
|
+
production.
|
|
8
|
+
|
|
9
|
+
This is the single source for the convention — don't duplicate this table or
|
|
10
|
+
these rules elsewhere; link here instead.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Layer directories
|
|
15
|
+
|
|
16
|
+
Top-level directories group tables/objects by conceptual layer — one
|
|
17
|
+
directory per Postgres schema, or per logical grouping within one schema.
|
|
18
|
+
Names and count are entirely per-project; a generic example:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
database/
|
|
22
|
+
├── schema.sql # CREATE SCHEMA statements
|
|
23
|
+
├── 0_public/ # shared types/functions usable from anywhere
|
|
24
|
+
├── 1_reference_data/ # dimension / reference tables
|
|
25
|
+
├── 2_transactional/ # fact / transactional tables
|
|
26
|
+
├── 3_app/ # user-editable, app-owned tables
|
|
27
|
+
├── 4_reporting/ # aggregated / statistics tables
|
|
28
|
+
├── permissions.sql # grants
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The leading number is a **display/sort aid only** — it groups related
|
|
32
|
+
folders together in a file listing so a human can scan them top-to-bottom in
|
|
33
|
+
a sensible order. It does not control apply order: `pgdb testdb` applies
|
|
34
|
+
files by object-type priority (below) and resolves cross-file dependencies
|
|
35
|
+
itself, regardless of which numbered folder a file sits in. Feel free to
|
|
36
|
+
renumber layers for readability without worrying about breaking anything.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Object-type subfolders, in apply order
|
|
41
|
+
|
|
42
|
+
Within a layer directory, group files by object type. This is what actually
|
|
43
|
+
controls apply order, across all layer directories — cross-file dependencies
|
|
44
|
+
between objects of the same type (e.g. one view selecting from another) are
|
|
45
|
+
resolved automatically; this table only fixes the order *between* types.
|
|
46
|
+
This table must match `_TYPE_ORDER` / `_SCHEMA_QUALIFIED_TYPES` in
|
|
47
|
+
[`pgdevkit/testdb/schema.py`](../pgdevkit/testdb/schema.py) exactly — update
|
|
48
|
+
both together.
|
|
49
|
+
|
|
50
|
+
| Priority | Directory | Object type |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| 1 | `schema` | `CREATE SCHEMA` |
|
|
53
|
+
| 2 | `types` | Custom types / enums |
|
|
54
|
+
| 3 | `tables` | Tables |
|
|
55
|
+
| 4 | `scalar_functions` | Scalar functions |
|
|
56
|
+
| 5 | `functions` | Functions |
|
|
57
|
+
| 6 | `views` | Views |
|
|
58
|
+
| 7 | `table_functions` | Table functions |
|
|
59
|
+
| 8 | `procedures` | Procedures |
|
|
60
|
+
| 100 | `permissions` | Grants |
|
|
61
|
+
| 101 | `indexes` | Indexes |
|
|
62
|
+
|
|
63
|
+
One object per file: `tables/user.sql`, `views/all_edits.sql`,
|
|
64
|
+
`types/measurement_unit.sql`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## File-naming conventions
|
|
69
|
+
|
|
70
|
+
| Suffix | Meaning |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `<name>.sql` | The object's live definition (`CREATE TABLE`, `CREATE OR REPLACE VIEW`, ...) |
|
|
73
|
+
| `<name>.test_data.json` | Seed rows for a table — a JSON array of row objects, loaded after the table is created |
|
|
74
|
+
| `<name>.init.sql` | One-time setup for an object (e.g. a backfill), run once, kept separate from the reusable definition |
|
|
75
|
+
| `<name>.prod.sql` / `.prod` anywhere in the name | Production-only (real permission grants, real user accounts) — skipped by `pgdb testdb` |
|
|
76
|
+
| `all.sql` | Generated concatenation of the whole tree — not hand-edited, not committed |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Migrations
|
|
81
|
+
|
|
82
|
+
One-off, non-idempotent changes (rename/drop column, backfill, data fix) go
|
|
83
|
+
in a `migrations/` (or `_migration_scripts/`) folder — one file per change,
|
|
84
|
+
named by date:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
database/migrations/2026-07-10_customer_geocode.sql
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Rules:
|
|
91
|
+
|
|
92
|
+
- Skipped by `pgdb testdb` — it only applies the layer directories above.
|
|
93
|
+
- Update the corresponding table/view `.sql` file in the same change so its
|
|
94
|
+
definition already reflects the new shape — the migration and the
|
|
95
|
+
source-of-truth file must never drift apart.
|
|
96
|
+
- Applied to production manually, once, by a human, after being verified
|
|
97
|
+
locally.
|
|
98
|
+
- Never edited after being applied — a further change gets a new dated file.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## `COMMENT ON` — document schema in the object's own file
|
|
103
|
+
|
|
104
|
+
Add a `COMMENT ON` for every table, and for any column whose purpose isn't
|
|
105
|
+
obvious from its name and type (flags, status codes, denormalized fields,
|
|
106
|
+
units). Put it directly in the table's `.sql` file, right after the
|
|
107
|
+
`CREATE TABLE` — not in a migration, wiki, or README. It lives with the
|
|
108
|
+
definition it describes and survives `\d+` / `pg_catalog` inspection.
|
|
109
|
+
|
|
110
|
+
```sql
|
|
111
|
+
-- database/1_reference_data/tables/user.sql
|
|
112
|
+
create table dim.user (
|
|
113
|
+
id bigint generated always as identity primary key,
|
|
114
|
+
email text not null,
|
|
115
|
+
is_active boolean not null default true
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
comment on table dim.user is 'End-user accounts; one row per registered person.';
|
|
119
|
+
comment on column dim.user.is_active is 'False once a user is soft-deleted; keep for audit trail.';
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Backfilling untracked objects
|
|
125
|
+
|
|
126
|
+
If a table, scalar function, or table function was created directly on the
|
|
127
|
+
database and never got a `.sql` file, use `pgdb fetch-missing` to find it and
|
|
128
|
+
generate one — see `pgdb fetch-missing --help`. It connects to Postgres,
|
|
129
|
+
diffs the live schema against what's already tracked under `database/`, and
|
|
130
|
+
for each object you select, reverse-engineers its DDL into the matching
|
|
131
|
+
layer folder's `tables/`, `views/`, `scalar_functions/`, or
|
|
132
|
+
`table_functions/` subfolder. The layer folder is matched by schema name
|
|
133
|
+
against the existing top-level directories under `database/`, ignoring their
|
|
134
|
+
leading sort number.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Quick checklist
|
|
139
|
+
|
|
140
|
+
- [ ] New table/view/function/type gets its own `.sql` file under the right layer + object-type folder
|
|
141
|
+
- [ ] Object-type folder (`tables`, `views`, ...) matches the apply-order table above — that's what governs ordering, not the layer's leading number
|
|
142
|
+
- [ ] One-off changes go in `migrations/`, dated, never edited after applying
|
|
143
|
+
- [ ] The live `.sql` file is updated in the same change as any migration touching that object
|
|
144
|
+
- [ ] `.prod` files are production-only and skipped by `pgdb testdb`
|
|
145
|
+
- [ ] Every table (and non-obvious column) has a `COMMENT ON`, placed in the object's own `.sql` file
|
|
File without changes
|