From 64734f8f0faebee9ff4b477f45c8a06d9a902fa4 Mon Sep 17 00:00:00 2001 From: randoneering Date: Sun, 5 Apr 2026 01:38:36 +0000 Subject: [PATCH 01/26] update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6f33c39..4c4c9a1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ pgpass # AI Files CLAUDE.md - +AGENTS.md # Python __pycache__/ .pytest_cache/ From 7a7b4d4eb31fa71a145889ddb224d0bb4acb9c26 Mon Sep 17 00:00:00 2001 From: randoneering Date: Sun, 5 Apr 2026 03:26:49 +0000 Subject: [PATCH 02/26] feat: add shared_buffers INFO and HIGH health checks --- pgFirstAid.sql | 25 +++++++++++++++++++++++++ testing/pgTAP/03_high_tests.sql | 11 ++++++++++- testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 27 ++++++++++++++++++++++++++- view_pgFirstAid_managed.sql | 27 ++++++++++++++++++++++++++- 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 2262943..0a9370b 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1433,6 +1433,31 @@ order by 'Keep PostgreSQL updated and review configuration settings' as recommended_action, 'https://www.postgresql.org/docs/current/upgrading.html' as documentation_link, 5 as severity_order; +-- INFO: shared_buffers current value +insert into health_results +select + 'INFO' as severity, + 'System Health' as category, + 'shared_buffers Setting' as check_name, + 'System' as object_name, + 'Current value of shared_buffers. Recommended: ~25% of total system RAM for dedicated database servers.' as issue_description, + current_setting('shared_buffers') as current_value, + 'No action needed if already tuned. For dedicated DB servers with 8GB+ RAM, target 25% of total RAM. Changes require a PostgreSQL restart.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, + 5 as severity_order; +-- HIGH: shared_buffers still at 128MB PostgreSQL default +insert into health_results +select + 'HIGH' as severity, + 'System Health' as category, + 'shared_buffers At Default' as check_name, + 'System' as object_name, + 'shared_buffers is set to the PostgreSQL default of 128MB. On any real workload this is almost certainly too low.' as issue_description, + current_setting('shared_buffers') as current_value, + 'Set shared_buffers to approximately 25% of total system RAM (e.g., 2GB on an 8GB server). Requires a PostgreSQL restart.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, + 2 as severity_order +where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB'); -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/03_high_tests.sql b/testing/pgTAP/03_high_tests.sql index f73e7bc..51aa2fe 100644 --- a/testing/pgTAP/03_high_tests.sql +++ b/testing/pgTAP/03_high_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(44); +SELECT plan(46); SELECT ok( (SELECT count(*) >= 0 FROM pg_firstAid() WHERE check_name = 'Current Blocked/Blocking Queries'), @@ -199,5 +199,14 @@ SELECT ok( 'View executes Long Running Queries check' ); +SELECT ok( + (SELECT count(*) >= 0 FROM pg_firstAid() WHERE check_name = 'shared_buffers At Default'), + 'Function executes shared_buffers At Default check' +); +SELECT ok( + (SELECT count(*) >= 0 FROM v_pgfirstaid WHERE check_name = 'shared_buffers At Default'), + 'View executes shared_buffers At Default check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index 9314369..a506f39 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(4); +SELECT plan(6); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -14,5 +14,14 @@ SELECT ok( 'View returns non-null check names' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'shared_buffers Setting'), + 'Function executes shared_buffers Setting check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'shared_buffers Setting'), + 'View executes shared_buffers Setting check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index 22f9eab..0801750 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1393,4 +1393,29 @@ select ' as documentation_link, 5 as severity_order from - ls); + ls) +union all +-- INFO: shared_buffers current value +select + 'INFO' as severity, + 'System Health' as category, + 'shared_buffers Setting' as check_name, + 'System' as object_name, + 'Current value of shared_buffers. Recommended: ~25% of total system RAM for dedicated database servers.' as issue_description, + current_setting('shared_buffers') as current_value, + 'No action needed if already tuned. For dedicated DB servers with 8GB+ RAM, target 25% of total RAM. Changes require a PostgreSQL restart.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, + 5 as severity_order +union all +-- HIGH: shared_buffers still at 128MB PostgreSQL default +select + 'HIGH' as severity, + 'System Health' as category, + 'shared_buffers At Default' as check_name, + 'System' as object_name, + 'shared_buffers is set to the PostgreSQL default of 128MB. On any real workload this is almost certainly too low.' as issue_description, + current_setting('shared_buffers') as current_value, + 'Set shared_buffers to approximately 25% of total system RAM (e.g., 2GB on an 8GB server). Requires a PostgreSQL restart.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, + 2 as severity_order +where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB'); diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 6b2ff2d..e287aa9 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1376,4 +1376,29 @@ select For GCP Cloud SQL: https://docs.cloud.google.com/sql/docs/postgres/logging / For Azure Database for PostgreSQL: https://learn.microsoft.com/en-us/cli/azure/postgres/flexible-server/server-logs?view=azure-cli-latest ' as documentation_link, - 5 as severity_order; + 5 as severity_order +union all +-- INFO: shared_buffers current value +select + 'INFO' as severity, + 'System Health' as category, + 'shared_buffers Setting' as check_name, + 'System' as object_name, + 'Current value of shared_buffers. Recommended: ~25% of total system RAM for dedicated database servers.' as issue_description, + current_setting('shared_buffers') as current_value, + 'No action needed if already tuned. For dedicated DB servers with 8GB+ RAM, target 25% of total RAM. Changes require a PostgreSQL restart.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, + 5 as severity_order +union all +-- HIGH: shared_buffers still at 128MB PostgreSQL default +select + 'HIGH' as severity, + 'System Health' as category, + 'shared_buffers At Default' as check_name, + 'System' as object_name, + 'shared_buffers is set to the PostgreSQL default of 128MB. On any real workload this is almost certainly too low.' as issue_description, + current_setting('shared_buffers') as current_value, + 'Set shared_buffers to approximately 25% of total system RAM (e.g., 2GB on an 8GB server). Requires a PostgreSQL restart.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, + 2 as severity_order +where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB'); From 0f090b11d05edab6e45a086a89fc08d60691ab4f Mon Sep 17 00:00:00 2001 From: randoneering Date: Sun, 5 Apr 2026 04:14:16 +0000 Subject: [PATCH 03/26] feat: add work_mem INFO and MEDIUM health checks --- pgFirstAid.sql | 28 ++++++++++++++++++++++++++++ testing/pgTAP/04_medium_tests.sql | 11 ++++++++++- testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 27 ++++++++++++++++++++++++++- view_pgFirstAid_managed.sql | 27 ++++++++++++++++++++++++++- 5 files changed, 100 insertions(+), 4 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 0a9370b..721f1c3 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1458,6 +1458,34 @@ select 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, 2 as severity_order where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB'); + +-- INFO: work_mem current value +insert into health_results +select + 'INFO' as severity, + 'System Health' as category, + 'work_mem Setting' as check_name, + 'System' as object_name, + 'Current value of work_mem. Allocated per sort/hash operation per session — multiply by max_connections and parallel workers to estimate peak memory consumption.' as issue_description, + current_setting('work_mem') || ' (max_connections: ' || current_setting('max_connections') || ')' as current_value, + 'For OLTP workloads, 16-32MB is a common starting point. Monitor pg_stat_statements for temp file spills to determine if higher is warranted. Use SET work_mem per-session for large one-off queries rather than setting globally.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, + 5 as severity_order; + +-- MEDIUM: work_mem still at 4MB PostgreSQL default +insert into health_results +select + 'MEDIUM' as severity, + 'System Health' as category, + 'work_mem At Default' as check_name, + 'System' as object_name, + 'work_mem is set to the PostgreSQL default of 4MB. On modern hardware this often causes unnecessary sort and hash spills to disk.' as issue_description, + current_setting('work_mem') as current_value, + 'Consider raising work_mem to 16-32MB for OLTP workloads. Be aware that work_mem is allocated per operation per session — high concurrency multiplies total memory usage.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, + 3 as severity_order +where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB'); + -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/04_medium_tests.sql b/testing/pgTAP/04_medium_tests.sql index 830eb39..e306733 100644 --- a/testing/pgTAP/04_medium_tests.sql +++ b/testing/pgTAP/04_medium_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(14); +SELECT plan(16); SELECT ok( (SELECT count(*) >= 0 FROM pg_firstAid() WHERE check_name = 'Missing FK Index'), @@ -64,5 +64,14 @@ SELECT ok( 'View executes Empty Table check' ); +SELECT ok( + (SELECT count(*) >= 0 FROM pg_firstAid() WHERE check_name = 'work_mem At Default'), + 'Function executes work_mem At Default check' +); +SELECT ok( + (SELECT count(*) >= 0 FROM v_pgfirstaid WHERE check_name = 'work_mem At Default'), + 'View executes work_mem At Default check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index a506f39..828cbb2 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(6); +SELECT plan(8); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -23,5 +23,14 @@ SELECT ok( 'View executes shared_buffers Setting check' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'work_mem Setting'), + 'Function executes work_mem Setting check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'work_mem Setting'), + 'View executes work_mem Setting check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index 0801750..4a73a51 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1418,4 +1418,29 @@ select 'Set shared_buffers to approximately 25% of total system RAM (e.g., 2GB on an 8GB server). Requires a PostgreSQL restart.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, 2 as severity_order -where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB'); +where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB') +union all +-- INFO: work_mem current value +select + 'INFO' as severity, + 'System Health' as category, + 'work_mem Setting' as check_name, + 'System' as object_name, + 'Current value of work_mem. Allocated per sort/hash operation per session — multiply by max_connections and parallel workers to estimate peak memory consumption.' as issue_description, + current_setting('work_mem') || ' (max_connections: ' || current_setting('max_connections') || ')' as current_value, + 'For OLTP workloads, 16-32MB is a common starting point. Monitor pg_stat_statements for temp file spills to determine if higher is warranted. Use SET work_mem per-session for large one-off queries rather than setting globally.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, + 5 as severity_order +union all +-- MEDIUM: work_mem still at 4MB PostgreSQL default +select + 'MEDIUM' as severity, + 'System Health' as category, + 'work_mem At Default' as check_name, + 'System' as object_name, + 'work_mem is set to the PostgreSQL default of 4MB. On modern hardware this often causes unnecessary sort and hash spills to disk.' as issue_description, + current_setting('work_mem') as current_value, + 'Consider raising work_mem to 16-32MB for OLTP workloads. Be aware that work_mem is allocated per operation per session — high concurrency multiplies total memory usage.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, + 3 as severity_order +where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB'); diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index e287aa9..1ea006c 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1401,4 +1401,29 @@ select 'Set shared_buffers to approximately 25% of total system RAM (e.g., 2GB on an 8GB server). Requires a PostgreSQL restart.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' as documentation_link, 2 as severity_order -where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB'); +where pg_size_bytes(current_setting('shared_buffers')) = pg_size_bytes('128MB') +union all +-- INFO: work_mem current value +select + 'INFO' as severity, + 'System Health' as category, + 'work_mem Setting' as check_name, + 'System' as object_name, + 'Current value of work_mem. Allocated per sort/hash operation per session — multiply by max_connections and parallel workers to estimate peak memory consumption.' as issue_description, + current_setting('work_mem') || ' (max_connections: ' || current_setting('max_connections') || ')' as current_value, + 'For OLTP workloads, 16-32MB is a common starting point. Monitor pg_stat_statements for temp file spills to determine if higher is warranted. Use SET work_mem per-session for large one-off queries rather than setting globally.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, + 5 as severity_order +union all +-- MEDIUM: work_mem still at 4MB PostgreSQL default +select + 'MEDIUM' as severity, + 'System Health' as category, + 'work_mem At Default' as check_name, + 'System' as object_name, + 'work_mem is set to the PostgreSQL default of 4MB. On modern hardware this often causes unnecessary sort and hash spills to disk.' as issue_description, + current_setting('work_mem') as current_value, + 'Consider raising work_mem to 16-32MB for OLTP workloads. Be aware that work_mem is allocated per operation per session — high concurrency multiplies total memory usage.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, + 3 as severity_order +where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB'); From faeaba6357bb1adab6e29ae5558802ca2a8588c7 Mon Sep 17 00:00:00 2001 From: randoneering Date: Sun, 5 Apr 2026 04:23:18 +0000 Subject: [PATCH 04/26] update .gitignore --- .gitignore | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4c4c9a1..40e13ba 100644 --- a/.gitignore +++ b/.gitignore @@ -14,13 +14,15 @@ credentials.json .pgpass pgpass # AI dirs -.claude .agent .copilot -# AI Files -CLAUDE.md +# AI files AGENTS.md + +# Local docs +docs/superpowers/plans/ + # Python __pycache__/ .pytest_cache/ From 8ccf33261eb49f8aff0cede6bf433f6a971498da Mon Sep 17 00:00:00 2001 From: justin Date: Thu, 9 Apr 2026 18:54:00 -0600 Subject: [PATCH 05/26] AI files to .gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 40e13ba..9392653 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,10 @@ pgpass # AI dirs .agent .copilot - +.claude # AI files AGENTS.md - +CLAUDE.md # Local docs docs/superpowers/plans/ From 490ef9c20d7eb0a99ac184fbcb961e4a9fd2eca6 Mon Sep 17 00:00:00 2001 From: justin Date: Fri, 10 Apr 2026 21:35:43 -0600 Subject: [PATCH 06/26] feat: add effective_cache_size INFO health check --- pgFirstAid.sql | 13 +++++++++++++ testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 14 +++++++++++++- view_pgFirstAid_managed.sql | 14 +++++++++++++- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 721f1c3..241b50f 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1486,6 +1486,19 @@ select 3 as severity_order where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB'); +-- INFO: effective_cache_size current value +insert into health_results +select + 'INFO' as severity, + 'System Health' as category, + 'effective_cache_size Setting' as check_name, + 'System' as object_name, + 'Current value of effective_cache_size. Tells the query planner how much memory is available for disk caching. Does not allocate memory — purely advisory.' as issue_description, + current_setting('effective_cache_size') as current_value, + 'Set to ~50-75% of total system RAM (shared_buffers + expected OS page cache). Underestimates cause the planner to prefer nested loops over index scans.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE' as documentation_link, + 5 as severity_order; + -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index 828cbb2..4df4dea 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(8); +SELECT plan(10); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -32,5 +32,14 @@ SELECT ok( 'View executes work_mem Setting check' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'effective_cache_size Setting'), + 'Function executes effective_cache_size Setting check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'effective_cache_size Setting'), + 'View executes effective_cache_size Setting check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index 4a73a51..a575899 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1443,4 +1443,16 @@ select 'Consider raising work_mem to 16-32MB for OLTP workloads. Be aware that work_mem is allocated per operation per session — high concurrency multiplies total memory usage.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, 3 as severity_order -where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB'); +where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB') +union all +-- INFO: effective_cache_size current value +select + 'INFO' as severity, + 'System Health' as category, + 'effective_cache_size Setting' as check_name, + 'System' as object_name, + 'Current value of effective_cache_size. Tells the query planner how much memory is available for disk caching. Does not allocate memory — purely advisory.' as issue_description, + current_setting('effective_cache_size') as current_value, + 'Set to ~50-75% of total system RAM (shared_buffers + expected OS page cache). Underestimates cause the planner to prefer nested loops over index scans.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE' as documentation_link, + 5 as severity_order; diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 1ea006c..b55a6a3 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1426,4 +1426,16 @@ select 'Consider raising work_mem to 16-32MB for OLTP workloads. Be aware that work_mem is allocated per operation per session — high concurrency multiplies total memory usage.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' as documentation_link, 3 as severity_order -where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB'); +where pg_size_bytes(current_setting('work_mem')) = pg_size_bytes('4MB') +union all +-- INFO: effective_cache_size current value +select + 'INFO' as severity, + 'System Health' as category, + 'effective_cache_size Setting' as check_name, + 'System' as object_name, + 'Current value of effective_cache_size. Tells the query planner how much memory is available for disk caching. Does not allocate memory — purely advisory.' as issue_description, + current_setting('effective_cache_size') as current_value, + 'Set to ~50-75% of total system RAM (shared_buffers + expected OS page cache). Underestimates cause the planner to prefer nested loops over index scans.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE' as documentation_link, + 5 as severity_order; From 4ca84cc29e0b50c3dcd168225f03f5798ea10bcf Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 09:09:38 -0600 Subject: [PATCH 07/26] feat: add maintenance_work_mem INFO health check --- pgFirstAid.sql | 13 +++++++++++++ testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 12 ++++++++++++ view_pgFirstAid_managed.sql | 12 ++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 241b50f..7fd6548 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1499,6 +1499,19 @@ select 'https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE' as documentation_link, 5 as severity_order; +-- INFO: maintenance_work_mem current value +insert into health_results +select + 'INFO' as severity, + 'System Health' as category, + 'maintenance_work_mem Setting' as check_name, + 'System' as object_name, + 'Current value of maintenance_work_mem. Used by VACUUM, CREATE INDEX, ALTER TABLE, and each autovacuum worker.' as issue_description, + current_setting('maintenance_work_mem') as current_value, + 'Consider 256MB-1GB on modern hardware. Higher values speed up index builds and autovacuum on large tables. Changes take effect immediately for new sessions.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM' as documentation_link, + 5 as severity_order; + -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index 4df4dea..e79bab1 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(10); +SELECT plan(12); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -41,5 +41,14 @@ SELECT ok( 'View executes effective_cache_size Setting check' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'maintenance_work_mem Setting'), + 'Function executes maintenance_work_mem Setting check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'maintenance_work_mem Setting'), + 'View executes maintenance_work_mem Setting check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index a575899..92a2341 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1455,4 +1455,16 @@ select current_setting('effective_cache_size') as current_value, 'Set to ~50-75% of total system RAM (shared_buffers + expected OS page cache). Underestimates cause the planner to prefer nested loops over index scans.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE' as documentation_link, + 5 as severity_order +union all +-- INFO: maintenance_work_mem current value +select + 'INFO' as severity, + 'System Health' as category, + 'maintenance_work_mem Setting' as check_name, + 'System' as object_name, + 'Current value of maintenance_work_mem. Used by VACUUM, CREATE INDEX, ALTER TABLE, and each autovacuum worker.' as issue_description, + current_setting('maintenance_work_mem') as current_value, + 'Consider 256MB-1GB on modern hardware. Higher values speed up index builds and autovacuum on large tables. Changes take effect immediately for new sessions.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM' as documentation_link, 5 as severity_order; diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index b55a6a3..6950143 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1438,4 +1438,16 @@ select current_setting('effective_cache_size') as current_value, 'Set to ~50-75% of total system RAM (shared_buffers + expected OS page cache). Underestimates cause the planner to prefer nested loops over index scans.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE' as documentation_link, + 5 as severity_order +union all +-- INFO: maintenance_work_mem current value +select + 'INFO' as severity, + 'System Health' as category, + 'maintenance_work_mem Setting' as check_name, + 'System' as object_name, + 'Current value of maintenance_work_mem. Used by VACUUM, CREATE INDEX, ALTER TABLE, and each autovacuum worker.' as issue_description, + current_setting('maintenance_work_mem') as current_value, + 'Consider 256MB-1GB on modern hardware. Higher values speed up index builds and autovacuum on large tables. Changes take effect immediately for new sessions.' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM' as documentation_link, 5 as severity_order; From aab2ce1aea9f9c2f8e2cea18f41341b42e3e157b Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 15:29:30 -0600 Subject: [PATCH 08/26] feat: add Transaction ID Wraparound Risk INFO health check --- pgFirstAid.sql | 19 +++++++++++++++++++ testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 18 +++++++++++++++++- view_pgFirstAid_managed.sql | 18 +++++++++++++++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 7fd6548..8bcad34 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1512,6 +1512,25 @@ select 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM' as documentation_link, 5 as severity_order; +-- INFO: Transaction ID wraparound risk per database +insert into health_results +select + 'INFO' as severity, + 'System Health' as category, + 'Transaction ID Wraparound Risk' as check_name, + datname as object_name, + 'Age of the oldest unfrozen transaction ID in this database. PostgreSQL must freeze XIDs before reaching ~2.1 billion to prevent data loss from wraparound.' as issue_description, + datname || ': XID age ' || age(datfrozenxid)::text as current_value, + 'Run VACUUM FREEZE on databases approaching high XID age. Ensure autovacuum is enabled and not blocked. Monitor databases with age > 500,000,000.' as recommended_action, + 'https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND' as documentation_link, + 5 as severity_order +from + pg_database +where + datallowconn = true +order by + age(datfrozenxid) desc; + -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index e79bab1..f3f23c2 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(12); +SELECT plan(14); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -50,5 +50,14 @@ SELECT ok( 'View executes maintenance_work_mem Setting check' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'Transaction ID Wraparound Risk'), + 'Function executes Transaction ID Wraparound Risk check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'Transaction ID Wraparound Risk'), + 'View executes Transaction ID Wraparound Risk check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index 92a2341..cc19539 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1467,4 +1467,20 @@ select current_setting('maintenance_work_mem') as current_value, 'Consider 256MB-1GB on modern hardware. Higher values speed up index builds and autovacuum on large tables. Changes take effect immediately for new sessions.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM' as documentation_link, - 5 as severity_order; + 5 as severity_order +union all +-- INFO: Transaction ID wraparound risk per database +select + 'INFO' as severity, + 'System Health' as category, + 'Transaction ID Wraparound Risk' as check_name, + datname as object_name, + 'Age of the oldest unfrozen transaction ID in this database. PostgreSQL must freeze XIDs before reaching ~2.1 billion to prevent data loss from wraparound.' as issue_description, + datname || ': XID age ' || age(datfrozenxid)::text as current_value, + 'Run VACUUM FREEZE on databases approaching high XID age. Ensure autovacuum is enabled and not blocked. Monitor databases with age > 500,000,000.' as recommended_action, + 'https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND' as documentation_link, + 5 as severity_order +from + pg_database +where + datallowconn = true; diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 6950143..45c2819 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1450,4 +1450,20 @@ select current_setting('maintenance_work_mem') as current_value, 'Consider 256MB-1GB on modern hardware. Higher values speed up index builds and autovacuum on large tables. Changes take effect immediately for new sessions.' as recommended_action, 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM' as documentation_link, - 5 as severity_order; + 5 as severity_order +union all +-- INFO: Transaction ID wraparound risk per database +select + 'INFO' as severity, + 'System Health' as category, + 'Transaction ID Wraparound Risk' as check_name, + datname as object_name, + 'Age of the oldest unfrozen transaction ID in this database. PostgreSQL must freeze XIDs before reaching ~2.1 billion to prevent data loss from wraparound.' as issue_description, + datname || ': XID age ' || age(datfrozenxid)::text as current_value, + 'Run VACUUM FREEZE on databases approaching high XID age. Ensure autovacuum is enabled and not blocked. Monitor databases with age > 500,000,000.' as recommended_action, + 'https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND' as documentation_link, + 5 as severity_order +from + pg_database +where + datallowconn = true; From dbd5d5d94db882eb97b0455c17b8cd0555d3f3fe Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 15:49:10 -0600 Subject: [PATCH 09/26] feat: add Checkpoint Stats INFO health check --- pgFirstAid.sql | 21 +++++++++++++++++++++ testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 22 +++++++++++++++++++++- view_pgFirstAid_managed.sql | 22 +++++++++++++++++++++- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 8bcad34..996b9c6 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1531,6 +1531,27 @@ where order by age(datfrozenxid) desc; +-- INFO: Checkpoint statistics from pg_stat_bgwriter +insert into health_results +select + 'INFO' as severity, + 'System Health' as category, + 'Checkpoint Stats' as check_name, + 'System' as object_name, + 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, + 'timed: ' || checkpoints_timed::text || + ', forced: ' || checkpoints_req::text || + ', forced ratio: ' || + case + when checkpoints_timed + checkpoints_req = 0 then '0%' + else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' + end as current_value, + 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, + 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, + 5 as severity_order +from + pg_stat_bgwriter; + -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index f3f23c2..b661047 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(14); +SELECT plan(16); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -59,5 +59,14 @@ SELECT ok( 'View executes Transaction ID Wraparound Risk check' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'Checkpoint Stats'), + 'Function executes Checkpoint Stats check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'Checkpoint Stats'), + 'View executes Checkpoint Stats check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index cc19539..1e8f252 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1483,4 +1483,24 @@ select from pg_database where - datallowconn = true; + datallowconn = true +union all +-- INFO: Checkpoint statistics from pg_stat_bgwriter +select + 'INFO' as severity, + 'System Health' as category, + 'Checkpoint Stats' as check_name, + 'System' as object_name, + 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, + 'timed: ' || checkpoints_timed::text || + ', forced: ' || checkpoints_req::text || + ', forced ratio: ' || + case + when checkpoints_timed + checkpoints_req = 0 then '0%' + else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' + end as current_value, + 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, + 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, + 5 as severity_order +from + pg_stat_bgwriter; diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 45c2819..922863b 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1466,4 +1466,24 @@ select from pg_database where - datallowconn = true; + datallowconn = true +union all +-- INFO: Checkpoint statistics from pg_stat_bgwriter +select + 'INFO' as severity, + 'System Health' as category, + 'Checkpoint Stats' as check_name, + 'System' as object_name, + 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, + 'timed: ' || checkpoints_timed::text || + ', forced: ' || checkpoints_req::text || + ', forced ratio: ' || + case + when checkpoints_timed + checkpoints_req = 0 then '0%' + else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' + end as current_value, + 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, + 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, + 5 as severity_order +from + pg_stat_bgwriter; From 297766ef2de75e7a71bd3ee388823e15258ed8f0 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 16:11:31 -0600 Subject: [PATCH 10/26] feat: add Server Role INFO health check --- pgFirstAid.sql | 16 ++++++++++++++++ testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 17 ++++++++++++++++- view_pgFirstAid_managed.sql | 17 ++++++++++++++++- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 996b9c6..3a18bd4 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1552,6 +1552,22 @@ select from pg_stat_bgwriter; +-- INFO: Server role (primary vs standby) +insert into health_results +select + 'INFO' as severity, + 'System Info' as category, + 'Server Role' as check_name, + 'System' as object_name, + 'Whether this server is operating as a primary or standby replica. Context for interpreting other checks — some checks are only relevant on standbys.' as issue_description, + case + when pg_is_in_recovery() then 'Standby (replica)' + else 'Primary' + end as current_value, + 'No action needed — informational.' as recommended_action, + 'https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-RECOVERY-INFO-TABLE' as documentation_link, + 5 as severity_order; + -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index b661047..4162707 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(16); +SELECT plan(18); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -68,5 +68,14 @@ SELECT ok( 'View executes Checkpoint Stats check' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'Server Role'), + 'Function executes Server Role check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'Server Role'), + 'View executes Server Role check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index 1e8f252..a7e04e9 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1503,4 +1503,19 @@ select 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order from - pg_stat_bgwriter; + pg_stat_bgwriter +union all +-- INFO: Server role (primary vs standby) +select + 'INFO' as severity, + 'System Info' as category, + 'Server Role' as check_name, + 'System' as object_name, + 'Whether this server is operating as a primary or standby replica. Context for interpreting other checks — some checks are only relevant on standbys.' as issue_description, + case + when pg_is_in_recovery() then 'Standby (replica)' + else 'Primary' + end as current_value, + 'No action needed — informational.' as recommended_action, + 'https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-RECOVERY-INFO-TABLE' as documentation_link, + 5 as severity_order; diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 922863b..bd0eca3 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1486,4 +1486,19 @@ select 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order from - pg_stat_bgwriter; + pg_stat_bgwriter +union all +-- INFO: Server role (primary vs standby) +select + 'INFO' as severity, + 'System Info' as category, + 'Server Role' as check_name, + 'System' as object_name, + 'Whether this server is operating as a primary or standby replica. Context for interpreting other checks — some checks are only relevant on standbys.' as issue_description, + case + when pg_is_in_recovery() then 'Standby (replica)' + else 'Primary' + end as current_value, + 'No action needed — informational.' as recommended_action, + 'https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-RECOVERY-INFO-TABLE' as documentation_link, + 5 as severity_order; From 429769bcd0fd7b051daed340359d31515a76ba86 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 16:40:19 -0600 Subject: [PATCH 11/26] feat: add Connection Utilization INFO health check --- pgFirstAid.sql | 16 ++++++++++++++++ testing/pgTAP/06_info_tests.sql | 11 ++++++++++- view_pgFirstAid.sql | 17 ++++++++++++++++- view_pgFirstAid_managed.sql | 17 ++++++++++++++++- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 3a18bd4..ed3aa87 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1568,6 +1568,22 @@ select 'https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-RECOVERY-INFO-TABLE' as documentation_link, 5 as severity_order; +-- INFO: Connection utilization +insert into health_results +select + 'INFO' as severity, + 'System Health' as category, + 'Connection Utilization' as check_name, + 'System' as object_name, + 'Current connection usage as a percentage of max_connections. Includes all connection states, not just active queries.' as issue_description, + count(*)::text || ' total / ' || current_setting('max_connections') || ' max (' || + round(100.0 * count(*) / current_setting('max_connections')::int, 1)::text || '% used)' as current_value, + 'If consistently above 80%, consider a connection pooler such as PgBouncer. Reserve headroom for superuser connections (superuser_reserved_connections).' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-connection.html' as documentation_link, + 5 as severity_order +from + pg_stat_activity; + -- INFO: Installed Extensions insert into diff --git a/testing/pgTAP/06_info_tests.sql b/testing/pgTAP/06_info_tests.sql index 4162707..a7f3fd0 100644 --- a/testing/pgTAP/06_info_tests.sql +++ b/testing/pgTAP/06_info_tests.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(18); +SELECT plan(20); SELECT ok((SELECT count(*) >= 0 FROM pg_firstAid()), 'pg_firstAid() executes'); SELECT ok((SELECT count(*) >= 0 FROM v_pgfirstaid), 'v_pgfirstaid executes'); @@ -77,5 +77,14 @@ SELECT ok( 'View executes Server Role check' ); +SELECT ok( + (SELECT count(*) >= 1 FROM pg_firstAid() WHERE check_name = 'Connection Utilization'), + 'Function executes Connection Utilization check' +); +SELECT ok( + (SELECT count(*) >= 1 FROM v_pgfirstaid WHERE check_name = 'Connection Utilization'), + 'View executes Connection Utilization check' +); + SELECT * FROM finish(); ROLLBACK; diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index a7e04e9..e051b8b 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1518,4 +1518,19 @@ select end as current_value, 'No action needed — informational.' as recommended_action, 'https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-RECOVERY-INFO-TABLE' as documentation_link, - 5 as severity_order; + 5 as severity_order +union all +-- INFO: Connection utilization +select + 'INFO' as severity, + 'System Health' as category, + 'Connection Utilization' as check_name, + 'System' as object_name, + 'Current connection usage as a percentage of max_connections. Includes all connection states, not just active queries.' as issue_description, + count(*)::text || ' total / ' || current_setting('max_connections') || ' max (' || + round(100.0 * count(*) / current_setting('max_connections')::int, 1)::text || '% used)' as current_value, + 'If consistently above 80%, consider a connection pooler such as PgBouncer. Reserve headroom for superuser connections (superuser_reserved_connections).' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-connection.html' as documentation_link, + 5 as severity_order +from + pg_stat_activity; diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index bd0eca3..c5cea65 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1501,4 +1501,19 @@ select end as current_value, 'No action needed — informational.' as recommended_action, 'https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-RECOVERY-INFO-TABLE' as documentation_link, - 5 as severity_order; + 5 as severity_order +union all +-- INFO: Connection utilization +select + 'INFO' as severity, + 'System Health' as category, + 'Connection Utilization' as check_name, + 'System' as object_name, + 'Current connection usage as a percentage of max_connections. Includes all connection states, not just active queries.' as issue_description, + count(*)::text || ' total / ' || current_setting('max_connections') || ' max (' || + round(100.0 * count(*) / current_setting('max_connections')::int, 1)::text || '% used)' as current_value, + 'If consistently above 80%, consider a connection pooler such as PgBouncer. Reserve headroom for superuser connections (superuser_reserved_connections).' as recommended_action, + 'https://www.postgresql.org/docs/current/runtime-config-connection.html' as documentation_link, + 5 as severity_order +from + pg_stat_activity; From f0d8359976f82cea5da81b0d1ce7f439a1d17686 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 20:20:23 -0600 Subject: [PATCH 12/26] fix: remove ineffective ORDER BY from Transaction ID Wraparound Risk insert --- pgFirstAid.sql | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index ed3aa87..4746b8a 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1527,9 +1527,7 @@ select from pg_database where - datallowconn = true -order by - age(datfrozenxid) desc; + datallowconn = true; -- INFO: Checkpoint statistics from pg_stat_bgwriter insert into health_results From fce48682b9a3cfa6f61241ef90bf7033ee6838f0 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 21:07:04 -0600 Subject: [PATCH 13/26] fix: add version-compatible checkpoint stats helper for PG17+ compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgreSQL 17 moved checkpoint columns from pg_stat_bgwriter to the new pg_stat_checkpointer view with renamed columns (checkpoints_timed → num_timed, checkpoints_req → num_requested). Adds _pg_firstaid_checkpoint_stats() helper function using PL/pgSQL lazy compilation to branch on server version, so both the main function and views work correctly on PG15/16 and PG17+. --- pgFirstAid.sql | 46 ++++++++++++++++++++++++++++--------- view_pgFirstAid.sql | 12 ++-------- view_pgFirstAid_managed.sql | 12 ++-------- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 4746b8a..0c6cd1d 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -267,6 +267,38 @@ with pss as ( end; $$ language plpgsql; +-- Helper: returns formatted checkpoint stats compatible with PG15/16 (pg_stat_bgwriter) +-- and PG17+ (pg_stat_checkpointer, which replaced the checkpoint columns in pg_stat_bgwriter) +create or replace +function _pg_firstaid_checkpoint_stats() +returns text +language plpgsql +stable +as $$ +declare + v_timed bigint; + v_forced bigint; +begin + if current_setting('server_version_num')::int >= 170000 then + select num_timed, num_requested + into v_timed, v_forced + from pg_stat_checkpointer; + else + select checkpoints_timed, checkpoints_req + into v_timed, v_forced + from pg_stat_bgwriter; + end if; + + return 'timed: ' || v_timed::text || + ', forced: ' || v_forced::text || + ', forced ratio: ' || + case + when v_timed + v_forced = 0 then '0%' + else round(100.0 * v_forced / (v_timed + v_forced), 1)::text || '%' + end; +end; +$$; + create or replace function pg_firstAid() returns table ( @@ -1529,7 +1561,7 @@ from where datallowconn = true; --- INFO: Checkpoint statistics from pg_stat_bgwriter +-- INFO: Checkpoint statistics (PG15/16: pg_stat_bgwriter, PG17+: pg_stat_checkpointer) insert into health_results select 'INFO' as severity, @@ -1537,18 +1569,10 @@ select 'Checkpoint Stats' as check_name, 'System' as object_name, 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, - 'timed: ' || checkpoints_timed::text || - ', forced: ' || checkpoints_req::text || - ', forced ratio: ' || - case - when checkpoints_timed + checkpoints_req = 0 then '0%' - else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' - end as current_value, + _pg_firstaid_checkpoint_stats() as current_value, 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, - 5 as severity_order -from - pg_stat_bgwriter; + 5 as severity_order; -- INFO: Server role (primary vs standby) insert into health_results diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index e051b8b..514edf5 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1485,25 +1485,17 @@ from where datallowconn = true union all --- INFO: Checkpoint statistics from pg_stat_bgwriter +-- INFO: Checkpoint statistics (PG15/16: pg_stat_bgwriter, PG17+: pg_stat_checkpointer) select 'INFO' as severity, 'System Health' as category, 'Checkpoint Stats' as check_name, 'System' as object_name, 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, - 'timed: ' || checkpoints_timed::text || - ', forced: ' || checkpoints_req::text || - ', forced ratio: ' || - case - when checkpoints_timed + checkpoints_req = 0 then '0%' - else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' - end as current_value, + _pg_firstaid_checkpoint_stats() as current_value, 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order -from - pg_stat_bgwriter union all -- INFO: Server role (primary vs standby) select diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index c5cea65..6ab4049 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1468,25 +1468,17 @@ from where datallowconn = true union all --- INFO: Checkpoint statistics from pg_stat_bgwriter +-- INFO: Checkpoint statistics (PG15/16: pg_stat_bgwriter, PG17+: pg_stat_checkpointer) select 'INFO' as severity, 'System Health' as category, 'Checkpoint Stats' as check_name, 'System' as object_name, 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, - 'timed: ' || checkpoints_timed::text || - ', forced: ' || checkpoints_req::text || - ', forced ratio: ' || - case - when checkpoints_timed + checkpoints_req = 0 then '0%' - else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' - end as current_value, + _pg_firstaid_checkpoint_stats() as current_value, 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order -from - pg_stat_bgwriter union all -- INFO: Server role (primary vs standby) select From b04f245b7c15f09ad947e9e7809b86c2f6fae017 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 21:27:05 -0600 Subject: [PATCH 14/26] test: drop _pg_firstaid_checkpoint_stats helper in pgTAP teardown --- testing/pgTAP/99_teardown.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/pgTAP/99_teardown.sql b/testing/pgTAP/99_teardown.sql index c5054cc..cfa399f 100644 --- a/testing/pgTAP/99_teardown.sql +++ b/testing/pgTAP/99_teardown.sql @@ -1,2 +1,3 @@ -- Session-level pgTAP teardown for Python harness DROP SCHEMA IF EXISTS pgfirstaid_test CASCADE; +DROP FUNCTION IF EXISTS _pg_firstaid_checkpoint_stats(); From eb9f81157516e2a541e3fe252f9ac1e7932f6124 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 11 Apr 2026 21:34:11 -0600 Subject: [PATCH 15/26] fix: adding helper function --- pgFirstAid.sql | 46 ++++++++++++++++++++++++++--------- testing/pgTAP/99_teardown.sql | 1 + view_pgFirstAid.sql | 12 ++------- view_pgFirstAid_managed.sql | 12 ++------- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 4746b8a..0c6cd1d 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -267,6 +267,38 @@ with pss as ( end; $$ language plpgsql; +-- Helper: returns formatted checkpoint stats compatible with PG15/16 (pg_stat_bgwriter) +-- and PG17+ (pg_stat_checkpointer, which replaced the checkpoint columns in pg_stat_bgwriter) +create or replace +function _pg_firstaid_checkpoint_stats() +returns text +language plpgsql +stable +as $$ +declare + v_timed bigint; + v_forced bigint; +begin + if current_setting('server_version_num')::int >= 170000 then + select num_timed, num_requested + into v_timed, v_forced + from pg_stat_checkpointer; + else + select checkpoints_timed, checkpoints_req + into v_timed, v_forced + from pg_stat_bgwriter; + end if; + + return 'timed: ' || v_timed::text || + ', forced: ' || v_forced::text || + ', forced ratio: ' || + case + when v_timed + v_forced = 0 then '0%' + else round(100.0 * v_forced / (v_timed + v_forced), 1)::text || '%' + end; +end; +$$; + create or replace function pg_firstAid() returns table ( @@ -1529,7 +1561,7 @@ from where datallowconn = true; --- INFO: Checkpoint statistics from pg_stat_bgwriter +-- INFO: Checkpoint statistics (PG15/16: pg_stat_bgwriter, PG17+: pg_stat_checkpointer) insert into health_results select 'INFO' as severity, @@ -1537,18 +1569,10 @@ select 'Checkpoint Stats' as check_name, 'System' as object_name, 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, - 'timed: ' || checkpoints_timed::text || - ', forced: ' || checkpoints_req::text || - ', forced ratio: ' || - case - when checkpoints_timed + checkpoints_req = 0 then '0%' - else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' - end as current_value, + _pg_firstaid_checkpoint_stats() as current_value, 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, - 5 as severity_order -from - pg_stat_bgwriter; + 5 as severity_order; -- INFO: Server role (primary vs standby) insert into health_results diff --git a/testing/pgTAP/99_teardown.sql b/testing/pgTAP/99_teardown.sql index c5054cc..cfa399f 100644 --- a/testing/pgTAP/99_teardown.sql +++ b/testing/pgTAP/99_teardown.sql @@ -1,2 +1,3 @@ -- Session-level pgTAP teardown for Python harness DROP SCHEMA IF EXISTS pgfirstaid_test CASCADE; +DROP FUNCTION IF EXISTS _pg_firstaid_checkpoint_stats(); diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index e051b8b..514edf5 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -1485,25 +1485,17 @@ from where datallowconn = true union all --- INFO: Checkpoint statistics from pg_stat_bgwriter +-- INFO: Checkpoint statistics (PG15/16: pg_stat_bgwriter, PG17+: pg_stat_checkpointer) select 'INFO' as severity, 'System Health' as category, 'Checkpoint Stats' as check_name, 'System' as object_name, 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, - 'timed: ' || checkpoints_timed::text || - ', forced: ' || checkpoints_req::text || - ', forced ratio: ' || - case - when checkpoints_timed + checkpoints_req = 0 then '0%' - else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' - end as current_value, + _pg_firstaid_checkpoint_stats() as current_value, 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order -from - pg_stat_bgwriter union all -- INFO: Server role (primary vs standby) select diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index c5cea65..6ab4049 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -1468,25 +1468,17 @@ from where datallowconn = true union all --- INFO: Checkpoint statistics from pg_stat_bgwriter +-- INFO: Checkpoint statistics (PG15/16: pg_stat_bgwriter, PG17+: pg_stat_checkpointer) select 'INFO' as severity, 'System Health' as category, 'Checkpoint Stats' as check_name, 'System' as object_name, 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, - 'timed: ' || checkpoints_timed::text || - ', forced: ' || checkpoints_req::text || - ', forced ratio: ' || - case - when checkpoints_timed + checkpoints_req = 0 then '0%' - else round(100.0 * checkpoints_req / (checkpoints_timed + checkpoints_req), 1)::text || '%' - end as current_value, + _pg_firstaid_checkpoint_stats() as current_value, 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order -from - pg_stat_bgwriter union all -- INFO: Server role (primary vs standby) select From db3797a104138f1732b6447b3a19d3a3d8087234 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 15:14:38 -0600 Subject: [PATCH 16/26] add flox env, modify pytests with psycopg2 --- testing/.flox/.gitattributes | 1 + testing/.flox/.gitignore | 5 + testing/.flox/env.json | 4 + testing/.flox/env/manifest.lock | 680 ++++++++++++++++++ testing/.flox/env/manifest.toml | 100 +++ testing/.flox/pip.ini | 2 + testing/integration/pyproject.toml | 2 +- .../src/pgfirstaid_pytest/config.py | 20 +- .../integration/src/pgfirstaid_pytest/db.py | 22 +- testing/integration/tests/conftest.py | 8 +- .../tests/integration/test_health_checks.py | 9 +- .../tests/integration/test_pgtap_suite.py | 8 +- testing/integration/tests/test_config.py | 33 + testing/integration/uv.lock | 133 ++-- 14 files changed, 920 insertions(+), 107 deletions(-) create mode 100644 testing/.flox/.gitattributes create mode 100644 testing/.flox/.gitignore create mode 100644 testing/.flox/env.json create mode 100644 testing/.flox/env/manifest.lock create mode 100644 testing/.flox/env/manifest.toml create mode 100644 testing/.flox/pip.ini create mode 100644 testing/integration/tests/test_config.py diff --git a/testing/.flox/.gitattributes b/testing/.flox/.gitattributes new file mode 100644 index 0000000..bb5491e --- /dev/null +++ b/testing/.flox/.gitattributes @@ -0,0 +1 @@ +env/manifest.lock linguist-generated=true linguist-language=JSON diff --git a/testing/.flox/.gitignore b/testing/.flox/.gitignore new file mode 100644 index 0000000..8d21186 --- /dev/null +++ b/testing/.flox/.gitignore @@ -0,0 +1,5 @@ +run/ +cache/ +lib/ +log/ +!env/ diff --git a/testing/.flox/env.json b/testing/.flox/env.json new file mode 100644 index 0000000..eaa4626 --- /dev/null +++ b/testing/.flox/env.json @@ -0,0 +1,4 @@ +{ + "name": "pgFirstAid", + "version": 1 +} diff --git a/testing/.flox/env/manifest.lock b/testing/.flox/env/manifest.lock new file mode 100644 index 0000000..592c834 --- /dev/null +++ b/testing/.flox/env/manifest.lock @@ -0,0 +1,680 @@ +{ + "lockfile-version": 1, + "manifest": { + "schema-version": "1.10.0", + "install": { + "pip": { + "pkg-path": "python312Packages.pip" + }, + "psycopg2": { + "pkg-path": "python312Packages.psycopg2" + }, + "pytest": { + "pkg-path": "python312Packages.pytest" + }, + "python312Full": { + "pkg-path": "python312Full" + }, + "uv": { + "pkg-path": "uv" + } + }, + "hook": {}, + "profile": {}, + "options": {} + }, + "packages": [ + { + "attr_path": "python312Packages.pip", + "broken": false, + "derivation": "/nix/store/6c3vk1w26pz96c6jphzpn9yf0ik301y8-python3.12-pip-25.0.1.drv", + "description": "PyPA recommended tool for installing Python packages", + "install_id": "pip", + "license": "[ MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pip-25.0.1", + "pname": "pip", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:26:18.931395Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "25.0.1", + "outputs_to_install": [ + "man", + "out" + ], + "outputs": { + "dist": "/nix/store/hj7j2kzxm8s04qj271s8yvdwlcrbr74d-python3.12-pip-25.0.1-dist", + "man": "/nix/store/nyajlkrlkl85d8m1vpnv7axvskphica3-python3.12-pip-25.0.1-man", + "out": "/nix/store/1r9wk052v005r2fd2awaw47ryqrb045n-python3.12-pip-25.0.1" + }, + "system": "aarch64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.pip", + "broken": false, + "derivation": "/nix/store/biv5zjq9fxr7vvh5gynjikbvsb5xzzj1-python3.12-pip-25.0.1.drv", + "description": "PyPA recommended tool for installing Python packages", + "install_id": "pip", + "license": "[ MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pip-25.0.1", + "pname": "pip", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:35:05.188700Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "25.0.1", + "outputs_to_install": [ + "man", + "out" + ], + "outputs": { + "dist": "/nix/store/m7h2qkisxfq784wx69s6qzyhqkgiqf7a-python3.12-pip-25.0.1-dist", + "man": "/nix/store/xham2r3yqfvz512mp8wqz9mi78x8a546-python3.12-pip-25.0.1-man", + "out": "/nix/store/y3jnqnyni83z59ca9fd1sfn1llvcxahw-python3.12-pip-25.0.1" + }, + "system": "aarch64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.pip", + "broken": false, + "derivation": "/nix/store/jhygj5afchyy0l41mpzdppjfvp59x785-python3.12-pip-25.0.1.drv", + "description": "PyPA recommended tool for installing Python packages", + "install_id": "pip", + "license": "[ MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pip-25.0.1", + "pname": "pip", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:44:10.811511Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "25.0.1", + "outputs_to_install": [ + "man", + "out" + ], + "outputs": { + "dist": "/nix/store/0dhn33kvy534p6290di1akrr1ry0k9yg-python3.12-pip-25.0.1-dist", + "man": "/nix/store/vpklfd7g7zlg16kf6z10fhaxz6vc10qa-python3.12-pip-25.0.1-man", + "out": "/nix/store/hriq8hl1ckrl1mrvkx281k4n2y7wq1ga-python3.12-pip-25.0.1" + }, + "system": "x86_64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.pip", + "broken": false, + "derivation": "/nix/store/1yixssx3y6nkh99xskx0lk7lzaq7s3j2-python3.12-pip-25.0.1.drv", + "description": "PyPA recommended tool for installing Python packages", + "install_id": "pip", + "license": "[ MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pip-25.0.1", + "pname": "pip", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:52:05.076504Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "25.0.1", + "outputs_to_install": [ + "man", + "out" + ], + "outputs": { + "dist": "/nix/store/ybqryzn42pga752yf9agl0m97p0rvglc-python3.12-pip-25.0.1-dist", + "man": "/nix/store/492i2sc237lp36i7y0hn6da25fsq5flf-python3.12-pip-25.0.1-man", + "out": "/nix/store/jcxmz9bq3ds1x6f0l8lkp8s0nsh1n9h6-python3.12-pip-25.0.1" + }, + "system": "x86_64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.psycopg2", + "broken": false, + "derivation": "/nix/store/90q9xmchbvmrscp02q9ixbgaiyq1p8mv-python3.12-psycopg2-2.9.10.drv", + "description": "PostgreSQL database adapter for the Python programming language", + "install_id": "psycopg2", + "license": "[ LGPL-3.0-or-later, ZPL-2.0 ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-psycopg2-2.9.10", + "pname": "psycopg2", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:26:19.185249Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3.12-psycopg2-2.9.10", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/4f7ixm4hczh4jvkp0y84am8ajw49v5c8-python3.12-psycopg2-2.9.10-dist", + "doc": "/nix/store/vbv3cy5dhsg0bha0d1vlvbqf33g52ppr-python3.12-psycopg2-2.9.10-doc", + "out": "/nix/store/w6y5kl7j705a7cb8ry4giisgd0298xh1-python3.12-psycopg2-2.9.10" + }, + "system": "aarch64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.psycopg2", + "broken": false, + "derivation": "/nix/store/p44l3nm3k7ygp02zhaiw1xh14js321vc-python3.12-psycopg2-2.9.10.drv", + "description": "PostgreSQL database adapter for the Python programming language", + "install_id": "psycopg2", + "license": "[ LGPL-3.0-or-later, ZPL-2.0 ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-psycopg2-2.9.10", + "pname": "psycopg2", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:35:05.651141Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3.12-psycopg2-2.9.10", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/lz5fqp8qc1vlf0sazsj7bkl7p85kmv84-python3.12-psycopg2-2.9.10-dist", + "doc": "/nix/store/sf26mddkwxyqcfaz6y0yrpvd50ijgrs1-python3.12-psycopg2-2.9.10-doc", + "out": "/nix/store/l8vklqnplarlf7b6vsdvy7iq4wpkmqm2-python3.12-psycopg2-2.9.10" + }, + "system": "aarch64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.psycopg2", + "broken": false, + "derivation": "/nix/store/5gdfasp58xjh2wvjk9w414z79pmzy7qx-python3.12-psycopg2-2.9.10.drv", + "description": "PostgreSQL database adapter for the Python programming language", + "install_id": "psycopg2", + "license": "[ LGPL-3.0-or-later, ZPL-2.0 ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-psycopg2-2.9.10", + "pname": "psycopg2", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:44:11.065286Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3.12-psycopg2-2.9.10", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/1vz0mc2dzf8m6lv2flpzng6zw8yc4rdm-python3.12-psycopg2-2.9.10-dist", + "doc": "/nix/store/5rgy6355rg0qfxh9gmgdlp9rm63chk7j-python3.12-psycopg2-2.9.10-doc", + "out": "/nix/store/50cks2kladwn6glaqxp98i9j7760y9x6-python3.12-psycopg2-2.9.10" + }, + "system": "x86_64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.psycopg2", + "broken": false, + "derivation": "/nix/store/1igd397cnlj9z0ksn381sqghrmhglp49-python3.12-psycopg2-2.9.10.drv", + "description": "PostgreSQL database adapter for the Python programming language", + "install_id": "psycopg2", + "license": "[ LGPL-3.0-or-later, ZPL-2.0 ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-psycopg2-2.9.10", + "pname": "psycopg2", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:52:05.470710Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3.12-psycopg2-2.9.10", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/jk2gg3r12pdq9nfcnqqjklrdis1il2yx-python3.12-psycopg2-2.9.10-dist", + "doc": "/nix/store/q69nfvcl9y6j0s57qfz4xlp842kiqlk3-python3.12-psycopg2-2.9.10-doc", + "out": "/nix/store/g4hf7rhbm43qza92v3ln46zhdn6gx006-python3.12-psycopg2-2.9.10" + }, + "system": "x86_64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.pytest", + "broken": false, + "derivation": "/nix/store/4hjqzk5sakb5xfmvzjg671kgci1rknck-python3.12-pytest-8.4.1.drv", + "description": "Framework for writing tests", + "install_id": "pytest", + "license": "MIT", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pytest-8.4.1", + "pname": "pytest", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:26:20.302363Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "8.4.1", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/9n5scdgf4ri1mxk90zabpy9jv88h27z4-python3.12-pytest-8.4.1-dist", + "out": "/nix/store/5imkz6mw3936s8wawn71i1iinq4zxh4l-python3.12-pytest-8.4.1", + "testout": "/nix/store/vyp6788wds35pp2rq01wl4dskhs3gcdj-python3.12-pytest-8.4.1-testout" + }, + "system": "aarch64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.pytest", + "broken": false, + "derivation": "/nix/store/i6nz6zb54lji5i1ivnky4c1ya3m048s6-python3.12-pytest-8.4.1.drv", + "description": "Framework for writing tests", + "install_id": "pytest", + "license": "MIT", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pytest-8.4.1", + "pname": "pytest", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:35:07.574632Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "8.4.1", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/fk10nyiwsaxpqyi9wf13i3156r10awb1-python3.12-pytest-8.4.1-dist", + "out": "/nix/store/hqsrkwskfh8q9zc6l9bggy7zahfa9y6w-python3.12-pytest-8.4.1", + "testout": "/nix/store/dahn4x0ji0rl95c617bz6rjhd6sjiyrb-python3.12-pytest-8.4.1-testout" + }, + "system": "aarch64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.pytest", + "broken": false, + "derivation": "/nix/store/bf5k292in4zd6wp2iib3xgz3jdrga9fz-python3.12-pytest-8.4.1.drv", + "description": "Framework for writing tests", + "install_id": "pytest", + "license": "MIT", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pytest-8.4.1", + "pname": "pytest", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:44:12.173575Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "8.4.1", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/wla07j5gfrvzyv4s53dw509c9c85fk76-python3.12-pytest-8.4.1-dist", + "out": "/nix/store/nc05alv8nnydj0rc2pc7k6y7y03jcvxk-python3.12-pytest-8.4.1", + "testout": "/nix/store/81n7kzi6zglzhl4hslvd7bxixl9mpc30-python3.12-pytest-8.4.1-testout" + }, + "system": "x86_64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Packages.pytest", + "broken": false, + "derivation": "/nix/store/xsjw0p8899g5j7y6bhm8pgds58c92ipw-python3.12-pytest-8.4.1.drv", + "description": "Framework for writing tests", + "install_id": "pytest", + "license": "MIT", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3.12-pytest-8.4.1", + "pname": "pytest", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:52:07.254188Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "8.4.1", + "outputs_to_install": [ + "out" + ], + "outputs": { + "dist": "/nix/store/2q6nfg0df9p5fr44s9m30h7l6pm2yb94-python3.12-pytest-8.4.1-dist", + "out": "/nix/store/ggk45c8i53zyrs8w9vy7l4bqmmsjjjlc-python3.12-pytest-8.4.1", + "testout": "/nix/store/21giixazd8iyl84w6n097gw5rflsz0rc-python3.12-pytest-8.4.1-testout" + }, + "system": "x86_64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Full", + "broken": false, + "derivation": "/nix/store/x6c9qj50npdybsz6hp59xmki89jafj6d-python3-3.12.11.drv", + "description": "High-level dynamically-typed programming language", + "install_id": "python312Full", + "license": "Python-2.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3-3.12.11", + "pname": "python312Full", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:26:13.141205Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3-3.12.11", + "outputs_to_install": [ + "out" + ], + "outputs": { + "out": "/nix/store/03p3nrkwhpzjr55dfcxhjq1hd3cg4pwy-python3-3.12.11" + }, + "system": "aarch64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Full", + "broken": false, + "derivation": "/nix/store/5siqr82ark2r66lhq3wxymgij7sl3s68-python3-3.12.11.drv", + "description": "High-level dynamically-typed programming language", + "install_id": "python312Full", + "license": "Python-2.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3-3.12.11", + "pname": "python312Full", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:34:54.887531Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3-3.12.11", + "outputs_to_install": [ + "out" + ], + "outputs": { + "debug": "/nix/store/q70n8zbzknvnaqp4l0bwx5y3kazwwnj2-python3-3.12.11-debug", + "out": "/nix/store/jij9z6imnjl6ns2x3fr0cgfssy3j273d-python3-3.12.11" + }, + "system": "aarch64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Full", + "broken": false, + "derivation": "/nix/store/axj8r2hnwjj941y08qn3jq5rrxcvczvl-python3-3.12.11.drv", + "description": "High-level dynamically-typed programming language", + "install_id": "python312Full", + "license": "Python-2.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3-3.12.11", + "pname": "python312Full", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:44:04.927920Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3-3.12.11", + "outputs_to_install": [ + "out" + ], + "outputs": { + "out": "/nix/store/y3z25yr49hyabwjgjxn45v8i97wrgi25-python3-3.12.11" + }, + "system": "x86_64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "python312Full", + "broken": false, + "derivation": "/nix/store/hhjkb1fmbi3h9kn1jpqjd7gmq2jbfrw3-python3-3.12.11.drv", + "description": "High-level dynamically-typed programming language", + "install_id": "python312Full", + "license": "Python-2.0", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "python3-3.12.11", + "pname": "python312Full", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:51:55.789109Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "python3-3.12.11", + "outputs_to_install": [ + "out" + ], + "outputs": { + "debug": "/nix/store/xv76sfvnw9zi996yxbz8jyk6dj0ijk29-python3-3.12.11-debug", + "out": "/nix/store/a4vblf4lkfvdrysnzh7ir9m61irk70d0-python3-3.12.11" + }, + "system": "x86_64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "uv", + "broken": false, + "derivation": "/nix/store/g2kfc67p7fg7q5zpri6432d36ak1bsj3-uv-0.8.6.drv", + "description": "Extremely fast Python package installer and resolver, written in Rust", + "install_id": "uv", + "license": "[ Apache-2.0, MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "uv-0.8.6", + "pname": "uv", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:27:20.423821Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "0.8.6", + "outputs_to_install": [ + "out" + ], + "outputs": { + "out": "/nix/store/hxazvw40v90nic1yjx23cdkj6hqqlywa-uv-0.8.6" + }, + "system": "aarch64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "uv", + "broken": false, + "derivation": "/nix/store/5m6laj37dkg7jpxmr69dnb3q2m422d1a-uv-0.8.6.drv", + "description": "Extremely fast Python package installer and resolver, written in Rust", + "install_id": "uv", + "license": "[ Apache-2.0, MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "uv-0.8.6", + "pname": "uv", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:36:40.649674Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "0.8.6", + "outputs_to_install": [ + "out" + ], + "outputs": { + "out": "/nix/store/li6y1v2mjj30fd5prwkawidq5lcn9ja9-uv-0.8.6" + }, + "system": "aarch64-linux", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "uv", + "broken": false, + "derivation": "/nix/store/324hhmwqzqphn6vq37i4zkv3glamr2vg-uv-0.8.6.drv", + "description": "Extremely fast Python package installer and resolver, written in Rust", + "install_id": "uv", + "license": "[ Apache-2.0, MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "uv-0.8.6", + "pname": "uv", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:45:11.571680Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "0.8.6", + "outputs_to_install": [ + "out" + ], + "outputs": { + "out": "/nix/store/l37rdvvgc46jpydmyrwlxfhrslbfcnpm-uv-0.8.6" + }, + "system": "x86_64-darwin", + "group": "toplevel", + "priority": 5 + }, + { + "attr_path": "uv", + "broken": false, + "derivation": "/nix/store/fsv3g2p4y7c04678a0bpk6yparh2ysb7-uv-0.8.6.drv", + "description": "Extremely fast Python package installer and resolver, written in Rust", + "install_id": "uv", + "license": "[ Apache-2.0, MIT ]", + "locked_url": "https://github.com/flox/nixpkgs?rev=8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "name": "uv-0.8.6", + "pname": "uv", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev_count": 852432, + "rev_date": "2025-08-27T03:49:43Z", + "scrape_date": "2025-08-28T03:53:31.236928Z", + "stabilities": [ + "unstable", + "staging", + "stable" + ], + "unfree": false, + "version": "0.8.6", + "outputs_to_install": [ + "out", + "out", + "out" + ], + "outputs": { + "out": "/nix/store/hxvcjq4a053l4gm8asl4ynjyhlz2azib-uv-0.8.6" + }, + "system": "x86_64-linux", + "group": "toplevel", + "priority": 5 + } + ] +} diff --git a/testing/.flox/env/manifest.toml b/testing/.flox/env/manifest.toml new file mode 100644 index 0000000..a83fe09 --- /dev/null +++ b/testing/.flox/env/manifest.toml @@ -0,0 +1,100 @@ +schema-version = "1.10.0" + + +## Install Packages -------------------------------------------------- +## $ flox install gum <- puts a package in [install] section below +## $ flox search gum <- search for a package +## $ flox show gum <- show all versions of a package +## ------------------------------------------------------------------- +[install] +pip.pkg-path = "python312Packages.pip" +python312Full.pkg-path = "python312Full" +uv.pkg-path = "uv" +psycopg2.pkg-path = "python312Packages.psycopg2" +pytest.pkg-path = "python312Packages.pytest" +# gum.pkg-path = "gum" +# gum.version = "^0.14.5" + + +## Environment Variables --------------------------------------------- +## ... available for use in the activated environment +## as well as [hook], [profile] scripts and [services] below. +## ------------------------------------------------------------------- +[vars] +# INTRO_MESSAGE = "It's gettin' Flox in here" + + +## Activation Hook --------------------------------------------------- +## ... run by _bash_ shell when you run 'flox activate'. +## ------------------------------------------------------------------- +[hook] +# on-activate = ''' +# # -> Set variables, create files and directories +# # -> Perform initialization steps, e.g. create a python venv +# # -> Useful environment variables: +# # - FLOX_ENV_PROJECT=/home/user/example +# # - FLOX_ENV=/home/user/example/.flox/run +# # - FLOX_ENV_CACHE=/home/user/example/.flox/cache +# ''' + + +## Profile script ---------------------------------------------------- +## ... sourced by _your shell_ when you run 'flox activate'. +## ------------------------------------------------------------------- +[profile] +# common = ''' +# gum style \ +# --foreground 212 --border-foreground 212 --border double \ +# --align center --width 50 --margin "1 2" --padding "2 4" \ +# $INTRO_MESSAGE +# ''' +## Shell-specific customizations such as setting aliases go here: +# bash = ... +# zsh = ... +# fish = ... + + +## Services --------------------------------------------------------- +## $ flox services start <- Starts all services +## $ flox services status <- Status of running services +## $ flox activate --start-services <- Activates & starts all +## ------------------------------------------------------------------ +[services] +# myservice.command = "python3 -m http.server" + + +## Include ---------------------------------------------------------- +## ... environments to create a composed environment +## ------------------------------------------------------------------ +[include] +# environments = [ +# { dir = "../common" } +# ] + + +## Build and publish your own packages ------------------------------ +## $ flox build +## $ flox publish +## ------------------------------------------------------------------ +[build] +# [build.myproject] +# description = "The coolest project ever" +# version = "0.0.1" +# command = """ +# mkdir -p $out/bin +# cargo build --release +# cp target/release/myproject $out/bin/myproject +# """ + + +## Other Environment Options ----------------------------------------- +[options] +# Systems that environment is compatible with +# systems = [ +# "aarch64-darwin", +# "aarch64-linux", +# "x86_64-darwin", +# "x86_64-linux", +# ] +# Uncomment to disable CUDA detection. +# cuda-detection = false diff --git a/testing/.flox/pip.ini b/testing/.flox/pip.ini new file mode 100644 index 0000000..64f805d --- /dev/null +++ b/testing/.flox/pip.ini @@ -0,0 +1,2 @@ + [global] + require-virtualenv = true diff --git a/testing/integration/pyproject.toml b/testing/integration/pyproject.toml index 8296390..51e2202 100644 --- a/testing/integration/pyproject.toml +++ b/testing/integration/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Python integration harness for pgFirstAid health checks" requires-python = ">=3.11" dependencies = [ - "psycopg[binary]>=3.2.0", + "psycopg2-binary>=2.9.11", "pytest>=8.3.0", ] diff --git a/testing/integration/src/pgfirstaid_pytest/config.py b/testing/integration/src/pgfirstaid_pytest/config.py index 7c7cbbf..3ea24b7 100644 --- a/testing/integration/src/pgfirstaid_pytest/config.py +++ b/testing/integration/src/pgfirstaid_pytest/config.py @@ -2,6 +2,9 @@ import os +_REQUIRED_ENV_VARS = ("PGHOST", "PGPORT", "PGUSER", "PGDATABASE") + + @dataclass(frozen=True) class TestConfig: host: str @@ -16,12 +19,21 @@ class TestConfig: @classmethod def from_env(cls) -> "TestConfig": + missing = [name for name in _REQUIRED_ENV_VARS if not os.getenv(name)] + if missing: + missing_list = ", ".join(missing) + raise ValueError( + "Missing required PostgreSQL environment variables: " + f"{missing_list}. Set the standard PG* connection variables before " + "running integration tests." + ) + return cls( - host=os.getenv("PGHOST", "localhost"), - port=int(os.getenv("PGPORT", "5432")), - user=os.getenv("PGUSER", "postgres"), + host=os.environ["PGHOST"], + port=int(os.environ["PGPORT"]), + user=os.environ["PGUSER"], password=os.getenv("PGPASSWORD"), - database=os.getenv("PGDATABASE", "postgres"), + database=os.environ["PGDATABASE"], sslmode=os.getenv("PGSSLMODE"), active_conn_target=int(os.getenv("PGFA_TEST_ACTIVE_CONN_TARGET", "52")), active_conn_sleep_seconds=int( diff --git a/testing/integration/src/pgfirstaid_pytest/db.py b/testing/integration/src/pgfirstaid_pytest/db.py index 9581c67..23976b5 100644 --- a/testing/integration/src/pgfirstaid_pytest/db.py +++ b/testing/integration/src/pgfirstaid_pytest/db.py @@ -1,24 +1,26 @@ +from contextlib import contextmanager from pathlib import Path import time -from typing import Any +from typing import Any, Iterator -import psycopg +import psycopg2 +from psycopg2.extensions import connection as PgConnection from .config import TestConfig +@contextmanager def connect( config: TestConfig, *, autocommit: bool = True, application_name: str | None = None, -) -> psycopg.Connection[Any]: +) -> Iterator[PgConnection]: kwargs: dict[str, Any] = { "host": config.host, "port": config.port, "user": config.user, "dbname": config.database, - "autocommit": autocommit, } if config.password is not None: kwargs["password"] = config.password @@ -26,17 +28,23 @@ def connect( kwargs["sslmode"] = config.sslmode if application_name is not None: kwargs["application_name"] = application_name - return psycopg.connect(**kwargs) + conn = psycopg2.connect(**kwargs) + conn.autocommit = autocommit + try: + yield conn + finally: + conn.close() -def execute_sql_file(conn: psycopg.Connection[Any], file_path: Path) -> None: + +def execute_sql_file(conn: PgConnection, file_path: Path) -> None: sql_text = file_path.read_text(encoding="utf-8") with conn.cursor() as cur: cur.execute(sql_text) def wait_for_sql_true( - conn: psycopg.Connection[Any], + conn: PgConnection, sql: str, params: tuple[Any, ...] | None = None, *, diff --git a/testing/integration/tests/conftest.py b/testing/integration/tests/conftest.py index 0403477..5f8eeab 100644 --- a/testing/integration/tests/conftest.py +++ b/testing/integration/tests/conftest.py @@ -1,8 +1,10 @@ from pathlib import Path import sys from typing import Iterator + import pytest -import psycopg +import psycopg2 +from psycopg2.extensions import connection as PgConnection sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -39,13 +41,13 @@ def prepared_database(config: TestConfig) -> Iterator[None]: def db_conn( config: TestConfig, prepared_database: None, -) -> Iterator[psycopg.Connection[tuple[object, ...]]]: +) -> Iterator[PgConnection]: with connect(config, autocommit=True) as conn: yield conn @pytest.fixture -def test_schema(db_conn: psycopg.Connection[tuple[object, ...]]) -> Iterator[str]: +def test_schema(db_conn: PgConnection) -> Iterator[str]: schema_name = "pgfirstaid_pytest" with db_conn.cursor() as cur: cur.execute(f"CREATE SCHEMA IF NOT EXISTS {schema_name}") diff --git a/testing/integration/tests/integration/test_health_checks.py b/testing/integration/tests/integration/test_health_checks.py index c2a129f..21cbe2e 100644 --- a/testing/integration/tests/integration/test_health_checks.py +++ b/testing/integration/tests/integration/test_health_checks.py @@ -2,7 +2,8 @@ import threading import time -import psycopg +import psycopg2 +from psycopg2.extensions import connection as PgConnection import pytest from pgfirstaid_pytest import TestConfig as PgConfig @@ -27,13 +28,13 @@ def _start_active_connection( with conn.cursor() as cur: cur.execute("SELECT pg_sleep(%s)", (sleep_seconds,)) return True - except psycopg.Error: + except psycopg2.Error: return False @pytest.mark.integration def test_outdated_statistics_detected( - db_conn: psycopg.Connection[tuple[object, ...]], + db_conn: PgConnection, test_schema: str, config: PgConfig, ) -> None: @@ -106,7 +107,7 @@ def test_outdated_statistics_detected( @pytest.mark.integration @pytest.mark.slow def test_high_connection_count_detected( - db_conn: psycopg.Connection[tuple[object, ...]], + db_conn: PgConnection, config: PgConfig, ) -> None: start_event = threading.Event() diff --git a/testing/integration/tests/integration/test_pgtap_suite.py b/testing/integration/tests/integration/test_pgtap_suite.py index 4fa2b09..b714cbd 100644 --- a/testing/integration/tests/integration/test_pgtap_suite.py +++ b/testing/integration/tests/integration/test_pgtap_suite.py @@ -5,7 +5,7 @@ import re import subprocess -import psycopg +from psycopg2.extensions import connection as PgConnection import pytest from pgfirstaid_pytest import TestConfig as PgConfig @@ -69,7 +69,7 @@ def _tap_failures(output: str) -> list[str]: def test_pgtap_sql_file_passes( config: PgConfig, prepared_database: None, - db_conn: psycopg.Connection[tuple[object, ...]], + db_conn: PgConnection, pgtap_file: Path, ) -> None: _ = prepared_database @@ -151,7 +151,7 @@ def test_both_view_sql_files_cover_all_health_checks() -> None: @pytest.mark.integration @pytest.mark.parametrize("view_sql", _view_sql_files()) def test_view_variant_matches_function_check_names( - db_conn: psycopg.Connection[tuple[object, ...]], + db_conn: PgConnection, view_sql: Path, ) -> None: execute_sql_file(db_conn, view_sql) @@ -189,7 +189,7 @@ def test_view_variant_matches_function_check_names( @pytest.mark.integration def test_view_parity_for_all_health_checks( - db_conn: psycopg.Connection[tuple[object, ...]], + db_conn: PgConnection, ) -> None: with db_conn.cursor() as cur: cur.execute( diff --git a/testing/integration/tests/test_config.py b/testing/integration/tests/test_config.py new file mode 100644 index 0000000..0465826 --- /dev/null +++ b/testing/integration/tests/test_config.py @@ -0,0 +1,33 @@ +import pytest + +from pgfirstaid_pytest import TestConfig + + +def test_from_env_requires_connection_variables( + monkeypatch: pytest.MonkeyPatch, +) -> None: + for name in ("PGHOST", "PGPORT", "PGUSER", "PGDATABASE", "PGPASSWORD"): + monkeypatch.delenv(name, raising=False) + + with pytest.raises( + ValueError, match="Missing required PostgreSQL environment variables" + ): + TestConfig.from_env() + + +def test_from_env_reads_standard_pg_variables(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PGHOST", "db.example.test") + monkeypatch.setenv("PGPORT", "6543") + monkeypatch.setenv("PGUSER", "pgfirstaid") + monkeypatch.setenv("PGDATABASE", "appdb") + monkeypatch.setenv("PGPASSWORD", "secret") + monkeypatch.setenv("PGSSLMODE", "require") + + config = TestConfig.from_env() + + assert config.host == "db.example.test" + assert config.port == 6543 + assert config.user == "pgfirstaid" + assert config.password == "secret" + assert config.database == "appdb" + assert config.sslmode == "require" diff --git a/testing/integration/uv.lock b/testing/integration/uv.lock index 449ba4c..cabf8d5 100644 --- a/testing/integration/uv.lock +++ b/testing/integration/uv.lock @@ -34,13 +34,13 @@ name = "pgfirstaid-python-tests" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "psycopg", extra = ["binary"] }, + { name = "psycopg2-binary" }, { name = "pytest" }, ] [package.metadata] requires-dist = [ - { name = "psycopg", extras = ["binary"], specifier = ">=3.2.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pytest", specifier = ">=8.3.0" }, ] @@ -54,72 +54,55 @@ wheels = [ ] [[package]] -name = "psycopg" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" }, -] - -[package.optional-dependencies] -binary = [ - { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, -] - -[[package]] -name = "psycopg-binary" -version = "3.3.2" +name = "psycopg2-binary" +version = "2.9.11" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" }, - { url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" }, - { url = "https://files.pythonhosted.org/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" }, - { url = "https://files.pythonhosted.org/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" }, - { url = "https://files.pythonhosted.org/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" }, - { url = "https://files.pythonhosted.org/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" }, - { url = "https://files.pythonhosted.org/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" }, - { url = "https://files.pythonhosted.org/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" }, - { url = "https://files.pythonhosted.org/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" }, - { url = "https://files.pythonhosted.org/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" }, - { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" }, - { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" }, - { url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" }, - { url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" }, - { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" }, - { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" }, - { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" }, - { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" }, - { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" }, - { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" }, - { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" }, - { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" }, - { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" }, - { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" }, - { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" }, - { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, ] [[package]] @@ -146,21 +129,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049dd wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, -] From 037ce1552b7badcc28b9ad46e3c0f135c3281fc4 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 15:19:55 -0600 Subject: [PATCH 17/26] fix: incorrect teardown order --- .../tests/integration/test_pgtap_suite.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/testing/integration/tests/integration/test_pgtap_suite.py b/testing/integration/tests/integration/test_pgtap_suite.py index b714cbd..b4907c5 100644 --- a/testing/integration/tests/integration/test_pgtap_suite.py +++ b/testing/integration/tests/integration/test_pgtap_suite.py @@ -20,6 +20,14 @@ def _pgtap_dir() -> Path: return _repo_root() / "testing" / "pgTAP" +def _pgtap_setup_sql() -> Path: + return _pgtap_dir() / "00_setup.sql" + + +def _pgtap_teardown_sql() -> Path: + return _pgtap_dir() / "99_teardown.sql" + + def _pgtap_test_files() -> list[Path]: return sorted(_pgtap_dir().glob("0[1-9]_*.sql")) @@ -88,6 +96,22 @@ def test_pgtap_sql_file_passes( ) +@pytest.mark.integration +def test_session_teardown_handles_installed_objects(db_conn: PgConnection) -> None: + # The shared teardown runs at session end, after pg_firstAid() and v_pgfirstaid + # have been installed for the integration suite. + execute_sql_file(db_conn, _pgtap_setup_sql()) + execute_sql_file(db_conn, _repo_root() / "pgFirstAid.sql") + execute_sql_file(db_conn, _default_view_sql()) + + try: + execute_sql_file(db_conn, _pgtap_teardown_sql()) + finally: + execute_sql_file(db_conn, _pgtap_setup_sql()) + execute_sql_file(db_conn, _repo_root() / "pgFirstAid.sql") + execute_sql_file(db_conn, _default_view_sql()) + + def _extract_check_names_from_source(sql_text: str) -> set[str]: return set( re.findall(r"'([^']+)'\s+as\s+check_name", sql_text, flags=re.IGNORECASE) From 2c6c7b265daded63b31b13d7adae8df90ba2f28f Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 15:20:06 -0600 Subject: [PATCH 18/26] fix: incorrect teardown order --- testing/pgTAP/99_teardown.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/pgTAP/99_teardown.sql b/testing/pgTAP/99_teardown.sql index cfa399f..23f6af1 100644 --- a/testing/pgTAP/99_teardown.sql +++ b/testing/pgTAP/99_teardown.sql @@ -1,3 +1,5 @@ -- Session-level pgTAP teardown for Python harness DROP SCHEMA IF EXISTS pgfirstaid_test CASCADE; +DROP VIEW IF EXISTS v_pgfirstaid; +DROP FUNCTION IF EXISTS pg_firstAid(); DROP FUNCTION IF EXISTS _pg_firstaid_checkpoint_stats(); From 9af50186bc9cbc88bd555c2e3bef50d7992f5704 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 16:20:50 -0600 Subject: [PATCH 19/26] fix: resolve integration and pgTap issues --- pgFirstAid.sql | 6 +- .../tests/integration/test_pgtap_suite.py | 59 +++++++++++++++++++ view_pgFirstAid.sql | 24 +++++++- view_pgFirstAid_managed.sql | 50 ++++++++++++---- 4 files changed, 124 insertions(+), 15 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 0c6cd1d..d482451 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1552,7 +1552,11 @@ select 'Transaction ID Wraparound Risk' as check_name, datname as object_name, 'Age of the oldest unfrozen transaction ID in this database. PostgreSQL must freeze XIDs before reaching ~2.1 billion to prevent data loss from wraparound.' as issue_description, - datname || ': XID age ' || age(datfrozenxid)::text as current_value, + datname || ': XID age ' || trim(to_char(age(datfrozenxid), 'FM999,999,999,990')) || + ' (' || round(age(datfrozenxid)::numeric * 100 / 2000000000, 1)::text || + '% of wraparound window, ~' || + trim(to_char(greatest(2000000000::bigint - age(datfrozenxid)::bigint, 0), 'FM999,999,999,990')) || + ' remaining)' as current_value, 'Run VACUUM FREEZE on databases approaching high XID age. Ensure autovacuum is enabled and not blocked. Monitor databases with age > 500,000,000.' as recommended_action, 'https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND' as documentation_link, 5 as severity_order diff --git a/testing/integration/tests/integration/test_pgtap_suite.py b/testing/integration/tests/integration/test_pgtap_suite.py index b4907c5..727ec4c 100644 --- a/testing/integration/tests/integration/test_pgtap_suite.py +++ b/testing/integration/tests/integration/test_pgtap_suite.py @@ -242,3 +242,62 @@ def test_view_parity_for_all_health_checks( "These checks exist in v_pgfirstaid but are missing in pg_firstAid(): " + ", ".join(extra_in_view) ) + + +@pytest.mark.integration +def test_view_matches_function_row_order( + db_conn: PgConnection, +) -> None: + with db_conn.cursor() as cur: + cur.execute( + """ + SELECT severity, category, check_name + FROM pg_firstAid() + """ + ) + function_rows = [tuple(str(value) for value in row) for row in cur.fetchall()] + + cur.execute( + """ + SELECT severity, category, check_name + FROM v_pgfirstaid + """ + ) + view_rows = [tuple(str(value) for value in row) for row in cur.fetchall()] + + assert function_rows == view_rows + + +@pytest.mark.integration +def test_wraparound_risk_current_value_is_human_readable( + db_conn: PgConnection, +) -> None: + expected_pattern = re.compile( + r"^[^:]+: XID age [\d,]+ \([\d.]+% of wraparound window, ~[\d,]+ remaining\)$" + ) + + with db_conn.cursor() as cur: + cur.execute( + """ + SELECT current_value + FROM pg_firstAid() + WHERE check_name = 'Transaction ID Wraparound Risk' + """ + ) + function_values = [str(row[0]) for row in cur.fetchall()] + + cur.execute( + """ + SELECT current_value + FROM v_pgfirstaid + WHERE check_name = 'Transaction ID Wraparound Risk' + """ + ) + view_values = [str(row[0]) for row in cur.fetchall()] + + assert function_values, "Expected wraparound risk rows from pg_firstAid()" + assert view_values, "Expected wraparound risk rows from v_pgfirstaid" + assert all(expected_pattern.match(value) for value in function_values), ( + function_values + ) + assert all(expected_pattern.match(value) for value in view_values), view_values diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index 514edf5..ae5427e 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -272,6 +272,17 @@ $$ language plpgsql; drop view if exists v_pgfirstAid; create view v_pgfirstAid as +select + health_results.severity, + health_results.category, + health_results.check_name, + health_results.object_name, + health_results.issue_description, + health_results.current_value, + health_results.recommended_action, + health_results.documentation_link, + health_results.severity_order +from ( -- CRITICAL: Tables without primary keys select 'CRITICAL' as severity, @@ -1476,7 +1487,11 @@ select 'Transaction ID Wraparound Risk' as check_name, datname as object_name, 'Age of the oldest unfrozen transaction ID in this database. PostgreSQL must freeze XIDs before reaching ~2.1 billion to prevent data loss from wraparound.' as issue_description, - datname || ': XID age ' || age(datfrozenxid)::text as current_value, + datname || ': XID age ' || trim(to_char(age(datfrozenxid), 'FM999,999,999,990')) || + ' (' || round(age(datfrozenxid)::numeric * 100 / 2000000000, 1)::text || + '% of wraparound window, ~' || + trim(to_char(greatest(2000000000::bigint - age(datfrozenxid)::bigint, 0), 'FM999,999,999,990')) || + ' remaining)' as current_value, 'Run VACUUM FREEZE on databases approaching high XID age. Ensure autovacuum is enabled and not blocked. Monitor databases with age > 500,000,000.' as recommended_action, 'https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND' as documentation_link, 5 as severity_order @@ -1525,4 +1540,9 @@ select 'https://www.postgresql.org/docs/current/runtime-config-connection.html' as documentation_link, 5 as severity_order from - pg_stat_activity; + pg_stat_activity +) health_results +order by + health_results.severity_order, + health_results.category, + health_results.check_name; diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 6ab4049..3becbfd 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -272,6 +272,17 @@ $$ language plpgsql; drop view if exists v_pgfirstAid; create view v_pgfirstAid as +select + health_results.severity, + health_results.category, + health_results.check_name, + health_results.object_name, + health_results.issue_description, + health_results.current_value, + health_results.recommended_action, + health_results.documentation_link, + health_results.severity_order +from ( -- CRITICAL: Tables without primary keys select 'CRITICAL' as severity, @@ -557,25 +568,31 @@ from ts) union all -- HIGH: Duplicate or redundant indexes +-- Compare actual index structure (columns, operator class) not string definitions select 'HIGH' as severity, 'Table Health' as category, 'Duplicate Index' as check_name, - quote_ident(i1.schemaname) || '.' || i1.indexname || ' & ' || i2.indexname as object_name, - 'Multiple indexes with identical or overlapping column sets' as issue_description, - 'Indexes: ' || i1.indexname || ', ' || i2.indexname as current_value, + quote_ident(n1.nspname) || '.' || c1.relname || ': ' || i1.relname || ' & ' || i2.relname as object_name, + 'Multiple indexes with identical column sets and operator classes' as issue_description, + 'Indexes: ' || i1.relname || ', ' || i2.relname as current_value, 'Review and consolidate duplicate indexes and focus on keeping the most efficient one' as recommended_action, 'https://www.postgresql.org/docs/current/indexes-multicolumn.html' as documentation_link, 2 as severity_order from - pg_indexes i1 -join pg_indexes i2 on - i1.schemaname = i2.schemaname - and i1.tablename = i2.tablename - and i1.indexname < i2.indexname - and i1.indexdef = i2.indexdef + pg_index idx1 +join pg_class i1 on idx1.indexrelid = i1.oid +join pg_class c1 on idx1.indrelid = c1.oid +join pg_namespace n1 on c1.relnamespace = n1.oid +join pg_index idx2 on + idx1.indrelid = idx2.indrelid + and idx1.indexrelid < idx2.indexrelid + and idx1.indkey = idx2.indkey + and idx1.indclass = idx2.indclass + and idx1.indoption = idx2.indoption +join pg_class i2 on idx2.indexrelid = i2.oid where - i1.schemaname not like all(array['information_schema', 'pg_catalog', 'pg_toast', 'pg_temp%']) + n1.nspname not like all(array['information_schema', 'pg_catalog', 'pg_toast', 'pg_temp%']) union all -- HIGH: Table with more than 200 columns (with cc as ( @@ -1459,7 +1476,11 @@ select 'Transaction ID Wraparound Risk' as check_name, datname as object_name, 'Age of the oldest unfrozen transaction ID in this database. PostgreSQL must freeze XIDs before reaching ~2.1 billion to prevent data loss from wraparound.' as issue_description, - datname || ': XID age ' || age(datfrozenxid)::text as current_value, + datname || ': XID age ' || trim(to_char(age(datfrozenxid), 'FM999,999,999,990')) || + ' (' || round(age(datfrozenxid)::numeric * 100 / 2000000000, 1)::text || + '% of wraparound window, ~' || + trim(to_char(greatest(2000000000::bigint - age(datfrozenxid)::bigint, 0), 'FM999,999,999,990')) || + ' remaining)' as current_value, 'Run VACUUM FREEZE on databases approaching high XID age. Ensure autovacuum is enabled and not blocked. Monitor databases with age > 500,000,000.' as recommended_action, 'https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND' as documentation_link, 5 as severity_order @@ -1508,4 +1529,9 @@ select 'https://www.postgresql.org/docs/current/runtime-config-connection.html' as documentation_link, 5 as severity_order from - pg_stat_activity; + pg_stat_activity +) health_results +order by + health_results.severity_order, + health_results.category, + health_results.check_name; From 0f7d545f352301184548e269848ab34485aa2e70 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 17:01:42 -0600 Subject: [PATCH 20/26] fix: ordering, missing function, nitpicks --- .gitignore | 8 ++ pgFirstAid.sql | 9 +- .../tests/integration/test_pgtap_suite.py | 103 ++++++++++++++++++ view_pgFirstAid.sql | 39 ++++++- view_pgFirstAid_managed.sql | 43 +++++++- 5 files changed, 195 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 9392653..5903727 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,17 @@ terraform.tfstate terraform.tfstate.backup *.tfvars +*.tfvars.json !terraform.tfvars.example *.tfstate +*.tfstate.* *.tfstate.backup +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json # Sensitive files *.pem *.key diff --git a/pgFirstAid.sql b/pgFirstAid.sql index d482451..6be916f 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -1572,9 +1572,14 @@ select 'System Health' as category, 'Checkpoint Stats' as check_name, 'System' as object_name, - 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, + 'Checkpoint activity since stats last reset. Forced checkpoints occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small. PG15/16 reads from pg_stat_bgwriter; PG17+ reads from pg_stat_checkpointer.' as issue_description, _pg_firstaid_checkpoint_stats() as current_value, - 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, + 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''' || + case + when current_setting('server_version_num')::int >= 170000 then 'checkpointer' + else 'bgwriter' + end || + ''').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order; diff --git a/testing/integration/tests/integration/test_pgtap_suite.py b/testing/integration/tests/integration/test_pgtap_suite.py index 727ec4c..bbdc26c 100644 --- a/testing/integration/tests/integration/test_pgtap_suite.py +++ b/testing/integration/tests/integration/test_pgtap_suite.py @@ -211,6 +211,71 @@ def test_view_variant_matches_function_check_names( execute_sql_file(db_conn, _default_view_sql()) +@pytest.mark.integration +@pytest.mark.parametrize("view_sql", _view_sql_files()) +def test_view_variant_installs_standalone( + db_conn: PgConnection, + view_sql: Path, +) -> None: + execute_sql_file(db_conn, _pgtap_teardown_sql()) + execute_sql_file(db_conn, _pgtap_setup_sql()) + + try: + execute_sql_file(db_conn, view_sql) + + with db_conn.cursor() as cur: + cur.execute("SELECT count(*) >= 0 FROM v_pgfirstaid") + assert cur.fetchone()[0] is True + finally: + execute_sql_file(db_conn, _pgtap_setup_sql()) + execute_sql_file(db_conn, _repo_root() / "pgFirstAid.sql") + execute_sql_file(db_conn, _default_view_sql()) + + +@pytest.mark.integration +def test_managed_view_duplicate_index_check_ignores_partial_indexes( + db_conn: PgConnection, + test_schema: str, +) -> None: + table_name = f"{test_schema}.partial_index_table" + managed_view = _repo_root() / "view_pgFirstAid_managed.sql" + + with db_conn.cursor() as cur: + cur.execute( + f""" + CREATE TABLE {table_name} ( + id serial PRIMARY KEY, + value integer NOT NULL + ) + """ + ) + cur.execute( + f"CREATE INDEX partial_idx_a ON {table_name} (value) WHERE value > 0" + ) + cur.execute( + f"CREATE INDEX partial_idx_b ON {table_name} (value) WHERE value > 10" + ) + + execute_sql_file(db_conn, managed_view) + + try: + with db_conn.cursor() as cur: + cur.execute( + """ + SELECT EXISTS ( + SELECT 1 + FROM v_pgfirstaid + WHERE check_name = 'Duplicate Index' + AND object_name LIKE %s + ) + """, + (f"{test_schema}.partial_index_table:%",), + ) + assert cur.fetchone()[0] is False + finally: + execute_sql_file(db_conn, _default_view_sql()) + + @pytest.mark.integration def test_view_parity_for_all_health_checks( db_conn: PgConnection, @@ -253,6 +318,7 @@ def test_view_matches_function_row_order( """ SELECT severity, category, check_name FROM pg_firstAid() + ORDER BY severity, category, check_name """ ) function_rows = [tuple(str(value) for value in row) for row in cur.fetchall()] @@ -261,6 +327,7 @@ def test_view_matches_function_row_order( """ SELECT severity, category, check_name FROM v_pgfirstaid + ORDER BY severity, category, check_name """ ) view_rows = [tuple(str(value) for value in row) for row in cur.fetchall()] @@ -301,3 +368,39 @@ def test_wraparound_risk_current_value_is_human_readable( function_values ) assert all(expected_pattern.match(value) for value in view_values), view_values + + +@pytest.mark.integration +def test_checkpoint_stats_guidance_matches_server_version( + db_conn: PgConnection, +) -> None: + with db_conn.cursor() as cur: + cur.execute("SELECT current_setting('server_version_num')::int") + server_version_num = cur.fetchone()[0] + + cur.execute( + """ + SELECT recommended_action + FROM pg_firstAid() + WHERE check_name = 'Checkpoint Stats' + """ + ) + function_action = str(cur.fetchone()[0]) + + cur.execute( + """ + SELECT recommended_action + FROM v_pgfirstaid + WHERE check_name = 'Checkpoint Stats' + """ + ) + view_action = str(cur.fetchone()[0]) + + expected_reset = ( + "pg_stat_reset_shared('checkpointer')" + if server_version_num >= 170000 + else "pg_stat_reset_shared('bgwriter')" + ) + + assert expected_reset in function_action + assert expected_reset in view_action diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index ae5427e..857dc68 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -271,6 +271,36 @@ $$ language plpgsql; -- This way we start with a fresh view. drop view if exists v_pgfirstAid; +create or replace +function _pg_firstaid_checkpoint_stats() +returns text +language plpgsql +stable +as $$ +declare + v_timed bigint; + v_forced bigint; +begin + if current_setting('server_version_num')::int >= 170000 then + select num_timed, num_requested + into v_timed, v_forced + from pg_stat_checkpointer; + else + select checkpoints_timed, checkpoints_req + into v_timed, v_forced + from pg_stat_bgwriter; + end if; + + return 'timed: ' || v_timed::text || + ', forced: ' || v_forced::text || + ', forced ratio: ' || + case + when v_timed + v_forced = 0 then '0%' + else round(100.0 * v_forced / (v_timed + v_forced), 1)::text || '%' + end; +end; +$$; + create view v_pgfirstAid as select health_results.severity, @@ -1506,9 +1536,14 @@ select 'System Health' as category, 'Checkpoint Stats' as check_name, 'System' as object_name, - 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, + 'Checkpoint activity since stats last reset. Forced checkpoints occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small. PG15/16 reads from pg_stat_bgwriter; PG17+ reads from pg_stat_checkpointer.' as issue_description, _pg_firstaid_checkpoint_stats() as current_value, - 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, + 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''' || + case + when current_setting('server_version_num')::int >= 170000 then 'checkpointer' + else 'bgwriter' + end || + ''').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order union all diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 3becbfd..426cdbc 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -271,6 +271,36 @@ $$ language plpgsql; -- This way we start with a fresh view. drop view if exists v_pgfirstAid; +create or replace +function _pg_firstaid_checkpoint_stats() +returns text +language plpgsql +stable +as $$ +declare + v_timed bigint; + v_forced bigint; +begin + if current_setting('server_version_num')::int >= 170000 then + select num_timed, num_requested + into v_timed, v_forced + from pg_stat_checkpointer; + else + select checkpoints_timed, checkpoints_req + into v_timed, v_forced + from pg_stat_bgwriter; + end if; + + return 'timed: ' || v_timed::text || + ', forced: ' || v_forced::text || + ', forced ratio: ' || + case + when v_timed + v_forced = 0 then '0%' + else round(100.0 * v_forced / (v_timed + v_forced), 1)::text || '%' + end; +end; +$$; + create view v_pgfirstAid as select health_results.severity, @@ -573,7 +603,7 @@ union all 'HIGH' as severity, 'Table Health' as category, 'Duplicate Index' as check_name, - quote_ident(n1.nspname) || '.' || c1.relname || ': ' || i1.relname || ' & ' || i2.relname as object_name, + quote_ident(n1.nspname) || '.' || quote_ident(c1.relname) || ': ' || quote_ident(i1.relname) || ' & ' || quote_ident(i2.relname) as object_name, 'Multiple indexes with identical column sets and operator classes' as issue_description, 'Indexes: ' || i1.relname || ', ' || i2.relname as current_value, 'Review and consolidate duplicate indexes and focus on keeping the most efficient one' as recommended_action, @@ -590,6 +620,8 @@ join pg_index idx2 on and idx1.indkey = idx2.indkey and idx1.indclass = idx2.indclass and idx1.indoption = idx2.indoption + and idx1.indpred is not distinct from idx2.indpred + and idx1.indexprs is not distinct from idx2.indexprs join pg_class i2 on idx2.indexrelid = i2.oid where n1.nspname not like all(array['information_schema', 'pg_catalog', 'pg_toast', 'pg_temp%']) @@ -1495,9 +1527,14 @@ select 'System Health' as category, 'Checkpoint Stats' as check_name, 'System' as object_name, - 'Checkpoint activity since stats last reset. Forced checkpoints (checkpoints_req) occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small.' as issue_description, + 'Checkpoint activity since stats last reset. Forced checkpoints occur when WAL fills up before the scheduled interval — high ratios suggest max_wal_size may be too small. PG15/16 reads from pg_stat_bgwriter; PG17+ reads from pg_stat_checkpointer.' as issue_description, _pg_firstaid_checkpoint_stats() as current_value, - 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''bgwriter'').' as recommended_action, + 'If forced checkpoints are consistently above 50% of total, consider increasing max_wal_size. Reset stats with: SELECT pg_stat_reset_shared(''' || + case + when current_setting('server_version_num')::int >= 170000 then 'checkpointer' + else 'bgwriter' + end || + ''').' as recommended_action, 'https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-BGWRITER-VIEW' as documentation_link, 5 as severity_order union all From 750bc599e39507ef13af207f909db580683b2c3b Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 17:05:21 -0600 Subject: [PATCH 21/26] fix: terraform --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5903727..d5c37af 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ pgpass # AI files AGENTS.md CLAUDE.md + # Local docs docs/superpowers/plans/ From fbefc98a96cc2c1302488cdb3c17cfd27acc6c8f Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 18:46:10 -0600 Subject: [PATCH 22/26] revert to personal ip address for testing-temp --- testing/gcp/deploy/pg15/main.tf | 2 +- testing/gcp/deploy/pg15/vars.tf | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/testing/gcp/deploy/pg15/main.tf b/testing/gcp/deploy/pg15/main.tf index e765a22..13d9bb3 100644 --- a/testing/gcp/deploy/pg15/main.tf +++ b/testing/gcp/deploy/pg15/main.tf @@ -11,7 +11,7 @@ module "postgres" { region = local.region database_name = local.database_name db_user = local.db_user - authorized_networks = var.authorized_networks + personal_ip = var.personal_ip db_password = var.db_password } diff --git a/testing/gcp/deploy/pg15/vars.tf b/testing/gcp/deploy/pg15/vars.tf index 1c4a1c4..dd13297 100644 --- a/testing/gcp/deploy/pg15/vars.tf +++ b/testing/gcp/deploy/pg15/vars.tf @@ -1,9 +1,6 @@ -variable "authorized_networks" { - description = "Authorized networks for Cloud SQL" - type = list(object({ - name = string - value = string - })) +variable "personal_ip" { + description = "Personal IP to allow connections from" + type = string } variable "db_password" { From 0cc7d56c26773ebcc0be69ac7ae53e1c534b181b Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 18:47:39 -0600 Subject: [PATCH 23/26] revert to personal ip address for testing-temp --- testing/gcp/deploy/pg16/main.tf | 2 +- testing/gcp/deploy/pg16/vars.tf | 9 +++------ testing/gcp/deploy/pg17/main.tf | 2 +- testing/gcp/deploy/pg17/vars.tf | 9 +++------ 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/testing/gcp/deploy/pg16/main.tf b/testing/gcp/deploy/pg16/main.tf index e765a22..13d9bb3 100644 --- a/testing/gcp/deploy/pg16/main.tf +++ b/testing/gcp/deploy/pg16/main.tf @@ -11,7 +11,7 @@ module "postgres" { region = local.region database_name = local.database_name db_user = local.db_user - authorized_networks = var.authorized_networks + personal_ip = var.personal_ip db_password = var.db_password } diff --git a/testing/gcp/deploy/pg16/vars.tf b/testing/gcp/deploy/pg16/vars.tf index 1c4a1c4..dd13297 100644 --- a/testing/gcp/deploy/pg16/vars.tf +++ b/testing/gcp/deploy/pg16/vars.tf @@ -1,9 +1,6 @@ -variable "authorized_networks" { - description = "Authorized networks for Cloud SQL" - type = list(object({ - name = string - value = string - })) +variable "personal_ip" { + description = "Personal IP to allow connections from" + type = string } variable "db_password" { diff --git a/testing/gcp/deploy/pg17/main.tf b/testing/gcp/deploy/pg17/main.tf index e765a22..13d9bb3 100644 --- a/testing/gcp/deploy/pg17/main.tf +++ b/testing/gcp/deploy/pg17/main.tf @@ -11,7 +11,7 @@ module "postgres" { region = local.region database_name = local.database_name db_user = local.db_user - authorized_networks = var.authorized_networks + personal_ip = var.personal_ip db_password = var.db_password } diff --git a/testing/gcp/deploy/pg17/vars.tf b/testing/gcp/deploy/pg17/vars.tf index 1c4a1c4..dd13297 100644 --- a/testing/gcp/deploy/pg17/vars.tf +++ b/testing/gcp/deploy/pg17/vars.tf @@ -1,9 +1,6 @@ -variable "authorized_networks" { - description = "Authorized networks for Cloud SQL" - type = list(object({ - name = string - value = string - })) +variable "personal_ip" { + description = "Personal IP to allow connections from" + type = string } variable "db_password" { From 150a6c625990a291eb66e2518b4e81f418e03e27 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 18:50:52 -0600 Subject: [PATCH 24/26] update pull request template to exclude azure/gcp, add supa/neon --- .github/pull_request_template.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d23d180..1a82787 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -43,8 +43,10 @@ **Has this code been deployed and tested on the following platforms?** - [ ] Amazon RDS for PostgreSQL -- [ ] Google Cloud SQL for PostgreSQL -- [ ] Azure Database for PostgreSQL +- [ ] Google Cloud SQL for PostgreSQL (currently unable to test) +- [ ] Azure Database for PostgreSQL (currently unable to test) +- [ ] Neon +- [ ] Supabase - [ ] Self-managed PostgreSQL **Platform-specific notes:** From 78880c52843a800c3f100068f7632b722f6124ca Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 19:05:07 -0600 Subject: [PATCH 25/26] update README --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c7e43b..e3105b6 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ That's it! No configuration needed. Deploy as a user with the highest possible p - **Table Bloat** - Tables with >20% bloat affecting performance (tables >100MB) - **Missing Statistics** - Tables never analyzed, leaving the query planner without statistics -- **Duplicate Indexes** - Multiple indexes with identical or overlapping column sets +- **Duplicate Indexes** - Indexes with the same structure, including predicates and expressions - **Inactive Replication Slots** - Identifies replication slots that are inactive and can be removed if no longer needed - **Tables Larger Than 100GB** - Identifies tables that are larger than 100GB - **Tables With More Than 200 Columns** - List tables with more than 200 columns. You should probably look into those... @@ -121,8 +121,8 @@ That's it! No configuration needed. Deploy as a user with the highest possible p - **PostgreSQL Version** - Version information and configuration details - **Installed Extensions** - Lists installed extensions on the Server - **Server Uptime** - Server uptime since last restart -- **Log Directory** - Location of Log File(s). Results will vary for managed services like AWS RDS. (note: need access to AWS/Azure/GCP environments where I can test against!) -- **Log File Sizes** - The size of the log files. Again, this will vary for managed services. +- **Log Directory** - Current log directory when the platform exposes it +- **Log File Sizes** - Current log file sizes when the platform exposes them ## Usage Tips @@ -210,11 +210,14 @@ pgFirstAid is designed to be lightweight and safe to run on production systems: - A coverage guard ensures every `check_name` in `pgFirstAid.sql` is referenced by at least one pgTAP assertion. - Managed database validation is exercised through the reusable workflow in `.github/workflows/managed-db-validate.yml`. +> **Important:** We currently validate managed-database testing against AWS, but we do not have the funding or credits needed to keep Azure and GCP test environments running. If you have access to Azure Database for PostgreSQL or GCP Cloud SQL and want to help validate pgFirstAid there, we would be happy to have the help. + ## Compatibility - **PostgreSQL 10+** - Supported, with active automated validation focused on PostgreSQL 15-18 - **PostgreSQL 9.x** - Most features work (minor syntax adjustments may be needed) - Works with PostgreSQL-compatible databases, including Amazon RDS, Aurora, Azure Database for PostgreSQL, GCP Cloud SQL, and self-hosted PostgreSQL +- Automated managed-database validation is active for AWS today. Azure and GCP support is best-effort until we can fund those test environments. ## Contributing From 998dbf2961a451d1a297553d84356e5895017b39 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 12 Apr 2026 19:34:15 -0600 Subject: [PATCH 26/26] further edits from qodo --- .../src/pgfirstaid_pytest/config.py | 6 ++- testing/integration/tests/conftest.py | 9 ++++ .../tests/integration/test_pgtap_suite.py | 47 +++++++++++++++++++ testing/integration/tests/test_config.py | 11 +++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/testing/integration/src/pgfirstaid_pytest/config.py b/testing/integration/src/pgfirstaid_pytest/config.py index 3ea24b7..6cf4cef 100644 --- a/testing/integration/src/pgfirstaid_pytest/config.py +++ b/testing/integration/src/pgfirstaid_pytest/config.py @@ -17,9 +17,13 @@ class TestConfig: active_conn_sleep_seconds: int wait_timeout_seconds: int + @classmethod + def missing_env_vars(cls) -> tuple[str, ...]: + return tuple(name for name in _REQUIRED_ENV_VARS if not os.getenv(name)) + @classmethod def from_env(cls) -> "TestConfig": - missing = [name for name in _REQUIRED_ENV_VARS if not os.getenv(name)] + missing = cls.missing_env_vars() if missing: missing_list = ", ".join(missing) raise ValueError( diff --git a/testing/integration/tests/conftest.py b/testing/integration/tests/conftest.py index 5f8eeab..363681d 100644 --- a/testing/integration/tests/conftest.py +++ b/testing/integration/tests/conftest.py @@ -25,6 +25,15 @@ def _teardown_sql_path() -> Path: @pytest.fixture(scope="session") def config() -> TestConfig: + missing = TestConfig.missing_env_vars() + if missing: + missing_list = ", ".join(missing) + pytest.skip( + "Missing required PostgreSQL environment variables: " + f"{missing_list}. Set the standard PG* connection variables before " + "running integration tests." + ) + return TestConfig.from_env() diff --git a/testing/integration/tests/integration/test_pgtap_suite.py b/testing/integration/tests/integration/test_pgtap_suite.py index bbdc26c..0446391 100644 --- a/testing/integration/tests/integration/test_pgtap_suite.py +++ b/testing/integration/tests/integration/test_pgtap_suite.py @@ -122,6 +122,21 @@ def _extract_check_names_from_pgtap(sql_text: str) -> set[str]: return set(re.findall(r"check_name\s*=\s*'([^']+)'", sql_text, flags=re.IGNORECASE)) +def _extract_function_body(sql_text: str, func_name: str) -> str | None: + # Match the dollar-quoted body of a named PL/pgSQL function. + # Use [$] character classes for literal $ because \$ loses the backslash + # in Python raw strings when $ is not a recognised escape sequence. + match = re.search( + rf"function\s+{re.escape(func_name)}\b[^$]*[$][$](.*?)[$][$]", + sql_text, + flags=re.IGNORECASE | re.DOTALL, + ) + if match is None: + return None + # Normalise whitespace so trivial formatting differences don't cause false diffs. + return "\n".join(line.strip() for line in match.group(1).strip().splitlines()) + + def _view_sql_files() -> list[Path]: root = _repo_root() all_views = [root / "view_pgFirstAid.sql", root / "view_pgFirstAid_managed.sql"] @@ -172,6 +187,38 @@ def test_both_view_sql_files_cover_all_health_checks() -> None: ) +@pytest.mark.integration +def test_checkpoint_stats_helper_body_matches_across_install_scripts() -> None: + # _pg_firstaid_checkpoint_stats() is duplicated in all three install scripts so + # they each remain standalone. This test catches accidental divergence. + root = _repo_root() + scripts = [ + root / "pgFirstAid.sql", + root / "view_pgFirstAid.sql", + root / "view_pgFirstAid_managed.sql", + ] + + bodies: dict[str, str] = {} + for script in scripts: + body = _extract_function_body( + script.read_text(encoding="utf-8"), + "_pg_firstaid_checkpoint_stats", + ) + assert body is not None, ( + f"_pg_firstaid_checkpoint_stats() not found in {script.name} — " + "was it renamed or removed?" + ) + bodies[script.name] = body + + reference_name = scripts[0].name + for script in scripts[1:]: + assert bodies[script.name] == bodies[reference_name], ( + f"_pg_firstaid_checkpoint_stats() in {script.name} differs from " + f"{reference_name} — keep these identical so the checkpoint stats " + "check produces consistent results regardless of install path." + ) + + @pytest.mark.integration @pytest.mark.parametrize("view_sql", _view_sql_files()) def test_view_variant_matches_function_check_names( diff --git a/testing/integration/tests/test_config.py b/testing/integration/tests/test_config.py index 0465826..3dd1cc0 100644 --- a/testing/integration/tests/test_config.py +++ b/testing/integration/tests/test_config.py @@ -15,6 +15,17 @@ def test_from_env_requires_connection_variables( TestConfig.from_env() +def test_missing_env_vars_reports_unset_connection_variables( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("PGHOST", "db.example.test") + monkeypatch.delenv("PGPORT", raising=False) + monkeypatch.delenv("PGUSER", raising=False) + monkeypatch.setenv("PGDATABASE", "appdb") + + assert TestConfig.missing_env_vars() == ("PGPORT", "PGUSER") + + def test_from_env_reads_standard_pg_variables(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PGHOST", "db.example.test") monkeypatch.setenv("PGPORT", "6543")