pgdevkit 0.2.4__tar.gz → 0.3.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.3.0/.github/workflows/python-test.yml +56 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/PKG-INFO +57 -5
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/README.md +54 -4
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/docs/database-layout.md +15 -0
- pgdevkit-0.3.0/pgdevkit/backends/__init__.py +21 -0
- pgdevkit-0.3.0/pgdevkit/backends/base.py +27 -0
- pgdevkit-0.3.0/pgdevkit/backends/mssql.py +27 -0
- pgdevkit-0.3.0/pgdevkit/backends/postgres.py +19 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/cli.py +16 -7
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/db/__init__.py +2 -1
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/db/model.py +9 -2
- pgdevkit-0.3.0/pgdevkit/db/mssql_crud.py +290 -0
- pgdevkit-0.3.0/pgdevkit/db/mssql_sql.py +48 -0
- pgdevkit-0.3.0/pgdevkit/dialect.py +93 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/diff.py +25 -38
- pgdevkit-0.3.0/pgdevkit/mssql_introspect.py +275 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/parser.py +103 -42
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/__init__.py +2 -2
- pgdevkit-0.3.0/pgdevkit/testdb/_docker.py +41 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/api.py +36 -4
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/config.py +13 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/container.py +1 -34
- pgdevkit-0.3.0/pgdevkit/testdb/mssql/__init__.py +1 -0
- pgdevkit-0.3.0/pgdevkit/testdb/mssql/api.py +216 -0
- pgdevkit-0.3.0/pgdevkit/testdb/mssql/constants.py +60 -0
- pgdevkit-0.3.0/pgdevkit/testdb/mssql/container.py +90 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/query.py +38 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/schema.py +25 -7
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pyproject.toml +3 -1
- pgdevkit-0.3.0/tests/db/test_mssql_crud_live.py +106 -0
- pgdevkit-0.3.0/tests/db/test_mssql_crud_sql.py +163 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/test_cli_compare.py +2 -2
- pgdevkit-0.3.0/tests/test_compare_mssql_live.py +39 -0
- pgdevkit-0.3.0/tests/test_dialect.py +44 -0
- pgdevkit-0.3.0/tests/test_diff_mssql.py +60 -0
- pgdevkit-0.3.0/tests/test_mssql_introspect.py +18 -0
- pgdevkit-0.3.0/tests/test_parser_mssql.py +107 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/conftest.py +22 -6
- pgdevkit-0.3.0/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +13 -0
- pgdevkit-0.3.0/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +1 -0
- pgdevkit-0.3.0/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +2 -0
- pgdevkit-0.3.0/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +2 -0
- pgdevkit-0.3.0/tests/testdb/fixtures/database_mssql/schema/app.sql +4 -0
- pgdevkit-0.3.0/tests/testdb/test_api_mssql.py +58 -0
- pgdevkit-0.3.0/tests/testdb/test_api_mssql_live.py +66 -0
- pgdevkit-0.3.0/tests/testdb/test_config_mssql.py +43 -0
- pgdevkit-0.3.0/tests/testdb/test_mssql_constants.py +31 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_query.py +27 -1
- pgdevkit-0.3.0/tests/testdb/test_schema_mssql.py +35 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/uv.lock +38 -2
- pgdevkit-0.2.4/.github/workflows/python-test.yml +0 -34
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/.gitignore +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/.python-version +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/db/complex_types.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/fetch_missing.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/models.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/constants.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/skills/pgdevkit/SKILL.md +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/__init__.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/conftest.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/db/__init__.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/db/test_complex_types.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/test_compare.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/test_connection.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_constants.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_container.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.2.4 → pgdevkit-0.3.0}/tests/testdb/test_schema.py +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
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: Install uv
|
|
28
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
29
|
+
- name: Install project dependencies
|
|
30
|
+
run: uv sync --all-extras --all-groups
|
|
31
|
+
- name: ty check
|
|
32
|
+
run: uv run ty check pgdevkit
|
|
33
|
+
- name: Test with pytest
|
|
34
|
+
run: uv run -m pytest --capture=tee-sys --maxfail=3 -m "not mssql" tests
|
|
35
|
+
|
|
36
|
+
mssql-test:
|
|
37
|
+
# Separate job (not folded into `build`) so a live SQL Server -- a much
|
|
38
|
+
# larger image and slower cold start than the Postgres container --
|
|
39
|
+
# never slows down or blocks the fast, always-run Postgres suite above.
|
|
40
|
+
# mssql-python bundles its own ODBC driver, so unlike pyodbc this needs
|
|
41
|
+
# no system driver package install here.
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
with:
|
|
46
|
+
submodules: "recursive"
|
|
47
|
+
- name: Set up Python 3.14
|
|
48
|
+
uses: actions/setup-python@v5
|
|
49
|
+
with:
|
|
50
|
+
python-version: "3.14"
|
|
51
|
+
- name: Install uv
|
|
52
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
53
|
+
- name: Install project dependencies
|
|
54
|
+
run: uv sync --all-extras --all-groups
|
|
55
|
+
- name: Run MSSQL-only tests
|
|
56
|
+
run: uv run -m pytest --capture=tee-sys --maxfail=3 -m mssql tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgdevkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A helper for developing with Postgres
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: docker>=7.1.0
|
|
@@ -14,6 +14,8 @@ Requires-Dist: typer>=0.26.7; extra == 'cli'
|
|
|
14
14
|
Provides-Extra: db
|
|
15
15
|
Requires-Dist: psycopg-pool>=3.3.0; extra == 'db'
|
|
16
16
|
Requires-Dist: pydantic>=2.0; extra == 'db'
|
|
17
|
+
Provides-Extra: mssql
|
|
18
|
+
Requires-Dist: mssql-python>=1.0.0; extra == 'mssql'
|
|
17
19
|
Description-Content-Type: text/markdown
|
|
18
20
|
|
|
19
21
|
# pgdevkit
|
|
@@ -56,6 +58,26 @@ pgdb compare --url postgresql://instance-abc.database.azuredatabricks.net:5432/d
|
|
|
56
58
|
(`--url`'s own user/password, if any, are discarded and replaced — `--entra-user`
|
|
57
59
|
plus the fetched token become the connection's actual credentials.)
|
|
58
60
|
|
|
61
|
+
### MSSQL
|
|
62
|
+
|
|
63
|
+
`pgdb compare`/`pgdb fetch-missing` default to Postgres. Pass `--dialect mssql`
|
|
64
|
+
to compare against a SQL Server database instead:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pgdb compare --dialect mssql --url "Server=host,1433;Database=db;UID=user;PWD=pass" path/to/database/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Requires the `mssql` extra: `pip install pgdevkit[mssql]` (pulls in
|
|
71
|
+
[mssql-python](https://github.com/microsoft/mssql-python), which bundles its
|
|
72
|
+
own driver — no system ODBC driver install needed). MSSQL has no composite
|
|
73
|
+
type or native enum equivalent, so those areas of a `database/` tree don't
|
|
74
|
+
have a direct equivalent on this backend — see `docs/database-layout.md`.
|
|
75
|
+
Current Azure SQL/SQL Server (2025+) does have a native `json` column type,
|
|
76
|
+
which parses/introspects/diffs like any other column type; see
|
|
77
|
+
"`pgdevkit.db` — helpers for application code" below for how JSON values are
|
|
78
|
+
handled on the CRUD side (write-side serialization only, no auto-parsing on
|
|
79
|
+
read — `mssql-python` doesn't distinguish `json` columns from `nvarchar`).
|
|
80
|
+
|
|
59
81
|
## `pgdb testdb`
|
|
60
82
|
|
|
61
83
|
Manages a single shared, Podman-backed Postgres container for local tests
|
|
@@ -107,12 +129,29 @@ The role named by `PGDEVKIT_TESTDB_USER` must exist and match your OS user
|
|
|
107
129
|
(`CREATE ROLE <user> SUPERUSER LOGIN;`) and `pg_hba.conf` must allow `peer`
|
|
108
130
|
auth for local connections (Debian/Ubuntu Postgres ships this by default).
|
|
109
131
|
|
|
132
|
+
### MSSQL
|
|
133
|
+
|
|
134
|
+
Add `engine = "mssql"` to `[tool.pgdevkit]` (or set
|
|
135
|
+
`PGDEVKIT_TESTDB_ENGINE=mssql` for a one-off run) to manage a shared SQL
|
|
136
|
+
Server container instead of Postgres — same one-container-per-machine,
|
|
137
|
+
one-database-per-workspace model. Requires the `mssql` extra (see above).
|
|
138
|
+
|
|
139
|
+
Container defaults (`localhost:14330`, `sa`/a generated complexity-valid
|
|
140
|
+
password) can be overridden with `PGDEVKIT_TESTDB_MSSQL_HOST`, `_PORT`,
|
|
141
|
+
`_USER`, `_PASSWORD`, `_IMAGE`, `_MEMORY_LIMIT_MB`. The container only
|
|
142
|
+
bootstraps the `sa` login — additional logins are a known limitation.
|
|
143
|
+
`pgdb testdb shell` execs into
|
|
144
|
+
[`sqlcmd`](https://github.com/microsoft/go-sqlcmd) (an external prerequisite,
|
|
145
|
+
the same category as `psql` for the Postgres path) rather than a Python
|
|
146
|
+
REPL.
|
|
147
|
+
|
|
110
148
|
## `pgdevkit.db` — helpers for application code
|
|
111
149
|
|
|
112
150
|
Install with the `db` extra: `pip install pgdevkit[db]`.
|
|
113
151
|
|
|
114
|
-
- **`
|
|
115
|
-
that
|
|
152
|
+
- **`TableModel`** (formerly `PostgresTableModel`, still importable under
|
|
153
|
+
that name) — a `pydantic.BaseModel` base class for models that map 1:1 to
|
|
154
|
+
a table row, for either engine. Implement `get_table_name()` (returns
|
|
116
155
|
`(schema, table)`) and `get_primary_key()` on each model.
|
|
117
156
|
- **`PgPool`** — an async connection pool keyed off
|
|
118
157
|
`{env_prefix}HOST/PORT/DB/USER/PASSWORD` env vars. Call `await pool.open()`
|
|
@@ -125,8 +164,21 @@ Install with the `db` extra: `pip install pgdevkit[db]`.
|
|
|
125
164
|
- **CRUD functions** — `pg_retrieve`, `pg_retrieve_many`, `pg_insert`,
|
|
126
165
|
`pg_insert_many`, `pg_update`, `pg_update_dict`, `pg_upsert`,
|
|
127
166
|
`pg_upsert_dict`, `pg_upsert_many`, `pg_upsert_many_dict`, `pg_delete`,
|
|
128
|
-
`pg_delete_dict` — typed (`
|
|
129
|
-
|
|
167
|
+
`pg_delete_dict` — typed (`TableModel`-based) or dict-based CRUD against a
|
|
168
|
+
table, built on `psycopg` for safe identifier/value handling. The `mssql`
|
|
169
|
+
extra provides an `mssql_*`-prefixed mirror of the same functions in
|
|
170
|
+
`pgdevkit.db.mssql_crud`, built on `mssql-python` (`MERGE`-based upsert,
|
|
171
|
+
`OUTPUT` instead of `RETURNING`) — MSSQL has no composite/enum equivalent,
|
|
172
|
+
so `complex_helper` is always `None` on that path. It does have a native
|
|
173
|
+
`json` column type on current versions (and the older
|
|
174
|
+
`NVARCHAR(MAX)`-plus-`OPENJSON()` convention works on any version), but
|
|
175
|
+
`mssql-python` has no auto-serialization for dict/list parameter values
|
|
176
|
+
(binding one raises `TypeError`) and no way to distinguish a `json`
|
|
177
|
+
column from `nvarchar` on fetch — so every `mssql_*` write function
|
|
178
|
+
serializes dict/list values to JSON text automatically
|
|
179
|
+
(`db.mssql_sql.json_encode_value`), while reads always come back as plain
|
|
180
|
+
`str`; deserialize with `json.loads()` yourself if you need the parsed
|
|
181
|
+
value back.
|
|
130
182
|
- **`SqlLoader`** — loads and caches `.sql` files from
|
|
131
183
|
`{root}/<topic>/<name>.sql`, for keeping hand-written queries out of
|
|
132
184
|
Python source.
|
|
@@ -38,6 +38,26 @@ pgdb compare --url postgresql://instance-abc.database.azuredatabricks.net:5432/d
|
|
|
38
38
|
(`--url`'s own user/password, if any, are discarded and replaced — `--entra-user`
|
|
39
39
|
plus the fetched token become the connection's actual credentials.)
|
|
40
40
|
|
|
41
|
+
### MSSQL
|
|
42
|
+
|
|
43
|
+
`pgdb compare`/`pgdb fetch-missing` default to Postgres. Pass `--dialect mssql`
|
|
44
|
+
to compare against a SQL Server database instead:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pgdb compare --dialect mssql --url "Server=host,1433;Database=db;UID=user;PWD=pass" path/to/database/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requires the `mssql` extra: `pip install pgdevkit[mssql]` (pulls in
|
|
51
|
+
[mssql-python](https://github.com/microsoft/mssql-python), which bundles its
|
|
52
|
+
own driver — no system ODBC driver install needed). MSSQL has no composite
|
|
53
|
+
type or native enum equivalent, so those areas of a `database/` tree don't
|
|
54
|
+
have a direct equivalent on this backend — see `docs/database-layout.md`.
|
|
55
|
+
Current Azure SQL/SQL Server (2025+) does have a native `json` column type,
|
|
56
|
+
which parses/introspects/diffs like any other column type; see
|
|
57
|
+
"`pgdevkit.db` — helpers for application code" below for how JSON values are
|
|
58
|
+
handled on the CRUD side (write-side serialization only, no auto-parsing on
|
|
59
|
+
read — `mssql-python` doesn't distinguish `json` columns from `nvarchar`).
|
|
60
|
+
|
|
41
61
|
## `pgdb testdb`
|
|
42
62
|
|
|
43
63
|
Manages a single shared, Podman-backed Postgres container for local tests
|
|
@@ -89,12 +109,29 @@ The role named by `PGDEVKIT_TESTDB_USER` must exist and match your OS user
|
|
|
89
109
|
(`CREATE ROLE <user> SUPERUSER LOGIN;`) and `pg_hba.conf` must allow `peer`
|
|
90
110
|
auth for local connections (Debian/Ubuntu Postgres ships this by default).
|
|
91
111
|
|
|
112
|
+
### MSSQL
|
|
113
|
+
|
|
114
|
+
Add `engine = "mssql"` to `[tool.pgdevkit]` (or set
|
|
115
|
+
`PGDEVKIT_TESTDB_ENGINE=mssql` for a one-off run) to manage a shared SQL
|
|
116
|
+
Server container instead of Postgres — same one-container-per-machine,
|
|
117
|
+
one-database-per-workspace model. Requires the `mssql` extra (see above).
|
|
118
|
+
|
|
119
|
+
Container defaults (`localhost:14330`, `sa`/a generated complexity-valid
|
|
120
|
+
password) can be overridden with `PGDEVKIT_TESTDB_MSSQL_HOST`, `_PORT`,
|
|
121
|
+
`_USER`, `_PASSWORD`, `_IMAGE`, `_MEMORY_LIMIT_MB`. The container only
|
|
122
|
+
bootstraps the `sa` login — additional logins are a known limitation.
|
|
123
|
+
`pgdb testdb shell` execs into
|
|
124
|
+
[`sqlcmd`](https://github.com/microsoft/go-sqlcmd) (an external prerequisite,
|
|
125
|
+
the same category as `psql` for the Postgres path) rather than a Python
|
|
126
|
+
REPL.
|
|
127
|
+
|
|
92
128
|
## `pgdevkit.db` — helpers for application code
|
|
93
129
|
|
|
94
130
|
Install with the `db` extra: `pip install pgdevkit[db]`.
|
|
95
131
|
|
|
96
|
-
- **`
|
|
97
|
-
that
|
|
132
|
+
- **`TableModel`** (formerly `PostgresTableModel`, still importable under
|
|
133
|
+
that name) — a `pydantic.BaseModel` base class for models that map 1:1 to
|
|
134
|
+
a table row, for either engine. Implement `get_table_name()` (returns
|
|
98
135
|
`(schema, table)`) and `get_primary_key()` on each model.
|
|
99
136
|
- **`PgPool`** — an async connection pool keyed off
|
|
100
137
|
`{env_prefix}HOST/PORT/DB/USER/PASSWORD` env vars. Call `await pool.open()`
|
|
@@ -107,8 +144,21 @@ Install with the `db` extra: `pip install pgdevkit[db]`.
|
|
|
107
144
|
- **CRUD functions** — `pg_retrieve`, `pg_retrieve_many`, `pg_insert`,
|
|
108
145
|
`pg_insert_many`, `pg_update`, `pg_update_dict`, `pg_upsert`,
|
|
109
146
|
`pg_upsert_dict`, `pg_upsert_many`, `pg_upsert_many_dict`, `pg_delete`,
|
|
110
|
-
`pg_delete_dict` — typed (`
|
|
111
|
-
|
|
147
|
+
`pg_delete_dict` — typed (`TableModel`-based) or dict-based CRUD against a
|
|
148
|
+
table, built on `psycopg` for safe identifier/value handling. The `mssql`
|
|
149
|
+
extra provides an `mssql_*`-prefixed mirror of the same functions in
|
|
150
|
+
`pgdevkit.db.mssql_crud`, built on `mssql-python` (`MERGE`-based upsert,
|
|
151
|
+
`OUTPUT` instead of `RETURNING`) — MSSQL has no composite/enum equivalent,
|
|
152
|
+
so `complex_helper` is always `None` on that path. It does have a native
|
|
153
|
+
`json` column type on current versions (and the older
|
|
154
|
+
`NVARCHAR(MAX)`-plus-`OPENJSON()` convention works on any version), but
|
|
155
|
+
`mssql-python` has no auto-serialization for dict/list parameter values
|
|
156
|
+
(binding one raises `TypeError`) and no way to distinguish a `json`
|
|
157
|
+
column from `nvarchar` on fetch — so every `mssql_*` write function
|
|
158
|
+
serializes dict/list values to JSON text automatically
|
|
159
|
+
(`db.mssql_sql.json_encode_value`), while reads always come back as plain
|
|
160
|
+
`str`; deserialize with `json.loads()` yourself if you need the parsed
|
|
161
|
+
value back.
|
|
112
162
|
- **`SqlLoader`** — loads and caches `.sql` files from
|
|
113
163
|
`{root}/<topic>/<name>.sql`, for keeping hand-written queries out of
|
|
114
164
|
Python source.
|
|
@@ -121,6 +121,21 @@ comment on column dim.user.is_active is 'False once a user is soft-deleted; keep
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
+
## MSSQL projects (`engine = "mssql"`)
|
|
125
|
+
|
|
126
|
+
Everything above is engine-agnostic *as a folder/apply-order convention*,
|
|
127
|
+
with two exceptions:
|
|
128
|
+
|
|
129
|
+
- `types/` (custom types / enums) has no direct T-SQL equivalent -- MSSQL
|
|
130
|
+
has neither a native enum type nor composite types, so a `CREATE TYPE ...
|
|
131
|
+
AS ENUM`/composite `.sql` file is a Postgres-only construct. `pgdb compare`
|
|
132
|
+
reports every such object as missing on an MSSQL database (correctly --
|
|
133
|
+
it genuinely doesn't exist there), rather than erroring.
|
|
134
|
+
- T-SQL scripts conventionally separate batches with a standalone `GO` line
|
|
135
|
+
(an `sqlcmd`/SSMS scripting convention, not valid inside a single
|
|
136
|
+
driver `execute()` call). `pgdb testdb` splits on these automatically when
|
|
137
|
+
applying a file; hand-written `.sql` files may use `GO` freely.
|
|
138
|
+
|
|
124
139
|
## Backfilling untracked objects
|
|
125
140
|
|
|
126
141
|
If a table, scalar function, or table function was created directly on the
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..dialect import Dialect, resolve_dialect
|
|
4
|
+
from .base import Backend
|
|
5
|
+
from .mssql import MssqlBackend
|
|
6
|
+
from .postgres import PostgresBackend
|
|
7
|
+
|
|
8
|
+
_REGISTRY: dict[str, Backend] = {
|
|
9
|
+
"postgres": PostgresBackend(),
|
|
10
|
+
"mssql": MssqlBackend(),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_backend(dialect: str | Dialect = "postgres") -> Backend:
|
|
15
|
+
"""Look up the `Backend` for a dialect name (or an already-resolved
|
|
16
|
+
`Dialect`). Defaults to postgres."""
|
|
17
|
+
resolved = resolve_dialect(dialect)
|
|
18
|
+
return _REGISTRY[resolved.name]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["Backend", "MssqlBackend", "PostgresBackend", "get_backend"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Protocol
|
|
4
|
+
|
|
5
|
+
from ..dialect import Dialect
|
|
6
|
+
from ..models import DatabaseSchema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Backend(Protocol):
|
|
10
|
+
"""Introspection + a couple of engine facts, behind one interface.
|
|
11
|
+
|
|
12
|
+
CRUD is deliberately NOT part of this protocol -- psycopg's
|
|
13
|
+
`AsyncConnection` and an MSSQL driver's connection type are unrelated,
|
|
14
|
+
so a unified `backend.retrieve()`/`backend.insert()` surface would force
|
|
15
|
+
existing Postgres callers to go through a new indirection just to keep
|
|
16
|
+
working. Callers that want CRUD import `pgdevkit.db.crud`'s `pg_*`
|
|
17
|
+
functions or `pgdevkit.db.mssql_crud`'s `mssql_*` functions directly,
|
|
18
|
+
exactly as `db/crud.py`'s functions are imported today."""
|
|
19
|
+
|
|
20
|
+
dialect: Dialect
|
|
21
|
+
|
|
22
|
+
def introspect(self, conninfo: str) -> DatabaseSchema: ...
|
|
23
|
+
|
|
24
|
+
def complex_helper_factory(self) -> Callable[..., Any] | None:
|
|
25
|
+
"""A `ComplexHelper`-like factory for composite/enum/JSONB columns,
|
|
26
|
+
or None when the engine has no equivalent (MSSQL)."""
|
|
27
|
+
...
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from ..dialect import MSSQL, Dialect
|
|
6
|
+
from ..models import DatabaseSchema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MssqlBackend:
|
|
10
|
+
dialect: Dialect = MSSQL
|
|
11
|
+
|
|
12
|
+
def introspect(self, conninfo: str) -> DatabaseSchema:
|
|
13
|
+
# Imported lazily so `import pgdevkit.backends` (and thus
|
|
14
|
+
# `pgdevkit.cli`) doesn't require mssql-python/the mssql extra to
|
|
15
|
+
# be installed unless a caller actually asks for the mssql backend.
|
|
16
|
+
from ..mssql_introspect import introspect_mssql_db
|
|
17
|
+
|
|
18
|
+
return introspect_mssql_db(conninfo)
|
|
19
|
+
|
|
20
|
+
def complex_helper_factory(self) -> Callable[..., Any] | None:
|
|
21
|
+
# MSSQL has no composite type, native enum, or first-class JSONB
|
|
22
|
+
# column type -- there is nothing for a ComplexHelper to adapt.
|
|
23
|
+
# Every `complex_helper` parameter in db/crud.py (and its
|
|
24
|
+
# db/mssql_crud.py counterpart) is already Optional, so callers on
|
|
25
|
+
# this backend simply pass/receive None and every complex-type
|
|
26
|
+
# branch takes its existing no-op path.
|
|
27
|
+
return None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from ..dialect import POSTGRES, Dialect
|
|
6
|
+
from ..introspect import introspect_db
|
|
7
|
+
from ..models import DatabaseSchema
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PostgresBackend:
|
|
11
|
+
dialect: Dialect = POSTGRES
|
|
12
|
+
|
|
13
|
+
def introspect(self, conninfo: str) -> DatabaseSchema:
|
|
14
|
+
return introspect_db(conninfo)
|
|
15
|
+
|
|
16
|
+
def complex_helper_factory(self) -> Callable[..., Any] | None:
|
|
17
|
+
from ..db.complex_types import ComplexHelper
|
|
18
|
+
|
|
19
|
+
return ComplexHelper
|
|
@@ -10,10 +10,10 @@ from rich.table import Table
|
|
|
10
10
|
from rich import box
|
|
11
11
|
|
|
12
12
|
from . import testdb
|
|
13
|
+
from .backends import get_backend
|
|
13
14
|
from .connection import build_conninfo
|
|
14
15
|
from .diff import DiffKind, compute_diff
|
|
15
16
|
from .fetch_missing import SUBFOLDER, find_missing_objects, layer_folder_for, reconstruct_ddl
|
|
16
|
-
from .introspect import introspect_db
|
|
17
17
|
from .parser import parse_directory
|
|
18
18
|
|
|
19
19
|
app = typer.Typer(name="pgdb", help="PostgreSQL database schema tools")
|
|
@@ -37,9 +37,10 @@ def compare(
|
|
|
37
37
|
None, "--databricks-instance", help="Lakebase instance name (required for Lakebase hosts)"
|
|
38
38
|
),
|
|
39
39
|
report_extra_db: bool = typer.Option(False, "--report-extra-db", help="Report objects in DB but not in scripts"),
|
|
40
|
+
dialect: str = typer.Option("postgres", "--dialect", help="postgres (default) or mssql"),
|
|
40
41
|
scripts_dir: Path = typer.Argument(..., help="Directory containing SQL scripts"),
|
|
41
42
|
) -> None:
|
|
42
|
-
"""Compare SQL scripts to a live
|
|
43
|
+
"""Compare SQL scripts to a live database and report differences."""
|
|
43
44
|
if not scripts_dir.is_dir():
|
|
44
45
|
err_console.print(f"[red]Error:[/red] {scripts_dir} is not a directory")
|
|
45
46
|
raise typer.Exit(2)
|
|
@@ -55,13 +56,19 @@ def compare(
|
|
|
55
56
|
err_console.print(f"[red]Error:[/red] {e}")
|
|
56
57
|
raise typer.Exit(2)
|
|
57
58
|
|
|
59
|
+
try:
|
|
60
|
+
backend = get_backend(dialect)
|
|
61
|
+
except ValueError as e:
|
|
62
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
63
|
+
raise typer.Exit(2)
|
|
64
|
+
|
|
58
65
|
with console.status("Parsing SQL scripts..."):
|
|
59
|
-
scripts_schema = parse_directory(scripts_dir)
|
|
66
|
+
scripts_schema = parse_directory(scripts_dir, dialect=backend.dialect)
|
|
60
67
|
|
|
61
68
|
with console.status("Introspecting database..."):
|
|
62
|
-
db_schema =
|
|
69
|
+
db_schema = backend.introspect(conninfo)
|
|
63
70
|
|
|
64
|
-
diffs = compute_diff(scripts_schema, db_schema, report_extra_db=report_extra_db)
|
|
71
|
+
diffs = compute_diff(scripts_schema, db_schema, report_extra_db=report_extra_db, dialect=backend.dialect)
|
|
65
72
|
|
|
66
73
|
if not diffs:
|
|
67
74
|
console.print("[green]No differences found.[/green]")
|
|
@@ -204,8 +211,10 @@ def testdb_status() -> None:
|
|
|
204
211
|
|
|
205
212
|
@testdb_app.command("shell")
|
|
206
213
|
def testdb_shell() -> None:
|
|
207
|
-
"""Drop into psql
|
|
208
|
-
|
|
214
|
+
"""Drop into an interactive shell (psql, or sqlcmd for MSSQL) against
|
|
215
|
+
this workspace's database."""
|
|
216
|
+
binary, argv = testdb.shell_argv()
|
|
217
|
+
os.execvp(binary, argv)
|
|
209
218
|
|
|
210
219
|
|
|
211
220
|
@testdb_app.command("clean")
|
|
@@ -17,13 +17,14 @@ from .crud import (
|
|
|
17
17
|
pg_upsert_many_dict,
|
|
18
18
|
)
|
|
19
19
|
from .loader import SqlLoader
|
|
20
|
-
from .model import PostgresTableModel
|
|
20
|
+
from .model import PostgresTableModel, TableModel
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"ComplexHelper",
|
|
24
24
|
"PgPool",
|
|
25
25
|
"PostgresTableModel",
|
|
26
26
|
"SqlLoader",
|
|
27
|
+
"TableModel",
|
|
27
28
|
"pg_delete",
|
|
28
29
|
"pg_delete_dict",
|
|
29
30
|
"pg_insert",
|
|
@@ -6,8 +6,10 @@ from typing import Sequence
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class
|
|
10
|
-
"""Base class for models that map 1:1 to a database table/row
|
|
9
|
+
class TableModel(BaseModel, ABC):
|
|
10
|
+
"""Base class for models that map 1:1 to a database table/row (any
|
|
11
|
+
engine -- schema/table naming is equally meaningful for Postgres and
|
|
12
|
+
MSSQL, this base class was never actually Postgres-specific).
|
|
11
13
|
|
|
12
14
|
Models representing partial results (joins, aggregations, projections)
|
|
13
15
|
should extend `pydantic.BaseModel` directly instead."""
|
|
@@ -21,3 +23,8 @@ class PostgresTableModel(BaseModel, ABC):
|
|
|
21
23
|
@abstractmethod
|
|
22
24
|
def get_primary_key() -> Sequence[str]:
|
|
23
25
|
"""Return the primary key column name(s)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Backward-compat alias -- this class was named PostgresTableModel before
|
|
29
|
+
# MSSQL support existed; kept so existing imports keep working unchanged.
|
|
30
|
+
PostgresTableModel = TableModel
|