Adding a concrete version of the pattern above.
Postgres ties ownership to the role's OID, not its name, so a recreated service principal role never gets its old tables back automatically. The fix is not to let individual service principals own anything.
Create a NOLOGIN owner role, have Alembic SET ROLE to it so every table it creates is owned by that role, and grant membership to whichever SP is current. Recovery after a recreate then takes one GRANT, with no ownership repair.
One-time bootstrap (run as an admin role, idempotent)
CREATE EXTENSION IF NOT EXISTS databricks_auth;
SELECT databricks_create_role('<app-sp-application-id>', 'SERVICE_PRINCIPAL'); -- skip if the app resource already created it
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_owner') THEN
CREATE ROLE app_owner NOLOGIN;
END IF;
END $$;
CREATE SCHEMA IF NOT EXISTS app AUTHORIZATION app_owner;
GRANT app_owner TO "<app-sp-application-id>";
-- one-off: move anything the SP already owns
REASSIGN OWNED BY "<app-sp-application-id>" TO app_owner;
Keep the synced tables in their respective schema, owned by the sync SP. If the app only reads them, give it GRANT USAGE + SELECT there. Add ALTER DEFAULT PRIVILEGES FOR ROLE "<sync-sp>" IN SCHEMA synced GRANT SELECT ON TABLES TO app_owner; so tables the sync job creates later are readable too.
Alembic env.py
from sqlalchemy import text
with connectable.connect() as connection:
connection.execute(text("SET ROLE app_owner")) # objects created are owned by app_owner
connection.execute(text("SET search_path TO app"))
context.configure(connection=connection, target_metadata=target_metadata,
version_table_schema="app") # autogenerate stays out of the synced schema
with context.begin_transaction():
context.run_migrations()
Keeping it outside the DAB lifecycle
Put the bootstrap SQL in a job task (or a step in your CD pipeline) that runs after databricks bundle deploy, connecting as a Lakebase admin rather than the app SP. Because every statement is idempotent, running it on every deploy is safe. If the app or its resource is recreated with a new SP, the only change is GRANT app_owner TO "<new-sp-id>". To detect drift, the same job can fail on:
SELECT schemaname, tablename, tableowner FROM pg_tables
WHERE schemaname = 'app' AND tableowner <> 'app_owner';
Docs:
Lakebase Postgres roles https://docs.databricks.com/aws/en/oltp/instances/pg-roles ,
Apps with Lakebase https://docs.databricks.com/aws/en/dev-tools/databricks-apps/lakebase ,
and REASSIGN OWNED https://www.postgresql.org/docs/current/sql-reassign-owned.html