pgdevkit 0.8.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pgdevkit
3
- Version: 0.8.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
@@ -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
@@ -43,7 +43,7 @@ pgdevkit/docs/database-layout.md,sha256=j9N8GuT63D2oVf9rIOEv8GTWs4yTtjZTGH-zM5Wr
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.8.0.dist-info/METADATA,sha256=vLpobYq-Amjixm5ZHz8041Syn8y6_41Wu6j3wmhbGUs,21824
47
- pgdevkit-0.8.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
48
- pgdevkit-0.8.0.dist-info/entry_points.txt,sha256=FK2P55lf0WaGlOZJIINOGgCt8WB7ep0OnPsgnrp80bg,42
49
- pgdevkit-0.8.0.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,,