This is an automated email from the ASF dual-hosted git repository.

david-streamlio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-connectors.git


The following commit(s) were added to refs/heads/master by this push:
     new 0a711671 [fix][io] Upgrade OpenMLDB client to 0.9.2 and add sink 
integration test (#105)
0a711671 is described below

commit 0a71167118a7ff05ac18dada23b592bb509742a4
Author: David Kjerrumgaard <[email protected]>
AuthorDate: Fri Jul 10 18:55:29 2026 -0700

    [fix][io] Upgrade OpenMLDB client to 0.9.2 and add sink integration test 
(#105)
    
    * [fix][io] Upgrade OpenMLDB client to 0.9.2 so the connector can run on 
current hosts
    
    The pinned OpenMLDB client (0.4.4-hotfix1) is unusable in a current
    build, for three independent reasons:
    
    - Its native SDK (libsql_jsdk.so) references _dl_sym@GLIBC_PRIVATE,
      which glibc 2.34 removed, so loading the driver hard-crashes the JVM
      on any modern Linux host.
    - Its JDBC driver has no META-INF/services/java.sql.Driver entry, so
      DriverManager cannot discover it and JdbcAbstractSink.open() fails
      with 'No suitable driver'.
    - 0.9.2 is the newest version with a matching 4pdosc/openmldb Docker
      image.
    
    The 0.9.2 SDK still needs two module-scoped version forces because it
    is compiled against APIs that the platform-enforced versions removed:
    Curator 4.2.0 (NodeCache.getListenable() signature changed in 5.x) and
    protobuf-java 3.21.12 (generated classes call makeExtensionsImmutable(),
    removed in 3.22). NARs never bundle protobuf, so the protobuf force only
    affects compile/test classpaths.
    
    * [improve][test] Add OpenMLDB JDBC sink integration test
    
    Fixes #46
    
    Add OpenMLDBJdbcSinkIntegrationTest, the first integration test for the
    OpenMLDB JDBC sink. It starts an OpenMLDB 0.9.2 cluster (the image's
    init.sh: ZooKeeper on localhost:2181, zkPath /openmldb) via
    Testcontainers with host networking, creates the database and table,
    writes an Avro GenericRecord through the sink, and reads the row back
    over a fresh JDBC connection.
    
    Host networking is required because the JDBC driver discovers
    tablet/nameserver endpoints through ZooKeeper and dials them directly,
    so the advertised addresses must be reachable from the test JVM; that
    also makes the test Linux-only, as is the driver's native SDK
    (libsql_jsdk.so, Linux x86-64 only).
    
    Two OpenMLDB driver limitations shape the test: key/nonKey must not be
    configured (the driver only supports INSERT prepared statements, and
    key/nonKey makes the sink also prepare UPDATE/DELETE, which throw
    'unsupported sql'), and ResultSet getters must be positional (getters
    by column label throw 'current do not support this method').
    
    * Potential fix for pull request finding
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 gradle/libs.versions.toml                          |   5 +-
 jdbc/openmldb/build.gradle.kts                     |  33 +++
 .../io/jdbc/OpenMLDBJdbcSinkIntegrationTest.java   | 222 +++++++++++++++++++++
 3 files changed, 259 insertions(+), 1 deletion(-)

diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4b6d3500..a45f3350 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -187,7 +187,10 @@ solr = "9.8.0"
 hbase = "2.6.4-hadoop3"
 hadoop3 = "3.5.0"
 jclouds = "2.6.0"
-openmldb = "0.4.4-hotfix1"
+# 0.4.4's native SDK (libsql_jsdk.so) references _dl_sym@GLIBC_PRIVATE, which 
was removed in
+# glibc 2.34, so it crashes the JVM on any modern Linux host. 0.9.2 matches 
the newest
+# 4pdosc/openmldb Docker image.
+openmldb = "0.9.2"
 docker-java = "3.7.1"
 # Shading
 shadow = "9.4.1"
diff --git a/jdbc/openmldb/build.gradle.kts b/jdbc/openmldb/build.gradle.kts
index 7eae514a..eea5fd47 100644
--- a/jdbc/openmldb/build.gradle.kts
+++ b/jdbc/openmldb/build.gradle.kts
@@ -21,8 +21,41 @@ plugins {
     id("pulsar-connectors.java-conventions")
     id("pulsar-connectors.nar-conventions")
 }
+
+// The OpenMLDB SDK is compiled against APIs that later dependency versions 
removed, so the
+// platform-enforced versions break it at runtime; force the versions the SDK 
can use. Nothing
+// else in this connector uses Curator or (unshaded) protobuf. 
resolutionStrategy.force is needed
+// (rather than a platform exclusion) because the enforced platform also 
reaches this module
+// transitively through the jdbc-core project dependency.
+// - Curator: the SDK calls NodeCache.getListenable() expecting the 4.x 
signature (returning
+//   ListenerContainer); Curator 5.x changed it to return Listenable -> 
NoSuchMethodError on
+//   connect under the platform's 5.7.1.
+// - Protobuf: the SDK's generated classes (protoc 3.16) call 
makeExtensionsImmutable(), removed
+//   in protobuf-java 3.22 -> NoSuchMethodError on insert under the platform's 
3.25.5. 3.21.12 is
+//   the newest runtime that still has it. (NARs never bundle protobuf — the 
Pulsar runtime
+//   provides it — so this only affects compile/test classpaths.)
+configurations.all {
+    resolutionStrategy.force(
+        "org.apache.curator:curator-client:4.2.0",
+        "org.apache.curator:curator-framework:4.2.0",
+        "org.apache.curator:curator-recipes:4.2.0",
+        "com.google.protobuf:protobuf-java:3.21.12",
+    )
+}
+
 dependencies {
     implementation(project(":jdbc:pulsar-io-jdbc-core"))
     runtimeOnly(libs.openmldb.jdbc)
     runtimeOnly(libs.openmldb.native)
+
+    // Integration test: the OpenMLDB JDBC driver's native SDK is Linux x86-64 
only, so this test
+    // only runs on Linux/CI — see OpenMLDBJdbcSinkIntegrationTest.
+    testImplementation(libs.testcontainers)
+    testImplementation(libs.pulsar.client)
+    testImplementation(libs.pulsar.functions.instance)
+    testImplementation(libs.avro)
+    // The JDBC driver + native SDK must be on the TEST classpath (they are 
runtimeOnly for the
+    // NAR, which is not built for unit tests).
+    testRuntimeOnly(libs.openmldb.jdbc)
+    testRuntimeOnly(libs.openmldb.native)
 }
diff --git 
a/jdbc/openmldb/src/test/java/org/apache/pulsar/io/jdbc/OpenMLDBJdbcSinkIntegrationTest.java
 
b/jdbc/openmldb/src/test/java/org/apache/pulsar/io/jdbc/OpenMLDBJdbcSinkIntegrationTest.java
new file mode 100644
index 00000000..53433c25
--- /dev/null
+++ 
b/jdbc/openmldb/src/test/java/org/apache/pulsar/io/jdbc/OpenMLDBJdbcSinkIntegrationTest.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.io.jdbc;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pulsar.client.api.Message;
+import org.apache.pulsar.client.api.schema.GenericObject;
+import org.apache.pulsar.client.api.schema.GenericRecord;
+import org.apache.pulsar.client.api.schema.SchemaDefinition;
+import org.apache.pulsar.client.impl.MessageImpl;
+import org.apache.pulsar.client.impl.schema.AutoConsumeSchema;
+import org.apache.pulsar.client.impl.schema.AvroSchema;
+import org.apache.pulsar.client.impl.schema.generic.GenericAvroSchema;
+import org.apache.pulsar.functions.api.Record;
+import org.apache.pulsar.functions.source.PulsarRecord;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Integration test for the OpenMLDB JDBC sink, exercised against a real 
OpenMLDB cluster via
+ * Testcontainers, mirroring {@code ClickHouseJdbcSinkIntegrationTest}: create 
the table, open the
+ * sink, write an Avro {@code GenericRecord} through it, read the row back and 
assert.
+ *
+ * <p><b>Why this test is Linux-x86-64-only.</b> The OpenMLDB JDBC driver is a 
thin JNI wrapper over
+ * a native SDK ({@code libsql_jsdk.so}) shipped in {@code openmldb-native}, 
which publishes no
+ * macOS/arm64 binary — the driver cannot initialise on Apple Silicon, so this 
test only runs on a
+ * Linux x86-64 host (and in CI, which is Linux x86-64). See issue #46.
+ *
+ * <p><b>Why cluster mode + host networking.</b> The OpenMLDB JDBC driver 
mandates {@code zk} and
+ * {@code zkPath} params ({@code 
jdbc:openmldb:///db?zk=host:port&zkPath=/path}); the host after
+ * {@code //} is ignored. It reads tablet/nameserver endpoints from ZooKeeper 
and dials them
+ * directly, so OpenMLDB must run in <b>cluster mode</b> (standalone has no 
zk) and its advertised
+ * endpoints must be reachable from the test JVM. Host networking (Linux only) 
is the simplest way
+ * to guarantee that — the container binds the OpenMLDB ports on localhost and 
zk advertises
+ * host-reachable addresses.
+ *
+ * <p>The image tag matches the pinned OpenMLDB client version; the 
sink-driving logic mirrors the
+ * ClickHouse test, minus key/nonKey config and label-based ResultSet getters, 
which the OpenMLDB
+ * driver does not support.
+ */
+@Slf4j
+public class OpenMLDBJdbcSinkIntegrationTest {
+
+    /** Match the module's pinned client (libs.openmldb == 0.9.2). */
+    private static final DockerImageName OPENMLDB_IMAGE = 
DockerImageName.parse("4pdosc/openmldb:0.9.2");
+
+    /** ZooKeeper coordinates the JDBC driver discovers cluster endpoints 
through. */
+    private static final String ZK_ENDPOINT = "127.0.0.1:2181";
+    private static final String ZK_PATH = "/openmldb";
+
+    private static final String DATABASE = "pulsar_test";
+    private static final String TABLE_NAME = "pulsar_messages";
+
+    private GenericContainer<?> openmldb;
+    private BaseJdbcAutoSchemaSink jdbcSink;
+
+    /** A simple record class matching the {@link #TABLE_NAME} columns. */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class Foo {
+        private String field1;
+        private String field2;
+        private int field3;
+    }
+
+    /** JDBC URL for the OpenMLDB driver — zk-based; the host after {@code //} 
is ignored. */
+    private static String jdbcUrl(String database) {
+        return "jdbc:openmldb:///" + database + "?zk=" + ZK_ENDPOINT + 
"&zkPath=" + ZK_PATH;
+    }
+
+    @BeforeClass(alwaysRun = true)
+public void setUp() throws Exception {
+        final String osName = System.getProperty("os.name", 
"").toLowerCase(java.util.Locale.ROOT);
+        final String osArch = System.getProperty("os.arch", 
"").toLowerCase(java.util.Locale.ROOT);
+        if (!osName.contains("linux") || !(osArch.equals("amd64") || 
osArch.equals("x86_64"))) {
+            throw new org.testng.SkipException(
+                    "Skipping OpenMLDB JDBC integration test: requires Linux 
x86-64 (native openmldb SDK + host-network Docker)."
+            );
+        }
+        // Host networking requires binding ZooKeeper to localhost:2181; skip 
if the port is already in use.
+        try (java.net.ServerSocket ss = new java.net.ServerSocket()) {
+            ss.setReuseAddress(false);
+            ss.bind(new java.net.InetSocketAddress("127.0.0.1", 2181));
+        } catch (java.io.IOException e) {
+            throw new org.testng.SkipException(
+                    "Skipping OpenMLDB JDBC integration test: port 2181 is 
already in use on localhost.",
+                    e
+            );
+        }
+        // Host networking (Linux only): binds OpenMLDB's ports on localhost 
so the endpoints zk
+        // advertises are reachable from this JVM. The image's init.sh deploys 
and starts the
+        // cluster (zk on localhost:2181, zkPath /openmldb) and prints 
"OpenMLDB start success".
+        openmldb = new GenericContainer<>(OPENMLDB_IMAGE)
+                .withNetworkMode("host")
+                .withCommand("bash", "-c", "/work/init.sh && tail -f 
/dev/null")
+                .waitingFor(Wait.forLogMessage(".*OpenMLDB start success.*", 
1))
+                .withStartupTimeout(Duration.ofMinutes(4));
+        openmldb.start();
+
+        // Admin connection (a separate driver instance from the one under 
test) creates the
+        // database and table. OpenMLDB needs online execute mode for 
inserts/reads to be visible.
+        try (Connection conn = DriverManager.getConnection(jdbcUrl(""));
+             Statement stmt = conn.createStatement()) {
+            stmt.execute("SET @@execute_mode='online'");
+            stmt.execute("CREATE DATABASE IF NOT EXISTS " + DATABASE);
+            stmt.execute("USE " + DATABASE);
+            stmt.execute("CREATE TABLE " + TABLE_NAME
+                    + " (field1 string, field2 string, field3 int)");
+        }
+
+        Map<String, Object> conf = new HashMap<>();
+        conf.put("jdbcUrl", jdbcUrl(DATABASE));
+        conf.put("tableName", TABLE_NAME);
+        // No key/nonKey: OpenMLDB's JDBC driver only supports INSERT prepared 
statements, and
+        // configuring key/nonKey makes the sink also prepare UPDATE/DELETE 
("unsupported sql").
+        // OpenMLDB does not support JDBC transactions; run in auto-commit 
mode.
+        conf.put("useTransactions", false);
+        // Flush on each write.
+        conf.put("batchSize", 1);
+
+        jdbcSink = new OpenMLDBJdbcAutoSchemaSink();
+        jdbcSink.open(conf, null);
+    }
+
+    @AfterClass(alwaysRun = true)
+    public void tearDown() throws Exception {
+        if (jdbcSink != null) {
+            jdbcSink.close();
+        }
+        if (openmldb != null) {
+            openmldb.stop();
+        }
+    }
+
+    @Test(timeOut = 600_000)
+    public void testOpenWritesAndReadsBack() throws Exception {
+        Foo insertObj = new Foo("ValueOfField1", "ValueOfField2", 3);
+        CompletableFuture<Boolean> future = new CompletableFuture<>();
+        final Record<GenericObject> record = createMockFooRecord(insertObj, 
future);
+
+        jdbcSink.write(record);
+        log.info("executed write");
+
+        Assert.assertTrue(future.get(60, TimeUnit.SECONDS), "record should be 
acknowledged");
+
+        // Read the row back through a fresh admin connection and assert it 
landed.
+        try (Connection conn = DriverManager.getConnection(jdbcUrl(DATABASE));
+             Statement stmt = conn.createStatement()) {
+            stmt.execute("SET @@execute_mode='online'");
+            try (ResultSet rs = stmt.executeQuery(
+                    "SELECT field1, field2, field3 FROM " + TABLE_NAME + " 
WHERE field3 = 3")) {
+                Assert.assertTrue(rs.next(), "exactly one matching row should 
have been written");
+                // Positional getters: OpenMLDB's SQLResultSet does not 
support access by column
+                // label ("current do not support this method").
+                Assert.assertEquals(rs.getString(1), insertObj.getField1());
+                Assert.assertEquals(rs.getString(2), insertObj.getField2());
+                Assert.assertEquals(rs.getInt(3), insertObj.getField3());
+                Assert.assertFalse(rs.next(), "there should be exactly one 
matching row");
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private Record<GenericObject> createMockFooRecord(Foo record, 
CompletableFuture<Boolean> future) {
+        Message<GenericRecord> insertMessage = mock(MessageImpl.class);
+        AvroSchema<Foo> schema = AvroSchema.of(SchemaDefinition.<Foo>builder()
+                .withPojo(Foo.class).withAlwaysAllowNull(true).build());
+        AutoConsumeSchema autoConsumeSchema = new AutoConsumeSchema();
+        autoConsumeSchema.setSchema(schema);
+
+        byte[] insertBytes = schema.encode(record);
+
+        Record<? extends GenericObject> insertRecord = 
PulsarRecord.<GenericRecord>builder()
+                .message(insertMessage)
+                .topicName("fake_topic_name")
+                .schema(autoConsumeSchema)
+                .ackFunction(() -> future.complete(true))
+                .failFunction(() -> future.complete(false))
+                .build();
+
+        GenericAvroSchema genericAvroSchema = new 
GenericAvroSchema(schema.getSchemaInfo());
+        
when(insertMessage.getValue()).thenReturn(genericAvroSchema.decode(insertBytes));
+        when(insertMessage.getProperties()).thenReturn(new HashMap<>());
+        return (Record<GenericObject>) insertRecord;
+    }
+}

Reply via email to