mirror of
https://github.com/open-metadata/OpenMetadata
synced 2026-05-24 09:39:11 +00:00
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:
parent
2d9f047b71
commit
4951a04776
5 changed files with 72 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue