pgdevkit 0.7.2__tar.gz → 0.9.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.7.2 → pgdevkit-0.9.0}/PKG-INFO +31 -6
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/README.md +30 -5
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/migrate.py +42 -5
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/_docker.py +33 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/constants.py +8 -1
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/container.py +11 -1
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/constants.py +9 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/container.py +9 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pyproject.toml +1 -1
- pgdevkit-0.9.0/tests/test_migrate_grants.py +91 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_constants.py +28 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_container.py +24 -0
- pgdevkit-0.9.0/tests/testdb/test_container_mssql.py +41 -0
- pgdevkit-0.9.0/tests/testdb/test_docker.py +46 -0
- pgdevkit-0.9.0/tests/testdb/test_mssql_constants.py +65 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/uv.lock +1 -1
- pgdevkit-0.7.2/tests/testdb/test_mssql_constants.py +0 -31
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/.github/workflows/auto-release.yml +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/.github/workflows/python-test.yml +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/.gitignore +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/.python-version +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/docs/database-layout.md +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/areas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/backends/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/backends/base.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/backends/mssql.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/backends/postgres.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/cli.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/complex_types.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/model.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/mssql_crud.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/db/mssql_sql.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/dialect.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/diff.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/envtag.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/fetch_missing.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/models.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/mssql_introspect.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/parser.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/schemas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/sql_text.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/api.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/config.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/api.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/query.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/pgdevkit/testdb/schema.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/skills/pgdevkit/SKILL.md +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/conftest.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/db/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/db/test_complex_types.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/db/test_mssql_crud_live.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/db/test_mssql_crud_sql.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_areas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_cli_compare.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_compare.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_compare_mssql_live.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_connection.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_dialect.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_diff_mssql.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_envtag.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_migrate.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_migrate_areas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_migrate_env.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_migrate_schemas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_mssql_introspect.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_parser_areas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_parser_mssql.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_parser_schemas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/test_schemas.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/conftest.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/event.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/event_kind.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget.init.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/permissions/grants.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_api_mssql.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_api_mssql_live.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_config_mssql.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_query.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_schema.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_schema_filtering.py +0 -0
- {pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/test_schema_mssql.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pgdevkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A helper for developing with Postgres
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: docker>=7.1.0
|
|
@@ -261,11 +261,25 @@ worktree and wants to know exactly which DB(s) go with it.
|
|
|
261
261
|
|
|
262
262
|
Container connection defaults (`localhost:54322`, `postgres`/`testpwd`) can
|
|
263
263
|
be overridden with `PGDEVKIT_TESTDB_HOST`, `PGDEVKIT_TESTDB_PORT`,
|
|
264
|
-
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD
|
|
265
|
-
Docker API, pgdevkit first
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
that check too.
|
|
264
|
+
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD`, and the image with
|
|
265
|
+
`PGDEVKIT_TESTDB_IMAGE`. Before touching the Docker API, pgdevkit first
|
|
266
|
+
checks (with a short timeout) whether Postgres is already reachable at that
|
|
267
|
+
address and skips container management if so. Set `PGDEVKIT_SKIP_CONTAINER=1`
|
|
268
|
+
to always assume it's already there and skip that check too.
|
|
269
|
+
|
|
270
|
+
Docker-level resource knobs are unset by default (Docker/Podman's own
|
|
271
|
+
defaults apply) and can be overridden with `PGDEVKIT_TESTDB_SHM_SIZE` (e.g.
|
|
272
|
+
`256m`, useful when Docker's 64MB default shared-memory size is too small
|
|
273
|
+
for parallel query workers), `PGDEVKIT_TESTDB_MEM_LIMIT` (e.g. `1g`),
|
|
274
|
+
`PGDEVKIT_TESTDB_CPUS` (e.g. `2` or `0.5`), and `PGDEVKIT_TESTDB_TMPFS` —
|
|
275
|
+
docker-CLI `--tmpfs` syntax (`/path:options`), semicolon-separated for
|
|
276
|
+
multiple mounts, e.g. `/var/lib/postgresql/data:size=512m`.
|
|
277
|
+
|
|
278
|
+
All of the above (including `_HOST`/`_PORT`/`_USER`/`_PASSWORD`/`_IMAGE`)
|
|
279
|
+
only take effect when pgdevkit actually creates the container, not when it
|
|
280
|
+
finds and restarts an existing stopped one with the same name — remove the
|
|
281
|
+
existing container (`docker rm -f pgdevkit-postgres`) first if you've
|
|
282
|
+
changed any of these on a machine that already has one.
|
|
269
283
|
|
|
270
284
|
Container management goes through the Docker API (the `docker` package,
|
|
271
285
|
`docker.from_env()`, falling back to Podman's rootful/rootless socket) — it
|
|
@@ -291,6 +305,17 @@ Container defaults (`localhost:14330`, `sa`/a generated complexity-valid
|
|
|
291
305
|
password) can be overridden with `PGDEVKIT_TESTDB_MSSQL_HOST`, `_PORT`,
|
|
292
306
|
`_USER`, `_PASSWORD`, `_IMAGE`, `_MEMORY_LIMIT_MB`. The container only
|
|
293
307
|
bootstraps the `sa` login — additional logins are a known limitation.
|
|
308
|
+
|
|
309
|
+
The same Docker-level resource knobs as the Postgres container are
|
|
310
|
+
available here too: `PGDEVKIT_TESTDB_MSSQL_SHM_SIZE`,
|
|
311
|
+
`PGDEVKIT_TESTDB_MSSQL_MEM_LIMIT` (a container-level cgroup limit, distinct
|
|
312
|
+
from `_MEMORY_LIMIT_MB` above which only tunes SQL Server's own internal
|
|
313
|
+
memory management), `PGDEVKIT_TESTDB_MSSQL_CPUS`, and
|
|
314
|
+
`PGDEVKIT_TESTDB_MSSQL_TMPFS`. As with the Postgres container, these only
|
|
315
|
+
take effect when pgdevkit creates the container, not when restarting an
|
|
316
|
+
existing stopped one (`docker rm -f pgdevkit-mssql` first if you've changed
|
|
317
|
+
any of them).
|
|
318
|
+
|
|
294
319
|
`pgdb testdb shell` execs into
|
|
295
320
|
[`sqlcmd`](https://github.com/microsoft/go-sqlcmd) (an external prerequisite,
|
|
296
321
|
the same category as `psql` for the Postgres path) rather than a Python
|
|
@@ -240,11 +240,25 @@ worktree and wants to know exactly which DB(s) go with it.
|
|
|
240
240
|
|
|
241
241
|
Container connection defaults (`localhost:54322`, `postgres`/`testpwd`) can
|
|
242
242
|
be overridden with `PGDEVKIT_TESTDB_HOST`, `PGDEVKIT_TESTDB_PORT`,
|
|
243
|
-
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD
|
|
244
|
-
Docker API, pgdevkit first
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
that check too.
|
|
243
|
+
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD`, and the image with
|
|
244
|
+
`PGDEVKIT_TESTDB_IMAGE`. Before touching the Docker API, pgdevkit first
|
|
245
|
+
checks (with a short timeout) whether Postgres is already reachable at that
|
|
246
|
+
address and skips container management if so. Set `PGDEVKIT_SKIP_CONTAINER=1`
|
|
247
|
+
to always assume it's already there and skip that check too.
|
|
248
|
+
|
|
249
|
+
Docker-level resource knobs are unset by default (Docker/Podman's own
|
|
250
|
+
defaults apply) and can be overridden with `PGDEVKIT_TESTDB_SHM_SIZE` (e.g.
|
|
251
|
+
`256m`, useful when Docker's 64MB default shared-memory size is too small
|
|
252
|
+
for parallel query workers), `PGDEVKIT_TESTDB_MEM_LIMIT` (e.g. `1g`),
|
|
253
|
+
`PGDEVKIT_TESTDB_CPUS` (e.g. `2` or `0.5`), and `PGDEVKIT_TESTDB_TMPFS` —
|
|
254
|
+
docker-CLI `--tmpfs` syntax (`/path:options`), semicolon-separated for
|
|
255
|
+
multiple mounts, e.g. `/var/lib/postgresql/data:size=512m`.
|
|
256
|
+
|
|
257
|
+
All of the above (including `_HOST`/`_PORT`/`_USER`/`_PASSWORD`/`_IMAGE`)
|
|
258
|
+
only take effect when pgdevkit actually creates the container, not when it
|
|
259
|
+
finds and restarts an existing stopped one with the same name — remove the
|
|
260
|
+
existing container (`docker rm -f pgdevkit-postgres`) first if you've
|
|
261
|
+
changed any of these on a machine that already has one.
|
|
248
262
|
|
|
249
263
|
Container management goes through the Docker API (the `docker` package,
|
|
250
264
|
`docker.from_env()`, falling back to Podman's rootful/rootless socket) — it
|
|
@@ -270,6 +284,17 @@ Container defaults (`localhost:14330`, `sa`/a generated complexity-valid
|
|
|
270
284
|
password) can be overridden with `PGDEVKIT_TESTDB_MSSQL_HOST`, `_PORT`,
|
|
271
285
|
`_USER`, `_PASSWORD`, `_IMAGE`, `_MEMORY_LIMIT_MB`. The container only
|
|
272
286
|
bootstraps the `sa` login — additional logins are a known limitation.
|
|
287
|
+
|
|
288
|
+
The same Docker-level resource knobs as the Postgres container are
|
|
289
|
+
available here too: `PGDEVKIT_TESTDB_MSSQL_SHM_SIZE`,
|
|
290
|
+
`PGDEVKIT_TESTDB_MSSQL_MEM_LIMIT` (a container-level cgroup limit, distinct
|
|
291
|
+
from `_MEMORY_LIMIT_MB` above which only tunes SQL Server's own internal
|
|
292
|
+
memory management), `PGDEVKIT_TESTDB_MSSQL_CPUS`, and
|
|
293
|
+
`PGDEVKIT_TESTDB_MSSQL_TMPFS`. As with the Postgres container, these only
|
|
294
|
+
take effect when pgdevkit creates the container, not when restarting an
|
|
295
|
+
existing stopped one (`docker rm -f pgdevkit-mssql` first if you've changed
|
|
296
|
+
any of them).
|
|
297
|
+
|
|
273
298
|
`pgdb testdb shell` execs into
|
|
274
299
|
[`sqlcmd`](https://github.com/microsoft/go-sqlcmd) (an external prerequisite,
|
|
275
300
|
the same category as `psql` for the Postgres path) rather than a Python
|
|
@@ -303,6 +303,14 @@ def record_applied(conninfo: str, tracking_table: str, filename: str) -> bool:
|
|
|
303
303
|
return False
|
|
304
304
|
|
|
305
305
|
|
|
306
|
+
def created_table_names(sql: str) -> list[str]:
|
|
307
|
+
"""Table names any CREATE TABLE statement in this raw SQL script targets. Public
|
|
308
|
+
wrapper around the same detection `apply_migration` uses internally, for callers that
|
|
309
|
+
run a script directly (e.g. via `execute_sql_script`) instead of through a tracked
|
|
310
|
+
migration file, and still want to know what tables -- if any -- it created."""
|
|
311
|
+
return _created_table_names(_split_sql(sql))
|
|
312
|
+
|
|
313
|
+
|
|
306
314
|
def verify_created_tables(conninfo: str, stmts: list[str]) -> list[str]:
|
|
307
315
|
"""Table names from this migration's CREATE TABLE statements that do NOT exist in the
|
|
308
316
|
database. Empty means everything landed."""
|
|
@@ -318,6 +326,39 @@ def verify_created_tables(conninfo: str, stmts: list[str]) -> list[str]:
|
|
|
318
326
|
return missing
|
|
319
327
|
|
|
320
328
|
|
|
329
|
+
def missing_privileges(conninfo: str, role: str, tables: list[str], privilege: str = "select") -> list[str]:
|
|
330
|
+
"""Table names from `tables` that `role` cannot currently exercise `privilege` on
|
|
331
|
+
(checked via Postgres's own `has_table_privilege`). Empty means the role can access
|
|
332
|
+
all of them. For guarding against the classic "migration creates a table, nobody
|
|
333
|
+
grants it to the app's runtime role" gap: check the tables a migration just created
|
|
334
|
+
against the role that will actually query them at runtime."""
|
|
335
|
+
if not tables:
|
|
336
|
+
return []
|
|
337
|
+
missing = []
|
|
338
|
+
with psycopg.connect(conninfo) as con:
|
|
339
|
+
for tbl in tables:
|
|
340
|
+
row = con.execute("select has_table_privilege(%s, %s, %s)", (role, tbl, privilege)).fetchone()
|
|
341
|
+
if not (row and row[0]):
|
|
342
|
+
missing.append(tbl)
|
|
343
|
+
return missing
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _execute_stmts(conninfo: str, stmts: list[str]) -> None:
|
|
347
|
+
with psycopg.connect(conninfo) as con:
|
|
348
|
+
for stmt in stmts:
|
|
349
|
+
con.execute(cast(LiteralString, stmt))
|
|
350
|
+
con.commit()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def execute_sql_script(conninfo: str, sql: str) -> None:
|
|
354
|
+
"""Run a raw SQL script as one committed transaction, split into statements the same
|
|
355
|
+
statement-boundary-safe way apply_migration is (dollar-quoted blocks, string literals,
|
|
356
|
+
and line comments never get split mid-statement). Unlike apply_migration, this does
|
|
357
|
+
no tracking-table bookkeeping and isn't forward-only -- for scripts meant to re-run
|
|
358
|
+
every time, like an idempotent `GRANT ... ON ALL TABLES IN SCHEMA` privilege sync."""
|
|
359
|
+
_execute_stmts(conninfo, _split_sql(sql))
|
|
360
|
+
|
|
361
|
+
|
|
321
362
|
@dataclass
|
|
322
363
|
class ApplyResult:
|
|
323
364
|
filename: str
|
|
@@ -337,11 +378,7 @@ def apply_migration(
|
|
|
337
378
|
stmts = _split_sql(sql)
|
|
338
379
|
|
|
339
380
|
if not already_done:
|
|
340
|
-
|
|
341
|
-
with psycopg.connect(conninfo) as con:
|
|
342
|
-
for stmt in stmts:
|
|
343
|
-
con.execute(cast(LiteralString, stmt))
|
|
344
|
-
con.commit()
|
|
381
|
+
_execute_stmts(conninfo, stmts)
|
|
345
382
|
|
|
346
383
|
# Tracking insert is a separate connection/transaction so a missing tracking table
|
|
347
384
|
# never rolls back the DDL that was just applied.
|
|
@@ -39,3 +39,36 @@ def client() -> docker.DockerClient:
|
|
|
39
39
|
"Could not reach a Docker-compatible API. Set DOCKER_HOST, or make sure "
|
|
40
40
|
"Docker or Podman's API socket is running."
|
|
41
41
|
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_tmpfs(spec: str) -> dict[str, str]:
|
|
45
|
+
"""Parse a docker-CLI-style --tmpfs spec into the dict shape docker-py's
|
|
46
|
+
`containers.run(tmpfs=...)` expects. Multiple mounts are semicolon-separated
|
|
47
|
+
(commas are already used by docker's own comma-separated mount options),
|
|
48
|
+
e.g. "/path:rw,size=64m;/other/path:ro,size=32m", matching `docker run
|
|
49
|
+
--tmpfs /path:rw,size=64m --tmpfs /other/path:ro,size=32m`."""
|
|
50
|
+
mounts: dict[str, str] = {}
|
|
51
|
+
for entry in spec.split(";"):
|
|
52
|
+
entry = entry.strip()
|
|
53
|
+
if not entry:
|
|
54
|
+
continue
|
|
55
|
+
path, _, options = entry.partition(":")
|
|
56
|
+
mounts[path] = options
|
|
57
|
+
return mounts
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resource_kwargs(*, tmpfs: str, shm_size: str, mem_limit: str, cpus: str) -> dict:
|
|
61
|
+
"""Build the `containers.run()` kwargs for the tmpfs/shm_size/mem_limit/cpus
|
|
62
|
+
overrides, omitting any that aren't set so Docker's own defaults apply.
|
|
63
|
+
Shared by the Postgres and MSSQL container modules -- these are Docker-level
|
|
64
|
+
resource knobs, not anything specific to either engine."""
|
|
65
|
+
kwargs: dict = {}
|
|
66
|
+
if tmpfs:
|
|
67
|
+
kwargs["tmpfs"] = _parse_tmpfs(tmpfs)
|
|
68
|
+
if shm_size:
|
|
69
|
+
kwargs["shm_size"] = shm_size
|
|
70
|
+
if mem_limit:
|
|
71
|
+
kwargs["mem_limit"] = mem_limit
|
|
72
|
+
if cpus:
|
|
73
|
+
kwargs["nano_cpus"] = int(float(cpus) * 1_000_000_000)
|
|
74
|
+
return kwargs
|
|
@@ -5,11 +5,18 @@ import os
|
|
|
5
5
|
from psycopg.conninfo import make_conninfo
|
|
6
6
|
|
|
7
7
|
CONTAINER_NAME = "pgdevkit-postgres"
|
|
8
|
-
IMAGE = "pgvector/pgvector:pg18-trixie"
|
|
8
|
+
IMAGE = os.environ.get("PGDEVKIT_TESTDB_IMAGE", "pgvector/pgvector:pg18-trixie")
|
|
9
9
|
HOST = os.environ.get("PGDEVKIT_TESTDB_HOST", "localhost")
|
|
10
10
|
PORT = int(os.environ.get("PGDEVKIT_TESTDB_PORT", "54322"))
|
|
11
11
|
USER = os.environ.get("PGDEVKIT_TESTDB_USER", "postgres")
|
|
12
12
|
PASSWORD = os.environ.get("PGDEVKIT_TESTDB_PASSWORD", "testpwd")
|
|
13
|
+
# Docker-level resource overrides, passed through to `containers.run()` --
|
|
14
|
+
# unset (the default) means "let Docker/Podman use their own defaults".
|
|
15
|
+
# TMPFS uses docker-CLI --tmpfs syntax; see _docker._parse_tmpfs().
|
|
16
|
+
TMPFS = os.environ.get("PGDEVKIT_TESTDB_TMPFS", "")
|
|
17
|
+
SHM_SIZE = os.environ.get("PGDEVKIT_TESTDB_SHM_SIZE", "")
|
|
18
|
+
MEM_LIMIT = os.environ.get("PGDEVKIT_TESTDB_MEM_LIMIT", "")
|
|
19
|
+
CPUS = os.environ.get("PGDEVKIT_TESTDB_CPUS", "")
|
|
13
20
|
PG_SPEED_FLAGS = ["-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
|
14
21
|
# Production connects with session timezone=UTC; the container image's own default
|
|
15
22
|
# (baked into its base OS, not something pgdevkit ever set) can differ, silently
|
|
@@ -7,7 +7,7 @@ import docker
|
|
|
7
7
|
import docker.errors
|
|
8
8
|
import psycopg
|
|
9
9
|
|
|
10
|
-
from . import constants
|
|
10
|
+
from . import _docker, constants
|
|
11
11
|
from ._docker import client as _client
|
|
12
12
|
|
|
13
13
|
|
|
@@ -43,6 +43,12 @@ def _create_container(client: docker.DockerClient) -> None:
|
|
|
43
43
|
"PGTZ": "UTC",
|
|
44
44
|
},
|
|
45
45
|
command=["postgres", *constants.PG_STARTUP_FLAGS],
|
|
46
|
+
**_docker.resource_kwargs(
|
|
47
|
+
tmpfs=constants.TMPFS,
|
|
48
|
+
shm_size=constants.SHM_SIZE,
|
|
49
|
+
mem_limit=constants.MEM_LIMIT,
|
|
50
|
+
cpus=constants.CPUS,
|
|
51
|
+
),
|
|
46
52
|
)
|
|
47
53
|
except docker.errors.APIError as e:
|
|
48
54
|
if getattr(e, "status_code", None) == 409 or "already in use" in str(e):
|
|
@@ -79,6 +85,10 @@ def ensure_container() -> None:
|
|
|
79
85
|
except docker.errors.NotFound:
|
|
80
86
|
container = None
|
|
81
87
|
if container is not None:
|
|
88
|
+
# An existing container (even stopped) is just restarted as-is --
|
|
89
|
+
# constants like TMPFS/SHM_SIZE/IMAGE/etc. only take effect via
|
|
90
|
+
# _create_container(), so changing them has no effect here until
|
|
91
|
+
# the stale container is removed. Documented in the README.
|
|
82
92
|
if container.status != "running":
|
|
83
93
|
container.start()
|
|
84
94
|
else:
|
|
@@ -22,6 +22,15 @@ USER = os.environ.get("PGDEVKIT_TESTDB_MSSQL_USER", "sa")
|
|
|
22
22
|
# default -- see validate_sa_password().
|
|
23
23
|
PASSWORD = os.environ.get("PGDEVKIT_TESTDB_MSSQL_PASSWORD", "TestPwd!2026")
|
|
24
24
|
MEMORY_LIMIT_MB = int(os.environ.get("PGDEVKIT_TESTDB_MSSQL_MEMORY_LIMIT_MB", "2048"))
|
|
25
|
+
# Docker-level resource overrides, passed through to `containers.run()` --
|
|
26
|
+
# unset (the default) means "let Docker/Podman use their own defaults". Distinct
|
|
27
|
+
# from MEMORY_LIMIT_MB above, which only controls SQL Server's own internal memory
|
|
28
|
+
# management, not the container's actual cgroup limit. TMPFS uses docker-CLI
|
|
29
|
+
# --tmpfs syntax; see _docker._parse_tmpfs().
|
|
30
|
+
TMPFS = os.environ.get("PGDEVKIT_TESTDB_MSSQL_TMPFS", "")
|
|
31
|
+
SHM_SIZE = os.environ.get("PGDEVKIT_TESTDB_MSSQL_SHM_SIZE", "")
|
|
32
|
+
MEM_LIMIT = os.environ.get("PGDEVKIT_TESTDB_MSSQL_MEM_LIMIT", "")
|
|
33
|
+
CPUS = os.environ.get("PGDEVKIT_TESTDB_MSSQL_CPUS", "")
|
|
25
34
|
|
|
26
35
|
|
|
27
36
|
def validate_sa_password(password: str) -> None:
|
|
@@ -40,6 +40,12 @@ def _create_container(client: docker.DockerClient) -> None:
|
|
|
40
40
|
"MSSQL_PID": "Developer",
|
|
41
41
|
"MSSQL_MEMORY_LIMIT_MB": str(constants.MEMORY_LIMIT_MB),
|
|
42
42
|
},
|
|
43
|
+
**_docker.resource_kwargs(
|
|
44
|
+
tmpfs=constants.TMPFS,
|
|
45
|
+
shm_size=constants.SHM_SIZE,
|
|
46
|
+
mem_limit=constants.MEM_LIMIT,
|
|
47
|
+
cpus=constants.CPUS,
|
|
48
|
+
),
|
|
43
49
|
)
|
|
44
50
|
except docker.errors.APIError as e:
|
|
45
51
|
if getattr(e, "status_code", None) == 409 or "already in use" in str(e):
|
|
@@ -83,6 +89,9 @@ def ensure_mssql_container() -> None:
|
|
|
83
89
|
except docker.errors.NotFound:
|
|
84
90
|
container = None
|
|
85
91
|
if container is not None:
|
|
92
|
+
# See the Postgres container's ensure_container() -- an existing
|
|
93
|
+
# (even stopped) container is just restarted as-is, so config
|
|
94
|
+
# constants only take effect via _create_container().
|
|
86
95
|
if container.status != "running":
|
|
87
96
|
container.start()
|
|
88
97
|
else:
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import psycopg
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pgdevkit.migrate import created_table_names, execute_sql_script, missing_privileges
|
|
7
|
+
|
|
8
|
+
ROLE = "pgdevkit_test_grants_role"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def app_role(clean_db: str):
|
|
13
|
+
"""A throwaway, no-login role to check privileges against -- dropped afterwards since
|
|
14
|
+
roles are cluster-wide, not scoped to the (per-test) database like clean_db's schemas."""
|
|
15
|
+
with psycopg.connect(clean_db, autocommit=True) as con:
|
|
16
|
+
con.execute(f"DROP ROLE IF EXISTS {ROLE}")
|
|
17
|
+
con.execute(f"CREATE ROLE {ROLE} NOLOGIN")
|
|
18
|
+
yield ROLE
|
|
19
|
+
with psycopg.connect(clean_db, autocommit=True) as con:
|
|
20
|
+
# DROP OWNED BY revokes any grants left on tables the role can still see --
|
|
21
|
+
# otherwise DROP ROLE fails with "cannot be dropped because some objects depend
|
|
22
|
+
# on it" for as long as the granted-on table (e.g. widgets) still exists.
|
|
23
|
+
con.execute(f"DROP OWNED BY {ROLE}")
|
|
24
|
+
con.execute(f"DROP ROLE IF EXISTS {ROLE}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_created_table_names_finds_tables_in_a_raw_script():
|
|
28
|
+
sql = "GRANT SELECT ON foo TO some_role; CREATE TABLE public.widgets (id int primary key);"
|
|
29
|
+
assert created_table_names(sql) == ["public.widgets"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_created_table_names_empty_for_a_script_with_no_create_table():
|
|
33
|
+
assert created_table_names("GRANT SELECT ON ALL TABLES IN SCHEMA public TO some_role;") == []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_execute_sql_script_runs_multiple_statements_in_one_transaction(clean_db: str):
|
|
37
|
+
execute_sql_script(
|
|
38
|
+
clean_db,
|
|
39
|
+
"""
|
|
40
|
+
-- a leading comment shouldn't confuse statement splitting
|
|
41
|
+
CREATE TABLE public.widgets (id int primary key);
|
|
42
|
+
INSERT INTO public.widgets VALUES (1);
|
|
43
|
+
""",
|
|
44
|
+
)
|
|
45
|
+
with psycopg.connect(clean_db) as con:
|
|
46
|
+
row = con.execute("select count(*) from public.widgets").fetchone()
|
|
47
|
+
assert row == (1,)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_execute_sql_script_rolls_back_all_statements_on_any_failure(clean_db: str):
|
|
51
|
+
with pytest.raises(psycopg.Error):
|
|
52
|
+
execute_sql_script(
|
|
53
|
+
clean_db,
|
|
54
|
+
"CREATE TABLE public.widgets (id int primary key); NOT VALID SQL HERE;",
|
|
55
|
+
)
|
|
56
|
+
with psycopg.connect(clean_db) as con:
|
|
57
|
+
row = con.execute("select to_regclass('public.widgets')").fetchone()
|
|
58
|
+
assert row is not None and row[0] is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_missing_privileges_reports_table_without_grant(clean_db: str, app_role: str):
|
|
62
|
+
with psycopg.connect(clean_db, autocommit=True) as con:
|
|
63
|
+
con.execute("CREATE TABLE public.widgets (id int primary key)")
|
|
64
|
+
assert missing_privileges(clean_db, app_role, ["public.widgets"]) == ["public.widgets"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_missing_privileges_empty_once_granted(clean_db: str, app_role: str):
|
|
68
|
+
with psycopg.connect(clean_db, autocommit=True) as con:
|
|
69
|
+
con.execute("CREATE TABLE public.widgets (id int primary key)")
|
|
70
|
+
con.execute(f"GRANT SELECT ON public.widgets TO {app_role}")
|
|
71
|
+
assert missing_privileges(clean_db, app_role, ["public.widgets"]) == []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_missing_privileges_empty_for_no_tables(clean_db: str, app_role: str):
|
|
75
|
+
assert missing_privileges(clean_db, app_role, []) == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_execute_sql_script_grant_on_all_tables_covers_tables_created_after_the_fact(
|
|
79
|
+
clean_db: str, app_role: str
|
|
80
|
+
):
|
|
81
|
+
"""The actual use case: a re-run of `GRANT ... ON ALL TABLES IN SCHEMA` after a new
|
|
82
|
+
table appears picks it up automatically -- unlike ALTER DEFAULT PRIVILEGES, which only
|
|
83
|
+
covers tables created later by the exact role that ran the ALTER DEFAULT PRIVILEGES
|
|
84
|
+
statement itself."""
|
|
85
|
+
with psycopg.connect(clean_db, autocommit=True) as con:
|
|
86
|
+
con.execute("CREATE TABLE public.widgets (id int primary key)")
|
|
87
|
+
assert missing_privileges(clean_db, app_role, ["public.widgets"]) == ["public.widgets"]
|
|
88
|
+
|
|
89
|
+
execute_sql_script(clean_db, f"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {app_role}")
|
|
90
|
+
|
|
91
|
+
assert missing_privileges(clean_db, app_role, ["public.widgets"]) == []
|
|
@@ -10,12 +10,22 @@ def test_defaults_when_env_vars_unset(monkeypatch):
|
|
|
10
10
|
monkeypatch.delenv("PGDEVKIT_TESTDB_PORT", raising=False)
|
|
11
11
|
monkeypatch.delenv("PGDEVKIT_TESTDB_USER", raising=False)
|
|
12
12
|
monkeypatch.delenv("PGDEVKIT_TESTDB_PASSWORD", raising=False)
|
|
13
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_IMAGE", raising=False)
|
|
14
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_TMPFS", raising=False)
|
|
15
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_SHM_SIZE", raising=False)
|
|
16
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_MEM_LIMIT", raising=False)
|
|
17
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_CPUS", raising=False)
|
|
13
18
|
try:
|
|
14
19
|
importlib.reload(constants)
|
|
15
20
|
assert constants.HOST == "localhost"
|
|
16
21
|
assert constants.PORT == 54322
|
|
17
22
|
assert constants.USER == "postgres"
|
|
18
23
|
assert constants.PASSWORD == "testpwd"
|
|
24
|
+
assert constants.IMAGE == "pgvector/pgvector:pg18-trixie"
|
|
25
|
+
assert constants.TMPFS == ""
|
|
26
|
+
assert constants.SHM_SIZE == ""
|
|
27
|
+
assert constants.MEM_LIMIT == ""
|
|
28
|
+
assert constants.CPUS == ""
|
|
19
29
|
finally:
|
|
20
30
|
importlib.reload(constants)
|
|
21
31
|
|
|
@@ -37,3 +47,21 @@ def test_env_vars_override_connection_defaults(monkeypatch):
|
|
|
37
47
|
# override values right back in, leaking them into every later test.
|
|
38
48
|
monkeypatch.undo()
|
|
39
49
|
importlib.reload(constants)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_env_vars_override_docker_resource_defaults(monkeypatch):
|
|
53
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_IMAGE", "otherimage:latest")
|
|
54
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_TMPFS", "/var/lib/postgresql/data:size=512m")
|
|
55
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_SHM_SIZE", "256m")
|
|
56
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_MEM_LIMIT", "1g")
|
|
57
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_CPUS", "2")
|
|
58
|
+
try:
|
|
59
|
+
importlib.reload(constants)
|
|
60
|
+
assert constants.IMAGE == "otherimage:latest"
|
|
61
|
+
assert constants.TMPFS == "/var/lib/postgresql/data:size=512m"
|
|
62
|
+
assert constants.SHM_SIZE == "256m"
|
|
63
|
+
assert constants.MEM_LIMIT == "1g"
|
|
64
|
+
assert constants.CPUS == "2"
|
|
65
|
+
finally:
|
|
66
|
+
monkeypatch.undo()
|
|
67
|
+
importlib.reload(constants)
|
|
@@ -79,6 +79,30 @@ def test_create_container_forces_utc_timezone():
|
|
|
79
79
|
assert client.containers.run_kwargs["environment"]["PGTZ"] == "UTC"
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def test_create_container_omits_resource_kwargs_by_default():
|
|
83
|
+
client = _FakeClient()
|
|
84
|
+
|
|
85
|
+
_create_container(client)
|
|
86
|
+
|
|
87
|
+
for key in ("tmpfs", "shm_size", "mem_limit", "nano_cpus"):
|
|
88
|
+
assert key not in client.containers.run_kwargs
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_create_container_passes_through_resource_overrides(monkeypatch):
|
|
92
|
+
monkeypatch.setattr(constants, "TMPFS", "/var/lib/postgresql/data:size=512m")
|
|
93
|
+
monkeypatch.setattr(constants, "SHM_SIZE", "256m")
|
|
94
|
+
monkeypatch.setattr(constants, "MEM_LIMIT", "1g")
|
|
95
|
+
monkeypatch.setattr(constants, "CPUS", "2")
|
|
96
|
+
client = _FakeClient()
|
|
97
|
+
|
|
98
|
+
_create_container(client)
|
|
99
|
+
|
|
100
|
+
assert client.containers.run_kwargs["tmpfs"] == {"/var/lib/postgresql/data": "size=512m"}
|
|
101
|
+
assert client.containers.run_kwargs["shm_size"] == "256m"
|
|
102
|
+
assert client.containers.run_kwargs["mem_limit"] == "1g"
|
|
103
|
+
assert client.containers.run_kwargs["nano_cpus"] == 2_000_000_000
|
|
104
|
+
|
|
105
|
+
|
|
82
106
|
def _admin_dsn() -> str:
|
|
83
107
|
return constants.conninfo("postgres", connect_timeout=5)
|
|
84
108
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pgdevkit.testdb.mssql import constants
|
|
4
|
+
from pgdevkit.testdb.mssql.container import _create_container
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _FakeContainers:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.run_kwargs: dict | None = None
|
|
10
|
+
|
|
11
|
+
def run(self, image, **kwargs):
|
|
12
|
+
self.run_kwargs = kwargs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeClient:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.containers = _FakeContainers()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_create_container_omits_resource_kwargs_by_default():
|
|
21
|
+
client = _FakeClient()
|
|
22
|
+
|
|
23
|
+
_create_container(client)
|
|
24
|
+
|
|
25
|
+
for key in ("tmpfs", "shm_size", "mem_limit", "nano_cpus"):
|
|
26
|
+
assert key not in client.containers.run_kwargs
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_create_container_passes_through_resource_overrides(monkeypatch):
|
|
30
|
+
monkeypatch.setattr(constants, "TMPFS", "/var/opt/mssql:size=512m")
|
|
31
|
+
monkeypatch.setattr(constants, "SHM_SIZE", "256m")
|
|
32
|
+
monkeypatch.setattr(constants, "MEM_LIMIT", "4g")
|
|
33
|
+
monkeypatch.setattr(constants, "CPUS", "2")
|
|
34
|
+
client = _FakeClient()
|
|
35
|
+
|
|
36
|
+
_create_container(client)
|
|
37
|
+
|
|
38
|
+
assert client.containers.run_kwargs["tmpfs"] == {"/var/opt/mssql": "size=512m"}
|
|
39
|
+
assert client.containers.run_kwargs["shm_size"] == "256m"
|
|
40
|
+
assert client.containers.run_kwargs["mem_limit"] == "4g"
|
|
41
|
+
assert client.containers.run_kwargs["nano_cpus"] == 2_000_000_000
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pgdevkit.testdb._docker import _parse_tmpfs, resource_kwargs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_parse_tmpfs_single_mount():
|
|
7
|
+
assert _parse_tmpfs("/var/lib/postgresql/data:rw,size=512m") == {
|
|
8
|
+
"/var/lib/postgresql/data": "rw,size=512m"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_parse_tmpfs_multiple_mounts_semicolon_separated():
|
|
13
|
+
assert _parse_tmpfs("/data:size=512m;/tmp:rw,size=64m") == {
|
|
14
|
+
"/data": "size=512m",
|
|
15
|
+
"/tmp": "rw,size=64m",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_tmpfs_path_without_options():
|
|
20
|
+
assert _parse_tmpfs("/tmp") == {"/tmp": ""}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_parse_tmpfs_ignores_blank_entries():
|
|
24
|
+
assert _parse_tmpfs("/data:size=512m;;/tmp:size=64m;") == {
|
|
25
|
+
"/data": "size=512m",
|
|
26
|
+
"/tmp": "size=64m",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_resource_kwargs_all_unset_returns_empty_dict():
|
|
31
|
+
assert resource_kwargs(tmpfs="", shm_size="", mem_limit="", cpus="") == {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_resource_kwargs_only_includes_set_values():
|
|
35
|
+
kwargs = resource_kwargs(tmpfs="/data:size=512m", shm_size="", mem_limit="1g", cpus="")
|
|
36
|
+
assert kwargs == {"tmpfs": {"/data": "size=512m"}, "mem_limit": "1g"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_resource_kwargs_cpus_converted_to_nano_cpus():
|
|
40
|
+
assert resource_kwargs(tmpfs="", shm_size="", mem_limit="", cpus="1.5") == {
|
|
41
|
+
"nano_cpus": 1_500_000_000
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_resource_kwargs_shm_size_passed_through():
|
|
46
|
+
assert resource_kwargs(tmpfs="", shm_size="256m", mem_limit="", cpus="") == {"shm_size": "256m"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from pgdevkit.testdb.mssql import constants
|
|
8
|
+
from pgdevkit.testdb.mssql.constants import conninfo, validate_sa_password
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_validate_sa_password_accepts_complexity_valid_password():
|
|
12
|
+
validate_sa_password("TestPwd!2026") # must not raise
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_validate_sa_password_rejects_too_short():
|
|
16
|
+
with pytest.raises(ValueError, match="8 characters"):
|
|
17
|
+
validate_sa_password("Ab1!")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_validate_sa_password_rejects_containing_login_name():
|
|
21
|
+
with pytest.raises(ValueError, match="'sa'"):
|
|
22
|
+
validate_sa_password("Sup3rSaPass!")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_validate_sa_password_rejects_insufficient_character_classes():
|
|
26
|
+
with pytest.raises(ValueError, match="3 of"):
|
|
27
|
+
validate_sa_password("alllowercase")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_conninfo_omits_driver_and_app_keys():
|
|
31
|
+
cs = conninfo("mydb")
|
|
32
|
+
assert "Driver=" not in cs
|
|
33
|
+
assert "APP=" not in cs
|
|
34
|
+
assert "Database=mydb" in cs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_docker_resource_defaults_unset(monkeypatch):
|
|
38
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_MSSQL_TMPFS", raising=False)
|
|
39
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_MSSQL_SHM_SIZE", raising=False)
|
|
40
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_MSSQL_MEM_LIMIT", raising=False)
|
|
41
|
+
monkeypatch.delenv("PGDEVKIT_TESTDB_MSSQL_CPUS", raising=False)
|
|
42
|
+
try:
|
|
43
|
+
importlib.reload(constants)
|
|
44
|
+
assert constants.TMPFS == ""
|
|
45
|
+
assert constants.SHM_SIZE == ""
|
|
46
|
+
assert constants.MEM_LIMIT == ""
|
|
47
|
+
assert constants.CPUS == ""
|
|
48
|
+
finally:
|
|
49
|
+
importlib.reload(constants)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_docker_resource_env_vars_override_defaults(monkeypatch):
|
|
53
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_MSSQL_TMPFS", "/var/opt/mssql:size=512m")
|
|
54
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_MSSQL_SHM_SIZE", "256m")
|
|
55
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_MSSQL_MEM_LIMIT", "4g")
|
|
56
|
+
monkeypatch.setenv("PGDEVKIT_TESTDB_MSSQL_CPUS", "2")
|
|
57
|
+
try:
|
|
58
|
+
importlib.reload(constants)
|
|
59
|
+
assert constants.TMPFS == "/var/opt/mssql:size=512m"
|
|
60
|
+
assert constants.SHM_SIZE == "256m"
|
|
61
|
+
assert constants.MEM_LIMIT == "4g"
|
|
62
|
+
assert constants.CPUS == "2"
|
|
63
|
+
finally:
|
|
64
|
+
monkeypatch.undo()
|
|
65
|
+
importlib.reload(constants)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from pgdevkit.testdb.mssql.constants import conninfo, validate_sa_password
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_validate_sa_password_accepts_complexity_valid_password():
|
|
9
|
-
validate_sa_password("TestPwd!2026") # must not raise
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_validate_sa_password_rejects_too_short():
|
|
13
|
-
with pytest.raises(ValueError, match="8 characters"):
|
|
14
|
-
validate_sa_password("Ab1!")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_validate_sa_password_rejects_containing_login_name():
|
|
18
|
-
with pytest.raises(ValueError, match="'sa'"):
|
|
19
|
-
validate_sa_password("Sup3rSaPass!")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_validate_sa_password_rejects_insufficient_character_classes():
|
|
23
|
-
with pytest.raises(ValueError, match="3 of"):
|
|
24
|
-
validate_sa_password("alllowercase")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_conninfo_omits_driver_and_app_keys():
|
|
28
|
-
cs = conninfo("mydb")
|
|
29
|
-
assert "Driver=" not in cs
|
|
30
|
-
assert "APP=" not in cs
|
|
31
|
-
assert "Database=mydb" in cs
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json
RENAMED
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
{pgdevkit-0.7.2 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|