fix(h2): detect remote server version via H2VERSION()

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-07 21:26:30 +08:00
1 parent 96a3dc052a
commit 11e0db5acb
3 files changed
+142 -1

No files matched your search

@@ -92,7 +92,29 @@ public class H2Agent extends AbstractJdbcAgent {
@Override
protected void afterConnect(ConnectParams params, Connection connection) {
databaseName = params.getDatabase();
databaseMajorVersion = unchecked(() -> connection.getMetaData().getDatabaseMajorVersion());
databaseMajorVersion = detectDatabaseMajorVersion(connection);
}
// For a remote (TCP/SSL) connection, JDBC metadata such as
// getDatabaseMajorVersion() reflects the *loaded driver's* own version, not
// the server's, whenever the driver profile could not be auto-detected from
// a local database file (see H2FileFormatDetector) and fell back to the
// newest bundled driver. H2VERSION() is evaluated by the server itself, so
// it reports the real engine version regardless of which driver connected.
private static int detectDatabaseMajorVersion(Connection connection) {
try (java.sql.Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT H2VERSION()")) {
if (resultSet.next()) {
String version = resultSet.getString(1);
int dot = version == null ? -1 : version.indexOf('.');
if (dot > 0) {
return Integer.parseInt(version.substring(0, dot));
}
}
} catch (SQLException | NumberFormatException ignored) {
// Fall back to JDBC driver metadata below.
}
return unchecked(() -> connection.getMetaData().getDatabaseMajorVersion());
}
H2DriverVersion driverVersion() {
@@ -12,6 +12,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
@@ -99,6 +100,123 @@ class H2AgentProcessTest {
}
}
// Regression test for a legacy (H2 1.x) *remote* server: the file-format
// auto-detector (H2FileFormatDetector) cannot inspect a TCP database's
// storage file, so driver selection falls back to the newest bundled
// driver (H2 2.4.240). That mismatched driver's own JDBC metadata
// (getDatabaseMajorVersion()) then reports its own version rather than the
// server's, which used to make the agent send an H2 2.x-only
// INFORMATION_SCHEMA.ROUTINES query to a database that doesn't have that
// catalog table, failing list_objects entirely (dbx#8356).
@Test
@Timeout(45)
void listObjectsAutoDetectsLegacyServerOverTcp() throws Exception {
Path h2V1Jar = Path.of(System.getProperty("dbx.h2.v1.driver.jar"));
Assertions.assertTrue(Files.isRegularFile(h2V1Jar), () -> "Missing bundled H2 1.4.200 driver jar: " + h2V1Jar);
Path agentJar = Path.of(System.getProperty("dbx.h2.agent.jar"));
Assertions.assertTrue(Files.isRegularFile(agentJar), () -> "Missing shaded H2 Agent: " + agentJar);
try (LegacyTcpServer server = new LegacyTcpServer(h2V1Jar)) {
try (RpcProcess rpc = new RpcProcess(agentJar)) {
String sessionId = "session-legacy-tcp";
JsonObject params = sessionParams(sessionId);
params.addProperty("host", "127.0.0.1");
params.addProperty("port", server.port());
// A fresh, unique database per run: the TCP server process's
// working directory (and any .mv.db file it writes) persists
// across separate test invocations, so a fixed name would leak
// schema objects between runs.
params.addProperty("database", "./legacy-tcp-test-" + java.util.UUID.randomUUID());
params.addProperty("username", "sa");
params.addProperty("password", "");
params.addProperty("connection_string", "");
params.addProperty("ssl", false);
// driver_profile intentionally omitted: exercise the "auto" path.
JsonObject connected = rpc.result(rpc.request("open_session", params));
Assertions.assertTrue(connected.get("ok").getAsBoolean());
rpc.result(rpc.request("execute_query", queryParams(sessionId, "CREATE TABLE LEGACY_TABLE (ID INT PRIMARY KEY)")));
rpc.result(rpc.request("execute_query", queryParams(sessionId, "CREATE ALIAS LEGACY_FUNC FOR \"java.lang.Integer.reverse\"")));
JsonArray objects = rpc.resultArray(rpc.request("list_objects", metadataParams(sessionId, "PUBLIC")));
List<String> names = objects.asList().stream().map(value -> value.getAsJsonObject().get("name").getAsString()).toList();
Assertions.assertTrue(names.contains("LEGACY_TABLE"), names.toString());
Assertions.assertTrue(names.contains("LEGACY_FUNC"), names.toString());
Assertions.assertTrue(rpc.result(rpc.request("close_session", sessionParams(sessionId))).get("ok").getAsBoolean());
}
}
}
private static final class LegacyTcpServer implements AutoCloseable {
private final Process process;
private final int port;
private LegacyTcpServer(Path h2V1Jar) throws Exception {
this.port = findFreePort();
Path java = Path.of(System.getProperty("java.home"), "bin", "java");
process = new ProcessBuilder(
java.toString(), "-cp", h2V1Jar.toString(), "org.h2.tools.Server",
"-tcp", "-tcpPort", Integer.toString(port), "-ifNotExists"
).redirectErrorStream(true).start();
// -tcpDaemon is intentionally omitted: it marks the *listener* thread
// as a daemon for embedding inside a host JVM that outlives it. Here
// the server *is* the whole process, so a daemon-only listener thread
// would let the JVM exit (dropping sessions) the moment main() returns.
drainOutput();
waitUntilListening();
}
private void drainOutput() {
Thread thread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
while (reader.readLine() != null) {
// Discard: only draining to prevent the pipe buffer from filling.
}
} catch (java.io.IOException ignored) {
}
}, "legacy-h2-server-stdout");
thread.setDaemon(true);
thread.start();
}
private int port() {
return port;
}
private static int findFreePort() throws Exception {
try (java.net.ServerSocket socket = new java.net.ServerSocket(0)) {
return socket.getLocalPort();
}
}
private void waitUntilListening() throws Exception {
long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos();
while (System.nanoTime() < deadline) {
try (java.net.Socket probe = new java.net.Socket()) {
probe.connect(new java.net.InetSocketAddress("127.0.0.1", port), 200);
return;
} catch (java.io.IOException notReady) {
Thread.sleep(100);
}
}
throw new AssertionError("Legacy H2 TCP server never started listening on port " + port);
}
@Override
public void close() {
process.destroy();
try {
if (!process.waitFor(5, TimeUnit.SECONDS)) {
process.destroyForcibly();
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
}
private static JsonObject connectParams(String sessionId, String profile) {
JsonObject params = sessionParams(sessionId);
params.addProperty("host", "");