Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,32 @@ for a real one.
functions the CLI calls, so a project can script around them without shelling
out.

### Guarding against missing GRANTs in a production apply path

A migration that creates a table doesn't automatically get that table granted
to your app's runtime role — see `docs/database-layout.md`'s "permissions
files always apply last" section for why a one-time `permissions`/`grants`
file running through a forward-only migration path (unlike `pgdb testdb`,
which re-derives the whole schema every time) can silently stop covering new
tables. Three functions in `pgdevkit.migrate` exist for building your own
guard around this:

- `execute_sql_script(conninfo, sql)` — runs a raw SQL script as one committed
transaction, split the same statement-boundary-safe way `apply_migration`
is, but with **no** tracking-table bookkeeping. For a script meant to
re-run every time (e.g. an idempotent `GRANT ... ON ALL TABLES IN SCHEMA x
TO role`), call this directly instead of routing it through
`apply_migration`'s once-ever tracked-migration path.
- `created_table_names(sql)` — table names any `CREATE TABLE` in a raw script
targets, for when you ran it through `execute_sql_script` (which returns
nothing) rather than `apply_migration` (whose `ApplyResult.verified_tables`
already gives you this).
- `missing_privileges(conninfo, role, tables, privilege="select")` — checks
`has_table_privilege` for `role` against each of `tables`, returning the
ones it can't access. Run it after applying a migration (or an
`execute_sql_script` grants sync) to confirm the grant actually landed,
instead of finding out from a production 500.

## `pgdevkit.db` — helpers for application code

Install with the `db` extra: `pip install pgdevkit[db]`.
Expand Down
21 changes: 21 additions & 0 deletions docs/database-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ back unconditionally and applies it only once the whole rest of the tree
`permissions` files as if every table/view they reference is guaranteed to
already exist, because it is.

**That guarantee is specific to `pgdb testdb` — a production apply path built
on `pgdevkit.migrate` doesn't get it for free.** `pgdb testdb`/`ensure_testdb()`
re-derive the whole schema from scratch every time and can afford to hold
`permissions` back until everything else settles. A production migration
runner is forward-only: it runs each file from `migrations`/
`_migration_scripts` exactly once, ever, as whatever shared role the runner
itself connects as. If your `permissions`/`grants` file goes through that
path as an ordinary one-time migration, it covers whatever tables existed the
one time it ran — full stop. A table added by any later migration, by any
migrant runner or connecting role, silently never gets the grant, and the
first symptom is a production `InsufficientPrivilege` 500 on that table.
Two `pgdevkit.migrate` functions exist specifically to close that gap in your
own production-apply tooling: `execute_sql_script(conninfo, sql)` runs an
idempotent script (e.g. that same blanket `GRANT ... ON ALL TABLES IN SCHEMA`)
outside the tracked/forward-only flow, so your tooling can re-run it
unconditionally on every deploy instead of once; `missing_privileges(conninfo,
role, tables)` (paired with `created_table_names(sql)` for a raw script, or
`ApplyResult.verified_tables` from `apply_migration`) checks whether a role
actually has the access your `permissions` file was supposed to grant it, so
a gap surfaces immediately instead of as a later outage.

---

## File-naming conventions
Expand Down
Loading