pgdevkit 0.8.0__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.
Files changed (130) hide show
  1. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/PKG-INFO +1 -1
  2. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/migrate.py +42 -5
  3. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pyproject.toml +1 -1
  4. pgdevkit-0.9.0/tests/test_migrate_grants.py +91 -0
  5. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/uv.lock +1 -1
  6. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/.github/workflows/auto-release.yml +0 -0
  7. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/.github/workflows/python-publish.yml +0 -0
  8. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/.github/workflows/python-test.yml +0 -0
  9. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/.gitignore +0 -0
  10. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/.python-version +0 -0
  11. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/README.md +0 -0
  12. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/docs/database-layout.md +0 -0
  13. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/__init__.py +0 -0
  14. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/areas.py +0 -0
  15. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/backends/__init__.py +0 -0
  16. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/backends/base.py +0 -0
  17. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/backends/mssql.py +0 -0
  18. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/backends/postgres.py +0 -0
  19. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/cli.py +0 -0
  20. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/connection.py +0 -0
  21. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/__init__.py +0 -0
  22. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/complex_types.py +0 -0
  23. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/connection.py +0 -0
  24. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/crud.py +0 -0
  25. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/loader.py +0 -0
  26. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/model.py +0 -0
  27. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/mssql_crud.py +0 -0
  28. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/db/mssql_sql.py +0 -0
  29. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/dialect.py +0 -0
  30. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/diff.py +0 -0
  31. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/envtag.py +0 -0
  32. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/fetch_missing.py +0 -0
  33. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/introspect.py +0 -0
  34. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/lakebase.py +0 -0
  35. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/models.py +0 -0
  36. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/mssql_introspect.py +0 -0
  37. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/parser.py +0 -0
  38. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/schemas.py +0 -0
  39. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/sql_text.py +0 -0
  40. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/__init__.py +0 -0
  41. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/_docker.py +0 -0
  42. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/api.py +0 -0
  43. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/config.py +0 -0
  44. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/constants.py +0 -0
  45. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/container.py +0 -0
  46. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/__init__.py +0 -0
  47. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/api.py +0 -0
  48. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/constants.py +0 -0
  49. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/mssql/container.py +0 -0
  50. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/naming.py +0 -0
  51. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/query.py +0 -0
  52. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/pgdevkit/testdb/schema.py +0 -0
  53. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/skills/pgdevkit/SKILL.md +0 -0
  54. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/skills/pgdevkit/references/dynamic-sql.md +0 -0
  55. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/skills/pgdevkit/references/temporal-tables.md +0 -0
  56. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/__init__.py +0 -0
  57. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/conftest.py +0 -0
  58. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/db/__init__.py +0 -0
  59. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/db/test_complex_types.py +0 -0
  60. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/db/test_connection.py +0 -0
  61. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/db/test_crud.py +0 -0
  62. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/db/test_loader.py +0 -0
  63. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/db/test_mssql_crud_live.py +0 -0
  64. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/db/test_mssql_crud_sql.py +0 -0
  65. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/fixtures/01_schema.sql +0 -0
  66. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/fixtures/02_types.sql +0 -0
  67. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/fixtures/03_tables.sql +0 -0
  68. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/fixtures/04_views.sql +0 -0
  69. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/fixtures/05_functions.sql +0 -0
  70. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/fixtures/06_indexes.sql +0 -0
  71. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_areas.py +0 -0
  72. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_cli_compare.py +0 -0
  73. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_compare.py +0 -0
  74. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_compare_mssql_live.py +0 -0
  75. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_connection.py +0 -0
  76. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_dialect.py +0 -0
  77. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_diff_mssql.py +0 -0
  78. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_envtag.py +0 -0
  79. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_fetch_missing.py +0 -0
  80. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_fetch_missing_cli.py +0 -0
  81. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_lakebase.py +0 -0
  82. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_migrate.py +0 -0
  83. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_migrate_areas.py +0 -0
  84. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_migrate_env.py +0 -0
  85. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_migrate_schemas.py +0 -0
  86. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_mssql_introspect.py +0 -0
  87. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_parser_areas.py +0 -0
  88. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_parser_mssql.py +0 -0
  89. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_parser_schemas.py +0 -0
  90. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/test_schemas.py +0 -0
  91. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/__init__.py +0 -0
  92. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/conftest.py +0 -0
  93. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
  94. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/event.sql +0 -0
  95. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/event_kind.sql +0 -0
  96. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
  97. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
  98. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql +0 -0
  99. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget.init.sql +0 -0
  100. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
  101. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
  102. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
  103. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
  104. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
  105. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
  106. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
  107. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
  108. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/permissions/grants.sql +0 -0
  109. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database/schema/app.sql +0 -0
  110. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
  111. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
  112. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
  113. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
  114. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
  115. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_api.py +0 -0
  116. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_api_mssql.py +0 -0
  117. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_api_mssql_live.py +0 -0
  118. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_cli.py +0 -0
  119. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_config.py +0 -0
  120. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_config_mssql.py +0 -0
  121. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_constants.py +0 -0
  122. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_container.py +0 -0
  123. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_container_mssql.py +0 -0
  124. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_docker.py +0 -0
  125. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_mssql_constants.py +0 -0
  126. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_naming.py +0 -0
  127. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_query.py +0 -0
  128. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_schema.py +0 -0
  129. {pgdevkit-0.8.0 → pgdevkit-0.9.0}/tests/testdb/test_schema_filtering.py +0 -0
  130. {pgdevkit-0.8.0 → 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.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
@@ -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.
@@ -11,7 +11,7 @@ packages = ["pgdevkit"]
11
11
 
12
12
  [project]
13
13
  name = "pgdevkit"
14
- version = "0.8.0"
14
+ version = "0.9.0"
15
15
  description = "A helper for developing with Postgres"
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.14"
@@ -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"]) == []
@@ -313,7 +313,7 @@ wheels = [
313
313
 
314
314
  [[package]]
315
315
  name = "pgdevkit"
316
- version = "0.8.0"
316
+ version = "0.9.0"
317
317
  source = { editable = "." }
318
318
  dependencies = [
319
319
  { name = "docker" },
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