Add db-tune ops subcommand + production RDS runbook#27890
Add db-tune ops subcommand + production RDS runbook#27890
Conversation
Generate a per-table autovacuum (Postgres) / InnoDB stats (MySQL) tuning report from the live database; --apply executes the ALTER statements after operator confirmation; --analyze refreshes planner stats on the changed tables. Heuristic skips small dev tables (row-count gated) and recognizes already-tightened settings as OK. Catalog values come from the 600k-container production audit. The companion runbook in docs/rds-postgres-production-runbook.md captures the parameter-group settings, extensions, and the indexes that must be applied by hand because the migration runner can't dedup cross-version. Tests: 38 unit tests on the heuristic + report formatter; DbTuneIT covers analyze / apply / analyzeOne / dry-run against the IT bootstrap's live Postgres or MySQL Testcontainer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an operator-facing db-tune ops subcommand plus a production RDS Postgres runbook, backed by a new dbtune Java utility package that can analyze table stats, produce tuning recommendations, and (optionally) apply per-table settings and refresh planner stats for Postgres/MySQL.
Changes:
- Introduces
org.openmetadata.service.util.dbtune(catalogs, engine-specific tuners, report renderer, result models). - Wires a new
db-tunesubcommand intoOpenMetadataOperationswith--apply/--yes/--analyze. - Adds unit tests for heuristic/report formatting and an integration test (
DbTuneIT), plus an RDS Postgres production runbook.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java | Adds db-tune CLI command, prompts, and apply/analyze execution flow |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/Action.java | Defines actionable vs non-actionable recommendation actions |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/AutoTuner.java | Defines the engine-specific tuner contract (analyze/recommend/apply/analyzeOne/sql builder) |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/DbTuneReport.java | Renders ASCII report output and ALTER statement previews |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/DbTuneResult.java | Bundles engine/version + server param checks + per-table recommendations |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/MysqlAutoTuner.java | Implements MySQL stats/options inspection, recommendations, and ALTER/ANALYZE generation |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/MysqlTuningCatalog.java | Maps MySQL tables to stats sampling “profiles” and thresholds |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/PostgresAutoTuner.java | Implements Postgres reloptions inspection, recommendations, and ALTER/ANALYZE generation |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/PostgresTuningCatalog.java | Maps Postgres tables to autovacuum “profiles” and thresholds |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/ServerParamCheck.java | Models server-level parameter compliance rows |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/TableRecommendation.java | Models per-table recommendation outputs |
| openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/TableStats.java | Models per-table observed stats/settings inputs |
| openmetadata-service/src/test/java/org/openmetadata/service/util/dbtune/DbTuneReportTest.java | Unit tests for report formatting helpers and sections |
| openmetadata-service/src/test/java/org/openmetadata/service/util/dbtune/MysqlAutoTunerTest.java | Unit tests for MySQL heuristic decisions, parsing, and SQL formatting |
| openmetadata-service/src/test/java/org/openmetadata/service/util/dbtune/PostgresAutoTunerTest.java | Unit tests for Postgres heuristic decisions, parsing, quoting, and SQL formatting |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DbTuneIT.java | Integration test exercising analyze/apply/analyze-one against Testcontainers DB |
| docs/rds-postgres-production-runbook.md | Consolidated ops runbook for RDS Postgres parameter groups, extensions, table overrides, and manual indexes |
| /** | ||
| * End-to-end tests for {@link AutoTuner} against the live Testcontainers database. The bootstrap | ||
| * runs every migration up to the current version, so the tracked entity tables (e.g. | ||
| * {@code storage_container_entity}) exist; we exercise the analyze → apply → analyze-one path | ||
| * against the real schema and reset the modified reloptions / table options at the end. | ||
| * | ||
| * <p>Sequential because each test mutates table-level reloptions on shared production tables; | ||
| * parallel execution would race between read-stats and apply. | ||
| */ |
There was a problem hiding this comment.
Fixed in a719d47: added @execution(ExecutionMode.SAME_THREAD) on the class so the tests don't race on shared reloptions when the suite runs in parallel.
🔴 Playwright Results — 2 failure(s), 11 flaky✅ 3991 passed · ❌ 2 failed · 🟡 11 flaky · ⏭️ 86 skipped
Genuine Failures (failed on all attempts)❌
|
…/match disambiguation, script path - ServerParamCheck.STATUS_UNDERSIZED -> STATUS_MISMATCH. Some recommended values are deliberately lower than engine defaults (random_page_cost 1.1 vs 4.0, autovacuum scale factors). Labelling those as "UNDERSIZED" was wrong. Added a regression test that pins random_page_cost as MISMATCH when the current value is higher than recommended. - dbTune --apply: distinguish "no tracked tables on this DB" from "all tables already match" in the Nothing-to-apply log line. Mirrors the same disambiguation already in DbTuneReport.appendNextSteps. - DbTuneReport next-steps now points at ./bootstrap/openmetadata-ops.sh (the actual location of the wrapper script) instead of the bare ./openmetadata-ops.sh which would be file-not-found from the repo root. - DbTuneIT analyzeReturnsRecommendationsForKnownTables: assertion message now references TEST_TABLE so it stays correct after the switch from storage_container_entity to entity_relationship. Tests: 40 unit tests passing (+1 for the higher-than-recommended MISMATCH case). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round 2 review (4230257768) — addressed all four findings in f5a945a.
Unit tests: 40 passing (was 39, +1 for the new direction-agnostic mismatch case). 🤖 Generated with Claude Code |
The earlier IT had applyChangesReloptionsAndIsIdempotent + analyzeOne running ALTER TABLE / ANALYZE TABLE against entity_relationship — a real catalog table that the rest of the suite writes to. On MySQL this broke ~17 downstream ITs (GlossaryResourceIT, GlossaryTermRelationsIT, DomainResourceIT, LineageBrokenReferenceIT, OpenLineageLineageResolutionIT) with "Cannot create a JSON value from a string with CHARACTER SET 'binary'" on inserts into entity_relationship.json. Root cause: ALTER TABLE bumps MySQL's per-table metadata version, which invalidates JDBC prepared-statement caches across the whole shared Testcontainer. After re-prepare, the JSON column metadata sometimes comes back as VARBINARY in Connector/J's cache, breaking subsequent JSON inserts. Confirmed via CI log timing: DbTuneIT finished at 17:58:12, GlossaryTermRelationsIT failed starting 17:58:53. The recommendations themselves are correct — STATS_PERSISTENT=1, STATS_AUTO_RECALC=1, STATS_SAMPLE_PAGES=100 are safe and modern InnoDB defaults except for SAMPLE_PAGES. The test just cannot afford the side effect on a shared DB. Fix: introduce dbtune_it_isolated_table created/dropped per test in @BeforeEach/@AfterEach, build a TableRecommendation for it directly (bypassing the static catalog), and run apply + analyzeOne against it. The read-only tests (analyze, dry-run) continue to use the real catalog since they don't mutate anything. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| jdbi.useHandle(handle -> tuner.apply(handle, rec)); | ||
|
|
||
| String built = tuner.buildAlterStatement(rec); | ||
| assertTrue(built.contains(ISOLATED_TABLE), "ALTER target table mismatch: " + built); | ||
|
|
||
| // Apply twice — second invocation must complete without throwing. | ||
| jdbi.useHandle(handle -> tuner.apply(handle, rec)); |
There was a problem hiding this comment.
💡 Quality: applyExecutesAndIsIdempotent no longer verifies DB state
The old test used assertSettingsMatch to confirm settings were actually persisted in the database after apply(). The new test only verifies that buildAlterStatement returns a string containing the table name and that apply() doesn't throw. While the rationale for using an isolated table is solid (avoiding MySQL metadata invalidation), the test could still read back the settings from the isolated table to confirm they took effect — there's no risk of side-effects on shared tables since ISOLATED_TABLE is private to this test.
This would restore the end-to-end confidence that apply() actually mutates the DB, rather than just not crashing.
Suggested fix:
// After the first apply, verify the settings were actually written:
Map<String, String> after = currentSettingsFor(tuner, jdbi, ISOLATED_TABLE);
assertSettingsMatch(rec.recommendedSettings(), after);
// Apply twice — must be idempotent
jdbi.useHandle(handle -> tuner.apply(handle, rec));
Map<String, String> afterSecond = currentSettingsFor(tuner, jdbi, ISOLATED_TABLE);
assertEquals(after, afterSecond, "Apply should be idempotent");
Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion
|
|
||
| jdbi.useHandle(handle -> tuner.apply(handle, rec)); | ||
|
|
||
| String built = tuner.buildAlterStatement(rec); | ||
| assertTrue(built.contains(ISOLATED_TABLE), "ALTER target table mismatch: " + built); | ||
|
|
||
| // Apply twice — second invocation must complete without throwing. | ||
| jdbi.useHandle(handle -> tuner.apply(handle, rec)); | ||
| } | ||
|
|
| * <li>Read observed table stats and current parameter-group settings from the database. | ||
| * <li>Compute a recommended table-level reloption set per table (pure logic — see | ||
| * {@link #recommend(TableStats)}). | ||
| * <li>Apply the recommendations via {@code ALTER TABLE ... SET (...)} when the operator opts in. |
| * the planner picks the right index against multi-GB JSONB heaps. {@code STATS_PERSISTENT=1} + | ||
| * {@code STATS_AUTO_RECALC=1} are the modern InnoDB defaults; we assert them explicitly so a | ||
| * tenant with stale my.cnf overrides converges. |
| @Command( | ||
| name = "db-tune", | ||
| description = | ||
| "Generate a per-table autovacuum / InnoDB stats tuning report and optionally apply it. " | ||
| + "Default mode is read-only — pass --apply to execute the ALTER TABLE statements " | ||
| + "and --analyze to refresh planner stats on changed tables.") | ||
| public Integer dbTune( |
The tuning report we already had is a static playbook (table → recipe + row-count gate). Operators rightly asked whether db-tune actually inspects the live database for *its own* signals — unused indexes, bloat, slow queries, cache hit ratios — and surfaces recommendations from those measurements. Until now: no. This adds a parallel read-only path: ./bootstrap/openmetadata-ops.sh db-tune --diagnose Postgres categories (each isolated in its own try block; missing extension or permissions surfaces in DbTuneDiagnosis.notes rather than failing the run): - UNUSED_INDEX — pg_stat_user_indexes idx_scan=0, size > 10 MB, non-unique non-pkey - HIGH_DEAD_TUPLES — n_dead_tup/n_live_tup > 0.2 with n_live_tup > 10k (autovacuum falling behind) - LOW_CACHE_HIT — heap_blks_read > 1000 AND hit ratio < 90% - STALE_STATS — last_autoanalyze NULL or > 14 days, n_live_tup > 1000 - SEQ_SCAN_HEAVY — seq_scan/idx_scan > 10 with > 1000 seq scans (suggests missing index) - SLOW_QUERY — pg_stat_statements top 10 by mean_exec_time, calls > 100 (gracefully skipped if extension absent) MySQL categories: - UNUSED_INDEX — sys.schema_unused_indexes filtered to current schema - LOW_BUFFER_POOL_HIT — Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests < 99% - SLOW_QUERY — performance_schema.events_statements_summary_by_digest top 10 by avg_timer_wait - FULL_TABLE_SCAN — sys.statements_with_full_table_scans Concept stays separate from AutoTuner: Diagnostic is read-only and never participates in --apply. Categories with zero findings are suppressed in the report; notes capture what couldn't be checked. Each finding is structured as (category, severity, attributes) with the attribute keys driven by DiagnosticCategory.columns() so the renderer dispatches a category-specific layout. Tests: 50 unit tests passing (40 → 50, +10 in DiagnosticReportTest covering EnumMap grouping order, empty/non-empty rendering, suppression, notes appending, query truncation, and column-list immutability). DbTuneIT gains diagnoseCompletesWithoutErrorAndReturnsStructuredResult that exercises the full end-to-end against the live Testcontainer (read-only, safe). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Added `--diagnose` in 2d9f047. The tuning report stays static (curated playbook of known-hot tables); the new diagnostic path is the data-driven half — it reads the live database for unused indexes, bloat, slow queries, cache hit, etc., and surfaces what it finds. Usage``` `--diagnose` is purely read-only. It never participates in `--apply` — diagnostic findings (e.g. "this index has 0 scans and is 1.2 GB") require human review before any DROP, so we deliberately don't auto-act on them. Postgres categories
MySQL categories
Sample output (Postgres)``` Unused indexes (3 found): Tables with low cache hit ratio (1 found): Top slowest queries (10 found): Notes:
The first three rows mirror exactly the slack-thread audit: 9 of 12 `tag_usage` indexes had 0 scans and consumed ~24 GB combined. `db-tune --diagnose` would have flagged that automatically. Architecture``` `Diagnostic` is intentionally separate from `AutoTuner` — diagnostics are read-only by definition and shouldn't be conflated with the apply path. Categories with zero findings are suppressed in the report; `notes` lists what couldn't be checked (missing extension, view permissions, etc.). Tests
🤖 Generated with Claude Code |
| List<Finding> seqScanHeavy(final Handle handle) { | ||
| return handle | ||
| .createQuery( | ||
| "SELECT relname AS table_name, seq_scan, idx_scan " | ||
| + "FROM pg_stat_user_tables " | ||
| + "WHERE seq_scan > :min_seq " | ||
| + " AND seq_scan::numeric / NULLIF(idx_scan, 0) > :ratio " | ||
| + "ORDER BY seq_scan DESC " | ||
| + "LIMIT 25") | ||
| .bind("min_seq", SEQ_SCAN_MIN) | ||
| .bind("ratio", SEQ_SCAN_RATIO) | ||
| .map( | ||
| (rs, ctx) -> | ||
| new Finding( | ||
| DiagnosticCategory.SEQ_SCAN_HEAVY, |
There was a problem hiding this comment.
💡 Edge Case: seqScanHeavy excludes worst-case tables (idx_scan=0)
The SQL WHERE clause uses seq_scan::numeric / NULLIF(idx_scan, 0) > :ratio. When idx_scan = 0, NULLIF returns NULL making the whole comparison NULL (falsy), so those rows are excluded from results. Ironically, tables with many sequential scans and zero index scans are the most concerning candidates for a missing index, yet they'll never appear in findings.
The Java mapper at line 204 has a dead-code branch handling idx_scan == 0 (returns "∞") that can never execute given the query filter.
Suggested fix:
Change the WHERE clause to handle idx_scan=0 explicitly:
"WHERE seq_scan > :min_seq "
+ " AND (idx_scan = 0 OR seq_scan::numeric / idx_scan > :ratio) "
Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion
Code Review 👍 Approved with suggestions 1 resolved / 3 findingsAdds a db-tune ops subcommand for database performance diagnostics and tuning, resolving an NPE in the catch block. Verify the DB state persistence in 💡 Quality: applyExecutesAndIsIdempotent no longer verifies DB state📄 openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DbTuneIT.java:117-123 The old test used This would restore the end-to-end confidence that Suggested fix💡 Edge Case: seqScanHeavy excludes worst-case tables (idx_scan=0)The SQL WHERE clause uses The Java mapper at line 204 has a dead-code branch handling Suggested fix✅ 1 resolved✅ Edge Case: NPE in catch block when exception message is null
🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|



Summary
Sample input / output
Report mode (default — no DB writes)
```
$ ./openmetadata-ops.sh db-tune
Database engine: PostgreSQL 17.2
=== Server-level parameter compliance ===
+---------------------------------+---------+-------------------+-------------+-------------------------+
| Parameter | Current | Recommended | Status | Note |
+---------------------------------+---------+-------------------+-------------+-------------------------+
| shared_buffers | 16384 | 40% of RAM | UNTUNED | RAM-relative; verify |
| effective_cache_size | 524288 | 75% of RAM | UNTUNED | RAM-relative; verify |
| work_mem | 4096 | 131072 | UNDERSIZED | |
| maintenance_work_mem | 65536 | 2097152 | UNDERSIZED | |
| random_page_cost | 4 | 1.1 | UNDERSIZED | |
| effective_io_concurrency | 1 | 200 | UNDERSIZED | |
| max_parallel_workers_per_gather | 2 | 4 | UNDERSIZED | |
| autovacuum_naptime | 60 | 15 | UNDERSIZED | |
| autovacuum_vacuum_scale_factor | 0.2 | 0.05 | UNDERSIZED | |
| autovacuum_analyze_scale_factor | 0.1 | 0.02 | UNDERSIZED | |
+---------------------------------+---------+-------------------+-------------+-------------------------+
These cannot be applied by this tool — change them in your DB parameter group / RDS console.
=== Per-table recommendations (26 tables) ===
+--------------------------+-----------+--------+-----------+----------------------------------------+---------+----------------------------------+
| Table | Rows | Size | Current | Recommended | Action | Reason |
+--------------------------+-----------+--------+-----------+----------------------------------------+---------+----------------------------------+
| entity_relationship | 8,400,000 | 3.2 GB | (default) | autovacuum_analyze_scale_factor=0.005, | APPLY | Join target, write-heavy |
| | | | | autovacuum_vacuum_cost_limit=4000, | | |
| | | | | autovacuum_vacuum_scale_factor=0.01 | | |
| tag_usage | 7,402,343 | 30 GB | autovac.. | autovacuum_analyze_scale_factor=0.005, | TIGHTEN | Hottest table on read path |
| | | | | autovacuum_vacuum_cost_delay=0, | | |
| | | | | autovacuum_vacuum_cost_limit=4000, | | |
| | | | | autovacuum_vacuum_scale_factor=0.01 | | |
| storage_container_entity | 580,123 | 1.5 GB | (default) | autovacuum_analyze_scale_factor=0.01, | APPLY | Large entity table; tighten |
| | | | | autovacuum_vacuum_scale_factor=0.02 | | autovacuum so list count stats |
| | | | | | | stay fresh |
| table_entity | 420,000 | 2.1 GB | (default) | autovacuum_analyze_scale_factor=0.01, | APPLY | Large entity table |
| | | | | autovacuum_vacuum_scale_factor=0.02 | | |
| change_event | 12,400,000| 4.0 GB | (default) | autovacuum_analyze_scale_factor=0.1, | RELAX | Append-only, relax autovacuum |
| | | | | autovacuum_vacuum_scale_factor=0.2 | | |
| dashboard_entity | 8,500 | 32 MB | (default) | (default) | SKIP | Row count 8500 below threshold |
| | | | | | | 10000 |
| api_collection_entity | 0 | 0 KB | (default) | (default) | SKIP | Row count 0 below threshold |
| | | | | | | 10000 |
+--------------------------+-----------+--------+-----------+----------------------------------------+---------+----------------------------------+
Next steps:
./openmetadata-ops.sh db-tune --apply --analyze # apply + refresh planner stats
./openmetadata-ops.sh db-tune --apply # apply only; run analyze-tables later
```
Apply mode (interactive prompt)
```
$ ./openmetadata-ops.sh db-tune --apply
[ ... same report as above ... ]
About to apply 5 ALTER statements:
ALTER TABLE "entity_relationship" SET (autovacuum_analyze_scale_factor = 0.005, autovacuum_vacuum_cost_limit = 4000, autovacuum_vacuum_scale_factor = 0.01);
ALTER TABLE "tag_usage" SET (autovacuum_analyze_scale_factor = 0.005, autovacuum_vacuum_cost_delay = 0, autovacuum_vacuum_cost_limit = 4000, autovacuum_vacuum_scale_factor = 0.01);
ALTER TABLE "storage_container_entity" SET (autovacuum_analyze_scale_factor = 0.01, autovacuum_vacuum_scale_factor = 0.02);
ALTER TABLE "table_entity" SET (autovacuum_analyze_scale_factor = 0.01, autovacuum_vacuum_scale_factor = 0.02);
ALTER TABLE "change_event" SET (autovacuum_analyze_scale_factor = 0.1, autovacuum_vacuum_scale_factor = 0.2);
Apply now? [y/N]: y
+--------------------------+---------+--------+----------+
| Table | Action | Status | Details |
+--------------------------+---------+--------+----------+
| entity_relationship | APPLY | OK | Applied |
| tag_usage | TIGHTEN | OK | Applied |
| storage_container_entity | APPLY | OK | Applied |
| table_entity | APPLY | OK | Applied |
| change_event | RELAX | OK | Applied |
+--------------------------+---------+--------+----------+
```
Apply + refresh planner stats
```
$ ./openmetadata-ops.sh db-tune --apply --yes --analyze
[ ... report ... ]
+--------------------------+---------+--------+--------------------+
| Table | Action | Status | Details |
+--------------------------+---------+--------+--------------------+
| entity_relationship | APPLY | OK | Applied + analyzed |
| tag_usage | TIGHTEN | OK | Applied + analyzed |
| storage_container_entity | APPLY | OK | Applied + analyzed |
| ... | ... | ... | ... |
+--------------------------+---------+--------+--------------------+
```
MySQL output
```
$ ./openmetadata-ops.sh db-tune
Database engine: MySQL 8.0.36
=== Server-level parameter compliance ===
+--------------------------------------+----------+-----------------------+-------------+
| Parameter | Current | Recommended | Status |
+--------------------------------------+----------+-----------------------+-------------+
| innodb_buffer_pool_size | 1.0 GB | 40-60% of RAM | UNTUNED |
| innodb_io_capacity | 200 | 2000 | UNDERSIZED |
| innodb_io_capacity_max | 2000 | 4000 | UNDERSIZED |
| innodb_stats_persistent_sample_pages | 20 | 64 | UNDERSIZED |
| ... | ... | ... | ... |
+--------------------------------------+----------+-----------------------+-------------+
=== Per-table recommendations (25 tables) ===
+--------------------------+-----------+--------+-----------+--------------------------+--------+----------------------------------+
| Table | Rows | Size | Current | Recommended | Action | Reason |
+--------------------------+-----------+--------+-----------+--------------------------+--------+----------------------------------+
| storage_container_entity | 580,123 | 1.5 GB | (default) | STATS_AUTO_RECALC=1, | APPLY | Large entity table; bump InnoDB |
| | | | | STATS_PERSISTENT=1, | | stats sampling |
| | | | | STATS_SAMPLE_PAGES=64 | | |
| ... | ... | ... | ... | ... | ... | ... |
+--------------------------+-----------+--------+-----------+--------------------------+--------+----------------------------------+
```
The `--apply` SQL on MySQL uses `ALTER TABLE \`name\` STATS_PERSISTENT=1, STATS_AUTO_RECALC=1, STATS_SAMPLE_PAGES=64` (no parens, no equals-spaces — MySQL syntax).
Architecture
```
openmetadata-service/src/main/java/org/openmetadata/service/util/dbtune/
├── Action.java # APPLY / TIGHTEN / RELAX / OK / SKIP
├── TableStats.java # input record (rows, bytes, current settings)
├── TableRecommendation.java # output record (action, current, recommended, reason)
├── ServerParamCheck.java # per-parameter compliance row
├── DbTuneResult.java # bundle: engine, version, params, recommendations
├── AutoTuner.java # interface: analyze / recommend / apply / analyzeOne
├── PostgresTuningCatalog.java # tier map: HOT_RELATIONSHIP, HOT_TAG_USAGE, ENTITY_LARGE, ENTITY_SERVICE, APPEND_ONLY
├── PostgresAutoTuner.java # autovacuum reloptions + ANALYZE
├── MysqlTuningCatalog.java # tier map: HOT, ENTITY_LARGE, ENTITY_SERVICE
├── MysqlAutoTuner.java # InnoDB STATS_* + ANALYZE TABLE
└── DbTuneReport.java # ASCII renderer + ALTER-statement preview
```
`recommend(stats)` is a pure function — input is observed table state, output is the action. Tested without a database. `apply()` and `analyzeOne()` are the I/O methods, tested in `DbTuneIT` against a live Testcontainer.
Heuristic
Tests
Test plan
🤖 Generated with Claude Code
Summary by Gitar
--diagnoseflag todb-tunefor read-only DBA reports (unused indexes, bloat, slow queries).PostgresDiagnosticandMysqlDiagnosticusingsys,performance_schema, andpg_stat_*views.DbTuneDiagnosisandDiagnosticCategoryto support structured reporting and predictable table layouts.DiagnosticReportTestfor logic coverage of findings grouping and report rendering.diagnoseCompletesWithoutErrortoDbTuneITto verify end-to-end execution against live Testcontainers.This will update automatically on new commits.