pgdevkit 0.7.1__tar.gz → 0.8.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 (130) hide show
  1. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/PKG-INFO +31 -6
  2. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/README.md +30 -5
  3. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/_docker.py +33 -0
  4. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/constants.py +8 -1
  5. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/container.py +11 -1
  6. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/mssql/constants.py +9 -0
  7. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/mssql/container.py +9 -0
  8. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/naming.py +10 -1
  9. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pyproject.toml +1 -1
  10. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_constants.py +28 -0
  11. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_container.py +24 -0
  12. pgdevkit-0.8.0/tests/testdb/test_container_mssql.py +41 -0
  13. pgdevkit-0.8.0/tests/testdb/test_docker.py +46 -0
  14. pgdevkit-0.8.0/tests/testdb/test_mssql_constants.py +65 -0
  15. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_naming.py +8 -0
  16. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/uv.lock +1 -1
  17. pgdevkit-0.7.1/tests/testdb/test_mssql_constants.py +0 -31
  18. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/.github/workflows/auto-release.yml +0 -0
  19. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/.github/workflows/python-publish.yml +0 -0
  20. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/.github/workflows/python-test.yml +0 -0
  21. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/.gitignore +0 -0
  22. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/.python-version +0 -0
  23. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/docs/database-layout.md +0 -0
  24. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/__init__.py +0 -0
  25. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/areas.py +0 -0
  26. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/backends/__init__.py +0 -0
  27. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/backends/base.py +0 -0
  28. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/backends/mssql.py +0 -0
  29. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/backends/postgres.py +0 -0
  30. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/cli.py +0 -0
  31. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/connection.py +0 -0
  32. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/__init__.py +0 -0
  33. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/complex_types.py +0 -0
  34. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/connection.py +0 -0
  35. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/crud.py +0 -0
  36. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/loader.py +0 -0
  37. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/model.py +0 -0
  38. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/mssql_crud.py +0 -0
  39. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/db/mssql_sql.py +0 -0
  40. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/dialect.py +0 -0
  41. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/diff.py +0 -0
  42. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/envtag.py +0 -0
  43. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/fetch_missing.py +0 -0
  44. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/introspect.py +0 -0
  45. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/lakebase.py +0 -0
  46. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/migrate.py +0 -0
  47. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/models.py +0 -0
  48. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/mssql_introspect.py +0 -0
  49. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/parser.py +0 -0
  50. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/schemas.py +0 -0
  51. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/sql_text.py +0 -0
  52. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/__init__.py +0 -0
  53. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/api.py +0 -0
  54. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/config.py +0 -0
  55. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/mssql/__init__.py +0 -0
  56. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/mssql/api.py +0 -0
  57. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/query.py +0 -0
  58. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/pgdevkit/testdb/schema.py +0 -0
  59. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/skills/pgdevkit/SKILL.md +0 -0
  60. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/skills/pgdevkit/references/dynamic-sql.md +0 -0
  61. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/skills/pgdevkit/references/temporal-tables.md +0 -0
  62. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/__init__.py +0 -0
  63. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/conftest.py +0 -0
  64. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/db/__init__.py +0 -0
  65. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/db/test_complex_types.py +0 -0
  66. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/db/test_connection.py +0 -0
  67. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/db/test_crud.py +0 -0
  68. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/db/test_loader.py +0 -0
  69. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/db/test_mssql_crud_live.py +0 -0
  70. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/db/test_mssql_crud_sql.py +0 -0
  71. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/fixtures/01_schema.sql +0 -0
  72. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/fixtures/02_types.sql +0 -0
  73. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/fixtures/03_tables.sql +0 -0
  74. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/fixtures/04_views.sql +0 -0
  75. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/fixtures/05_functions.sql +0 -0
  76. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/fixtures/06_indexes.sql +0 -0
  77. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_areas.py +0 -0
  78. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_cli_compare.py +0 -0
  79. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_compare.py +0 -0
  80. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_compare_mssql_live.py +0 -0
  81. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_connection.py +0 -0
  82. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_dialect.py +0 -0
  83. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_diff_mssql.py +0 -0
  84. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_envtag.py +0 -0
  85. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_fetch_missing.py +0 -0
  86. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_fetch_missing_cli.py +0 -0
  87. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_lakebase.py +0 -0
  88. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_migrate.py +0 -0
  89. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_migrate_areas.py +0 -0
  90. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_migrate_env.py +0 -0
  91. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_migrate_schemas.py +0 -0
  92. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_mssql_introspect.py +0 -0
  93. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_parser_areas.py +0 -0
  94. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_parser_mssql.py +0 -0
  95. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_parser_schemas.py +0 -0
  96. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/test_schemas.py +0 -0
  97. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/__init__.py +0 -0
  98. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/conftest.py +0 -0
  99. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
  100. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/event.sql +0 -0
  101. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/event_kind.sql +0 -0
  102. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
  103. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
  104. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql +0 -0
  105. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/widget.init.sql +0 -0
  106. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
  107. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
  108. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
  109. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
  110. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
  111. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
  112. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
  113. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
  114. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/permissions/grants.sql +0 -0
  115. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database/schema/app.sql +0 -0
  116. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
  117. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
  118. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
  119. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
  120. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
  121. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_api.py +0 -0
  122. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_api_mssql.py +0 -0
  123. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_api_mssql_live.py +0 -0
  124. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_cli.py +0 -0
  125. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_config.py +0 -0
  126. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_config_mssql.py +0 -0
  127. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_query.py +0 -0
  128. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_schema.py +0 -0
  129. {pgdevkit-0.7.1 → pgdevkit-0.8.0}/tests/testdb/test_schema_filtering.py +0 -0
  130. {pgdevkit-0.7.1 → pgdevkit-0.8.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.7.1
3
+ Version: 0.8.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`. Before touching the
265
- Docker API, pgdevkit first checks (with a short timeout) whether Postgres
266
- is already reachable at that address and skips container management if so.
267
- Set `PGDEVKIT_SKIP_CONTAINER=1` to always assume it's already there and skip
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`. Before touching the
244
- Docker API, pgdevkit first checks (with a short timeout) whether Postgres
245
- is already reachable at that address and skips container management if so.
246
- Set `PGDEVKIT_SKIP_CONTAINER=1` to always assume it's already there and skip
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
@@ -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:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
+ import os
4
5
  import re
5
6
  import subprocess
6
7
  from pathlib import Path
@@ -25,7 +26,15 @@ def slugify(value: str) -> str:
25
26
 
26
27
 
27
28
  def current_branch(cwd: Path | None = None) -> str:
28
- """Return the branch checked out in the git worktree rooted at cwd."""
29
+ """Return the branch checked out in the git worktree rooted at cwd.
30
+
31
+ PGDEVKIT_TESTDB_BRANCH overrides this when set -- lets concurrent CI
32
+ checkouts (which all otherwise resolve to detached-HEAD "HEAD") get
33
+ distinct database names on a shared test-DB container.
34
+ """
35
+ override = os.environ.get("PGDEVKIT_TESTDB_BRANCH")
36
+ if override:
37
+ return override
29
38
  result = subprocess.run(
30
39
  ["git", "rev-parse", "--abbrev-ref", "HEAD"],
31
40
  cwd=cwd,
@@ -11,7 +11,7 @@ packages = ["pgdevkit"]
11
11
 
12
12
  [project]
13
13
  name = "pgdevkit"
14
- version = "0.7.1"
14
+ version = "0.8.0"
15
15
  description = "A helper for developing with Postgres"
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.14"
@@ -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)
@@ -53,6 +53,14 @@ def test_current_branch_reads_the_checked_out_branch(tmp_path: Path):
53
53
  assert current_branch(tmp_path) == "my-feature"
54
54
 
55
55
 
56
+ def test_current_branch_env_override_skips_git_entirely(tmp_path: Path, monkeypatch):
57
+ # Not a git repo at all -- the override must short-circuit before the
58
+ # `git rev-parse` subprocess call, which would otherwise raise.
59
+ monkeypatch.setenv("PGDEVKIT_TESTDB_BRANCH", "ci-job-42")
60
+
61
+ assert current_branch(tmp_path) == "ci-job-42"
62
+
63
+
56
64
  def _init_repo(repo: Path, initial_branch: str) -> None:
57
65
  repo.mkdir()
58
66
  subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
@@ -313,7 +313,7 @@ wheels = [
313
313
 
314
314
  [[package]]
315
315
  name = "pgdevkit"
316
- version = "0.7.1"
316
+ version = "0.8.0"
317
317
  source = { editable = "." }
318
318
  dependencies = [
319
319
  { name = "docker" },
@@ -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