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.
Files changed (70) hide show
  1. pgdevkit-0.1.0/.github/workflows/python-publish.yml +39 -0
  2. pgdevkit-0.1.0/.github/workflows/python-test.yml +36 -0
  3. pgdevkit-0.1.0/.gitignore +22 -0
  4. pgdevkit-0.1.0/.python-version +1 -0
  5. pgdevkit-0.1.0/PKG-INFO +140 -0
  6. pgdevkit-0.1.0/README.md +123 -0
  7. pgdevkit-0.1.0/docs/database-layout.md +145 -0
  8. pgdevkit-0.1.0/pgdevkit/__init__.py +0 -0
  9. pgdevkit-0.1.0/pgdevkit/cli.py +217 -0
  10. pgdevkit-0.1.0/pgdevkit/connection.py +56 -0
  11. pgdevkit-0.1.0/pgdevkit/db/__init__.py +37 -0
  12. pgdevkit-0.1.0/pgdevkit/db/connection.py +74 -0
  13. pgdevkit-0.1.0/pgdevkit/db/crud.py +184 -0
  14. pgdevkit-0.1.0/pgdevkit/db/loader.py +19 -0
  15. pgdevkit-0.1.0/pgdevkit/db/model.py +23 -0
  16. pgdevkit-0.1.0/pgdevkit/diff.py +253 -0
  17. pgdevkit-0.1.0/pgdevkit/fetch_missing.py +229 -0
  18. pgdevkit-0.1.0/pgdevkit/introspect.py +190 -0
  19. pgdevkit-0.1.0/pgdevkit/lakebase.py +90 -0
  20. pgdevkit-0.1.0/pgdevkit/models.py +102 -0
  21. pgdevkit-0.1.0/pgdevkit/parser.py +336 -0
  22. pgdevkit-0.1.0/pgdevkit/testdb/__init__.py +3 -0
  23. pgdevkit-0.1.0/pgdevkit/testdb/api.py +137 -0
  24. pgdevkit-0.1.0/pgdevkit/testdb/config.py +51 -0
  25. pgdevkit-0.1.0/pgdevkit/testdb/constants.py +11 -0
  26. pgdevkit-0.1.0/pgdevkit/testdb/container.py +95 -0
  27. pgdevkit-0.1.0/pgdevkit/testdb/naming.py +41 -0
  28. pgdevkit-0.1.0/pgdevkit/testdb/query.py +65 -0
  29. pgdevkit-0.1.0/pgdevkit/testdb/schema.py +188 -0
  30. pgdevkit-0.1.0/pyproject.toml +53 -0
  31. pgdevkit-0.1.0/skills/pgdevkit/SKILL.md +283 -0
  32. pgdevkit-0.1.0/skills/pgdevkit/references/complex_helper.py +234 -0
  33. pgdevkit-0.1.0/skills/pgdevkit/references/dynamic-sql.md +92 -0
  34. pgdevkit-0.1.0/skills/pgdevkit/references/temporal-tables.md +33 -0
  35. pgdevkit-0.1.0/tests/__init__.py +0 -0
  36. pgdevkit-0.1.0/tests/conftest.py +34 -0
  37. pgdevkit-0.1.0/tests/db/__init__.py +0 -0
  38. pgdevkit-0.1.0/tests/db/test_connection.py +58 -0
  39. pgdevkit-0.1.0/tests/db/test_crud.py +108 -0
  40. pgdevkit-0.1.0/tests/db/test_loader.py +25 -0
  41. pgdevkit-0.1.0/tests/fixtures/01_schema.sql +1 -0
  42. pgdevkit-0.1.0/tests/fixtures/02_types.sql +7 -0
  43. pgdevkit-0.1.0/tests/fixtures/03_tables.sql +17 -0
  44. pgdevkit-0.1.0/tests/fixtures/04_views.sql +2 -0
  45. pgdevkit-0.1.0/tests/fixtures/05_functions.sql +8 -0
  46. pgdevkit-0.1.0/tests/fixtures/06_indexes.sql +1 -0
  47. pgdevkit-0.1.0/tests/test_cli_compare.py +64 -0
  48. pgdevkit-0.1.0/tests/test_compare.py +119 -0
  49. pgdevkit-0.1.0/tests/test_connection.py +58 -0
  50. pgdevkit-0.1.0/tests/test_fetch_missing.py +82 -0
  51. pgdevkit-0.1.0/tests/test_fetch_missing_cli.py +52 -0
  52. pgdevkit-0.1.0/tests/test_lakebase.py +106 -0
  53. pgdevkit-0.1.0/tests/testdb/__init__.py +0 -0
  54. pgdevkit-0.1.0/tests/testdb/conftest.py +50 -0
  55. pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget.sql +4 -0
  56. pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget.test_data.json +1 -0
  57. pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget_part.sql +4 -0
  58. pgdevkit-0.1.0/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +4 -0
  59. pgdevkit-0.1.0/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +2 -0
  60. pgdevkit-0.1.0/tests/testdb/fixtures/database/app/views/b_base_view.sql +2 -0
  61. pgdevkit-0.1.0/tests/testdb/fixtures/database/schema/app.sql +1 -0
  62. pgdevkit-0.1.0/tests/testdb/test_api.py +114 -0
  63. pgdevkit-0.1.0/tests/testdb/test_cli.py +107 -0
  64. pgdevkit-0.1.0/tests/testdb/test_config.py +71 -0
  65. pgdevkit-0.1.0/tests/testdb/test_constants.py +39 -0
  66. pgdevkit-0.1.0/tests/testdb/test_container.py +89 -0
  67. pgdevkit-0.1.0/tests/testdb/test_naming.py +51 -0
  68. pgdevkit-0.1.0/tests/testdb/test_query.py +76 -0
  69. pgdevkit-0.1.0/tests/testdb/test_schema.py +97 -0
  70. 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
@@ -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
+ ```
@@ -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