pg_reports 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/README.md +123 -370
  4. data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
  5. data/app/views/layouts/pg_reports/application.html.erb +135 -69
  6. data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
  7. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +105 -55
  8. data/app/views/pg_reports/dashboard/_show_styles.html.erb +49 -11
  9. data/app/views/pg_reports/dashboard/index.html.erb +123 -114
  10. data/app/views/pg_reports/dashboard/show.html.erb +30 -26
  11. data/config/locales/en.yml +597 -0
  12. data/config/locales/ru.yml +562 -0
  13. data/config/locales/uk.yml +607 -0
  14. data/lib/pg_reports/compatibility.rb +63 -0
  15. data/lib/pg_reports/configuration.rb +2 -0
  16. data/lib/pg_reports/dashboard/reports_registry.rb +112 -5
  17. data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
  18. data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
  19. data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
  20. data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
  21. data/lib/pg_reports/definitions/schema_analysis/always_null_columns.yml +31 -0
  22. data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -0
  23. data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
  24. data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
  25. data/lib/pg_reports/definitions/tables/unused_tables.yml +30 -0
  26. data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
  27. data/lib/pg_reports/engine.rb +6 -0
  28. data/lib/pg_reports/module_generator.rb +2 -1
  29. data/lib/pg_reports/modules/indexes.rb +3 -0
  30. data/lib/pg_reports/modules/queries.rb +1 -0
  31. data/lib/pg_reports/modules/schema_analysis.rb +261 -2
  32. data/lib/pg_reports/modules/system.rb +27 -0
  33. data/lib/pg_reports/modules/tables.rb +1 -0
  34. data/lib/pg_reports/query_monitor.rb +64 -36
  35. data/lib/pg_reports/report_definition.rb +20 -24
  36. data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
  37. data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
  38. data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
  39. data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
  40. data/lib/pg_reports/sql/schema_analysis/always_null_columns.sql +25 -0
  41. data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -0
  42. data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
  43. data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
  44. data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
  45. data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
  46. data/lib/pg_reports/sql/tables/unused_tables.sql +19 -0
  47. data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
  48. data/lib/pg_reports/version.rb +1 -1
  49. data/lib/pg_reports.rb +5 -0
  50. metadata +24 -1
@@ -0,0 +1,25 @@
1
+ -- Columns where ~100% of rows are NULL
2
+ -- Strong indicator nothing in the application populates this column anymore.
3
+
4
+ SELECT
5
+ s.schemaname AS schema,
6
+ s.tablename AS table_name,
7
+ s.attname AS column_name,
8
+ format_type(a.atttypid, a.atttypmod) AS data_type,
9
+ ROUND((s.null_frac * 100)::numeric, 2) AS null_pct,
10
+ pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
11
+ a.attnotnull AS not_null_constraint,
12
+ c.reltuples::bigint AS estimated_rows
13
+ FROM pg_stats s
14
+ JOIN pg_class c ON c.relname = s.tablename
15
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = s.schemaname
16
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = s.attname
17
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
18
+ WHERE s.schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
19
+ AND c.relkind = 'r'
20
+ AND a.attnum > 0
21
+ AND NOT a.attisdropped
22
+ AND a.attnotnull = false
23
+ AND s.null_frac >= 0.999
24
+ AND c.reltuples > 1000
25
+ ORDER BY c.reltuples DESC;
@@ -0,0 +1,36 @@
1
+ -- Columns that have only ever held a single value (likely never updated since creation)
2
+ -- Uses pg_stats.n_distinct = 1 as a proxy for "no UPDATE has ever changed this field".
3
+ -- Strong indicator the application code no longer references the column but it was never dropped.
4
+
5
+ SELECT
6
+ s.schemaname AS schema,
7
+ s.tablename AS table_name,
8
+ s.attname AS column_name,
9
+ format_type(a.atttypid, a.atttypmod) AS data_type,
10
+ CASE
11
+ WHEN s.most_common_vals IS NOT NULL
12
+ THEN substring((s.most_common_vals::text) FROM 1 FOR 80)
13
+ ELSE NULL
14
+ END AS sole_value,
15
+ pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
16
+ ROUND((s.null_frac * 100)::numeric, 2) AS null_pct,
17
+ c.reltuples::bigint AS estimated_rows
18
+ FROM pg_stats s
19
+ JOIN pg_class c ON c.relname = s.tablename
20
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = s.schemaname
21
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = s.attname
22
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
23
+ WHERE s.schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
24
+ AND c.relkind = 'r'
25
+ AND a.attnum > 0
26
+ AND NOT a.attisdropped
27
+ AND s.n_distinct = 1
28
+ AND s.null_frac < 0.999
29
+ AND c.reltuples > 1000
30
+ AND NOT EXISTS (
31
+ SELECT 1 FROM pg_index i
32
+ WHERE i.indrelid = c.oid
33
+ AND a.attnum = ANY(i.indkey)
34
+ AND (i.indisprimary OR i.indisunique)
35
+ )
36
+ ORDER BY c.reltuples DESC;
@@ -0,0 +1,20 @@
1
+ -- Checkpoint and background writer statistics (PostgreSQL 17+)
2
+ -- Uses pg_stat_checkpointer (introduced in PG 17) + pg_stat_bgwriter for bgwriter-only stats
3
+
4
+ SELECT
5
+ pg_stat_get_checkpointer_num_timed() AS checkpoints_timed,
6
+ pg_stat_get_checkpointer_num_requested() AS checkpoints_requested,
7
+ ROUND(pg_stat_get_checkpointer_write_time()::numeric / 1000, 2) AS checkpoint_write_time_sec,
8
+ ROUND(pg_stat_get_checkpointer_sync_time()::numeric / 1000, 2) AS checkpoint_sync_time_sec,
9
+ pg_stat_get_checkpointer_buffers_written() AS buffers_checkpoint,
10
+ pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
11
+ pg_stat_get_bgwriter_maxwritten_clean() AS bgwriter_stops,
12
+ pg_stat_get_buf_alloc() AS buffers_alloc,
13
+ CASE
14
+ WHEN (pg_stat_get_checkpointer_num_timed() + pg_stat_get_checkpointer_num_requested()) > 0
15
+ THEN ROUND(
16
+ pg_stat_get_checkpointer_num_requested()::numeric /
17
+ (pg_stat_get_checkpointer_num_timed() + pg_stat_get_checkpointer_num_requested()) * 100, 1)
18
+ ELSE 0
19
+ END AS requested_pct,
20
+ pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
@@ -0,0 +1,19 @@
1
+ -- Checkpoint and background writer statistics (PostgreSQL < 17)
2
+ -- All stats from pg_stat_bgwriter (before checkpoint columns were moved out)
3
+
4
+ SELECT
5
+ checkpoints_timed,
6
+ checkpoints_req AS checkpoints_requested,
7
+ ROUND(checkpoint_write_time::numeric / 1000, 2) AS checkpoint_write_time_sec,
8
+ ROUND(checkpoint_sync_time::numeric / 1000, 2) AS checkpoint_sync_time_sec,
9
+ buffers_checkpoint,
10
+ buffers_clean,
11
+ maxwritten_clean AS bgwriter_stops,
12
+ buffers_alloc,
13
+ CASE
14
+ WHEN (checkpoints_timed + checkpoints_req) > 0
15
+ THEN ROUND(checkpoints_req::numeric / (checkpoints_timed + checkpoints_req) * 100, 1)
16
+ ELSE 0
17
+ END AS requested_pct,
18
+ stats_reset
19
+ FROM pg_stat_bgwriter;
@@ -0,0 +1,21 @@
1
+ -- Transaction ID wraparound risk
2
+ -- When age(datfrozenxid) approaches 2 billion, PostgreSQL will shut down
3
+ -- to prevent data corruption. Monitor this to trigger preventive VACUUM FREEZE.
4
+
5
+ SELECT
6
+ d.datname AS database_name,
7
+ age(d.datfrozenxid) AS xid_age,
8
+ ROUND(age(d.datfrozenxid)::numeric / 2147483647 * 100, 2) AS pct_towards_wraparound,
9
+ 2147483647 - age(d.datfrozenxid) AS remaining_xids,
10
+ current_setting('autovacuum_freeze_max_age')::bigint AS freeze_max_age,
11
+ CASE
12
+ WHEN age(d.datfrozenxid) > current_setting('autovacuum_freeze_max_age')::bigint
13
+ THEN 'CRITICAL - exceeds freeze_max_age'
14
+ WHEN age(d.datfrozenxid) > current_setting('autovacuum_freeze_max_age')::bigint * 0.75
15
+ THEN 'WARNING - approaching freeze_max_age'
16
+ ELSE 'OK'
17
+ END AS status,
18
+ pg_size_pretty(pg_database_size(d.datname)) AS database_size
19
+ FROM pg_database d
20
+ WHERE d.datallowconn = true
21
+ ORDER BY age(d.datfrozenxid) DESC;
@@ -0,0 +1,20 @@
1
+ -- Tables without primary keys
2
+ -- Missing PKs break logical replication and make row identification unreliable
3
+
4
+ SELECT
5
+ n.nspname AS schema,
6
+ c.relname AS table_name,
7
+ c.reltuples::bigint AS estimated_rows,
8
+ pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
9
+ ROUND(pg_relation_size(c.oid) / 1024.0 / 1024.0, 2) AS table_size_mb
10
+ FROM pg_class c
11
+ JOIN pg_namespace n ON n.oid = c.relnamespace
12
+ WHERE c.relkind = 'r'
13
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
14
+ AND NOT EXISTS (
15
+ SELECT 1
16
+ FROM pg_index i
17
+ WHERE i.indrelid = c.oid
18
+ AND i.indisprimary
19
+ )
20
+ ORDER BY pg_relation_size(c.oid) DESC;
@@ -0,0 +1,19 @@
1
+ -- Tables that have not been read at all since the last stats reset
2
+ -- Zero seq_scan AND zero idx_scan -- application code never queries them.
3
+ -- Candidates for archival, deletion, or extraction to a separate database.
4
+
5
+ SELECT
6
+ t.schemaname AS schema,
7
+ t.relname AS table_name,
8
+ t.n_live_tup AS live_rows,
9
+ pg_size_pretty(pg_total_relation_size(t.relid)) AS total_size,
10
+ ROUND(pg_total_relation_size(t.relid) / 1024.0 / 1024.0, 2) AS total_size_mb,
11
+ COALESCE(t.last_autoanalyze, t.last_analyze) AS last_analyzed,
12
+ d.stats_reset AS db_stats_since
13
+ FROM pg_stat_user_tables t
14
+ LEFT JOIN pg_stat_database d ON d.datname = current_database()
15
+ WHERE t.schemaname NOT IN ('pg_catalog', 'information_schema')
16
+ AND t.seq_scan = 0
17
+ AND COALESCE(t.idx_scan, 0) = 0
18
+ AND t.n_live_tup > 0
19
+ ORDER BY pg_total_relation_size(t.relid) DESC;
@@ -0,0 +1,26 @@
1
+ -- Tables with disproportionately high write activity
2
+ -- updates_per_row = n_tup_upd / n_live_tup -- same rows being updated repeatedly
3
+ -- hot_update_pct = n_tup_hot_upd / n_tup_upd -- low value means indexed columns are being updated (expensive)
4
+
5
+ SELECT
6
+ schemaname AS schema,
7
+ relname AS table_name,
8
+ n_live_tup AS live_rows,
9
+ n_tup_upd AS updates,
10
+ n_tup_hot_upd AS hot_updates,
11
+ CASE WHEN n_tup_upd > 0
12
+ THEN ROUND((n_tup_hot_upd::numeric / n_tup_upd) * 100, 2)
13
+ ELSE 0
14
+ END AS hot_update_pct,
15
+ CASE WHEN n_live_tup > 0
16
+ THEN ROUND(n_tup_upd::numeric / n_live_tup, 2)
17
+ ELSE 0
18
+ END AS updates_per_row,
19
+ n_tup_ins AS inserts,
20
+ n_tup_del AS deletes,
21
+ n_dead_tup AS dead_rows
22
+ FROM pg_stat_user_tables
23
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
24
+ AND n_tup_upd > 1000
25
+ AND n_live_tup > 100
26
+ ORDER BY (n_tup_upd::numeric / GREATEST(n_live_tup, 1)) DESC;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.5.4"
4
+ VERSION = "0.6.1"
5
5
  end
data/lib/pg_reports.rb CHANGED
@@ -6,6 +6,7 @@ require "active_record"
6
6
 
7
7
  require_relative "pg_reports/version"
8
8
  require_relative "pg_reports/error"
9
+ require_relative "pg_reports/compatibility"
9
10
  require_relative "pg_reports/configuration"
10
11
  require_relative "pg_reports/sql_loader"
11
12
  require_relative "pg_reports/executor"
@@ -138,3 +139,7 @@ end
138
139
 
139
140
  # Generate YAML-based methods on load
140
141
  PgReports::ModuleGenerator.generate!
142
+
143
+ # Check Ruby and Rails versions immediately (no DB needed)
144
+ PgReports::Compatibility.check_ruby!
145
+ PgReports::Compatibility.check_rails!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_reports
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eldar Avatov
@@ -161,6 +161,7 @@ files:
161
161
  - config/routes.rb
162
162
  - lib/pg_reports.rb
163
163
  - lib/pg_reports/annotation_parser.rb
164
+ - lib/pg_reports/compatibility.rb
164
165
  - lib/pg_reports/configuration.rb
165
166
  - lib/pg_reports/dashboard/reports_registry.rb
166
167
  - lib/pg_reports/definitions/connections/active_connections.yml
@@ -175,8 +176,11 @@ files:
175
176
  - lib/pg_reports/definitions/connections/pool_wait_times.yml
176
177
  - lib/pg_reports/definitions/indexes/bloated_indexes.yml
177
178
  - lib/pg_reports/definitions/indexes/duplicate_indexes.yml
179
+ - lib/pg_reports/definitions/indexes/fk_without_indexes.yml
180
+ - lib/pg_reports/definitions/indexes/index_correlation.yml
178
181
  - lib/pg_reports/definitions/indexes/index_sizes.yml
179
182
  - lib/pg_reports/definitions/indexes/index_usage.yml
183
+ - lib/pg_reports/definitions/indexes/inefficient_indexes.yml
180
184
  - lib/pg_reports/definitions/indexes/invalid_indexes.yml
181
185
  - lib/pg_reports/definitions/indexes/missing_indexes.yml
182
186
  - lib/pg_reports/definitions/indexes/unused_indexes.yml
@@ -186,17 +190,24 @@ files:
186
190
  - lib/pg_reports/definitions/queries/low_cache_hit_queries.yml
187
191
  - lib/pg_reports/definitions/queries/missing_index_queries.yml
188
192
  - lib/pg_reports/definitions/queries/slow_queries.yml
193
+ - lib/pg_reports/definitions/queries/temp_file_queries.yml
194
+ - lib/pg_reports/definitions/schema_analysis/always_null_columns.yml
195
+ - lib/pg_reports/definitions/schema_analysis/unused_columns.yml
189
196
  - lib/pg_reports/definitions/system/activity_overview.yml
190
197
  - lib/pg_reports/definitions/system/cache_stats.yml
191
198
  - lib/pg_reports/definitions/system/database_sizes.yml
192
199
  - lib/pg_reports/definitions/system/extensions.yml
193
200
  - lib/pg_reports/definitions/system/settings.yml
201
+ - lib/pg_reports/definitions/system/wraparound_risk.yml
194
202
  - lib/pg_reports/definitions/tables/bloated_tables.yml
195
203
  - lib/pg_reports/definitions/tables/cache_hit_ratios.yml
196
204
  - lib/pg_reports/definitions/tables/recently_modified.yml
197
205
  - lib/pg_reports/definitions/tables/row_counts.yml
198
206
  - lib/pg_reports/definitions/tables/seq_scans.yml
199
207
  - lib/pg_reports/definitions/tables/table_sizes.yml
208
+ - lib/pg_reports/definitions/tables/tables_without_pk.yml
209
+ - lib/pg_reports/definitions/tables/unused_tables.yml
210
+ - lib/pg_reports/definitions/tables/update_hotspots.yml
200
211
  - lib/pg_reports/definitions/tables/vacuum_needed.yml
201
212
  - lib/pg_reports/engine.rb
202
213
  - lib/pg_reports/error.rb
@@ -226,8 +237,11 @@ files:
226
237
  - lib/pg_reports/sql/connections/pool_wait_times.sql
227
238
  - lib/pg_reports/sql/indexes/bloated_indexes.sql
228
239
  - lib/pg_reports/sql/indexes/duplicate_indexes.sql
240
+ - lib/pg_reports/sql/indexes/fk_without_indexes.sql
241
+ - lib/pg_reports/sql/indexes/index_correlation.sql
229
242
  - lib/pg_reports/sql/indexes/index_sizes.sql
230
243
  - lib/pg_reports/sql/indexes/index_usage.sql
244
+ - lib/pg_reports/sql/indexes/inefficient_indexes.sql
231
245
  - lib/pg_reports/sql/indexes/invalid_indexes.sql
232
246
  - lib/pg_reports/sql/indexes/missing_indexes.sql
233
247
  - lib/pg_reports/sql/indexes/unused_indexes.sql
@@ -237,20 +251,29 @@ files:
237
251
  - lib/pg_reports/sql/queries/low_cache_hit_queries.sql
238
252
  - lib/pg_reports/sql/queries/missing_index_queries.sql
239
253
  - lib/pg_reports/sql/queries/slow_queries.sql
254
+ - lib/pg_reports/sql/queries/temp_file_queries.sql
255
+ - lib/pg_reports/sql/schema_analysis/always_null_columns.sql
240
256
  - lib/pg_reports/sql/schema_analysis/unique_indexes.sql
257
+ - lib/pg_reports/sql/schema_analysis/unused_columns.sql
241
258
  - lib/pg_reports/sql/system/activity_overview.sql
242
259
  - lib/pg_reports/sql/system/cache_stats.sql
260
+ - lib/pg_reports/sql/system/checkpoint_stats.sql
261
+ - lib/pg_reports/sql/system/checkpoint_stats_legacy.sql
243
262
  - lib/pg_reports/sql/system/database_sizes.sql
244
263
  - lib/pg_reports/sql/system/databases_list.sql
245
264
  - lib/pg_reports/sql/system/extensions.sql
246
265
  - lib/pg_reports/sql/system/live_metrics.sql
247
266
  - lib/pg_reports/sql/system/settings.sql
267
+ - lib/pg_reports/sql/system/wraparound_risk.sql
248
268
  - lib/pg_reports/sql/tables/bloated_tables.sql
249
269
  - lib/pg_reports/sql/tables/cache_hit_ratios.sql
250
270
  - lib/pg_reports/sql/tables/recently_modified.sql
251
271
  - lib/pg_reports/sql/tables/row_counts.sql
252
272
  - lib/pg_reports/sql/tables/seq_scans.sql
253
273
  - lib/pg_reports/sql/tables/table_sizes.sql
274
+ - lib/pg_reports/sql/tables/tables_without_pk.sql
275
+ - lib/pg_reports/sql/tables/unused_tables.sql
276
+ - lib/pg_reports/sql/tables/update_hotspots.sql
254
277
  - lib/pg_reports/sql/tables/vacuum_needed.sql
255
278
  - lib/pg_reports/sql_loader.rb
256
279
  - lib/pg_reports/telegram_sender.rb