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
@@ -36,5 +36,6 @@ tasks.named('test') {
|
|||||||
dependsOn tasks.named('shadowJar')
|
dependsOn tasks.named('shadowJar')
|
||||||
doFirst {
|
doFirst {
|
||||||
systemProperty 'dbx.h2.agent.jar', tasks.named('shadowJar').get().archiveFile.get().asFile.absolutePath
|
systemProperty 'dbx.h2.agent.jar', tasks.named('shadowJar').get().archiveFile.get().asFile.absolutePath
|
||||||
|
systemProperty 'dbx.h2.v1.driver.jar', configurations.h2V1.asPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,29 @@ public class H2Agent extends AbstractJdbcAgent {
|
|||||||
@Override
|
@Override
|
||||||
protected void afterConnect(ConnectParams params, Connection connection) {
|
protected void afterConnect(ConnectParams params, Connection connection) {
|
||||||
databaseName = params.getDatabase();
|
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() {
|
H2DriverVersion driverVersion() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.CompletableFuture;
|
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) {
|
private static JsonObject connectParams(String sessionId, String profile) {
|
||||||
JsonObject params = sessionParams(sessionId);
|
JsonObject params = sessionParams(sessionId);
|
||||||
params.addProperty("host", "");
|
params.addProperty("host", "");
|
||||||
|
|||||||
Reference in new issue
Block a user