pg_reports 0.6.0 → 0.6.2

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +143 -378
  4. data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
  5. data/app/views/layouts/pg_reports/application.html.erb +65 -8
  6. data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
  7. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +55 -57
  8. data/app/views/pg_reports/dashboard/_show_styles.html.erb +18 -0
  9. data/app/views/pg_reports/dashboard/index.html.erb +109 -106
  10. data/app/views/pg_reports/dashboard/show.html.erb +26 -26
  11. data/config/locales/en.yml +488 -0
  12. data/config/locales/ru.yml +481 -0
  13. data/config/locales/uk.yml +481 -0
  14. data/lib/pg_reports/annotation_parser.rb +13 -1
  15. data/lib/pg_reports/compatibility.rb +3 -3
  16. data/lib/pg_reports/dashboard/reports_registry.rb +83 -12
  17. data/lib/pg_reports/definitions/schema_analysis/always_null_columns.yml +31 -0
  18. data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -0
  19. data/lib/pg_reports/definitions/tables/unused_tables.yml +30 -0
  20. data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
  21. data/lib/pg_reports/module_generator.rb +2 -1
  22. data/lib/pg_reports/modules/schema_analysis.rb +261 -2
  23. data/lib/pg_reports/modules/system.rb +3 -3
  24. data/lib/pg_reports/query_monitor.rb +2 -6
  25. data/lib/pg_reports/report_definition.rb +20 -24
  26. data/lib/pg_reports/sql/schema_analysis/always_null_columns.sql +25 -0
  27. data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -0
  28. data/lib/pg_reports/sql/tables/unused_tables.sql +19 -0
  29. data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
  30. data/lib/pg_reports/version.rb +1 -1
  31. metadata +9 -1
@@ -244,6 +244,29 @@ uk:
244
244
  - "Висока активність UPDATE створює dead tuples — слідкуйте за vacuum."
245
245
  - "HOT updates (Heap Only Tuple) ефективніші за звичайні — індекси не оновлюються."
246
246
 
247
+ update_hotspots:
248
+ title: "Гарячі точки UPDATE"
249
+ what: "Таблиці, де ті самі рядки оновлюються багаторазово, або де індексовані колонки отримують hot-update."
250
+ how: "Рахує два співвідношення з pg_stat_user_tables: updates_per_row = n_tup_upd / n_live_tup (амплифікація запису на рядок) та hot_update_pct = n_tup_hot_upd / n_tup_upd (низьке значення = індексована колонка потрапляє в SET, що ламає HOT)."
251
+ nuances:
252
+ - "updates_per_row >> 1 означає, що окремі рядки переписуються багаторазово — типово для лічильників, статусів, last_seen_at, позицій."
253
+ - "Патерни рефакторингу для гарячих рядків: винести мінливу колонку в сусідню 1:1 'state'-таблицю; перейти на event/log-таблицю з періодичними згортками; буферизувати записи у фоні; debounce на стороні застосунку."
254
+ - "Низький hot_update_pct (наприклад, <50%) = оновлюється індексована колонка. Або видаліть індекс, або винесіть колонку в менш індексовану таблицю."
255
+ - "PostgreSQL не рахує UPDATE по колонках. Щоб знайти винну колонку, парсіть pg_stat_statements: імена колонок у SET зберігаються (нормалізуються лише літерали)."
256
+ - "Гарячі рядки + fillfactor за замовчуванням (100) вбивають HOT. ALTER TABLE ... SET (fillfactor=80) залишає місце для in-place UPDATE."
257
+ - "Високий n_dead_tup корелює з гарячими рядками — autovacuum має встигати, інакше bloat зростатиме швидко."
258
+
259
+ unused_tables:
260
+ title: "Невикористовувані таблиці"
261
+ what: "Таблиці з нулем seq_scan та нулем idx_scan з моменту останнього скидання статистики — код їх не читає."
262
+ how: "Фільтрує pg_stat_user_tables за seq_scan = 0 AND idx_scan = 0. Показує db_stats_since (pg_stat_database.stats_reset), щоб було зрозуміло, на якому вікні зроблено висновок."
263
+ nuances:
264
+ - "Переконайтеся, що db_stats_since покриває репрезентативний період — мінімум тиждень, краще повний звітний/біллінговий цикл. Нещодавній stats_reset, рестарт або оновлення PG обнуляють висновки."
265
+ - "Таблиці, які читаються лише репліками, НЕ рахуються на праймарі. Перевірте репліки окремо перед видаленням."
266
+ - "Таблиці, до яких звертаються лише COPY, pg_dump або logical replication, можуть виглядати unused — ці операції не інкрементять лічильники сканів."
267
+ - "FK на цю таблицю можуть тримати її 'у роботі' через каскади без прямих запитів."
268
+ - "Безпечна послідовність: перейменувати → почекати повний цикл → видалити. Або перемістити в окрему archive-схему."
269
+
247
270
  # === CONNECTIONS ===
248
271
  active_connections:
249
272
  title: "Active Connections"
@@ -410,6 +433,74 @@ uk:
410
433
  - "Низький cache hit: збільште shared_buffers (до 25% RAM), або проблема в запитах."
411
434
 
412
435
  # === SCHEMA ANALYSIS ===
436
+ unused_columns:
437
+ title: "Невикористовувані колонки"
438
+ what: "Колонки, які коли-небудь зберігали лише одне унікальне значення — сильна ознака, що застосунок їх більше не оновлює."
439
+ how: "Читає pg_stats.n_distinct = 1 (одне значення за вибіркою) та джойнить pg_attrdef для відображення default. Виключає первинні ключі та колонки унікальних індексів, щоб прибрати шум."
440
+ nuances:
441
+ - "n_distinct — оцінка за вибіркою. Зробіть ANALYZE перед тим, як довіряти результату. Застаріла статистика дає хибні спрацювання."
442
+ - "Таблиці менше ~1000 рядків виключені — на маленькій вибірці одне значення статистично безглузде."
443
+ - "Хибні спрацювання: feature flags, enum-колонки з одним released-значенням, колонки з дефолтом і одним валідним варіантом."
444
+ - "Справжні знахідки: колонки, у яких видалили Ruby/ORM-аксесор, але міграцію на drop column не написали; статуси, замінені іншим механізмом, але залишені в схемі."
445
+ - "PostgreSQL не відстежує запис по колонках, це евристика. Перед видаленням grep'ніть кодову базу за іменем колонки."
446
+ - "Дивіться також `always_null_columns` — той самий діагноз з іншого ракурсу."
447
+
448
+ always_null_columns:
449
+ title: "Колонки, завжди NULL"
450
+ what: "Nullable-колонки, де 100% рядків = NULL — застосунок перестав (або ніколи не починав) їх писати."
451
+ how: "Читає pg_stats.null_frac >= 0.999 (фактично всі NULL) та виключає колонки з NOT NULL. Джойнить pg_attrdef, щоб показати залишений default."
452
+ nuances:
453
+ - "null_frac — оцінка за вибіркою. Зробіть ANALYZE, якщо сумніваєтеся. Перевірте після репрезентативного вікна навантаження."
454
+ - "Колонка може бути NULL, тому що шлях запису рідко спрацьовує (premium-only поля, opt-in фічі). Перевіряйте перед видаленням."
455
+ - "Якщо null_pct = 100 І є не-NULL default — default не використовується, видаляйте разом із колонкою."
456
+ - "Деякі ORM серіалізують порожні рядки як '' замість NULL; цей звіт їх не зловить. Якщо потрібен такий сигнал — окремий звіт."
457
+ - "Порівняйте з `unused_columns` — він ловить колонки з одним значенням, цей — все-NULL. Разом покривають основні патерни мертвих колонок."
458
+
459
+ polymorphic_without_index:
460
+ title: "Polymorphic без індексу"
461
+ what: "Поліморфні `belongs_to`-асоціації, у яких пара `(*_type, *_id)` не покрита складеним індексом."
462
+ how: "Обходить `ActiveRecord::Base.descendants`, збирає `belongs_to`-рефлексії з `polymorphic: true` та перевіряє `pg_index` на наявність індексу, що покриває обидві колонки."
463
+ nuances:
464
+ - "PostgreSQL не зможе використати два одиночні індекси так само ефективно, як один складений для `WHERE x_type = ? AND x_id = ?` — це базовий патерн завантаження поліморфної асоціації."
465
+ - "Підказка `coverage` уточнює прогалину: \"neither indexed\" / \"only id indexed\" / \"only type indexed\" / \"type and id indexed separately\". Останні два — часткові рішення, не повні."
466
+ - "На маленьких таблицях нешкідливо; на великих (Comment, Note, Activity, AuditLog) кожен запит до асоціації перетворюється на seq scan."
467
+ - "Запропонована міграція використовує порядок `(type, id)` — у type зазвичай нижча cardinality, індекс звужується швидше."
468
+ - "Перед обходом моделей виконується `eager_load!`, щоб у dev результат був повним."
469
+
470
+ counter_cache_issues:
471
+ title: "Проблеми counter_cache"
472
+ what: "`belongs_to ..., counter_cache: ...` декларації, у яких цільова колонка відсутня в батьківській таблиці."
473
+ how: "Обходить усі моделі, знаходить `belongs_to` з опцією `counter_cache`, розв'язує ім'я очікуваної колонки (`<child_table>_count` для `counter_cache: true` або явний символ/рядок) та перевіряє колонки батька."
474
+ nuances:
475
+ - "Відсутня колонка = counter мовчки зламаний: записи нікуди не йдуть, `parent.<assoc>_count` повертає nil, будь-який код, що читає cached-значення, отримує stale або нуль."
476
+ - "Зворотний напрямок (колонка `*_count` без декларації `counter_cache`) навмисно не флагується — занадто багато false positives для вручну підтримуваних лічильників."
477
+ - "Батьківський клас розв'язується через `assoc.klass`; якщо клас відсутній або не завантажується — рядок пропускається."
478
+ - "Ім'я за замовчуванням для `counter_cache: true`: на батька додається колонка з іменем за таблицею **дочірньої** моделі в множині + `_count`. Приклад: `Comment belongs_to :user, counter_cache: true` → `users.comments_count`."
479
+ - "Перед обходом моделей виконується `eager_load!`, щоб у dev результат був повним."
480
+
481
+ soft_delete_without_scope:
482
+ title: "Soft delete без scope"
483
+ what: "Таблиці з колонкою `deleted_at` / `discarded_at` / `archived_at`, у моделі яких немає scope, що фільтрує soft-deleted рядки."
484
+ how: "Для кожної таблиці перевіряється наявність канонічних імен soft-delete колонок. Якщо є — знаходиться модель і перевіряється `acts_as_paranoid` (paranoia), `discard_column` (discard) або `default_scope`, чий згенерований SQL посилається на колонку."
485
+ nuances:
486
+ - "Без default scope будь-який звичайний `Model.where(...)` повертає soft-deleted рядки. Це тихо протікає у звіти, індекси, пошук, експорти."
487
+ - "Якщо команда свідомо відмовилася від default scope (надаючи перевагу явним `kept`/`with_deleted`), звіт дасть по рядку на кожну таку модель — це intentional, можна ігнорувати."
488
+ - "Метод детекції default_scope: будує `model.all.to_sql` та шукає ім'я колонки. Ловить `default_scope { where(deleted_at: nil) }` і еквіваленти, але пропускає scope з concerns, які ще не завантажені."
489
+ - "Таблиці без моделі рапортуються окремо зі статусом `no_model` — зазвичай означає, що таблиця використовується лише raw SQL і потребує ручного рев'ю."
490
+ - "Перед обходом моделей виконується `eager_load!`, щоб у dev результат був повним."
491
+
492
+ orphan_tables:
493
+ title: "Orphan-таблиці"
494
+ what: "Таблиці БД без відповідного Rails-класу моделі."
495
+ how: "Перелічує всі таблиці з `pg_class` (за винятком `schema_migrations` та `ar_internal_metadata`) і намагається знайти модель за звичайними naming conventions. Таблиці, що не знаходять моделі, потрапляють у звіт."
496
+ nuances:
497
+ - "Три класифікації: `join_table_candidate` (рівно дві `*_id` колонки і більше нічого — імовірно, легітимна HABTM-таблиця), `join_model_without_class` (кілька FK + додаткові поля — імовірно, мала бути моделлю join'а), `legacy` (усе інше)."
498
+ - "`join_table_candidate` зазвичай intentional — Rails не вимагає моделі для HABTM. Переглянути та ігнорувати."
499
+ - "`legacy` — найцікавіші рядки: таблиці, створені до того, як додали модель; перейменовані-але-не-видалені; створені повз Rails іншим сервісом; що належать видаленій фічі."
500
+ - "Звірте з `unused_tables` (категорія Таблиці) — таблиця одночасно orphan І з нулем читань = сильний кандидат на видалення."
501
+ - "False positives: namespaced-моделі (`Admin::User` → `admin_users`), STI-підкласи на одній базовій таблиці, моделі в engines, що не eager-loadяться."
502
+ - "Row count приблизний (n_live_tup з pg_stat_user_tables) — нещодавно записані таблиці можуть показувати 0 до наступного ANALYZE."
503
+
413
504
  missing_validations:
414
505
  title: "Відсутні валідації"
415
506
  what: "Унікальні індекси в базі даних без відповідних валідацій uniqueness в Rails-моделях."
@@ -447,3 +538,393 @@ uk:
447
538
  pool_saturation: "Пул з'єднань насичений. Ризик вичерпання з'єднань та помилок застосунку."
448
539
  high_connection_churn: "Висока оборотність з'єднань. Впровадьте connection pooling для зниження накладних витрат."
449
540
  too_many_short_connections: "Занадто багато короткоживучих з'єднань. Застосунок повинен перевикористовувати з'єднання через pooling."
541
+ unused_column: "Колонка має одне значення у всіх рядках. Ймовірно, не оновлювалася з моменту створення — кандидат на видалення."
542
+ always_null_column: "Колонка на 100% NULL. Застосунок більше не пише в це поле — кандидат на видалення."
543
+ hot_rows: "Ті самі рядки оновлюються багаторазово. Розгляньте розділення гарячих/холодних колонок, батчинг запису або event-log таблицю."
544
+ low_hot_update: "Низький відсоток HOT updates — оновлюються індексовані колонки. Видаліть індекс або винесіть колонку, щоб знизити write amplification."
545
+ unused_table: "Таблиця не читалася з моменту останнього скидання статистики. Кандидат на архівацію або видалення — спочатку перевірте вікно статистики."
546
+ polymorphic_no_index: "Поліморфна асоціація без складеного індексу (type, id). Зі зростанням таблиці запити будуть seq-scan'ити."
547
+ counter_cache_missing_column: "Колонка counter_cache відсутня в батьківській таблиці. Counter мовчки зламаний — записи нікуди не йдуть."
548
+ soft_delete_unprotected: "Soft-delete колонка без scope, що фільтрує її. Звичайні запити повертають видалені рядки."
549
+ orphan_table_legacy: "Таблиця без Rails-моделі. Скоріше за все, legacy — перевірте перед видаленням."
550
+
551
+ # UI strings shown in the dashboard chrome (buttons, modals, toasts, etc.)
552
+ ui:
553
+ branding:
554
+ title: "PgReports"
555
+ subtitle: "Дашборд аналізу PostgreSQL"
556
+ page_title: "PgReports — Дашборд"
557
+ navigation:
558
+ dashboard: "Дашборд"
559
+ back: "← Назад"
560
+ actions:
561
+ cancel: "Скасувати"
562
+ retry: "Повторити"
563
+ copy: "📋 Копіювати"
564
+ copy_query: "📋 Копіювати запит"
565
+ copy_code: "📋 Копіювати код"
566
+ copy_to_clipboard_title: "Скопіювати в буфер обміну"
567
+ copied_feedback: "✓ Скопійовано!"
568
+ clear_all: "Очистити все"
569
+ run_report: "▶ Запустити звіт"
570
+ export: "⬇ Експорт"
571
+ download_text: "📄 Текст (.txt)"
572
+ download_csv: "📊 CSV (.csv)"
573
+ download_json: "📋 JSON (.json)"
574
+ download: "📥 Завантажити"
575
+ copy_ai_prompt: "Копіювати промпт"
576
+ send_telegram: "📨 Telegram"
577
+ sending: "Надсилання..."
578
+ reset_statistics: "🗑️ Скинути статистику"
579
+ resetting: "Скидання..."
580
+ confirm_reset: "Так, скинути"
581
+ create_extension: "⚡ Створити розширення"
582
+ creating: "Створення..."
583
+ ide_settings_button_title: "Налаштування IDE"
584
+ explain_analyze: "📊 EXPLAIN ANALYZE"
585
+ execute_query: "▶ Виконати запит"
586
+ create_migration_file: "📁 Створити файл і відкрити в IDE"
587
+ start_monitoring: "▶ Запустити моніторинг"
588
+ stop_monitoring: "⏹ Зупинити моніторинг"
589
+ starting: "Запуск..."
590
+ stopping: "Зупинка..."
591
+ load_history: "📜 Завантажити історію (50)"
592
+ loading: "Завантаження..."
593
+ running: "Виконання..."
594
+ save_for_comparison: "📌 Зберегти для порівняння"
595
+ saved_marker: "📌 Збережено"
596
+ status:
597
+ pg_stat_ready: "pg_stat_statements готовий"
598
+ extension_installed: "Розширення встановлено, не передзавантажено"
599
+ preloaded: "Передзавантажено, розширення не створено"
600
+ not_configured: "Не налаштовано"
601
+ monitoring_unavailable: "Live-моніторинг недоступний"
602
+ modals:
603
+ enable_pg_stat_title: "Увімкнення pg_stat_statements"
604
+ enable_pg_stat_intro: "Щоб увімкнути pg_stat_statements, виконайте такі кроки:"
605
+ edit_postgresql_conf: "Відредагуйте postgresql.conf:"
606
+ restart_postgresql: "Перезапустіть PostgreSQL:"
607
+ create_extension_step: "Створіть розширення:"
608
+ enable_button_note: "Або натисніть кнопку «Створити розширення» після перезапуску."
609
+ reset_stats_title: "⚠️ Скидання статистики"
610
+ reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
611
+ reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
612
+ ide_settings_title: "⚙️ Налаштування IDE"
613
+ problem_detected_title: "⚠️ Виявлено проблему"
614
+ query_analyzer_title: "📊 Аналізатор запиту"
615
+ query_label: "Запит:"
616
+ parameters_label: "Параметри:"
617
+ migration_title: "🗑️ Міграція видалення індексу"
618
+ migration_subtitle: "Згенерована міграція для видалення індексу:"
619
+ migration_warning: "Створення міграції згенерує файл міграції у вашому проєкті. Запуск цієї міграції видалить індекс із БД, що може значно вплинути на продуктивність застосунку."
620
+ migration_warning_dev_only: "Цю операцію слід виконувати лише в локальному dev-середовищі."
621
+ query_execution_disabled_title: "⚠️ Виконання запитів вимкнено"
622
+ query_execution_disabled_intro: "Щоб увімкнути цю функцію, додайте до конфігурації:"
623
+ settings:
624
+ default_ide_label: "IDE за замовчуванням для посилань на джерела:"
625
+ ide_show_menu: "Показувати меню (за замовчуванням)"
626
+ ide_vscode_wsl: "VS Code (WSL)"
627
+ ide_vscode: "VS Code"
628
+ ide_rubymine: "RubyMine"
629
+ ide_intellij: "IntelliJ IDEA"
630
+ ide_cursor_wsl: "Cursor (WSL)"
631
+ ide_cursor: "Cursor"
632
+ monitoring:
633
+ live_title: "Live-моніторинг"
634
+ update_interval: "Оновлення кожні 5с"
635
+ toggle_title: "Перемкнути live-моніторинг"
636
+ query_monitor_title: "Монітор SQL-запитів"
637
+ session_label: "Сесія:"
638
+ queries_label: "Запитів:"
639
+ feed_empty: "Натисніть «Запустити моніторинг», щоб почати захоплення SQL-запитів"
640
+ feed_no_queries: "Запитів поки не захоплено..."
641
+ unknown_source: "Джерело невідоме"
642
+ expand_collapse_title: "Розгорнути / згорнути"
643
+ metrics:
644
+ connections_label: "З'єднання"
645
+ tps_label: "TPS"
646
+ tps_unit: "тр/с"
647
+ commit_label: "commit:"
648
+ rollback_label: "rollback:"
649
+ cache_hit_label: "Cache hit"
650
+ cache_hit_detail: "блоки heap із кешу"
651
+ long_queries_label: "Довгі запити"
652
+ queries_unit: "запитів"
653
+ long_running_threshold: "> 60с виконання"
654
+ blocked_label: "Заблоковано"
655
+ processes_unit: "процесів"
656
+ waiting_for_locks: "чекають блокувань"
657
+ percent_used_suffix: "% використано"
658
+ categories:
659
+ requires_pg_stat: "🔒 Потрібен pg_stat_statements"
660
+ reports_count_suffix: "звітів"
661
+ documentation:
662
+ toggle_title: "📖 Що показує цей звіт?"
663
+ what_section: "📋 Що"
664
+ why_section: "❓ Чому це важливо"
665
+ nuances_section: "⚠️ Нюанси"
666
+ thresholds_section: "📊 Пороги"
667
+ threshold_warning_label: "⚠️ Warning:"
668
+ threshold_critical_label: "🔴 Critical:"
669
+ threshold_inverted_note: "(менше — гірше)"
670
+ filters:
671
+ title: "🔍 Параметри фільтрації"
672
+ current_value: "зараз"
673
+ saved:
674
+ title: "📌 Збережено для порівняння"
675
+ saved_at_prefix: "▸ Збережено:"
676
+ click_to_expand: "Натисніть, щоб розгорнути"
677
+ confirm_clear_all: "Видалити всі збережені записи для цього звіту?"
678
+ remove_title: "Видалити"
679
+ results:
680
+ title: "Результати"
681
+ click_run_hint: "Натисніть «Запустити звіт», щоб отримати дані"
682
+ empty_message: "Проблем не знайдено. Усе добре!"
683
+ showing_first_of_total: "Показані перші %{count} з %{total} рядків"
684
+ no_rows_returned: "Рядків немає"
685
+ rows_label: "Рядків:"
686
+ execution_time_label: "Час виконання:"
687
+ null_placeholder: "<null>"
688
+ sections:
689
+ recommendation: "💡 Рекомендація"
690
+ detected_issues: "⚠️ Виявлені проблеми"
691
+ execution_plan: "📊 План виконання"
692
+ line_label: "Рядок"
693
+ current_label: "Поточне:"
694
+ threshold_label: "Поріг:"
695
+ threshold_inverted_long: "(інверсія: менші значення — гірше)"
696
+ warning_eq: "warning"
697
+ critical_eq: "critical"
698
+ levels:
699
+ critical: "🔴 Критично"
700
+ warning: "⚠️ Увага"
701
+ errors:
702
+ error_prefix: "Помилка:"
703
+ unable_fetch_metrics: "Не вдалося отримати статистику БД."
704
+ possible_causes: "Можливі причини:"
705
+ cause_permissions: "Недостатньо прав для доступу до БД"
706
+ cause_views: "Системні представлення статистики недоступні"
707
+ cause_connection: "Проблеми зі з'єднанням"
708
+ fetch_metrics_failed: "Не вдалося отримати live-метрики"
709
+ fetch_metrics_check_perms: "Не вдалося отримати статистику БД. Перевірте права доступу."
710
+ insufficient_database_perms: "Недостатньо прав для доступу до системних представлень статистики"
711
+ network_error_prefix: "Мережева помилка:"
712
+ copy_failed: "Не вдалося скопіювати"
713
+ run_report_first: "Спочатку запустіть звіт"
714
+ run_report_failed: "Не вдалося запустити звіт"
715
+ report_not_found: "Звіт не знайдено"
716
+ send_telegram_failed: "Не вдалося надіслати в Telegram"
717
+ reset_stats_failed: "Не вдалося скинути статистику"
718
+ start_monitoring_failed: "Не вдалося запустити моніторинг"
719
+ stop_monitoring_failed: "Не вдалося зупинити моніторинг"
720
+ load_history_failed: "Не вдалося завантажити історію:"
721
+ no_query_history: "Історію запитів не знайдено"
722
+ decode_query_failed: "Не вдалося декодувати запит"
723
+ explain_analyze_failed: "Не вдалося виконати EXPLAIN ANALYZE"
724
+ execute_query_failed: "Не вдалося виконати запит"
725
+ create_migration_failed: "Не вдалося створити міграцію"
726
+ explain_disabled_toast: "⚠️ EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
727
+ execute_disabled_toast: "⚠️ Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
728
+ query_monitoring_error: "Помилка моніторингу запитів"
729
+ query_hash_required: "Потрібен хеш запиту"
730
+ query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
731
+ query_not_found_expired: "Запит не знайдено або термін дії минув. Оновіть сторінку."
732
+ security_violation_prefix: "Порушення безпеки:"
733
+ trigger_variables_not_allowed: "Не можна виконати EXPLAIN ANALYZE для запитів із тригерними змінними (NEW, OLD). Вони доступні лише в контексті тригерних функцій."
734
+ missing_parameter_values: "Вкажіть значення для всіх плейсхолдерів параметрів ($1, $2 тощо)"
735
+ migration_dev_only: "Створення міграцій дозволено лише в development-середовищі"
736
+ filename_code_required: "Ім'я файлу та код обов'язкові"
737
+ invalid_filename_format: "Невірний формат імені файлу міграції"
738
+ migrations_dir_not_found: "Каталог міграцій не знайдено"
739
+ success:
740
+ statistics_reset: "Статистику успішно скинуто"
741
+ report_generated: "Звіт успішно згенеровано"
742
+ ai_prompt_copied: "AI-промпт скопійовано в буфер обміну"
743
+ explain_copied: "Вивід EXPLAIN скопійовано в буфер обміну"
744
+ migration_copied: "Код міграції скопійовано в буфер обміну"
745
+ migration_created: "Міграцію успішно створено"
746
+ telegram_sent: "Звіт надіслано в Telegram"
747
+ queries_loaded: "Завантажено %{count} запитів з історії"
748
+ record_saved: "Запис збережено для порівняння"
749
+ record_removed: "Запис видалено"
750
+ record_removed_saved: "Запис видалено зі збережених"
751
+ all_saved_cleared: "Усі збережені записи очищено"
752
+
753
+ # Category names (shown on the dashboard grid)
754
+ categories:
755
+ queries: "Запити"
756
+ indexes: "Індекси"
757
+ tables: "Таблиці"
758
+ connections: "З'єднання"
759
+ system: "Система"
760
+ schema_analysis: "Аналіз схеми"
761
+
762
+ # Report names and short descriptions (shown on the dashboard listing)
763
+ reports:
764
+ slow_queries:
765
+ name: "Повільні запити"
766
+ description: "Запити з високим середнім часом виконання"
767
+ heavy_queries:
768
+ name: "Часті запити"
769
+ description: "Найчастіше викликані запити"
770
+ expensive_queries:
771
+ name: "Дорогі запити"
772
+ description: "Запити з найбільшим сумарним часом"
773
+ missing_index_queries:
774
+ name: "Без індексів"
775
+ description: "Запити, яким, можливо, потрібні індекси"
776
+ low_cache_hit_queries:
777
+ name: "Низький cache hit"
778
+ description: "Запити з поганою утилізацією кешу"
779
+ temp_file_queries:
780
+ name: "Скидання на диск"
781
+ description: "Запити, що скидають дані на диск"
782
+ all_queries:
783
+ name: "Усі запити"
784
+ description: "Повна статистика запитів"
785
+ unused_indexes:
786
+ name: "Невикористовувані індекси"
787
+ description: "Індекси, що рідко або ніколи не скануються"
788
+ duplicate_indexes:
789
+ name: "Дублікати індексів"
790
+ description: "Надлишкові індекси"
791
+ invalid_indexes:
792
+ name: "Невалідні індекси"
793
+ description: "Індекси, які не вдалося побудувати"
794
+ missing_indexes:
795
+ name: "Відсутні індекси"
796
+ description: "Таблиці, яким, можливо, потрібні індекси"
797
+ inefficient_indexes:
798
+ name: "Неефективні індекси"
799
+ description: "Індекси з високим read-to-fetch ratio"
800
+ index_usage:
801
+ name: "Використання індексів"
802
+ description: "Статистика сканування індексів"
803
+ bloated_indexes:
804
+ name: "Роздуті індекси"
805
+ description: "Індекси з високим bloat"
806
+ fk_without_indexes:
807
+ name: "FK без індексів"
808
+ description: "Зовнішні ключі без підтримуючого індексу"
809
+ index_correlation:
810
+ name: "Кореляція індексів"
811
+ description: "Індекси з низькою фізичною кореляцією"
812
+ index_sizes:
813
+ name: "Розміри індексів"
814
+ description: "Використання диска індексами"
815
+ table_sizes:
816
+ name: "Розміри таблиць"
817
+ description: "Використання диска таблицями"
818
+ bloated_tables:
819
+ name: "Роздуті таблиці"
820
+ description: "Таблиці з високою часткою dead tuples"
821
+ vacuum_needed:
822
+ name: "Потрібен VACUUM"
823
+ description: "Таблиці, яким потрібен VACUUM"
824
+ row_counts:
825
+ name: "Кількість рядків"
826
+ description: "Число рядків у таблицях"
827
+ cache_hit_ratios:
828
+ name: "Cache hit таблиць"
829
+ description: "Статистика кешу по таблицях"
830
+ seq_scans:
831
+ name: "Sequential scans"
832
+ description: "Таблиці з великою кількістю sequential scan"
833
+ tables_without_pk:
834
+ name: "Без первинного ключа"
835
+ description: "Таблиці без первинного ключа"
836
+ recently_modified:
837
+ name: "Нещодавно змінені"
838
+ description: "Таблиці з нещодавньою активністю"
839
+ update_hotspots:
840
+ name: "Гарячі точки UPDATE"
841
+ description: "Ті самі рядки або індексовані колонки часто оновлюються"
842
+ unused_tables:
843
+ name: "Невикористовувані таблиці"
844
+ description: "Таблиці, які не запитуються з моменту останнього скидання статистики"
845
+ active_connections:
846
+ name: "Активні з'єднання"
847
+ description: "Поточні підключення до БД"
848
+ connection_stats:
849
+ name: "Статистика з'єднань"
850
+ description: "З'єднання за станом"
851
+ long_running_queries:
852
+ name: "Довгі запити"
853
+ description: "Запити, що виконуються довго"
854
+ blocking_queries:
855
+ name: "Блокуючі запити"
856
+ description: "Запити, що блокують інші"
857
+ locks:
858
+ name: "Блокування"
859
+ description: "Поточні блокування в БД"
860
+ idle_connections:
861
+ name: "Idle з'єднання"
862
+ description: "З'єднання, що простоюють"
863
+ pool_usage:
864
+ name: "Використання пулу"
865
+ description: "Утилізація пулу з'єднань"
866
+ pool_wait_times:
867
+ name: "Час очікування"
868
+ description: "Аналіз очікування ресурсів"
869
+ pool_saturation:
870
+ name: "Насичення пулу"
871
+ description: "Попередження про здоров'я пулу"
872
+ connection_churn:
873
+ name: "Churn з'єднань"
874
+ description: "Аналіз життєвого циклу з'єднань"
875
+ database_sizes:
876
+ name: "Розміри баз даних"
877
+ description: "Розмір усіх баз"
878
+ settings:
879
+ name: "Налаштування"
880
+ description: "Конфігурація PostgreSQL"
881
+ extensions:
882
+ name: "Розширення"
883
+ description: "Встановлені розширення"
884
+ activity_overview:
885
+ name: "Огляд активності"
886
+ description: "Зведення поточної активності"
887
+ wraparound_risk:
888
+ name: "Ризик wraparound"
889
+ description: "Близькість до ліміту Transaction ID"
890
+ checkpoint_stats:
891
+ name: "Статистика checkpoint"
892
+ description: "Метрики чекпоінтів та bgwriter"
893
+ cache_stats:
894
+ name: "Статистика кешу"
895
+ description: "Статистика кешування БД"
896
+ missing_validations:
897
+ name: "Відсутні валідації"
898
+ description: "Унікальні індекси без валідацій моделі"
899
+ unused_columns:
900
+ name: "Невикористовувані колонки"
901
+ description: "Колонки, що мають лише одне значення"
902
+ always_null_columns:
903
+ name: "Завжди NULL"
904
+ description: "Nullable-колонки, що містять лише NULL"
905
+ polymorphic_without_index:
906
+ name: "Polymorphic без індексу"
907
+ description: "Поліморфні асоціації без складеного індексу"
908
+ counter_cache_issues:
909
+ name: "Проблеми counter_cache"
910
+ description: "counter_cache декларації без цільової колонки"
911
+ soft_delete_without_scope:
912
+ name: "Soft delete без scope"
913
+ description: "Soft-delete колонки без scope, що фільтрує їх"
914
+ orphan_tables:
915
+ name: "Orphan-таблиці"
916
+ description: "Таблиці БД без відповідної Rails-моделі"
917
+
918
+ # Filter parameter labels and descriptions
919
+ parameters:
920
+ limit:
921
+ label: "Ліміт"
922
+ description: "Максимальна кількість результатів"
923
+ min_calls:
924
+ label: "Мін. викликів"
925
+ description: "Мінімальна кількість викликів запиту"
926
+ min_duration_seconds:
927
+ label: "Мін. тривалість (сек)"
928
+ description: "Мінімальна тривалість запиту в секундах"
929
+ threshold_label: "%{field} — поріг"
930
+ threshold_description: "Перевизначити поріг для %{field}"
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
4
+
3
5
  module PgReports
4
6
  # Parses SQL query comments to extract source location and metadata
5
7
  # Supports:
6
8
  # - Marginalia format: /*application:myapp,controller:users,action:index*/
7
- # - Rails QueryLogs: /*action='index',controller='users'*/
9
+ # - Rails QueryLogs: /*action='index',controller='users',source_location='app/foo.rb:42'*/
8
10
  #
9
11
  module AnnotationParser
10
12
  class << self
@@ -32,6 +34,16 @@ module PgReports
32
34
  end
33
35
  end
34
36
 
37
+ # Rails QueryLogs custom tag — Rails URL-encodes tag values (CGI.escape) to keep
38
+ # SQL comments safe, so "/" → "%2F" and ":" → "%3A". Decode then split into :file/:line.
39
+ if result[:source_location] && !result[:file]
40
+ decoded = CGI.unescape(result[:source_location].to_s)
41
+ if (match = decoded.match(%r{^(.+):(\d+)$}))
42
+ result[:file] = match[1]
43
+ result[:line] = match[2]
44
+ end
45
+ end
46
+
35
47
  result
36
48
  end
37
49
 
@@ -5,9 +5,9 @@ module PgReports
5
5
  # Called once at boot (Ruby/Rails) and lazily on first DB access (PostgreSQL).
6
6
  module Compatibility
7
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
8
+ MINIMUM_RUBY_VERSION = "2.7"
9
+ MINIMUM_RAILS_VERSION = "5.0"
10
+ MINIMUM_PG_VERSION = 12_00_00 # server_version_num format
11
11
  MINIMUM_PG_VERSION_LABEL = "12"
12
12
 
13
13
  class << self