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

lgbo-ustc pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new b5d0205f26 [FLINK] Support Parquet format for Kafka to Hive streaming 
(#12317)
b5d0205f26 is described below

commit b5d0205f268caeaa7497605254e677a966882c8f
Author: kevinyhzou <[email protected]>
AuthorDate: Fri Jul 3 09:36:13 2026 +0800

    [FLINK] Support Parquet format for Kafka to Hive streaming (#12317)
    
    Squashed PR #12317 into a single commit.
---
 .github/workflows/flink.yml                        |   2 +-
 gluten-flink/docs/Flink.md                         |   2 +-
 .../apache/gluten/velox/FileSystemSinkFactory.java | 281 +++++++++++++---
 .../gluten/velox/FuzzerSourceSinkFactory.java      |   3 +-
 .../apache/gluten/velox/HiveSourceSinkFactory.java |  99 ++++++
 .../gluten/velox/KafkaSourceSinkFactory.java       |   7 +-
 .../org.apache.gluten.velox.VeloxSourceSinkFactory |   1 +
 .../api/operators/GlutenStreamSource.java          |  16 +-
 .../runtime/operators/GlutenOneInputOperator.java  |   5 +-
 .../GlutenStreamingFileWriterOperator.java         |  75 +++++
 .../java/org/apache/gluten/util/ReflectUtils.java  |  20 ++
 .../org/apache/gluten/util/VectorOutputBridge.java |  18 +
 gluten-flink/ut/pom.xml                            |  86 +++++
 .../runtime/stream/custom/KafkaToHiveE2ETest.java  | 362 +++++++++++++++++++++
 14 files changed, 913 insertions(+), 64 deletions(-)

diff --git a/.github/workflows/flink.yml b/.github/workflows/flink.yml
index 656a08d5cf..a222cc7a25 100644
--- a/.github/workflows/flink.yml
+++ b/.github/workflows/flink.yml
@@ -88,7 +88,7 @@ jobs:
           export fmt_SOURCE=BUNDLED
           export folly_SOURCE=BUNDLED
           git clone -b gluten-0530 https://github.com/bigo-sg/velox4j.git
-          cd velox4j && git reset --hard 
9811c337d5e37ab8d16d5bf0791b48836fcd2afd
+          cd velox4j && git reset --hard 
1b1ad0833220476be6a042b83a474cf58f8f076c
           git apply $GITHUB_WORKSPACE/gluten-flink/patches/fix-velox4j.patch
           $GITHUB_WORKSPACE/build/mvn clean install -DskipTests -Dgpg.skip 
-Dspotless.skip=true
           cd ..
diff --git a/gluten-flink/docs/Flink.md b/gluten-flink/docs/Flink.md
index 87f9a4e24b..9c1900f06b 100644
--- a/gluten-flink/docs/Flink.md
+++ b/gluten-flink/docs/Flink.md
@@ -48,7 +48,7 @@ As some features have not been committed to upstream, you 
have to use the follow
 ## fetch velox4j code
 git clone -b gluten-0530 https://github.com/bigo-sg/velox4j.git
 cd velox4j
-git reset --hard 9811c337d5e37ab8d16d5bf0791b48836fcd2afd
+git reset --hard 1b1ad0833220476be6a042b83a474cf58f8f076c
 mvn clean install -DskipTests -Dgpg.skip -Dspotless.skip=true
 ```
 **Get gluten**
diff --git 
a/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FileSystemSinkFactory.java
 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FileSystemSinkFactory.java
index ff08dcec63..57c9576322 100644
--- 
a/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FileSystemSinkFactory.java
+++ 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FileSystemSinkFactory.java
@@ -18,6 +18,7 @@ package org.apache.gluten.velox;
 
 import org.apache.gluten.streaming.api.operators.GlutenOneInputOperatorFactory;
 import org.apache.gluten.table.runtime.operators.GlutenOneInputOperator;
+import 
org.apache.gluten.table.runtime.operators.GlutenStreamingFileWriterOperator;
 import org.apache.gluten.util.LogicalTypeConverter;
 import org.apache.gluten.util.PlanNodeIdGenerator;
 import org.apache.gluten.util.ReflectUtils;
@@ -38,7 +39,6 @@ import 
org.apache.flink.streaming.api.transformations.OneInputTransformation;
 import org.apache.flink.streaming.api.transformations.SinkTransformation;
 import org.apache.flink.table.catalog.ResolvedSchema;
 import org.apache.flink.table.data.RowData;
-import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
 
 import java.util.HashMap;
 import java.util.List;
@@ -47,24 +47,20 @@ import java.util.stream.Collectors;
 
 public class FileSystemSinkFactory implements VeloxSourceSinkFactory {
 
-  @SuppressWarnings("unchecked")
+  protected static final String PARTITION_COMMITTER_CLASS =
+      "org.apache.flink.connector.file.table.stream.PartitionCommitter";
+  protected static final String ABSTRACT_STREAMING_WRITER_CLASS =
+      "org.apache.flink.connector.file.table.stream.AbstractStreamingWriter";
+  private static final String HIVE_TABLE_META_STORE_FACTORY_CLASS =
+      "org.apache.flink.connectors.hive.HiveTableMetaStoreFactory";
+  private static final String CONNECTOR_FILESYSTEM = "connector-filesystem";
+
   @Override
   public boolean match(Transformation<RowData> transformation) {
-    if (transformation instanceof SinkTransformation) {
-      SinkTransformation<RowData, RowData> sinkTransformation =
-          (SinkTransformation<RowData, RowData>) transformation;
-      Transformation<RowData> inputTransformation =
-          (Transformation<RowData>) sinkTransformation.getInputs().get(0);
-      if (inputTransformation instanceof OneInputTransformation
-          && inputTransformation.getName().equals("PartitionCommitter")) {
-        OneInputTransformation<RowData, RowData> oneInputTransformatin =
-            (OneInputTransformation<RowData, RowData>) inputTransformation;
-        Transformation<RowData> preInputTransformation =
-            (Transformation<RowData>) oneInputTransformatin.getInputs().get(0);
-        return preInputTransformation.getName().equals("StreamingFileWriter");
-      }
+    if (!isFileSystemSinkTransformation(transformation)) {
+      return false;
     }
-    return false;
+    return !isHiveConnector(transformation);
   }
 
   @Override
@@ -80,28 +76,22 @@ public class FileSystemSinkFactory implements 
VeloxSourceSinkFactory {
     SinkTransformation<RowData, RowData> sinkTransformation =
         (SinkTransformation<RowData, RowData>) transformation;
     OneInputTransformation<RowData, RowData> partitionCommitTransformation =
-        (OneInputTransformation<RowData, RowData>) 
sinkTransformation.getInputs().get(0);
+        getPartitionCommitTransformation(transformation);
     OneInputTransformation<RowData, RowData> fileWriterTransformation =
-        (OneInputTransformation<RowData, RowData>) 
partitionCommitTransformation.getInputs().get(0);
+        getFileWriterTransformation(transformation);
     OneInputStreamOperator<?, ?> operator = 
fileWriterTransformation.getOperator();
     List<String> partitionKeys =
         (List<String>) ReflectUtils.getObjectField(operator.getClass(), 
operator, "partitionKeys");
-    Map<String, String> tableParams = new HashMap<>();
-    Configuration tableOptions =
-        (Configuration)
-            ReflectUtils.getObjectField(
-                
"org.apache.flink.connector.file.table.stream.PartitionCommitter",
-                partitionCommitTransformation.getOperator(),
-                "conf");
-    tableParams.putAll(tableOptions.toMap());
-
+    Object partitionCommitter =
+        partitionCommitTransformation == null ? null : 
partitionCommitTransformation.getOperator();
+    Map<String, String> tableParams = buildTableParams(partitionCommitter, 
operator);
     ResolvedSchema schema = (ResolvedSchema) 
parameters.get(ResolvedSchema.class.getName());
     List<String> columnList = schema.getColumnNames();
     List<Integer> partitionIndexes =
         
partitionKeys.stream().mapToInt(columnList::indexOf).boxed().collect(Collectors.toList());
     org.apache.flink.table.types.logical.RowType inputType =
         (org.apache.flink.table.types.logical.RowType)
-            ((InternalTypeInfo<?>) 
fileWriterTransformation.getInputType()).toLogicalType();
+            schema.toPhysicalRowDataType().getLogicalType();
     RowType inputDataColumns = (RowType) 
LogicalTypeConverter.toVLType(inputType);
     FileSystemInsertTableHandle insertTableHandle =
         new FileSystemInsertTableHandle(
@@ -117,41 +107,45 @@ public class FileSystemSinkFactory implements 
VeloxSourceSinkFactory {
             inputDataColumns,
             inputDataColumns.getNames(),
             null,
-            "connector-filesystem",
+            CONNECTOR_FILESYSTEM,
             insertTableHandle,
             false,
             ignore,
             CommitStrategy.NO_COMMIT,
             List.of(new EmptyNode(inputDataColumns)));
     GlutenOneInputOperator onewInputOperator =
-        new GlutenOneInputOperator(
+        new GlutenStreamingFileWriterOperator(
             new StatefulPlanNode(fileSystemWriteNode.getId(), 
fileSystemWriteNode),
             PlanNodeIdGenerator.newId(),
             inputDataColumns,
             Map.of(fileSystemWriteNode.getId(), ignore),
             RowData.class,
-            RowData.class,
-            "FileSystemInsertTable");
-    GlutenOneInputOperatorFactory<?, ?> operatorFactory =
-        new GlutenOneInputOperatorFactory(onewInputOperator);
-    Transformation<RowData> veloxFileWriterTransformation =
+            getSinkDescription());
+    GlutenOneInputOperatorFactory<RowData, ?> operatorFactory =
+        new GlutenOneInputOperatorFactory<>(onewInputOperator);
+    OneInputTransformation<RowData, ?> veloxFileWriterTransformation =
         new OneInputTransformation(
             fileWriterTransformation.getInputs().get(0),
             fileWriterTransformation.getName(),
             operatorFactory,
             fileWriterTransformation.getOutputType(),
             fileWriterTransformation.getParallelism());
-    OneInputTransformation<RowData, RowData> newPartitionCommitTransformation =
-        new OneInputTransformation(
-            veloxFileWriterTransformation,
-            partitionCommitTransformation.getName(),
-            partitionCommitTransformation.getOperatorFactory(),
-            partitionCommitTransformation.getOutputType(),
-            partitionCommitTransformation.getParallelism());
+    Transformation<?> newSinkInputTransformation;
+    if (partitionCommitTransformation == null) {
+      newSinkInputTransformation = veloxFileWriterTransformation;
+    } else {
+      newSinkInputTransformation =
+          new OneInputTransformation(
+              veloxFileWriterTransformation,
+              partitionCommitTransformation.getName(),
+              partitionCommitTransformation.getOperatorFactory(),
+              partitionCommitTransformation.getOutputType(),
+              partitionCommitTransformation.getParallelism());
+    }
     DataStream<RowData> newInputStream =
         new DataStream<RowData>(
             sinkTransformation.getInputStream().getExecutionEnvironment(),
-            newPartitionCommitTransformation);
+            (Transformation<RowData>) newSinkInputTransformation);
     return new SinkTransformation<RowData, RowData>(
         newInputStream,
         sinkTransformation.getSink(),
@@ -161,4 +155,205 @@ public class FileSystemSinkFactory implements 
VeloxSourceSinkFactory {
         sinkTransformation.isParallelismConfigured(),
         sinkTransformation.getSinkOperatorsUidHashes());
   }
+
+  @SuppressWarnings("unchecked")
+  protected boolean isFileSystemSinkTransformation(Transformation<RowData> 
transformation) {
+    if (transformation instanceof SinkTransformation) {
+      SinkTransformation<RowData, RowData> sinkTransformation =
+          (SinkTransformation<RowData, RowData>) transformation;
+      Transformation<RowData> inputTransformation =
+          (Transformation<RowData>) sinkTransformation.getInputs().get(0);
+      if (inputTransformation instanceof OneInputTransformation) {
+        if (inputTransformation.getName().equals("StreamingFileWriter")) {
+          return true;
+        }
+        if (inputTransformation.getName().equals("PartitionCommitter")) {
+          OneInputTransformation<RowData, RowData> oneInputTransformation =
+              (OneInputTransformation<RowData, RowData>) inputTransformation;
+          Transformation<RowData> preInputTransformation =
+              (Transformation<RowData>) 
oneInputTransformation.getInputs().get(0);
+          return 
preInputTransformation.getName().equals("StreamingFileWriter");
+        }
+      }
+    }
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  protected OneInputTransformation<RowData, RowData> 
getPartitionCommitTransformation(
+      Transformation<RowData> transformation) {
+    SinkTransformation<RowData, RowData> sinkTransformation =
+        (SinkTransformation<RowData, RowData>) transformation;
+    Transformation<RowData> inputTransformation =
+        (Transformation<RowData>) sinkTransformation.getInputs().get(0);
+    if (inputTransformation.getName().equals("PartitionCommitter")) {
+      return (OneInputTransformation<RowData, RowData>) inputTransformation;
+    }
+    return null;
+  }
+
+  @SuppressWarnings("unchecked")
+  protected OneInputTransformation<RowData, RowData> 
getFileWriterTransformation(
+      Transformation<RowData> transformation) {
+    OneInputTransformation<RowData, RowData> partitionCommitTransformation =
+        getPartitionCommitTransformation(transformation);
+    if (partitionCommitTransformation != null) {
+      return (OneInputTransformation<RowData, RowData>)
+          partitionCommitTransformation.getInputs().get(0);
+    }
+    SinkTransformation<RowData, RowData> sinkTransformation =
+        (SinkTransformation<RowData, RowData>) transformation;
+    return (OneInputTransformation<RowData, RowData>) 
sinkTransformation.getInputs().get(0);
+  }
+
+  protected Object getPartitionCommitter(Transformation<RowData> 
transformation) {
+    OneInputTransformation<RowData, RowData> partitionCommitTransformation =
+        getPartitionCommitTransformation(transformation);
+    return partitionCommitTransformation == null
+        ? null
+        : partitionCommitTransformation.getOperator();
+  }
+
+  protected boolean isHiveConnector(Transformation<RowData> transformation) {
+    Object partitionCommitter = getPartitionCommitter(transformation);
+    if (partitionCommitter != null) {
+      return isHiveConnector(partitionCommitter);
+    }
+    return "hive"
+        .equals(
+            
getTableOptions(getFileWriterTransformation(transformation).getOperator())
+                .toMap()
+                .get("connector"));
+  }
+
+  protected boolean isHiveConnector(Object partitionCommitter) {
+    Object metaStoreFactory =
+        ReflectUtils.getObjectField(
+            PARTITION_COMMITTER_CLASS, partitionCommitter, "metaStoreFactory");
+    return 
HIVE_TABLE_META_STORE_FACTORY_CLASS.equals(metaStoreFactory.getClass().getName());
+  }
+
+  protected Map<String, String> buildTableParams(
+      Object partitionCommitter, OneInputStreamOperator<?, ?> 
fileWriterOperator) {
+    Configuration tableOptions = getTableOptions(partitionCommitter, 
fileWriterOperator);
+    Map<String, String> tableParams = new HashMap<>(tableOptions.toMap());
+    tableParams.putIfAbsent("path", getLocationPath(partitionCommitter, 
fileWriterOperator));
+    tableParams.putIfAbsent("format", resolveWriteFormat(fileWriterOperator));
+    tableParams.put("connector", "filesystem");
+    return tableParams;
+  }
+
+  protected String getSinkDescription() {
+    return "FileSystemInsertTable";
+  }
+
+  protected String getDefaultFormat() {
+    return "unknown";
+  }
+
+  protected Configuration getTableOptions(OneInputStreamOperator<?, ?> 
fileWriterOperator) {
+    return (Configuration)
+        ReflectUtils.getObjectField(fileWriterOperator.getClass(), 
fileWriterOperator, "conf");
+  }
+
+  protected Configuration getTableOptions(
+      Object partitionCommitter, OneInputStreamOperator<?, ?> 
fileWriterOperator) {
+    if (partitionCommitter != null) {
+      return (Configuration)
+          ReflectUtils.getObjectField(PARTITION_COMMITTER_CLASS, 
partitionCommitter, "conf");
+    }
+    return getTableOptions(fileWriterOperator);
+  }
+
+  protected String getLocationPath(
+      Object partitionCommitter, OneInputStreamOperator<?, ?> 
fileWriterOperator) {
+    if (partitionCommitter != null) {
+      Object locationPath =
+          ReflectUtils.getObjectField(
+              PARTITION_COMMITTER_CLASS, partitionCommitter, "locationPath");
+      return locationPath.toString();
+    }
+    Object bucketsBuilder =
+        ReflectUtils.getObjectField(
+            ABSTRACT_STREAMING_WRITER_CLASS, fileWriterOperator, 
"bucketsBuilder");
+    Object basePath = ReflectUtils.tryGetObjectField(bucketsBuilder, 
"basePath");
+    return basePath.toString();
+  }
+
+  protected String resolveWriteFormat(OneInputStreamOperator<?, ?> 
fileWriterOperator) {
+    Object bucketsBuilder =
+        ReflectUtils.getObjectField(
+            ABSTRACT_STREAMING_WRITER_CLASS, fileWriterOperator, 
"bucketsBuilder");
+    String format = resolveFormatFromBucketsBuilder(bucketsBuilder);
+    if (format != null) {
+      return format;
+    }
+    return getDefaultFormat();
+  }
+
+  protected String resolveFormatFromBucketsBuilder(Object bucketsBuilder) {
+    Class<?> builderClass = bucketsBuilder.getClass();
+    String builderName = builderClass.getName();
+    if (builderName.contains("HadoopPathBasedBulkFormatBuilder")) {
+      Object writerFactory = ReflectUtils.tryGetObjectField(bucketsBuilder, 
"writerFactory");
+      if (writerFactory != null) {
+        return resolveFormatFromHadoopBulkWriterFactory(writerFactory);
+      }
+      return inferFormatFromClassName(builderName);
+    }
+    if (builderName.contains("BulkFormatBuilder")) {
+      Object writerFactory = ReflectUtils.tryGetObjectField(bucketsBuilder, 
"writerFactory");
+      if (writerFactory != null) {
+        return resolveFormatFromBulkWriterFactory(writerFactory);
+      }
+      return inferFormatFromClassName(builderName);
+    }
+    if (builderName.contains("RowFormatBuilder")) {
+      Object encoder = ReflectUtils.tryGetObjectField(bucketsBuilder, 
"encoder");
+      if (encoder != null) {
+        return inferFormatFromClassName(encoder.getClass().getName());
+      }
+    }
+    return inferFormatFromClassName(builderName);
+  }
+
+  protected String resolveFormatFromHadoopBulkWriterFactory(Object 
writerFactory) {
+    return inferFormatFromClassName(writerFactory.getClass().getName());
+  }
+
+  protected String resolveFormatFromBulkWriterFactory(Object writerFactory) {
+    String format = 
inferFormatFromClassName(writerFactory.getClass().getName());
+    if (format != null) {
+      return format;
+    }
+    Object innerFactory =
+        ReflectUtils.tryGetObjectField(writerFactory.getClass(), 
writerFactory, "factory");
+    if (innerFactory != null) {
+      format = inferFormatFromClassName(innerFactory.getClass().getName());
+      if (format != null) {
+        return format;
+      }
+    }
+    return null;
+  }
+
+  protected String inferFormatFromClassName(String className) {
+    String lower = className.toLowerCase();
+    if (lower.contains("parquet")) {
+      return "parquet";
+    }
+    if (lower.contains("orc")) {
+      return "orc";
+    }
+    if (lower.contains("avro")) {
+      return "avro";
+    }
+    if (lower.contains("json")) {
+      return "json";
+    }
+    if (lower.contains("csv")) {
+      return "csv";
+    }
+    return null;
+  }
 }
diff --git 
a/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FuzzerSourceSinkFactory.java
 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FuzzerSourceSinkFactory.java
index 6e9c232f04..b16d0e405a 100644
--- 
a/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FuzzerSourceSinkFactory.java
+++ 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/FuzzerSourceSinkFactory.java
@@ -59,7 +59,8 @@ public class FuzzerSourceSinkFactory implements 
VeloxSourceSinkFactory {
           (SinkTransformation<RowData, RowData>) transformation;
       Transformation<?> inputTransformation = 
sinkTransformation.getInputs().get(0);
       if (sinkTransformation.getSink() instanceof DiscardingSink
-          && !inputTransformation.getName().equals("PartitionCommitter")) {
+          && !inputTransformation.getName().equals("PartitionCommitter")
+          && transformation.getOutputType() instanceof InternalTypeInfo) {
         return true;
       }
     } else if (transformation instanceof LegacySourceTransformation) {
diff --git 
a/gluten-flink/planner/src/main/java/org/apache/gluten/velox/HiveSourceSinkFactory.java
 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/HiveSourceSinkFactory.java
new file mode 100644
index 0000000000..56d356f237
--- /dev/null
+++ 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/HiveSourceSinkFactory.java
@@ -0,0 +1,99 @@
+/*
+ * 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.gluten.velox;
+
+import org.apache.gluten.util.ReflectUtils;
+
+import org.apache.flink.api.dag.Transformation;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.table.data.RowData;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HiveSourceSinkFactory extends FileSystemSinkFactory {
+
+  @Override
+  public boolean match(Transformation<RowData> transformation) {
+    if (!isFileSystemSinkTransformation(transformation)) {
+      return false;
+    }
+    return isHiveConnector(transformation);
+  }
+
+  @Override
+  protected Map<String, String> buildTableParams(
+      Object partitionCommitter, OneInputStreamOperator<?, ?> 
fileWriterOperator) {
+    Configuration tableOptions = getTableOptions(partitionCommitter, 
fileWriterOperator);
+    Map<String, String> tableParams = new HashMap<>(tableOptions.toMap());
+    tableParams.put("path", getLocationPath(partitionCommitter, 
fileWriterOperator));
+    tableParams.putIfAbsent("format", resolveWriteFormat(fileWriterOperator));
+    tableParams.put("connector", "hive");
+    return tableParams;
+  }
+
+  @Override
+  protected String getSinkDescription() {
+    return "HiveInsertTable";
+  }
+
+  @Override
+  protected String getDefaultFormat() {
+    return "hive";
+  }
+
+  @Override
+  protected String resolveFormatFromHadoopBulkWriterFactory(Object 
writerFactory) {
+    Class<?> factoryClass = writerFactory.getClass();
+    if (factoryClass.getName().contains("HiveBulkWriterFactory")) {
+      Object hiveWriterFactory =
+          ReflectUtils.getObjectField(factoryClass, writerFactory, "factory");
+      return resolveFormatFromHiveWriterFactory(hiveWriterFactory);
+    }
+    return super.resolveFormatFromHadoopBulkWriterFactory(writerFactory);
+  }
+
+  private String resolveFormatFromHiveWriterFactory(Object hiveWriterFactory) {
+    Class<?> factoryClass = hiveWriterFactory.getClass();
+    Object serDeInfoCached =
+        ReflectUtils.getObjectField(factoryClass, hiveWriterFactory, 
"serDeInfo");
+    Object serDeInfo =
+        ReflectUtils.invokeObjectMethod(
+            serDeInfoCached.getClass(),
+            serDeInfoCached,
+            "deserializeValue",
+            new Class<?>[] {},
+            new Object[] {});
+    String serializationLib =
+        (String)
+            ReflectUtils.invokeObjectMethod(
+                serDeInfo.getClass(),
+                serDeInfo,
+                "getSerializationLib",
+                new Class<?>[] {},
+                new Object[] {});
+    String format = inferFormatFromClassName(serializationLib);
+    if (format != null) {
+      return format;
+    }
+    Class<?> outputFormatClz =
+        (Class<?>)
+            ReflectUtils.getObjectField(factoryClass, hiveWriterFactory, 
"hiveOutputFormatClz");
+    return inferFormatFromClassName(outputFormatClz.getName());
+  }
+}
diff --git 
a/gluten-flink/planner/src/main/java/org/apache/gluten/velox/KafkaSourceSinkFactory.java
 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/KafkaSourceSinkFactory.java
index 589e57b0a2..3e6034faf2 100644
--- 
a/gluten-flink/planner/src/main/java/org/apache/gluten/velox/KafkaSourceSinkFactory.java
+++ 
b/gluten-flink/planner/src/main/java/org/apache/gluten/velox/KafkaSourceSinkFactory.java
@@ -31,6 +31,7 @@ import 
io.github.zhztheplayer.velox4j.plan.TableScanWithWatermarkNode;
 import io.github.zhztheplayer.velox4j.plan.WatermarkPushDownSpec;
 import io.github.zhztheplayer.velox4j.type.RowType;
 
+import org.apache.flink.api.connector.source.Boundedness;
 import org.apache.flink.api.connector.source.Source;
 import org.apache.flink.api.dag.Transformation;
 import 
org.apache.flink.streaming.api.transformations.LegacySourceTransformation;
@@ -54,7 +55,11 @@ public class KafkaSourceSinkFactory implements 
VeloxSourceSinkFactory {
   @Override
   public boolean match(Transformation<RowData> transformation) {
     if (transformation instanceof SourceTransformation) {
-      Source source = ((SourceTransformation) transformation).getSource();
+      SourceTransformation sourceTransformation = (SourceTransformation) 
transformation;
+      if (sourceTransformation.getBoundedness() == Boundedness.BOUNDED) {
+        return false;
+      }
+      Source source = sourceTransformation.getSource();
       return source.getClass().getSimpleName().equals("KafkaSource");
     }
     return false;
diff --git 
a/gluten-flink/planner/src/main/resources/META-INF/services/org.apache.gluten.velox.VeloxSourceSinkFactory
 
b/gluten-flink/planner/src/main/resources/META-INF/services/org.apache.gluten.velox.VeloxSourceSinkFactory
index db57ce53dc..862ffe305c 100644
--- 
a/gluten-flink/planner/src/main/resources/META-INF/services/org.apache.gluten.velox.VeloxSourceSinkFactory
+++ 
b/gluten-flink/planner/src/main/resources/META-INF/services/org.apache.gluten.velox.VeloxSourceSinkFactory
@@ -3,5 +3,6 @@ org.apache.gluten.velox.KafkaSourceSinkFactory
 org.apache.gluten.velox.PulsarSourceSinkFactory
 org.apache.gluten.velox.PrintSinkFactory
 org.apache.gluten.velox.NexmarkSourceFactory
+org.apache.gluten.velox.HiveSourceSinkFactory
 org.apache.gluten.velox.FileSystemSinkFactory
 org.apache.gluten.velox.FuzzerSourceSinkFactory
diff --git 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenStreamSource.java
 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenStreamSource.java
index 349261ff4d..acc6b2e60a 100644
--- 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenStreamSource.java
+++ 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenStreamSource.java
@@ -18,13 +18,11 @@ package org.apache.gluten.streaming.api.operators;
 
 import org.apache.gluten.table.runtime.operators.GlutenSourceFunction;
 import org.apache.gluten.util.ReflectUtils;
-import org.apache.gluten.util.Utils;
 
 import io.github.zhztheplayer.velox4j.connector.ConnectorSplit;
 import io.github.zhztheplayer.velox4j.plan.StatefulPlanNode;
 import io.github.zhztheplayer.velox4j.type.RowType;
 
-import org.apache.flink.api.common.TaskInfo;
 import org.apache.flink.streaming.api.functions.source.SourceFunction;
 import org.apache.flink.streaming.api.operators.StreamSource;
 
@@ -91,19 +89,7 @@ public class GlutenStreamSource extends StreamSource 
implements GlutenOperator {
   @Override
   public void notifyCheckpointComplete(long checkpointId) throws Exception {
     super.notifyCheckpointComplete(checkpointId);
-    String[] committed = sourceFunction.notifyCheckpointComplete(checkpointId);
-    SourceFunction.SourceContext sourceContext = getSourceContext();
-    TaskInfo taskInfo = getRuntimeContext().getTaskInfo();
-    if (sourceContext != null
-        && committed != null
-        && taskInfo.getTaskName().contains("StreamingFileWriter")) {
-      sourceContext.collect(
-          Utils.constructCommitInfo(
-              checkpointId,
-              taskInfo.getIndexOfThisSubtask(),
-              taskInfo.getNumberOfParallelSubtasks(),
-              committed));
-    }
+    sourceFunction.notifyCheckpointComplete(checkpointId);
   }
 
   @Override
diff --git 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
index 09e2b2bb62..ef9d0d845d 100644
--- 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
+++ 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
@@ -340,8 +340,9 @@ public class GlutenOneInputOperator<IN, OUT> extends 
TableStreamOperator<OUT>
 
   @Override
   public void notifyCheckpointComplete(long checkpointId) throws Exception {
-    // TODO: notify velox
-    task.notifyCheckpointComplete(checkpointId);
+    if (!(this instanceof GlutenStreamingFileWriterOperator)) {
+      task.notifyCheckpointComplete(checkpointId);
+    }
     super.notifyCheckpointComplete(checkpointId);
   }
 
diff --git 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenStreamingFileWriterOperator.java
 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenStreamingFileWriterOperator.java
new file mode 100644
index 0000000000..e508fb80c3
--- /dev/null
+++ 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenStreamingFileWriterOperator.java
@@ -0,0 +1,75 @@
+/*
+ * 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.gluten.table.runtime.operators;
+
+import org.apache.gluten.util.Utils;
+
+import io.github.zhztheplayer.velox4j.plan.StatefulPlanNode;
+import io.github.zhztheplayer.velox4j.type.RowType;
+
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.connector.file.table.stream.PartitionCommitInfo;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+
+import java.util.Map;
+
+public class GlutenStreamingFileWriterOperator<IN>
+    extends GlutenOneInputOperator<IN, PartitionCommitInfo> {
+
+  public GlutenStreamingFileWriterOperator(
+      StatefulPlanNode plan,
+      String id,
+      RowType inputType,
+      Map<String, RowType> outputTypes,
+      Class<IN> inClass,
+      String description) {
+    super(plan, id, inputType, outputTypes, inClass, 
PartitionCommitInfo.class, description);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public <NIN, NOUT> GlutenOneInputOperator<NIN, NOUT> 
cloneWithInputOutputClasses(
+      StatefulPlanNode plan, Class<NIN> inClass, Class<NOUT> outClass) {
+    return (GlutenOneInputOperator<NIN, NOUT>)
+        new GlutenStreamingFileWriterOperator<>(
+            plan, getId(), getInputType(), getOutputTypes(), inClass, 
getDescription());
+  }
+
+  @Override
+  public void processWatermark(Watermark mark) throws Exception {
+    super.processWatermark(mark);
+    output.emitWatermark(mark);
+  }
+
+  @Override
+  public void notifyCheckpointComplete(long checkpointId) throws Exception {
+    String[] committed = task.notifyCheckpointComplete(checkpointId);
+    if (committed != null) {
+      TaskInfo taskInfo = getRuntimeContext().getTaskInfo();
+      output.collect(
+          new StreamRecord<PartitionCommitInfo>(
+              (PartitionCommitInfo)
+                  Utils.constructCommitInfo(
+                      checkpointId,
+                      taskInfo.getIndexOfThisSubtask(),
+                      taskInfo.getNumberOfParallelSubtasks(),
+                      committed)));
+    }
+    super.notifyCheckpointComplete(checkpointId);
+  }
+}
diff --git 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/util/ReflectUtils.java 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/util/ReflectUtils.java
index 7dff789ae2..4d5e65ccc8 100644
--- 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/util/ReflectUtils.java
+++ 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/util/ReflectUtils.java
@@ -42,6 +42,26 @@ public class ReflectUtils {
     }
   }
 
+  public static Object tryGetObjectField(Class<?> clazz, Object obj, String 
fieldName) {
+    try {
+      return getObjectField(clazz, obj, fieldName);
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  public static Object tryGetObjectField(Object obj, String fieldName) {
+    Class<?> clazz = obj.getClass();
+    while (clazz != null) {
+      Object value = tryGetObjectField(clazz, obj, fieldName);
+      if (value != null) {
+        return value;
+      }
+      clazz = clazz.getSuperclass();
+    }
+    return null;
+  }
+
   public static Object invokeObjectMethod(
       Class<?> clazz, Object obj, String methodName, Class<?>[] paramTypes, 
Object[] paramValues) {
     try {
diff --git 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/util/VectorOutputBridge.java
 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/util/VectorOutputBridge.java
index 4849f90e94..bbc4ea796c 100644
--- 
a/gluten-flink/runtime/src/main/java/org/apache/gluten/util/VectorOutputBridge.java
+++ 
b/gluten-flink/runtime/src/main/java/org/apache/gluten/util/VectorOutputBridge.java
@@ -19,6 +19,7 @@ package org.apache.gluten.util;
 import io.github.zhztheplayer.velox4j.stateful.StatefulRecord;
 import io.github.zhztheplayer.velox4j.type.RowType;
 
+import org.apache.flink.connector.file.table.stream.PartitionCommitInfo;
 import org.apache.flink.streaming.api.operators.Output;
 import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
 
@@ -65,6 +66,11 @@ public interface VectorOutputBridge<OUT> extends 
Serializable {
         @SuppressWarnings("unchecked")
         VectorOutputBridge<OUT> bridge = (VectorOutputBridge<OUT>) new 
StatefulRecordOutputBridge();
         return bridge;
+      } else if (outputClass.isAssignableFrom(PartitionCommitInfo.class)) {
+        @SuppressWarnings("unchecked")
+        VectorOutputBridge<OUT> bridge =
+            (VectorOutputBridge<OUT>) new PartitionCommitInfoOutputBridge();
+        return bridge;
       } else {
         throw new UnsupportedOperationException(
             "Unsupported output class: " + outputClass.getName());
@@ -134,4 +140,16 @@ public interface VectorOutputBridge<OUT> extends 
Serializable {
       return outputElement;
     }
   }
+
+  class PartitionCommitInfoOutputBridge implements 
VectorOutputBridge<PartitionCommitInfo> {
+
+    @Override
+    public void collect(
+        Output<StreamRecord<PartitionCommitInfo>> collector,
+        StatefulRecord record,
+        BufferAllocator allocator,
+        RowType outputType) {
+      throw new UnsupportedOperationException("PartitionCommitInfo is not 
supported");
+    }
+  }
 }
diff --git a/gluten-flink/ut/pom.xml b/gluten-flink/ut/pom.xml
index f94d743e3e..c97c3838e1 100644
--- a/gluten-flink/ut/pom.xml
+++ b/gluten-flink/ut/pom.xml
@@ -118,6 +118,12 @@
       <artifactId>junit-jupiter-engine</artifactId>
       <version>5.9.2</version>
     </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.13.2</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.assertj</groupId>
       <artifactId>assertj-core</artifactId>
@@ -153,6 +159,12 @@
       <classifier>tests</classifier>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.flink</groupId>
+      <artifactId>flink-sql-gateway-api</artifactId>
+      <version>${flink.version}</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>com.salesforce.kafka.test</groupId>
       <artifactId>kafka-junit5</artifactId>
@@ -165,6 +177,30 @@
       <version>3.9.1</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-compress</artifactId>
+      <version>1.26.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-auth</artifactId>
+      <version>2.7.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-common</artifactId>
+      <version>2.7.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-hdfs</artifactId>
+      <version>2.7.2</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.flink</groupId>
       <artifactId>flink-connector-kafka</artifactId>
@@ -183,6 +219,56 @@
       <version>${flink.version}</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.flink</groupId>
+      <artifactId>flink-connector-hive_${scala.binary.version}</artifactId>
+      <version>${flink.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.flink</groupId>
+      <artifactId>flink-connector-hive_${scala.binary.version}</artifactId>
+      <version>${flink.version}</version>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hive</groupId>
+      <artifactId>hive-exec</artifactId>
+      <version>2.3.9</version>
+      <classifier>core</classifier>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>junit</groupId>
+          <artifactId>junit</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.pentaho</groupId>
+          <artifactId>pentaho-aggdesigner-algorithm</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hive</groupId>
+      <artifactId>hive-metastore</artifactId>
+      <version>2.3.9</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>jdk.tools</groupId>
+          <artifactId>jdk.tools</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>junit</groupId>
+          <artifactId>junit</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.pentaho</groupId>
+          <artifactId>pentaho-aggdesigner-algorithm</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
     <dependency>
       <groupId>org.apache.flink</groupId>
       <artifactId>flink-json</artifactId>
diff --git 
a/gluten-flink/ut/src/test/java/org/apache/gluten/table/runtime/stream/custom/KafkaToHiveE2ETest.java
 
b/gluten-flink/ut/src/test/java/org/apache/gluten/table/runtime/stream/custom/KafkaToHiveE2ETest.java
new file mode 100644
index 0000000000..0388cbf9d4
--- /dev/null
+++ 
b/gluten-flink/ut/src/test/java/org/apache/gluten/table/runtime/stream/custom/KafkaToHiveE2ETest.java
@@ -0,0 +1,362 @@
+/*
+ * 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.gluten.table.runtime.stream.custom;
+
+import org.apache.gluten.streaming.api.operators.GlutenOneInputOperatorFactory;
+import 
org.apache.gluten.table.runtime.operators.GlutenStreamingFileWriterOperator;
+
+import org.apache.flink.api.dag.Transformation;
+import org.apache.flink.core.execution.JobClient;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.transformations.OneInputTransformation;
+import org.apache.flink.table.api.EnvironmentSettings;
+import org.apache.flink.table.api.SqlDialect;
+import org.apache.flink.table.api.TableResult;
+import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
+import org.apache.flink.table.api.internal.TableEnvironmentImpl;
+import org.apache.flink.table.catalog.hive.HiveCatalog;
+import org.apache.flink.table.catalog.hive.HiveTestUtils;
+import org.apache.flink.table.operations.ModifyOperation;
+import org.apache.flink.table.operations.Operation;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.CloseableIterator;
+
+import com.salesforce.kafka.test.junit5.SharedKafkaTestResource;
+import com.salesforce.kafka.test.listeners.PlainListener;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class KafkaToHiveE2ETest {
+
+  private static final int KAFKA_PORT = 19092;
+  private static final String VELOX_SOURCE_SINK_FACTORY_SERVICE =
+      "META-INF/services/org.apache.gluten.velox.VeloxSourceSinkFactory";
+
+  @RegisterExtension
+  static final SharedKafkaTestResource KAFKA =
+      new SharedKafkaTestResource()
+          .withBrokerProperty("host.name", "127.0.0.1")
+          .withBrokers(1)
+          .registerListener(new PlainListener().onPorts(KAFKA_PORT));
+
+  @Test
+  void testInsertFromKafkaToHive(@TempDir Path tempDir) throws Exception {
+    String topic = "kafka_to_hive_" + 
UUID.randomUUID().toString().replace("-", "");
+    KAFKA.getKafkaTestUtils().createTopic(topic, 1, (short) 1);
+    KAFKA
+        .getKafkaTestUtils()
+        .produceRecords(
+            List.of(
+                jsonRecord(topic, "{\"id\":1,\"name\":\"alice\"}"),
+                jsonRecord(topic, "{\"id\":2,\"name\":\"bob\"}"),
+                jsonRecord(topic, "{\"id\":3,\"name\":\"carol\"}")));
+
+    StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+    env.setParallelism(1);
+    StreamTableEnvironment tEnv =
+        StreamTableEnvironment.create(
+            env, EnvironmentSettings.newInstance().inStreamingMode().build());
+
+    createKafkaSource(tEnv, topic);
+    Path sinkPath = tempDir.resolve("hive_sink");
+    createHiveCatalogAndSink(tEnv, sinkPath);
+
+    String insertSql =
+        "INSERT INTO hive_catalog.`default`.hive_sink "
+            + "SELECT id, name FROM 
default_catalog.default_database.kafka_source";
+    assertHiveSinkOffloadedToNative(tEnv, insertSql);
+    TableResult insertResult = executeSqlWithoutVeloxSourceSinkFactories(tEnv, 
insertSql);
+    JobClient jobClient = insertResult.getJobClient().orElseThrow();
+    jobClient.getJobExecutionResult().get(60000, TimeUnit.MILLISECONDS);
+
+    
assertThat(readHiveTextRows(sinkPath)).containsExactlyInAnyOrder("1,alice", 
"2,bob", "3,carol");
+  }
+
+  @Test
+  void testCommitHivePartitionWithProcessTime(@TempDir Path tempDir) throws 
Exception {
+    testCommitHivePartition(tempDir, "process-time");
+  }
+
+  @Test
+  void testCommitHivePartitionWithPartitionTime(@TempDir Path tempDir) throws 
Exception {
+    testCommitHivePartition(tempDir, "partition-time");
+  }
+
+  private void testCommitHivePartition(Path tempDir, String 
partitionCommitTrigger)
+      throws Exception {
+    String topic =
+        "kafka_to_partitioned_hive_"
+            + partitionCommitTrigger.replace("-", "_")
+            + "_"
+            + UUID.randomUUID().toString().replace("-", "");
+    KAFKA.getKafkaTestUtils().createTopic(topic, 1, (short) 1);
+    KAFKA
+        .getKafkaTestUtils()
+        .produceRecords(
+            List.of(
+                jsonRecord(
+                    topic, 
"{\"id\":1,\"name\":\"alice\",\"dt\":\"2026-06-29\",\"hm\":\"10:00\"}"),
+                jsonRecord(
+                    topic, 
"{\"id\":2,\"name\":\"bob\",\"dt\":\"2026-06-29\",\"hm\":\"10:00\"}"),
+                jsonRecord(
+                    topic,
+                    
"{\"id\":3,\"name\":\"carol\",\"dt\":\"2026-06-29\",\"hm\":\"10:01\"}")));
+
+    StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+    env.setParallelism(1);
+    StreamTableEnvironment tEnv =
+        StreamTableEnvironment.create(
+            env, EnvironmentSettings.newInstance().inStreamingMode().build());
+
+    createPartitionedKafkaSource(tEnv, topic);
+    Path sinkPath = tempDir.resolve("partitioned_hive_sink");
+    createHiveCatalogAndPartitionedSink(tEnv, sinkPath, 
partitionCommitTrigger);
+
+    TableResult insertResult =
+        executeSqlWithoutVeloxSourceSinkFactories(
+            tEnv,
+            "INSERT INTO hive_catalog.`default`.partitioned_hive_sink "
+                + "SELECT id, name, dt, hm FROM 
default_catalog.default_database.kafka_source");
+    JobClient jobClient = insertResult.getJobClient().orElseThrow();
+    jobClient.getJobExecutionResult().get(60000, TimeUnit.MILLISECONDS);
+
+    
assertThat(readHiveTextRows(sinkPath)).containsExactlyInAnyOrder("1,alice", 
"2,bob", "3,carol");
+    assertCommittedPartitions(tEnv);
+    assertCommittedPartition(sinkPath, "2026-06-29", "10:00");
+    assertCommittedPartition(sinkPath, "2026-06-29", "10:01");
+  }
+
+  private static ProducerRecord<byte[], byte[]> jsonRecord(String topic, 
String value) {
+    return new ProducerRecord<>(topic, value.getBytes(StandardCharsets.UTF_8));
+  }
+
+  private void createKafkaSource(StreamTableEnvironment tEnv, String topic) {
+    tEnv.executeSql(
+        "CREATE TABLE kafka_source ("
+            + " id INT,"
+            + " name STRING"
+            + ") WITH ("
+            + " 'connector' = 'kafka',"
+            + " 'topic' = '"
+            + topic
+            + "',"
+            + " 'properties.bootstrap.servers' = '"
+            + "127.0.0.1:"
+            + KAFKA_PORT
+            + "',"
+            + " 'properties.group.id' = 'kafka-to-hive-e2e',"
+            + " 'properties.broker.address.family' = 'v4',"
+            + " 'scan.startup.mode' = 'earliest-offset',"
+            + " 'scan.bounded.mode' = 'latest-offset',"
+            + " 'format' = 'json'"
+            + ")");
+  }
+
+  private void createPartitionedKafkaSource(StreamTableEnvironment tEnv, 
String topic) {
+    tEnv.executeSql(
+        "CREATE TABLE kafka_source ("
+            + " id INT,"
+            + " name STRING,"
+            + " dt STRING,"
+            + " hm STRING"
+            + ") WITH ("
+            + " 'connector' = 'kafka',"
+            + " 'topic' = '"
+            + topic
+            + "',"
+            + " 'properties.bootstrap.servers' = '"
+            + "127.0.0.1:"
+            + KAFKA_PORT
+            + "',"
+            + " 'properties.group.id' = 'kafka-to-partitioned-hive-e2e',"
+            + " 'properties.broker.address.family' = 'v4',"
+            + " 'scan.startup.mode' = 'earliest-offset',"
+            + " 'scan.bounded.mode' = 'latest-offset',"
+            + " 'format' = 'json'"
+            + ")");
+  }
+
+  private void createHiveCatalogAndSink(StreamTableEnvironment tEnv, Path 
sinkPath) {
+    HiveCatalog hiveCatalog = HiveTestUtils.createHiveCatalog("hive_catalog", 
"2.3.9");
+    tEnv.registerCatalog("hive_catalog", hiveCatalog);
+
+    tEnv.useCatalog("hive_catalog");
+    tEnv.getConfig().setSqlDialect(SqlDialect.HIVE);
+    tEnv.executeSql(
+        "CREATE TABLE hive_sink (id INT, name STRING) "
+            + "ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' "
+            + "STORED AS TEXTFILE LOCATION '"
+            + sinkPath.toUri()
+            + "'");
+    tEnv.getConfig().setSqlDialect(SqlDialect.DEFAULT);
+    tEnv.useCatalog("default_catalog");
+  }
+
+  private void createHiveCatalogAndPartitionedSink(
+      StreamTableEnvironment tEnv, Path sinkPath, String 
partitionCommitTrigger) {
+    HiveCatalog hiveCatalog = HiveTestUtils.createHiveCatalog("hive_catalog", 
"2.3.9");
+    tEnv.registerCatalog("hive_catalog", hiveCatalog);
+
+    tEnv.useCatalog("hive_catalog");
+    tEnv.getConfig().setSqlDialect(SqlDialect.HIVE);
+    tEnv.executeSql(
+        "CREATE TABLE partitioned_hive_sink (id INT, name STRING) "
+            + "PARTITIONED BY (dt STRING, hm STRING) "
+            + "ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' "
+            + "STORED AS TEXTFILE LOCATION '"
+            + sinkPath.toUri()
+            + "' "
+            + "TBLPROPERTIES ("
+            + " 'sink.partition-commit.trigger' = '"
+            + partitionCommitTrigger
+            + "',"
+            + " 'sink.partition-commit.delay' = '0 s',"
+            + " 'sink.partition-commit.policy.kind' = 
'metastore,success-file',"
+            + " 'partition.time-extractor.timestamp-pattern' = '$dt $hm:00'"
+            + ")");
+    tEnv.getConfig().setSqlDialect(SqlDialect.DEFAULT);
+    tEnv.useCatalog("default_catalog");
+  }
+
+  private static TableResult executeSqlWithoutVeloxSourceSinkFactories(
+      StreamTableEnvironment tEnv, String sql) {
+    ClassLoader originalClassLoader = 
Thread.currentThread().getContextClassLoader();
+    Thread.currentThread()
+        .setContextClassLoader(new 
VeloxSourceSinkFactoryFilteringClassLoader(originalClassLoader));
+    try {
+      return tEnv.executeSql(sql);
+    } finally {
+      Thread.currentThread().setContextClassLoader(originalClassLoader);
+    }
+  }
+
+  private static class VeloxSourceSinkFactoryFilteringClassLoader extends 
ClassLoader {
+    private VeloxSourceSinkFactoryFilteringClassLoader(ClassLoader parent) {
+      super(parent);
+    }
+
+    @Override
+    public URL getResource(String name) {
+      if (VELOX_SOURCE_SINK_FACTORY_SERVICE.equals(name)) {
+        return null;
+      }
+      return super.getResource(name);
+    }
+
+    @Override
+    public Enumeration<URL> getResources(String name) throws IOException {
+      if (VELOX_SOURCE_SINK_FACTORY_SERVICE.equals(name)) {
+        return Collections.emptyEnumeration();
+      }
+      return super.getResources(name);
+    }
+  }
+
+  private static List<String> readHiveTextRows(Path sinkPath) throws Exception 
{
+    try (java.util.stream.Stream<Path> paths = Files.walk(sinkPath)) {
+      return paths
+          .filter(Files::isRegularFile)
+          .filter(path -> !path.getFileName().toString().startsWith("."))
+          .filter(path -> !path.getFileName().toString().startsWith("_"))
+          .flatMap(
+              path -> {
+                try {
+                  return Files.lines(path, StandardCharsets.UTF_8);
+                } catch (Exception e) {
+                  throw new RuntimeException(e);
+                }
+              })
+          .collect(java.util.stream.Collectors.toList());
+    }
+  }
+
+  private static void assertCommittedPartitions(StreamTableEnvironment tEnv) 
throws Exception {
+    tEnv.useCatalog("hive_catalog");
+    List<String> partitions = new ArrayList<>();
+    try (CloseableIterator<Row> rows =
+        tEnv.executeSql("SHOW PARTITIONS partitioned_hive_sink").collect()) {
+      while (rows.hasNext()) {
+        partitions.add(String.valueOf(rows.next().getField(0)));
+      }
+    } finally {
+      tEnv.useCatalog("default_catalog");
+    }
+    assertThat(partitions)
+        .containsExactlyInAnyOrder("dt=2026-06-29/hm=10:00", 
"dt=2026-06-29/hm=10:01");
+  }
+
+  private static void assertCommittedPartition(Path sinkPath, String dt, 
String hm) {
+    Path partitionPath = sinkPath.resolve("dt=" + dt).resolve("hm=" + 
hm.replace(":", "%3A"));
+    assertThat(partitionPath).isDirectory();
+    assertThat(partitionPath.resolve("_SUCCESS")).exists();
+  }
+
+  private static void assertHiveSinkOffloadedToNative(StreamTableEnvironment 
tEnv, String insertSql)
+      throws Exception {
+    assertThat(
+            translateInsert(tEnv, insertSql).stream()
+                .anyMatch(KafkaToHiveE2ETest::containsNativeHiveSink))
+        .as("Hive HDFS sink should use GlutenStreamingFileWriterOperator")
+        .isTrue();
+  }
+
+  @SuppressWarnings("unchecked")
+  private static List<Transformation<?>> translateInsert(
+      StreamTableEnvironment tEnv, String insertSql) throws Exception {
+    TableEnvironmentImpl tableEnvironment = (TableEnvironmentImpl) tEnv;
+    List<Operation> operations = tableEnvironment.getParser().parse(insertSql);
+    assertThat(operations).hasSize(1);
+    assertThat(operations.get(0)).isInstanceOf(ModifyOperation.class);
+
+    Method translate = 
TableEnvironmentImpl.class.getDeclaredMethod("translate", List.class);
+    translate.setAccessible(true);
+    return (List<Transformation<?>>)
+        translate.invoke(tableEnvironment, List.of((ModifyOperation) 
operations.get(0)));
+  }
+
+  private static boolean containsNativeHiveSink(Transformation<?> 
transformation) {
+    if (transformation instanceof OneInputTransformation) {
+      OneInputTransformation<?, ?> oneInputTransformation =
+          (OneInputTransformation<?, ?>) transformation;
+      if (oneInputTransformation.getOperatorFactory() instanceof 
GlutenOneInputOperatorFactory) {
+        GlutenOneInputOperatorFactory<?, ?> operatorFactory =
+            (GlutenOneInputOperatorFactory<?, ?>) 
oneInputTransformation.getOperatorFactory();
+        if (operatorFactory.getOperator() instanceof 
GlutenStreamingFileWriterOperator) {
+          return true;
+        }
+      }
+    }
+    return 
transformation.getInputs().stream().anyMatch(KafkaToHiveE2ETest::containsNativeHiveSink);
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to