fix(h2): detect remote server version via H2VERSION()
This commit is contained in:
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", "");
|
||||
|
||||
Reference in new issue
Block a user