From 41a03e834d664f3cac60c0bb770bb0b063995ba6 Mon Sep 17 00:00:00 2001 From: Utkarash Singh Date: Fri, 18 Sep 2026 16:34:32 +0100 Subject: [PATCH 1/4] rescope pg_graphql and pg_cron event triggers during pg_upgrade Old projects still have these event triggers scoped to CREATE FUNCTION / CREATE SCHEMA while the fleet-updated grant functions only act on CREATE EXTENSION events, so the post-upgrade extension recreation never re-applies their wiring. Rescope in initiate.sh before the extensions are dropped (the fixed triggers travel through pg_upgrade) and again in complete.sh before the generated SQL runs, as a safety net. --- .../pg_upgrade_scripts/common.sh | 52 +++++++++++++++++++ .../pg_upgrade_scripts/complete.sh | 11 ++++ .../pg_upgrade_scripts/initiate.sh | 5 ++ tests/pg_upgrade/tests/01-schema.sql | 15 +++++- tests/pg_upgrade/tests/99-fixtures.sql | 16 ++++++ 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh index 297a234bad..f0baf213b0 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh @@ -69,6 +69,58 @@ function run_sql { psql -h localhost -U supabase_admin -d postgres "$@" } +# Old projects still have grant-access event triggers scoped to the tags their +# original seed used (issue_pg_graphql_access on CREATE FUNCTION before build +# 17.6.1.111 / 15.14.1.111, issue_pg_cron_access on CREATE SCHEMA before +# 15.1.0.130), while the grant functions themselves (updated fleet-wide) only +# act on CREATE EXTENSION events. With that mismatch, recreating the extension +# never re-applies its wiring (graphql_public.graphql wrapper, cron grants). +# Rescope the triggers so the CREATE EXTENSION statements that re-enable the +# extensions after pg_upgrade fire them. Trigger definitions kept identical to +# migrations 20260421000001_rescope_pg_graphql_access_trigger.sql and +# 20231020085357_revoke_writes_on_cron_job_from_postgres.sql; no-op on +# projects that already have the CREATE EXTENSION scope. +function rescope_extension_event_triggers { + local sql + sql=$( + cat <<'EOF' +do $rescope$ +begin + if exists ( + select 1 + from pg_proc p + join pg_namespace n on p.pronamespace = n.oid + where n.nspname = 'extensions' and p.proname = 'grant_pg_graphql_access' + ) then + execute 'drop event trigger if exists issue_pg_graphql_access'; + execute $q$ + create event trigger issue_pg_graphql_access + on ddl_command_end + when tag in ('CREATE EXTENSION') + execute procedure extensions.grant_pg_graphql_access() + $q$; + end if; + + if exists ( + select 1 + from pg_proc p + join pg_namespace n on p.pronamespace = n.oid + where n.nspname = 'extensions' and p.proname = 'grant_pg_cron_access' + ) then + execute 'drop event trigger if exists issue_pg_cron_access'; + execute $q$ + create event trigger issue_pg_cron_access + on ddl_command_end + when tag in ('CREATE EXTENSION') + execute procedure extensions.grant_pg_cron_access() + $q$; + end if; +end $rescope$; +EOF + ) + run_sql -c "$sql" +} + # Wrap a db name in dbname='...' (escaping \ and ') so characters special to -d # parsing (=, spaces, quotes) stay part of a literal name, not a conninfo fragment. # psql parses a -d value containing '=' as a full conninfo string, so a customer diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh index add5ac232a..97d39a0260 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh @@ -332,6 +332,17 @@ function complete_pg_upgrade { log "3.1. Patch Wrappers server options" execute_wrappers_patch + # Must run before run_generated_sql (which recreates pg_graphql) and + # execute_patches (which recreates pg_cron): the triggers only re-apply the + # extension wiring if they are already scoped to CREATE EXTENSION when the + # recreate happens. Normally initiate.sh has already rescoped on the source + # cluster; this is a safety net in case it ran from an older script bundle. + log "3.2. Rescoping extension event triggers" + rescope_extension_event_triggers || { + log "WARNING: failed to rescope extension event triggers" + warnings=1 + } + log "4. Running generated SQL files" # Deliberately fail-soft per file (a failed ALTER EXTENSION UPDATE shouldn't fail the upgrade) so it never returns non-zero — a retry wrapper here would be dead code, and re-running would make already-applied updates error spuriously run_generated_sql diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh index ccbe7503eb..10b9ea2231 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh @@ -204,6 +204,11 @@ ALTER SYSTEM SET jit = off; SELECT pg_reload_conf(); EOF + # Rescope before dropping extensions: the fixed triggers are carried into the new + # cluster by pg_upgrade, so both the post-upgrade re-enable and the failure-path + # re-enable below fire them. Fail-soft: a broken rescope should not block the upgrade. + rescope_extension_event_triggers || log "WARNING: failed to rescope extension event triggers" + # Disable extensions if they're enabled # Generate SQL script to re-enable them after upgrade for EXTENSION in "${EXTENSIONS_TO_DISABLE[@]}"; do diff --git a/tests/pg_upgrade/tests/01-schema.sql b/tests/pg_upgrade/tests/01-schema.sql index 3cf3a83ddf..8b03dc64f8 100644 --- a/tests/pg_upgrade/tests/01-schema.sql +++ b/tests/pg_upgrade/tests/01-schema.sql @@ -1,7 +1,7 @@ CREATE EXTENSION IF NOT EXISTS pgtap; BEGIN; -SELECT plan(15); +SELECT plan(18); select has_schema('public'); select has_schema('auth'); @@ -22,5 +22,18 @@ SELECT has_column('public', 'countries', 'continent', 'Column continent should e SELECT has_materialized_view('public', 'european_countries', 'Materialized view european_countries should exist'); +SELECT is( + (SELECT evttags FROM pg_event_trigger WHERE evtname = 'issue_pg_graphql_access'), + ARRAY['CREATE EXTENSION']::text[], + 'issue_pg_graphql_access should be rescoped to CREATE EXTENSION by the upgrade'); +SELECT is( + (SELECT evttags FROM pg_event_trigger WHERE evtname = 'issue_pg_cron_access'), + ARRAY['CREATE EXTENSION']::text[], + 'issue_pg_cron_access should be rescoped to CREATE EXTENSION by the upgrade'); +SELECT ok( + (SELECT prosrc LIKE '%graphql.resolve%' FROM pg_proc + WHERE proname = 'graphql' AND pronamespace = 'graphql_public'::regnamespace), + 'graphql_public.graphql should be the real wrapper after upgrade, not the placeholder'); + SELECT * FROM finish(); ROLLBACK; diff --git a/tests/pg_upgrade/tests/99-fixtures.sql b/tests/pg_upgrade/tests/99-fixtures.sql index 2b93d459a2..5e5315ea45 100644 --- a/tests/pg_upgrade/tests/99-fixtures.sql +++ b/tests/pg_upgrade/tests/99-fixtures.sql @@ -10,3 +10,19 @@ with no data; refresh materialized view public.european_countries; select count(*) from public.european_countries; + +-- simulate a project created before the trigger rescope migrations: the +-- fleet-wide function updates left these projects with triggers still scoped +-- to their original tags, so recreating the extensions no longer re-applies +-- their wiring (graphql_public.graphql wrapper, cron grants). The upgrade +-- must rescope both to CREATE EXTENSION. +drop event trigger if exists issue_pg_graphql_access; +create event trigger issue_pg_graphql_access + on ddl_command_end + when tag in ('CREATE FUNCTION') + execute procedure extensions.grant_pg_graphql_access(); +drop event trigger if exists issue_pg_cron_access; +create event trigger issue_pg_cron_access + on ddl_command_end + when tag in ('CREATE SCHEMA') + execute procedure extensions.grant_pg_cron_access(); From 7bec197fd2eaa8c5c6c5c59ec5246d5b1c43e541 Mon Sep 17 00:00:00 2001 From: Utkarash Singh Date: Tue, 22 Sep 2026 15:29:30 +0100 Subject: [PATCH 2/4] drop the complete.sh rescope; retry the initiate.sh one instead Both instances download the same target-version script bundle, so the older-bundle scenario the complete.sh safety net guarded against doesn't occur in practice. Keep the single rescope in initiate.sh and wrap it in retry, since it is fail-soft and a skipped rescope silently loses the extension wiring. --- .../admin_api_scripts/pg_upgrade_scripts/complete.sh | 11 ----------- .../admin_api_scripts/pg_upgrade_scripts/initiate.sh | 6 ++++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh index 97d39a0260..add5ac232a 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/complete.sh @@ -332,17 +332,6 @@ function complete_pg_upgrade { log "3.1. Patch Wrappers server options" execute_wrappers_patch - # Must run before run_generated_sql (which recreates pg_graphql) and - # execute_patches (which recreates pg_cron): the triggers only re-apply the - # extension wiring if they are already scoped to CREATE EXTENSION when the - # recreate happens. Normally initiate.sh has already rescoped on the source - # cluster; this is a safety net in case it ran from an older script bundle. - log "3.2. Rescoping extension event triggers" - rescope_extension_event_triggers || { - log "WARNING: failed to rescope extension event triggers" - warnings=1 - } - log "4. Running generated SQL files" # Deliberately fail-soft per file (a failed ALTER EXTENSION UPDATE shouldn't fail the upgrade) so it never returns non-zero — a retry wrapper here would be dead code, and re-running would make already-applied updates error spuriously run_generated_sql diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh index 10b9ea2231..7bc6eaebe1 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/initiate.sh @@ -206,8 +206,10 @@ EOF # Rescope before dropping extensions: the fixed triggers are carried into the new # cluster by pg_upgrade, so both the post-upgrade re-enable and the failure-path - # re-enable below fire them. Fail-soft: a broken rescope should not block the upgrade. - rescope_extension_event_triggers || log "WARNING: failed to rescope extension event triggers" + # re-enable below fire them. Fail-soft: a broken rescope should not block the + # upgrade — but retry first, since a skipped rescope means the recreation of + # these extensions silently loses their wiring. + retry 3 rescope_extension_event_triggers || log "WARNING: failed to rescope extension event triggers" # Disable extensions if they're enabled # Generate SQL script to re-enable them after upgrade From d05547b0f0444d3b5e1330910545674f42ce7e66 Mon Sep 17 00:00:00 2001 From: Utkarash Singh Date: Tue, 22 Sep 2026 16:18:10 +0100 Subject: [PATCH 3/4] note when the trigger rescope can be removed --- ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh index f0baf213b0..7229be3540 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh @@ -80,6 +80,9 @@ function run_sql { # migrations 20260421000001_rescope_pg_graphql_access_trigger.sql and # 20231020085357_revoke_writes_on_cron_job_from_postgres.sql; no-op on # projects that already have the CREATE EXTENSION scope. +# Drop this once no supported upgrade source predates 17.6.1.111 / +# 15.14.1.111 — from then on every project already has the CREATE +# EXTENSION triggers and this is dead code. function rescope_extension_event_triggers { local sql sql=$( From 66ba180400f65634543db454b28fc3d2c48b1889 Mon Sep 17 00:00:00 2001 From: Utkarash Singh Date: Tue, 22 Sep 2026 17:26:41 +0100 Subject: [PATCH 4/4] review: TODO-style comment, tab-indented heredoc --- .../pg_upgrade_scripts/common.sh | 101 +++++++++--------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh index 7229be3540..1b25ec875c 100755 --- a/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh +++ b/ansible/files/admin_api_scripts/pg_upgrade_scripts/common.sh @@ -69,57 +69,62 @@ function run_sql { psql -h localhost -U supabase_admin -d postgres "$@" } -# Old projects still have grant-access event triggers scoped to the tags their -# original seed used (issue_pg_graphql_access on CREATE FUNCTION before build -# 17.6.1.111 / 15.14.1.111, issue_pg_cron_access on CREATE SCHEMA before -# 15.1.0.130), while the grant functions themselves (updated fleet-wide) only -# act on CREATE EXTENSION events. With that mismatch, recreating the extension -# never re-applies its wiring (graphql_public.graphql wrapper, cron grants). -# Rescope the triggers so the CREATE EXTENSION statements that re-enable the -# extensions after pg_upgrade fire them. Trigger definitions kept identical to -# migrations 20260421000001_rescope_pg_graphql_access_trigger.sql and -# 20231020085357_revoke_writes_on_cron_job_from_postgres.sql; no-op on -# projects that already have the CREATE EXTENSION scope. -# Drop this once no supported upgrade source predates 17.6.1.111 / -# 15.14.1.111 — from then on every project already has the CREATE -# EXTENSION triggers and this is dead code. +# TODO: Drop once all projects have been upgraded past 17.6.1.111, 15.14.1.111 +# +# Old projects still have grant-access event triggers scoped to the tags +# their original seed used while the grant functions themselves (updated +# fleet-wide) only act on CREATE EXTENSION events. +# +# * issue_pg_graphql_access on CREATE FUNCTION before build 17.6.1.111 / +# 15.14.1.111 +# * issue_pg_cron_access on CREATE SCHEMA before 15.1.0.130 +# +# With this mismatch, recreating the extension never re-applies its wiring +# (graphql_public.graphql wrapper, cron grants). Rescope the triggers so +# the CREATE EXTENSION statements that re-enable the extensions after +# pg_upgrade fire them. +# +# Trigger definitions kept identical to migrations +# 20260421000001_rescope_pg_graphql_access_trigger.sql and +# 20231020085357_revoke_writes_on_cron_job_from_postgres.sql. This is a +# no-op on projects that already have the CREATE EXTENSION scope. function rescope_extension_event_triggers { local sql sql=$( - cat <<'EOF' -do $rescope$ -begin - if exists ( - select 1 - from pg_proc p - join pg_namespace n on p.pronamespace = n.oid - where n.nspname = 'extensions' and p.proname = 'grant_pg_graphql_access' - ) then - execute 'drop event trigger if exists issue_pg_graphql_access'; - execute $q$ - create event trigger issue_pg_graphql_access - on ddl_command_end - when tag in ('CREATE EXTENSION') - execute procedure extensions.grant_pg_graphql_access() - $q$; - end if; - - if exists ( - select 1 - from pg_proc p - join pg_namespace n on p.pronamespace = n.oid - where n.nspname = 'extensions' and p.proname = 'grant_pg_cron_access' - ) then - execute 'drop event trigger if exists issue_pg_cron_access'; - execute $q$ - create event trigger issue_pg_cron_access - on ddl_command_end - when tag in ('CREATE EXTENSION') - execute procedure extensions.grant_pg_cron_access() - $q$; - end if; -end $rescope$; -EOF + cat <<-'EOF' + do $rescope$ + begin + if exists ( + select 1 + from pg_proc p + join pg_namespace n on p.pronamespace = n.oid + where n.nspname = 'extensions' and p.proname = 'grant_pg_graphql_access' + ) then + execute 'drop event trigger if exists issue_pg_graphql_access'; + execute $q$ + create event trigger issue_pg_graphql_access + on ddl_command_end + when tag in ('CREATE EXTENSION') + execute procedure extensions.grant_pg_graphql_access() + $q$; + end if; + + if exists ( + select 1 + from pg_proc p + join pg_namespace n on p.pronamespace = n.oid + where n.nspname = 'extensions' and p.proname = 'grant_pg_cron_access' + ) then + execute 'drop event trigger if exists issue_pg_cron_access'; + execute $q$ + create event trigger issue_pg_cron_access + on ddl_command_end + when tag in ('CREATE EXTENSION') + execute procedure extensions.grant_pg_cron_access() + $q$; + end if; + end $rescope$; + EOF ) run_sql -c "$sql" }