From 1af6467a3ee524fd27ebfeed1c92e793a01bbc5d Mon Sep 17 00:00:00 2001 From: Steven Eubank Date: Mon, 31 Aug 2026 12:29:54 +0200 Subject: [PATCH] feat/autovacuum-disabled-lint This is adding a new info level advisor which is a precursor and compliment to the prevent the table_bloat warning advisors This is not always required. This would be a good candidate for a conditional lint. Which we would only run in certain scenarios but as is, it is a pretty low invasive level check and fine for --- bin/installcheck | 2 +- docs/0030_autovacuum_disabled.md | 30 +++++++++++++++++ lints/0030_autovacuum_disabled.sql | 36 ++++++++++++++++++++ mkdocs.yaml | 1 + splinter.sql | 38 +++++++++++++++++++++- test/expected/0030_autovacuum_disabled.out | 37 +++++++++++++++++++++ test/expected/queries_are_unionable.out | 4 ++- test/sql/0030_autovacuum_disabled.sql | 29 +++++++++++++++++ test/sql/queries_are_unionable.sql | 4 ++- 9 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 docs/0030_autovacuum_disabled.md create mode 100644 lints/0030_autovacuum_disabled.sql create mode 100644 test/expected/0030_autovacuum_disabled.out create mode 100644 test/sql/0030_autovacuum_disabled.sql diff --git a/bin/installcheck b/bin/installcheck index b54f633..659aa2f 100755 --- a/bin/installcheck +++ b/bin/installcheck @@ -52,7 +52,7 @@ else fi # Execute the test fixtures -psql -v ON_ERROR_STOP= -f test/fixtures.sql -f lints/0001*.sql -f lints/0002*.sql -f lints/0003*.sql -f lints/0004*.sql -f lints/0005*.sql -f lints/0006*.sql -f lints/0007*.sql -f lints/0008*.sql -f lints/0009*.sql -f lints/0010*.sql -f lints/0011*.sql -f lints/0013*.sql -f lints/0014*.sql -f lints/0015*.sql -f lints/0016*.sql -f lints/0017*.sql -f lints/0018*.sql -f lints/0019*.sql -f lints/0020*.sql -f lints/0021*.sql -f lints/0022*.sql -f lints/0023*.sql -f lints/0024*.sql -f lints/0025*.sql -f lints/0026*.sql -f lints/0027*.sql -f lints/0028*.sql -f lints/0029*.sql -d contrib_regression +psql -v ON_ERROR_STOP= -f test/fixtures.sql -f lints/0001*.sql -f lints/0002*.sql -f lints/0003*.sql -f lints/0004*.sql -f lints/0005*.sql -f lints/0006*.sql -f lints/0007*.sql -f lints/0008*.sql -f lints/0009*.sql -f lints/0010*.sql -f lints/0011*.sql -f lints/0013*.sql -f lints/0014*.sql -f lints/0015*.sql -f lints/0016*.sql -f lints/0017*.sql -f lints/0018*.sql -f lints/0019*.sql -f lints/0020*.sql -f lints/0021*.sql -f lints/0022*.sql -f lints/0023*.sql -f lints/0024*.sql -f lints/0025*.sql -f lints/0026*.sql -f lints/0027*.sql -f lints/0028*.sql -f lints/0029*.sql -f lints/0030*.sql -d contrib_regression # Run tests ${REGRESS} --use-existing --dbname=contrib_regression --inputdir=${TESTDIR} ${TESTS} diff --git a/docs/0030_autovacuum_disabled.md b/docs/0030_autovacuum_disabled.md new file mode 100644 index 0000000..33e8efd --- /dev/null +++ b/docs/0030_autovacuum_disabled.md @@ -0,0 +1,30 @@ +**Level:** INFO + +**Summary:** Detects tables where `autovacuum_enabled=false` has been set as a storage parameter. + +**Ramification:** Dead tuples accumulate without bound, causing table bloat that degrades query performance and increases storage costs. The effect compounds after any UPDATE or DELETE workload. + +--- + +### Rationale + +PostgreSQL autovacuum reclaims space from dead tuples left by UPDATE and DELETE operations. Disabling it at the table level (`ALTER TABLE t SET (autovacuum_enabled = false)`) prevents this cleanup entirely for that table, regardless of the cluster-level autovacuum setting. + +### How to Resolve + +**Re-enable autovacuum and reclaim existing dead tuples immediately:** + +```sql +ALTER TABLE public.orders RESET (autovacuum_enabled); +VACUUM ANALYZE public.orders; +``` + +### False Positives + +This lint may fire when the setting is intentional: + +- **Read-only archive tables** — no UPDATEs or DELETEs means no dead tuples; autovacuum has nothing to do. +- **Bulk-load staging tables** — autovacuum is temporarily disabled to avoid I/O contention during ETL; should be re-enabled after the load completes. +- **Manual vacuum schedules** — tables vacuumed explicitly via `pg_cron` or another scheduler; autovacuum is disabled to avoid conflicts with the scheduled job. + +In these cases the lint can be safely ignored, but verify the table is not accumulating dead tuples via `pg_stat_user_tables.n_dead_tup`. diff --git a/lints/0030_autovacuum_disabled.sql b/lints/0030_autovacuum_disabled.sql new file mode 100644 index 0000000..aaaf07f --- /dev/null +++ b/lints/0030_autovacuum_disabled.sql @@ -0,0 +1,36 @@ +create view lint."0030_autovacuum_disabled" as +select + 'autovacuum_disabled' as name, + 'Autovacuum Disabled' as title, + 'INFO' as level, + 'EXTERNAL' as facing, + array['PERFORMANCE'] as categories, + 'Table has autovacuum_enabled=false set as a storage parameter. Without autovacuum, dead tuples accumulate and cause table bloat — see the docs for legitimate exceptions before acting.' as description, + format( + 'Table `%s`.`%s` has autovacuum_enabled=false set as a storage parameter.', + nsp.nspname, + cls.relname + ) as detail, + 'https://supabase.com/docs/guides/database/database-linter?lint=0030_autovacuum_disabled' as remediation, + jsonb_build_object( + 'schema', nsp.nspname, + 'name', cls.relname, + 'type', 'table' + ) as metadata, + format('autovacuum_disabled_%s_%s', nsp.nspname, cls.relname) as cache_key +from pg_catalog.pg_class cls +join pg_catalog.pg_namespace nsp on cls.relnamespace = nsp.oid +where + cls.relkind = 'r' + and 'autovacuum_enabled=false' = any(cls.reloptions) + and nsp.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', + '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', + 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', + 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', + 'realtime', 'repack', 'storage', 'supabase_functions', + 'supabase_migrations', 'tiger', 'topology', 'vault' + ) +order by + nsp.nspname, + cls.relname; diff --git a/mkdocs.yaml b/mkdocs.yaml index 1f5d05d..02864ac 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -37,6 +37,7 @@ nav: - Signed-In Users Can See Object in GraphQL Schema: '0027_pg_graphql_authenticated_table_exposed.md' - Public Can Execute SECURITY DEFINER Function: '0028_anon_security_definer_function_executable.md' - Signed-In Users Can Execute SECURITY DEFINER Function: '0029_authenticated_security_definer_function_executable.md' + - Autovacuum Disabled: '0030_autovacuum_disabled.md' theme: name: 'material' diff --git a/splinter.sql b/splinter.sql index 9cd66c9..eae8e33 100644 --- a/splinter.sql +++ b/splinter.sql @@ -1838,4 +1838,40 @@ from order by schema_name, function_name, - function_args) \ No newline at end of file + function_args) +union all +(select + 'autovacuum_disabled' as name, + 'Autovacuum Disabled' as title, + 'INFO' as level, + 'EXTERNAL' as facing, + array['PERFORMANCE'] as categories, + 'Table has autovacuum_enabled=false set as a storage parameter. Without autovacuum, dead tuples accumulate and cause table bloat — see the docs for legitimate exceptions before acting.' as description, + format( + 'Table `%s`.`%s` has autovacuum_enabled=false set as a storage parameter.', + nsp.nspname, + cls.relname + ) as detail, + 'https://supabase.com/docs/guides/database/database-linter?lint=0030_autovacuum_disabled' as remediation, + jsonb_build_object( + 'schema', nsp.nspname, + 'name', cls.relname, + 'type', 'table' + ) as metadata, + format('autovacuum_disabled_%s_%s', nsp.nspname, cls.relname) as cache_key +from pg_catalog.pg_class cls +join pg_catalog.pg_namespace nsp on cls.relnamespace = nsp.oid +where + cls.relkind = 'r' + and 'autovacuum_enabled=false' = any(cls.reloptions) + and nsp.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', + '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', + 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', + 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', + 'realtime', 'repack', 'storage', 'supabase_functions', + 'supabase_migrations', 'tiger', 'topology', 'vault' + ) +order by + nsp.nspname, + cls.relname) \ No newline at end of file diff --git a/test/expected/0030_autovacuum_disabled.out b/test/expected/0030_autovacuum_disabled.out new file mode 100644 index 0000000..7dc6e26 --- /dev/null +++ b/test/expected/0030_autovacuum_disabled.out @@ -0,0 +1,37 @@ +begin; + set local search_path = ''; + -- BASELINE: no user tables, expect 0 rows + select * from lint."0030_autovacuum_disabled"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- +(0 rows) + + savepoint a; + -- NEGATIVE: autovacuum_enabled=true explicitly set — must not fire + create table public.active_table (id int); + alter table public.active_table set (autovacuum_enabled = true); + select * from lint."0030_autovacuum_disabled"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- +(0 rows) + + rollback to savepoint a; + savepoint b; + -- POSITIVE: autovacuum_enabled=false explicitly set — must fire + create table public.orders (id int); + alter table public.orders set (autovacuum_enabled = false); + select name, detail, cache_key from lint."0030_autovacuum_disabled"; + name | detail | cache_key +---------------------+----------------------------------------------------------------------------------+----------------------------------- + autovacuum_disabled | Table `public`.`orders` has autovacuum_enabled=false set as a storage parameter. | autovacuum_disabled_public_orders +(1 row) + + -- RESOLUTION: reset the storage parameter to re-enable autovacuum + alter table public.orders reset (autovacuum_enabled); + select * from lint."0030_autovacuum_disabled"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- +(0 rows) + + rollback to savepoint b; +rollback; diff --git a/test/expected/queries_are_unionable.out b/test/expected/queries_are_unionable.out index 3b1005d..058f2dd 100644 --- a/test/expected/queries_are_unionable.out +++ b/test/expected/queries_are_unionable.out @@ -54,7 +54,9 @@ begin; union all select * from lint."0028_anon_security_definer_function_executable" union all - select * from lint."0029_authenticated_security_definer_function_executable"; + select * from lint."0029_authenticated_security_definer_function_executable" + union all + select * from lint."0030_autovacuum_disabled"; name | title | level | facing | categories | description | detail | remediation | metadata | cache_key ------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- (0 rows) diff --git a/test/sql/0030_autovacuum_disabled.sql b/test/sql/0030_autovacuum_disabled.sql new file mode 100644 index 0000000..8a6cc6e --- /dev/null +++ b/test/sql/0030_autovacuum_disabled.sql @@ -0,0 +1,29 @@ +begin; + set local search_path = ''; + + -- BASELINE: no user tables, expect 0 rows + select * from lint."0030_autovacuum_disabled"; + + savepoint a; + + -- NEGATIVE: autovacuum_enabled=true explicitly set — must not fire + create table public.active_table (id int); + alter table public.active_table set (autovacuum_enabled = true); + select * from lint."0030_autovacuum_disabled"; + + rollback to savepoint a; + + savepoint b; + + -- POSITIVE: autovacuum_enabled=false explicitly set — must fire + create table public.orders (id int); + alter table public.orders set (autovacuum_enabled = false); + select name, detail, cache_key from lint."0030_autovacuum_disabled"; + + -- RESOLUTION: reset the storage parameter to re-enable autovacuum + alter table public.orders reset (autovacuum_enabled); + select * from lint."0030_autovacuum_disabled"; + + rollback to savepoint b; + +rollback; diff --git a/test/sql/queries_are_unionable.sql b/test/sql/queries_are_unionable.sql index fe43d3d..3c5c39d 100644 --- a/test/sql/queries_are_unionable.sql +++ b/test/sql/queries_are_unionable.sql @@ -56,6 +56,8 @@ begin; union all select * from lint."0028_anon_security_definer_function_executable" union all - select * from lint."0029_authenticated_security_definer_function_executable"; + select * from lint."0029_authenticated_security_definer_function_executable" + union all + select * from lint."0030_autovacuum_disabled"; rollback;