pgdevkit 0.7.2__py3-none-any.whl → 0.9.0__py3-none-any.whl

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/migrate.py CHANGED
@@ -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
- # DDL in its own committed transaction.
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pgdevkit
3
- Version: 0.7.2
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`. 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
@@ -8,7 +8,7 @@ pgdevkit/envtag.py,sha256=FdRqjLRdS9O9LSvoEzrRsFNeqiyZTLLzFgEkIirJyZI,2006
8
8
  pgdevkit/fetch_missing.py,sha256=vb8hXWBKMWgqkxHso5SuhX9l6rxNXfaybAEsajPLYow,8495
9
9
  pgdevkit/introspect.py,sha256=QYIlRuZk4lBHGxaW7zb_kFRIuNfi3AxH0Q2F_GKzlVI,7870
10
10
  pgdevkit/lakebase.py,sha256=0PABOk4p-2klLTY8QAUibQf9QeyNHKywUWEWGdkUY8k,3165
11
- pgdevkit/migrate.py,sha256=AnlxOy6Sk0iQlGq5lHYIaIBDwljO9EhCVwDkyRZmvQQ,13499
11
+ pgdevkit/migrate.py,sha256=gyZLsioM-Kr8Qped2dqJykNUB3iGHp943vNN6tFZdeo,15380
12
12
  pgdevkit/models.py,sha256=dAZ8f7pgDsxoLVrDWsM4h0qwJ0pOzXDdWMGsnjrW80E,2103
13
13
  pgdevkit/mssql_introspect.py,sha256=Fhv1N4zHYEDZ0PEy0OIpawUmoIFeoi-Lm97pmEyDmYY,12031
14
14
  pgdevkit/parser.py,sha256=FmXL6-eCFzP_0FNyTrg4wF3aPzPeq7re2i0xvrSJIcs,16885
@@ -27,23 +27,23 @@ pgdevkit/db/model.py,sha256=ntuXonq-SCGkbNw-lvdMS5VuzLjxYRT8O6xaVqiw56I,980
27
27
  pgdevkit/db/mssql_crud.py,sha256=yN4izZ0fsFwAs14008JECHH4dqjNbdHUHGEG8kidyNs,11374
28
28
  pgdevkit/db/mssql_sql.py,sha256=DO3UMsTFN9pg0tNdaPGb0wf6lWlHGcqwqzkrGVhJF2g,2292
29
29
  pgdevkit/testdb/__init__.py,sha256=tphuORo4t0QHQp_gFi6ZHXXgd32w6qUoKlaQCab6izA,366
30
- pgdevkit/testdb/_docker.py,sha256=kj_0xrGO_mxHlK83ilsB-yE0ELX1kpH22ruYrS-N3iU,1467
30
+ pgdevkit/testdb/_docker.py,sha256=-W59YBbcSIl9H_YmySRtVbOV4ImZPHOq9hxmlO-p56s,2807
31
31
  pgdevkit/testdb/api.py,sha256=13YgTz6hPcYWfsUlQJhJ7k20-W_4hzepedVaT8gWhxI,9709
32
32
  pgdevkit/testdb/config.py,sha256=NyeskDeThUgwOuxiMgHVnUOiHW-xVO9s2lJQoJcQwW4,2839
33
- pgdevkit/testdb/constants.py,sha256=oykc7xoX2R0ykhjXpqIdwkZoeHcScme8S2UPYvi_d_w,1718
34
- pgdevkit/testdb/container.py,sha256=PD2xUksIlAWXXus2VtAil_8UnwlG5DUcyUvbZx0eZTg,3123
33
+ pgdevkit/testdb/constants.py,sha256=cvdj800WnkzpXC1FZP4tmDDwwoIgZpXo36XIJngjHR0,2194
34
+ pgdevkit/testdb/container.py,sha256=_BYqs5IZmf_c5CsxLCzB1yLSSM6F0NWb1K6CujkfJ44,3641
35
35
  pgdevkit/testdb/naming.py,sha256=iHZVYdBQjHOD5sihgmZFe2qOC1djgBb6iP6R8wLUfL0,4377
36
36
  pgdevkit/testdb/query.py,sha256=iYpy_EoYIq5USfwr6pNktmDjcGi_yQx0vHvjHZNGRr0,3854
37
37
  pgdevkit/testdb/schema.py,sha256=XYBK3xyay98a5gzFW6SAQ336Jge-NhOQhildSQDOaf4,15101
38
38
  pgdevkit/testdb/mssql/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
39
39
  pgdevkit/testdb/mssql/api.py,sha256=Ah95QTsOLvtubyHxRWDywjX9NCvTVgurmANLP5RY2Ck,8959
40
- pgdevkit/testdb/mssql/constants.py,sha256=7n_NcYuhabbtb18CqG_H_rRbI7Wb_qu_l4nKk3281vI,2847
41
- pgdevkit/testdb/mssql/container.py,sha256=_rG0AMDXrNwok_E60Xai0DZdRiZ6Ba1j3GaLUnSAueg,3364
40
+ pgdevkit/testdb/mssql/constants.py,sha256=qyA1FCv-2LAQ_C8OydePEXhyG_ZBPPB7vg1686xVpVU,3453
41
+ pgdevkit/testdb/mssql/container.py,sha256=sqJPAuhN1by3kZZ96gLet4pRt3uJmUMO6Kkmvjydg1w,3791
42
42
  pgdevkit/docs/database-layout.md,sha256=j9N8GuT63D2oVf9rIOEv8GTWs4yTtjZTGH-zM5Wrg5o,8163
43
43
  pgdevkit/skills/pgdevkit/SKILL.md,sha256=ADqEaQtMJPUUAiieqokzUUmIf27bWWJNB1ZvsfgiKAE,12205
44
44
  pgdevkit/skills/pgdevkit/references/dynamic-sql.md,sha256=aTR1sx81moiTwaMaiOecaqk-9rT8y5Zxz7W1BoC3vK0,2774
45
45
  pgdevkit/skills/pgdevkit/references/temporal-tables.md,sha256=LwZnItvQWI7bB0oRHxIfUc0ynnq7N0V5Mnv-QHvFN3Q,1364
46
- pgdevkit-0.7.2.dist-info/METADATA,sha256=6LQs_7tl-vQ-I8WnTSpl7uRIesuDropRBTL8fcE22GE,20367
47
- pgdevkit-0.7.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
48
- pgdevkit-0.7.2.dist-info/entry_points.txt,sha256=FK2P55lf0WaGlOZJIINOGgCt8WB7ep0OnPsgnrp80bg,42
49
- pgdevkit-0.7.2.dist-info/RECORD,,
46
+ pgdevkit-0.9.0.dist-info/METADATA,sha256=wmx2V7kjwyFEgTNZZhngSM8JnigCShnKc65XQIdhaa0,21824
47
+ pgdevkit-0.9.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
48
+ pgdevkit-0.9.0.dist-info/entry_points.txt,sha256=FK2P55lf0WaGlOZJIINOGgCt8WB7ep0OnPsgnrp80bg,42
49
+ pgdevkit-0.9.0.dist-info/RECORD,,