db-tune: restore apply DB verification + include idx_scan=0 in seqScanHeavy

Two findings from the review round on 2d9f047b71:

1. DbTuneIT.applyExecutesAndIsIdempotent stopped verifying that apply()
   actually persisted the settings to the DB after the rewrite to an
   isolated test table — it only asserted the ALTER statement
   contained the table name and that apply() didn't throw. The earlier
   shape (assertSettingsMatch) was load-bearing for catching regressions
   where apply() silently no-ops. Restored end-to-end verification.

   Added AutoTuner.currentSettingsForTable(Handle, String) so the test
   can read back settings on a table that is NOT in the static catalog
   (the isolated dbtune_it_isolated_table). Implemented for both
   engines by reusing the existing parseReloptions / parseCreateOptions
   logic. Falls out cleanly from a single pg_class / INFORMATION_SCHEMA
   query; no duplication.

   The IT now: builds the recommendation, asserts the generated SQL is
   well-formed, applies it, reads back via currentSettingsForTable,
   asserts each recommended key/value persisted numerically (handles
   PG lowercase / MySQL uppercase key conventions via case-insensitive
   lookup), then re-applies and asserts the snapshot is byte-equal.

2. PostgresDiagnostic.seqScanHeavy WHERE clause used
   seq_scan::numeric / NULLIF(idx_scan, 0) > :ratio, which silently
   excludes tables with idx_scan=0 — exactly the worst-case candidates
   for a missing index. The Java mapper even has a dead-code branch
   handling idx_scan=0 ("∞" ratio display) that could never trigger.

   Changed predicate to (idx_scan = 0 OR seq_scan::numeric/idx_scan > :ratio).
   The mapper's ∞ branch now actually executes. Inline comment added
   explaining the choice so a future audit doesn't undo it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sriharsha Chintalapani 2026-05-11 07:20:00 -07:00
parent 2d9f047b71
commit 4951a04776
5 changed files with 72 additions and 4 deletions

View file

@ -118,13 +118,39 @@ class DbTuneIT {
ConnectionType connType = currentConnectionType();
TableRecommendation rec = recommendationForIsolatedTable(connType);
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));
Map<String, String> after =
jdbi.withHandle(handle -> tuner.currentSettingsForTable(handle, ISOLATED_TABLE));
assertSettingsPersisted(rec.recommendedSettings(), after);
// Apply a second time must be idempotent (no exception, no value drift).
jdbi.useHandle(handle -> tuner.apply(handle, rec));
Map<String, String> afterSecond =
jdbi.withHandle(handle -> tuner.currentSettingsForTable(handle, ISOLATED_TABLE));
assertEquals(after, afterSecond, "Apply should be idempotent");
}
private void assertSettingsPersisted(
final Map<String, String> expected, final Map<String, String> actual) {
for (Map.Entry<String, String> e : expected.entrySet()) {
String key = e.getKey();
// Postgres lowercases reloption keys; MySQL uppercases STATS_*. Look up case-insensitively.
String got =
actual.entrySet().stream()
.filter(a -> a.getKey().equalsIgnoreCase(key))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
assertNotNull(got, "Missing setting after apply: " + key + " (got " + actual + ")");
assertEquals(
Double.parseDouble(e.getValue()),
Double.parseDouble(got),
0.0,
"Setting " + key + " did not take effect: expected " + e.getValue() + ", got " + got);
}
}
@Test

View file

@ -12,6 +12,7 @@
*/
package org.openmetadata.service.util.dbtune;
import java.util.Map;
import org.jdbi.v3.core.Handle;
/**
@ -30,6 +31,14 @@ public interface AutoTuner {
/** Read stats + settings, then turn them into recommendations. Mixes I/O and pure logic. */
DbTuneResult analyze(Handle handle);
/**
* Reads the current per-table reloption / table-option settings for an arbitrary table name
* including tables that are not in the static tuning catalog. Returns an empty map if the table
* has no overrides set (inherits cluster defaults). The returned keys use the same casing the
* engine reports (lowercase autovacuum_* keys for Postgres, uppercase STATS_* keys for MySQL).
*/
Map<String, String> currentSettingsForTable(Handle handle, String tableName);
/**
* Pure decision function. Given observed table stats, return the recommendation. Exposed
* separately so unit tests can assert the heuristic without hitting a database.

View file

@ -137,6 +137,21 @@ public final class MysqlAutoTuner implements AutoTuner {
.orElse(null);
}
@Override
public Map<String, String> currentSettingsForTable(final Handle handle, final String tableName) {
return handle
.createQuery(
"SELECT COALESCE(CREATE_OPTIONS, '') AS create_opts "
+ "FROM information_schema.TABLES "
+ "WHERE TABLE_SCHEMA = DATABASE() "
+ " AND TABLE_NAME = :name")
.bind("name", tableName)
.mapTo(String.class)
.findOne()
.map(MysqlAutoTuner::parseCreateOptions)
.orElse(Map.of());
}
List<ServerParamCheck> readServerParams(final Handle handle) {
List<ServerParamCheck> checks = new ArrayList<>();
Map<String, String> recommendations = recommendedServerParams();

View file

@ -153,6 +153,22 @@ public final class PostgresAutoTuner implements AutoTuner {
.orElse(null);
}
@Override
public Map<String, String> currentSettingsForTable(final Handle handle, final String tableName) {
return handle
.createQuery(
"SELECT COALESCE(c.reloptions, ARRAY[]::text[]) AS opts "
+ "FROM pg_class c "
+ "JOIN pg_namespace n ON n.oid = c.relnamespace "
+ "WHERE c.relkind = 'r' "
+ " AND n.nspname = ANY (current_schemas(false)) "
+ " AND c.relname = :name")
.bind("name", tableName)
.map((rs, ctx) -> parseReloptions((String[]) rs.getArray("opts").getArray()))
.findOne()
.orElse(Map.of());
}
List<ServerParamCheck> readServerParams(final Handle handle) {
List<ServerParamCheck> checks = new ArrayList<>();
Map<String, String> recommendations = recommendedServerParams();

View file

@ -181,12 +181,14 @@ public final class PostgresDiagnostic implements Diagnostic {
}
List<Finding> seqScanHeavy(final Handle handle) {
// Includes idx_scan=0 tables those are the *worst* candidates for a missing index, not
// edge cases to filter out. NULLIF would silently drop them via NULL comparison.
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 "
+ " AND (idx_scan = 0 OR seq_scan::numeric / idx_scan > :ratio) "
+ "ORDER BY seq_scan DESC "
+ "LIMIT 25")
.bind("min_seq", SEQ_SCAN_MIN)