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,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgReports
4
+ # Checks runtime environment and warns about outdated or unsupported versions.
5
+ # Called once at boot (Ruby/Rails) and lazily on first DB access (PostgreSQL).
6
+ module Compatibility
7
+ # Keep in sync with gemspec constraints
8
+ MINIMUM_RUBY_VERSION = "2.7"
9
+ MINIMUM_RAILS_VERSION = "5.0"
10
+ MINIMUM_PG_VERSION = 12_00_00 # server_version_num format
11
+ MINIMUM_PG_VERSION_LABEL = "12"
12
+
13
+ class << self
14
+ def check_ruby!
15
+ return if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(MINIMUM_RUBY_VERSION)
16
+
17
+ warn "[pg_reports] Ruby #{RUBY_VERSION} is not supported. " \
18
+ "Minimum required version is Ruby #{MINIMUM_RUBY_VERSION}."
19
+ end
20
+
21
+ def check_rails!
22
+ return unless defined?(Rails::VERSION::STRING)
23
+ return if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new(MINIMUM_RAILS_VERSION)
24
+
25
+ warn "[pg_reports] Rails #{Rails::VERSION::STRING} is not supported. " \
26
+ "Minimum required version is Rails #{MINIMUM_RAILS_VERSION}."
27
+ end
28
+
29
+ def check_postgresql!
30
+ version_num = pg_version_num
31
+ return if version_num.nil? # no connection yet — skip silently
32
+ return if version_num >= MINIMUM_PG_VERSION
33
+
34
+ label = pg_version_label(version_num)
35
+ warn "[pg_reports] PostgreSQL #{label} is not supported. " \
36
+ "Minimum required version is PostgreSQL #{MINIMUM_PG_VERSION_LABEL}. " \
37
+ "Some reports may return errors or incomplete data."
38
+ end
39
+
40
+ def check_all!
41
+ check_ruby!
42
+ check_rails!
43
+ check_postgresql!
44
+ end
45
+
46
+ private
47
+
48
+ def pg_version_num
49
+ connection = PgReports.config.connection
50
+ result = connection.exec_query("SELECT current_setting('server_version_num')::int AS v")
51
+ result.first&.fetch("v", 0).to_i
52
+ rescue
53
+ nil
54
+ end
55
+
56
+ def pg_version_label(version_num)
57
+ major = version_num / 1_00_00
58
+ minor = (version_num % 1_00_00) / 100
59
+ "#{major}.#{minor}"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -13,6 +13,7 @@ module PgReports
13
13
 
14
14
  # Index analysis thresholds
15
15
  attr_accessor :unused_index_threshold_scans # Index with fewer scans is unused
16
+ attr_accessor :inefficient_index_threshold_ratio # Read/fetch ratio above this is inefficient
16
17
 
17
18
  # Table analysis thresholds
18
19
  attr_accessor :bloat_threshold_percent # Tables with more bloat are problematic
@@ -53,6 +54,7 @@ module PgReports
53
54
 
54
55
  # Index thresholds
55
56
  @unused_index_threshold_scans = 50
57
+ @inefficient_index_threshold_ratio = 10
56
58
 
57
59
  # Table thresholds
58
60
  @bloat_threshold_percent = 20
@@ -32,6 +32,10 @@ module PgReports
32
32
  thresholds: {},
33
33
  problem_fields: []
34
34
  },
35
+ temp_file_queries: {
36
+ thresholds: {temp_mb_written: {warning: 100, critical: 1000}},
37
+ problem_fields: ["temp_mb_written"]
38
+ },
35
39
 
36
40
  # === INDEXES ===
37
41
  unused_indexes: {
@@ -50,6 +54,10 @@ module PgReports
50
54
  thresholds: {seq_scan_ratio: {warning: 0.5, critical: 0.9}},
51
55
  problem_fields: ["seq_scan", "seq_tup_read"]
52
56
  },
57
+ inefficient_indexes: {
58
+ thresholds: {read_to_fetch_ratio: {warning: 10, critical: 100}},
59
+ problem_fields: ["read_to_fetch_ratio"]
60
+ },
53
61
  index_usage: {
54
62
  thresholds: {},
55
63
  problem_fields: []
@@ -62,6 +70,14 @@ module PgReports
62
70
  thresholds: {size_bytes: {warning: 1073741824, critical: 10737418240}},
63
71
  problem_fields: ["size_bytes"]
64
72
  },
73
+ fk_without_indexes: {
74
+ thresholds: {},
75
+ problem_fields: ["child_table_size_mb"]
76
+ },
77
+ index_correlation: {
78
+ thresholds: {correlation: {warning: 0.5, critical: 0.2, inverted: true}},
79
+ problem_fields: ["correlation"]
80
+ },
65
81
 
66
82
  # === TABLES ===
67
83
  table_sizes: {
@@ -92,6 +108,10 @@ module PgReports
92
108
  thresholds: {},
93
109
  problem_fields: []
94
110
  },
111
+ tables_without_pk: {
112
+ thresholds: {},
113
+ problem_fields: []
114
+ },
95
115
 
96
116
  # === CONNECTIONS ===
97
117
  active_connections: {
@@ -159,11 +179,56 @@ module PgReports
159
179
  thresholds: {cache_hit_ratio: {warning: 0.95, critical: 0.90, inverted: true}},
160
180
  problem_fields: ["cache_hit_ratio"]
161
181
  },
182
+ wraparound_risk: {
183
+ thresholds: {pct_towards_wraparound: {warning: 50, critical: 75}},
184
+ problem_fields: ["pct_towards_wraparound"]
185
+ },
186
+ checkpoint_stats: {
187
+ thresholds: {requested_pct: {warning: 50, critical: 75}},
188
+ problem_fields: ["requested_pct"]
189
+ },
162
190
 
163
191
  # === SCHEMA ANALYSIS ===
164
192
  missing_validations: {
165
193
  thresholds: {},
166
194
  problem_fields: []
195
+ },
196
+ unused_columns: {
197
+ thresholds: {},
198
+ problem_fields: ["column_name"]
199
+ },
200
+ always_null_columns: {
201
+ thresholds: {null_pct: {warning: 99, critical: 100}},
202
+ problem_fields: ["column_name", "null_pct"]
203
+ },
204
+ polymorphic_without_index: {
205
+ thresholds: {},
206
+ problem_fields: ["coverage"]
207
+ },
208
+ counter_cache_issues: {
209
+ thresholds: {},
210
+ problem_fields: ["issue", "expected_column"]
211
+ },
212
+ soft_delete_without_scope: {
213
+ thresholds: {},
214
+ problem_fields: ["status", "soft_delete_column"]
215
+ },
216
+ orphan_tables: {
217
+ thresholds: {},
218
+ problem_fields: ["classification"]
219
+ },
220
+
221
+ # === TABLES (extra) ===
222
+ update_hotspots: {
223
+ thresholds: {
224
+ updates_per_row: {warning: 10, critical: 100},
225
+ hot_update_pct: {warning: 50, critical: 20, inverted: true}
226
+ },
227
+ problem_fields: ["updates_per_row", "hot_update_pct"]
228
+ },
229
+ unused_tables: {
230
+ thresholds: {total_size_mb: {warning: 10, critical: 100}},
231
+ problem_fields: ["table_name"]
167
232
  }
168
233
  }.freeze
169
234
 
@@ -178,6 +243,7 @@ module PgReports
178
243
  expensive_queries: {name: "Expensive Queries", description: "Queries consuming most total time"},
179
244
  missing_index_queries: {name: "Missing Index Queries", description: "Queries potentially missing indexes"},
180
245
  low_cache_hit_queries: {name: "Low Cache Hit", description: "Queries with poor cache utilization"},
246
+ temp_file_queries: {name: "Temp File Queries", description: "Queries spilling to disk"},
181
247
  all_queries: {name: "All Queries", description: "All query statistics"}
182
248
  }
183
249
  },
@@ -190,8 +256,11 @@ module PgReports
190
256
  duplicate_indexes: {name: "Duplicate Indexes", description: "Redundant indexes"},
191
257
  invalid_indexes: {name: "Invalid Indexes", description: "Indexes that failed to build"},
192
258
  missing_indexes: {name: "Missing Indexes", description: "Tables potentially missing indexes"},
259
+ inefficient_indexes: {name: "Inefficient Indexes", description: "Indexes with high read-to-fetch ratio"},
193
260
  index_usage: {name: "Index Usage", description: "Index scan statistics"},
194
261
  bloated_indexes: {name: "Bloated Indexes", description: "Indexes with high bloat"},
262
+ fk_without_indexes: {name: "FK Without Indexes", description: "Foreign keys missing indexes"},
263
+ index_correlation: {name: "Index Correlation", description: "Low physical correlation indexes"},
195
264
  index_sizes: {name: "Index Sizes", description: "Index disk usage"}
196
265
  }
197
266
  },
@@ -206,7 +275,10 @@ module PgReports
206
275
  row_counts: {name: "Row Counts", description: "Table row counts"},
207
276
  cache_hit_ratios: {name: "Cache Hit Ratios", description: "Table cache statistics"},
208
277
  seq_scans: {name: "Sequential Scans", description: "Tables with high sequential scans"},
209
- recently_modified: {name: "Recently Modified", description: "Tables with recent activity"}
278
+ tables_without_pk: {name: "No Primary Key", description: "Tables missing primary keys"},
279
+ recently_modified: {name: "Recently Modified", description: "Tables with recent activity"},
280
+ update_hotspots: {name: "Update Hotspots", description: "Same rows or indexed columns updated repeatedly", new: true},
281
+ unused_tables: {name: "Unused Tables", description: "Tables never queried since the last stats reset", new: true}
210
282
  }
211
283
  },
212
284
  connections: {
@@ -235,6 +307,8 @@ module PgReports
235
307
  settings: {name: "Settings", description: "PostgreSQL configuration"},
236
308
  extensions: {name: "Extensions", description: "Installed extensions"},
237
309
  activity_overview: {name: "Activity Overview", description: "Current activity summary"},
310
+ wraparound_risk: {name: "Wraparound Risk", description: "Transaction ID wraparound proximity"},
311
+ checkpoint_stats: {name: "Checkpoint Stats", description: "Checkpoint and bgwriter statistics"},
238
312
  cache_stats: {name: "Cache Stats", description: "Database cache statistics"}
239
313
  }
240
314
  },
@@ -243,21 +317,53 @@ module PgReports
243
317
  icon: "🔍",
244
318
  color: "#06b6d4",
245
319
  reports: {
246
- missing_validations: {name: "Missing Validations", description: "Unique indexes without model validations"}
320
+ missing_validations: {name: "Missing Validations", description: "Unique indexes without model validations"},
321
+ unused_columns: {name: "Unused Columns", description: "Columns that have only ever held a single value", new: true},
322
+ always_null_columns: {name: "Always-NULL Columns", description: "Nullable columns that contain only NULL", new: true},
323
+ polymorphic_without_index: {name: "Polymorphic Without Index", description: "Polymorphic associations missing composite index", new: true},
324
+ counter_cache_issues: {name: "Counter Cache Issues", description: "counter_cache declarations whose target column is missing", new: true},
325
+ soft_delete_without_scope: {name: "Soft Delete Without Scope", description: "Soft-delete columns with no model scope filtering them", new: true},
326
+ orphan_tables: {name: "Orphan Tables", description: "DB tables without a corresponding Rails model", new: true}
247
327
  }
248
328
  }
249
329
  }.freeze
250
330
 
251
331
  def self.all
252
- REPORTS
332
+ REPORTS.each_with_object({}) do |(cat_key, cat), result|
333
+ result[cat_key] = localized_category(cat_key, cat)
334
+ end
253
335
  end
254
336
 
255
337
  def self.find(category, report)
256
- REPORTS.dig(category.to_sym, :reports, report.to_sym)
338
+ rep = REPORTS.dig(category.to_sym, :reports, report.to_sym)
339
+ return nil unless rep
340
+
341
+ localized_report(report.to_sym, rep)
257
342
  end
258
343
 
259
344
  def self.category(category)
260
- REPORTS[category.to_sym]
345
+ cat = REPORTS[category.to_sym]
346
+ return nil unless cat
347
+
348
+ localized_category(category.to_sym, cat)
349
+ end
350
+
351
+ # Build category hash with localized name and report names
352
+ def self.localized_category(cat_key, cat)
353
+ cat.merge(
354
+ name: I18n.t("pg_reports.categories.#{cat_key}", default: cat[:name]),
355
+ reports: cat[:reports].each_with_object({}) do |(rep_key, rep), reports|
356
+ reports[rep_key] = localized_report(rep_key, rep)
357
+ end
358
+ )
359
+ end
360
+
361
+ # Build report hash with localized name and description
362
+ def self.localized_report(rep_key, rep)
363
+ rep.merge(
364
+ name: I18n.t("pg_reports.reports.#{rep_key}.name", default: rep[:name]),
365
+ description: I18n.t("pg_reports.reports.#{rep_key}.description", default: rep[:description])
366
+ )
261
367
  end
262
368
 
263
369
  # Returns full documentation for a report including I18n translations
@@ -272,6 +378,7 @@ module PgReports
272
378
  what: I18n.t("#{i18n_key}.what", default: ""),
273
379
  how: I18n.t("#{i18n_key}.how", default: ""),
274
380
  nuances: I18n.t("#{i18n_key}.nuances", default: []),
381
+ ai_prompt: I18n.t("#{i18n_key}.ai_prompt", default: nil),
275
382
  thresholds: config[:thresholds],
276
383
  problem_fields: config[:problem_fields]
277
384
  }
@@ -0,0 +1,30 @@
1
+ # Foreign keys without indexes on the child table
2
+ # Missing indexes cause sequential scans on parent DELETE/UPDATE
3
+
4
+ report:
5
+ name: fk_without_indexes
6
+ module: indexes
7
+ description: "Foreign keys missing indexes on the referencing table"
8
+
9
+ sql:
10
+ category: indexes
11
+ file: fk_without_indexes
12
+
13
+ title: "Foreign Keys Without Indexes"
14
+
15
+ columns:
16
+ - constraint_name
17
+ - child_table
18
+ - child_column
19
+ - parent_table
20
+ - parent_column
21
+ - child_table_size_mb
22
+
23
+ parameters:
24
+ limit:
25
+ type: integer
26
+ default: 50
27
+ description: "Maximum number of results"
28
+
29
+ problem_explanations:
30
+ child_table_size_mb: fk_without_index
@@ -0,0 +1,31 @@
1
+ # Index correlation analysis
2
+ # Low correlation means physical row order doesn't match index order, causing random I/O
3
+
4
+ report:
5
+ name: index_correlation
6
+ module: indexes
7
+ description: "Indexes with low physical correlation causing excessive random I/O"
8
+
9
+ sql:
10
+ category: indexes
11
+ file: index_correlation
12
+
13
+ title: "Index Correlation (tables > 10MB, scans > 100)"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - column_name
19
+ - index_name
20
+ - correlation
21
+ - table_size_mb
22
+ - idx_scan
23
+
24
+ parameters:
25
+ limit:
26
+ type: integer
27
+ default: 50
28
+ description: "Maximum number of results"
29
+
30
+ problem_explanations:
31
+ correlation: low_correlation
@@ -0,0 +1,45 @@
1
+ # Inefficient indexes - indexes that are used but scan far more entries than they fetch
2
+ # High read-to-fetch ratio indicates misaligned composite index column order
3
+
4
+ report:
5
+ name: inefficient_indexes
6
+ module: indexes
7
+ description: "Indexes with high read-to-fetch ratio indicating inefficient scans"
8
+
9
+ sql:
10
+ category: indexes
11
+ file: inefficient_indexes
12
+
13
+ title: "Inefficient Indexes (read/fetch ratio > ${threshold})"
14
+ title_vars:
15
+ threshold:
16
+ source: config
17
+ key: inefficient_index_threshold_ratio
18
+
19
+ columns:
20
+ - schema
21
+ - table_name
22
+ - index_name
23
+ - idx_scan
24
+ - idx_tup_read
25
+ - idx_tup_fetch
26
+ - read_to_fetch_ratio
27
+ - index_size_mb
28
+ - index_definition
29
+
30
+ parameters:
31
+ limit:
32
+ type: integer
33
+ default: 50
34
+ description: "Maximum number of results"
35
+
36
+ filters:
37
+ - field: read_to_fetch_ratio
38
+ operator: gte
39
+ value:
40
+ source: config
41
+ key: inefficient_index_threshold_ratio
42
+ cast: float
43
+
44
+ problem_explanations:
45
+ read_to_fetch_ratio: inefficient_index
@@ -0,0 +1,39 @@
1
+ # Temp file heavy queries
2
+ # Queries that spill to disk due to insufficient work_mem
3
+
4
+ report:
5
+ name: temp_file_queries
6
+ module: queries
7
+ description: "Queries spilling data to temporary files on disk"
8
+
9
+ sql:
10
+ category: queries
11
+ file: temp_file_queries
12
+ params:
13
+ max_query_length:
14
+ source: config
15
+ key: max_query_length
16
+
17
+ title: "Temp File Queries"
18
+
19
+ columns:
20
+ - query
21
+ - calls
22
+ - temp_mb_written
23
+ - temp_mb_read
24
+ - total_time_sec
25
+ - mean_time_ms
26
+ - rows
27
+
28
+ parameters:
29
+ limit:
30
+ type: integer
31
+ default: 20
32
+ description: "Maximum number of results"
33
+
34
+ enrichment:
35
+ module: queries
36
+ hook: enrich_with_annotations
37
+
38
+ problem_explanations:
39
+ temp_mb_written: temp_file_heavy
@@ -0,0 +1,31 @@
1
+ # Columns where 100% of rows are NULL
2
+ # Strong indicator the application no longer populates these fields
3
+
4
+ report:
5
+ name: always_null_columns
6
+ module: schema_analysis
7
+ description: "Nullable columns that contain only NULL values"
8
+
9
+ sql:
10
+ category: schema_analysis
11
+ file: always_null_columns
12
+
13
+ title: "Always-NULL Columns"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - column_name
19
+ - data_type
20
+ - null_pct
21
+ - column_default
22
+ - estimated_rows
23
+
24
+ parameters:
25
+ limit:
26
+ type: integer
27
+ default: 100
28
+ description: "Maximum number of results"
29
+
30
+ problem_explanations:
31
+ column_name: always_null_column
@@ -0,0 +1,32 @@
1
+ # Columns whose value has never changed since table creation
2
+ # Detects dead fields that the application code no longer touches
3
+
4
+ report:
5
+ name: unused_columns
6
+ module: schema_analysis
7
+ description: "Columns that appear to have never been updated (still hold a single value)"
8
+
9
+ sql:
10
+ category: schema_analysis
11
+ file: unused_columns
12
+
13
+ title: "Unused Columns"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - column_name
19
+ - data_type
20
+ - sole_value
21
+ - column_default
22
+ - null_pct
23
+ - estimated_rows
24
+
25
+ parameters:
26
+ limit:
27
+ type: integer
28
+ default: 100
29
+ description: "Maximum number of results"
30
+
31
+ problem_explanations:
32
+ column_name: unused_column
@@ -0,0 +1,31 @@
1
+ # Transaction ID wraparound risk
2
+ # Monitors proximity to the 2-billion XID limit that triggers emergency shutdown
3
+
4
+ report:
5
+ name: wraparound_risk
6
+ module: system
7
+ description: "Transaction ID wraparound risk for all databases"
8
+
9
+ sql:
10
+ category: system
11
+ file: wraparound_risk
12
+
13
+ title: "Transaction ID Wraparound Risk"
14
+
15
+ columns:
16
+ - database_name
17
+ - xid_age
18
+ - pct_towards_wraparound
19
+ - remaining_xids
20
+ - freeze_max_age
21
+ - status
22
+ - database_size
23
+
24
+ parameters:
25
+ limit:
26
+ type: integer
27
+ default: 50
28
+ description: "Maximum number of results"
29
+
30
+ problem_explanations:
31
+ pct_towards_wraparound: wraparound_risk
@@ -0,0 +1,28 @@
1
+ # Tables without primary keys
2
+ # Missing PKs break logical replication and make row identification unreliable
3
+
4
+ report:
5
+ name: tables_without_pk
6
+ module: tables
7
+ description: "Tables missing a primary key"
8
+
9
+ sql:
10
+ category: tables
11
+ file: tables_without_pk
12
+
13
+ title: "Tables Without Primary Keys"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - estimated_rows
19
+ - table_size_mb
20
+
21
+ parameters:
22
+ limit:
23
+ type: integer
24
+ default: 50
25
+ description: "Maximum number of results"
26
+
27
+ problem_explanations:
28
+ table_name: missing_pk
@@ -0,0 +1,30 @@
1
+ # Tables never read since the last stats reset
2
+ # Zero seq_scan and zero idx_scan -- code does not query them
3
+
4
+ report:
5
+ name: unused_tables
6
+ module: tables
7
+ description: "Tables with zero read activity since the last statistics reset"
8
+
9
+ sql:
10
+ category: tables
11
+ file: unused_tables
12
+
13
+ title: "Unused Tables"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - live_rows
19
+ - total_size_mb
20
+ - last_analyzed
21
+ - db_stats_since
22
+
23
+ parameters:
24
+ limit:
25
+ type: integer
26
+ default: 50
27
+ description: "Maximum number of results"
28
+
29
+ problem_explanations:
30
+ table_name: unused_table
@@ -0,0 +1,32 @@
1
+ # Tables with disproportionately heavy update activity
2
+ # High updates_per_row signals same rows updated repeatedly; low hot_update_pct signals indexed columns are being updated
3
+
4
+ report:
5
+ name: update_hotspots
6
+ module: tables
7
+ description: "Tables with high update-to-row ratio or poor HOT update efficiency"
8
+
9
+ sql:
10
+ category: tables
11
+ file: update_hotspots
12
+
13
+ title: "Update Hotspots"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - live_rows
19
+ - updates
20
+ - updates_per_row
21
+ - hot_update_pct
22
+ - dead_rows
23
+
24
+ parameters:
25
+ limit:
26
+ type: integer
27
+ default: 30
28
+ description: "Maximum number of results"
29
+
30
+ problem_explanations:
31
+ updates_per_row: hot_rows
32
+ hot_update_pct: low_hot_update
@@ -13,6 +13,12 @@ module PgReports
13
13
  config.i18n.load_path += Dir[root.join("config", "locales", "*.yml")]
14
14
  end
15
15
 
16
+ initializer "pg_reports.compatibility_check" do
17
+ ActiveSupport.on_load(:active_record) do
18
+ PgReports::Compatibility.check_postgresql!
19
+ end
20
+ end
21
+
16
22
  initializer "pg_reports.assets" do |_app|
17
23
  # Assets are inline in views, no precompilation needed
18
24
  end
@@ -17,7 +17,8 @@ module PgReports
17
17
  private
18
18
 
19
19
  def self.get_module(module_name)
20
- PgReports::Modules.const_get(module_name.capitalize)
20
+ const_name = module_name.to_s.split("_").map(&:capitalize).join
21
+ PgReports::Modules.const_get(const_name)
21
22
  rescue NameError
22
23
  # Module doesn't exist, skip it
23
24
  # We don't auto-create modules to avoid conflicts
@@ -12,6 +12,9 @@ module PgReports
12
12
  # - duplicate_indexes
13
13
  # - invalid_indexes
14
14
  # - missing_indexes(limit: 20)
15
+ # - inefficient_indexes(limit: 50)
16
+ # - fk_without_indexes(limit: 50)
17
+ # - index_correlation(limit: 50)
15
18
  # - index_usage(limit: 50)
16
19
  # - bloated_indexes(limit: 20)
17
20
  # - index_sizes(limit: 50)
@@ -13,6 +13,7 @@ module PgReports
13
13
  # - expensive_queries(limit: 20)
14
14
  # - missing_index_queries(limit: 20)
15
15
  # - low_cache_hit_queries(limit: 20, min_calls: 100)
16
+ # - temp_file_queries(limit: 20)
16
17
  # - all_queries(limit: 50)
17
18
 
18
19
  # Reset pg_stat_statements statistics