This is an automated email from the ASF dual-hosted git repository.
JNSimba pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-spark-connector.git
The following commit(s) were added to refs/heads/master by this push:
new ff27c83 [Feature] Support Spark 4.1 (#365)
ff27c83 is described below
commit ff27c8399305148dd665459a58ad0a59aa7fcc6b
Author: YangJie <[email protected]>
AuthorDate: Fri Jul 17 17:10:47 2026 +0800
[Feature] Support Spark 4.1 (#365)
Adds Apache Spark 4.1 support (JDK 17, Scala 2.13.17) with full unit +
integration + E2E coverage, plus the foundational changes that let the shared
base module cross-build Scala 2.12 and 2.13.
Per the maintainer discussion, this targets Spark 4.1 only (4.0 is skipped).
New modules
spark-doris-connector-spark-4-base — shared Spark 4 DataSource V2 base
(fork of spark-3-base)
spark-doris-connector-spark-4.1 — Spark 4.1.0 / Scala 2.13.17 adapter
---
.github/workflows/build-extension.yml | 22 ++++
.github/workflows/run-e2ecase.yml | 21 ++++
.github/workflows/run-itcase.yml | 21 ++++
spark-doris-connector/build.sh | 36 +++++-
spark-doris-connector/pom.xml | 66 ++++++++++-
.../spark-doris-connector-base/pom.xml | 20 ++++
.../spark/client/write/StreamLoadProcessor.java | 13 ++-
.../org/apache/doris/spark/config/DorisConfig.java | 3 +-
.../org/apache/doris/spark/sql/DorisRow.scala | 2 -
.../doris/spark/sql/sources/DorisRelation.scala | 2 +-
.../testcase/TestStreamLoadForArrowType.scala | 8 +-
.../scala/org/apache/doris/spark/util/Retry.scala | 33 +++++-
.../apache/spark/sql/util/DorisArrowUtils.scala | 2 +-
.../spark-doris-connector-it/pom.xml | 31 +++++-
.../spark/sql/DorisWriterFailoverITCase.scala | 5 +-
.../spark-doris-connector-spark-4-base/pom.xml | 51 +++++++++
.../doris/spark/catalog/DorisTableBase.scala | 82 ++++++++++++++
.../spark/catalog/DorisTableCatalogBase.scala | 108 ++++++++++++++++++
.../spark/catalog/DorisTableProviderBase.scala | 59 ++++++++++
.../doris/spark/read/AbstractDorisScan.scala | 61 ++++++++++
.../doris/spark/read/DorisPartitionReader.scala | 75 +++++++++++++
.../spark/read/DorisPartitionReaderFactory.scala | 31 ++++++
.../org/apache/doris/spark/read/DorisScan.scala | 33 ++++++
.../doris/spark/read/DorisScanBuilderBase.scala | 52 +++++++++
.../org/apache/doris/spark/read/ScanMode.java | 22 ++++
.../spark/sql/sources/DorisSourceProvider.scala | 29 +++++
.../apache/doris/spark/write/DorisDataWriter.scala | 123 +++++++++++++++++++++
.../doris/spark/write/DorisDataWriterFactory.scala | 37 +++++++
.../org/apache/doris/spark/write/DorisWrite.scala | 91 +++++++++++++++
.../doris/spark/write/DorisWriteBuilder.scala | 47 ++++++++
.../spark-doris-connector-spark-4.1/pom.xml | 58 ++++++++++
...org.apache.spark.sql.sources.DataSourceRegister | 18 +++
.../apache/doris/spark/catalog/DorisTable.scala | 34 ++++++
.../doris/spark/catalog/DorisTableCatalog.scala | 28 +++++
.../apache/doris/spark/read/DorisScanBuilder.scala | 53 +++++++++
.../org/apache/doris/spark/read/DorisScanV2.scala | 34 ++++++
.../read/expression/V2ExpressionBuilder.scala | 120 ++++++++++++++++++++
.../doris/spark/sql/sources/DorisDataSource.scala | 30 +++++
.../read/expression/V2ExpressionBuilderTest.scala | 65 +++++++++++
.../org/apache/spark/sql/ExpressionUtil.scala | 30 +++++
tools/releasing/deploy_staging_jars.sh | 10 +-
...aging_jars.sh => deploy_staging_jars_spark4.sh} | 55 +++++----
42 files changed, 1670 insertions(+), 51 deletions(-)
diff --git a/.github/workflows/build-extension.yml
b/.github/workflows/build-extension.yml
index 008bc35..1fe1c73 100644
--- a/.github/workflows/build-extension.yml
+++ b/.github/workflows/build-extension.yml
@@ -73,3 +73,25 @@ jobs:
- name: Build spark load
run: |
cd spark-load && mvn clean package ${MVN_OPT} -Pspark3,scala_2.12
+
+ build-extension-spark4:
+ name: "Build Extensions (Spark 4.x)"
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ # Spark 4.x requires JDK 17 and Scala 2.13, so it builds in a separate job.
+ - name: Setup java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: '17'
+ cache: 'maven'
+
+ - name: Build spark connector 4.1
+ run: |
+ cd spark-doris-connector && mvn clean install ${MVN_OPT} -Pspark-4.1
-pl spark-doris-connector-spark-4.1 -am
diff --git a/.github/workflows/run-e2ecase.yml
b/.github/workflows/run-e2ecase.yml
index 1991210..6ce6545 100644
--- a/.github/workflows/run-e2ecase.yml
+++ b/.github/workflows/run-e2ecase.yml
@@ -61,3 +61,24 @@ jobs:
- name: Run E2ECases for spark 3.5
run: |
cd spark-doris-connector && mvn clean test -Pspark-3-it,spark-3.5 -pl
spark-doris-connector-it -am -DfailIfNoTests=false -Dtest="*E2ECase"
-Dimage="apache/doris:doris-all-in-one-2.1.0"
+
+ run-e2ecase-spark4:
+ name: "Run E2ECases (Spark 4.x)"
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@master
+
+ # Spark 4.x requires JDK 17.
+ - name: Setup java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - name: Run E2ECases for spark 4.1
+ run: |
+ cd spark-doris-connector && mvn clean test -Pspark-4-it,spark-4.1 -pl
spark-doris-connector-it -am -DfailIfNoTests=false -Dtest="*E2ECase"
-Dimage="apache/doris:doris-all-in-one-2.1.0"
diff --git a/.github/workflows/run-itcase.yml b/.github/workflows/run-itcase.yml
index bdc1225..1d4b8d7 100644
--- a/.github/workflows/run-itcase.yml
+++ b/.github/workflows/run-itcase.yml
@@ -62,3 +62,24 @@ jobs:
run: |
cd spark-doris-connector && mvn clean test -Pspark-3-it,spark-3.5 -pl
spark-doris-connector-it -am -DfailIfNoTests=false -Dtest="*ITCase"
-Dimage="apache/doris:doris-all-in-one-2.1.0"
+ run-itcase-spark4:
+ name: "Run ITCases (Spark 4.x)"
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@master
+
+ # Spark 4.x requires JDK 17.
+ - name: Setup java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - name: Run ITCases for spark 4.1
+ run: |
+ cd spark-doris-connector && mvn clean test -Pspark-4-it,spark-4.1 -pl
spark-doris-connector-it -am -DfailIfNoTests=false -Dtest="*ITCase"
-Dimage="apache/doris:doris-all-in-one-2.1.0"
+
diff --git a/spark-doris-connector/build.sh b/spark-doris-connector/build.sh
index c1b1ea0..7cb56aa 100755
--- a/spark-doris-connector/build.sh
+++ b/spark-doris-connector/build.sh
@@ -122,8 +122,8 @@ if [[ -f ${DORIS_HOME}/custom_env.sh ]]; then
fi
selectScala() {
- echo 'Spark-Doris-Connector supports Scala 2.11 and 2.12. Which version do
you need ?'
- select scala in "2.11" "2.12"
+ echo 'Spark-Doris-Connector supports Scala 2.11, 2.12 and 2.13. Which
version do you need ?'
+ select scala in "2.11" "2.12" "2.13"
do
case $scala in
"2.11")
@@ -132,6 +132,9 @@ selectScala() {
"2.12")
return 2
;;
+ "2.13")
+ return 3
+ ;;
*)
echo "invalid selected, exit.."
exit 1
@@ -142,7 +145,7 @@ selectScala() {
selectSpark() {
echo 'Spark-Doris-Connector supports multiple versions of spark. Which
version do you need ?'
- select spark in "2.4" "3.1" "3.2" "3.3" "3.4" "3.5" "other"
+ select spark in "2.4" "3.1" "3.2" "3.3" "3.4" "3.5" "4.1" "other"
do
case $spark in
"2.4")
@@ -163,9 +166,12 @@ selectSpark() {
"3.5")
return 6
;;
- "other")
+ "4.1")
return 7
;;
+ "other")
+ return 8
+ ;;
esac
done
}
@@ -177,6 +183,8 @@ if [ ${ScalaVer} -eq 1 ]; then
SCALA_VERSION="2.11.12"
elif [ ${ScalaVer} -eq 2 ]; then
SCALA_VERSION="2.12.18"
+elif [ ${ScalaVer} -eq 3 ]; then
+ SCALA_VERSION="2.13.17"
fi
@@ -196,6 +204,8 @@ elif [ ${SparkVer} -eq 5 ]; then
elif [ ${SparkVer} -eq 6 ]; then
SPARK_VERSION="3.5.3"
elif [ ${SparkVer} -eq 7 ]; then
+ SPARK_VERSION="4.1.0"
+elif [ ${SparkVer} -eq 8 ]; then
# shellcheck disable=SC2162
read -p 'Which spark version do you need? please input
:' ver
@@ -207,6 +217,22 @@ if [[ $SPARK_VERSION =~ ^3.* && $SCALA_VERSION ==
"2.11.12" ]]; then
exit 1
fi
+if [[ $SPARK_VERSION =~ ^4.* && $SCALA_VERSION != "2.13.17" ]]; then
+ echo_r "Spark 4.x requires scala 2.13, will exit."
+ exit 1
+fi
+
+if [[ ! $SPARK_VERSION =~ ^4.* && $SCALA_VERSION == "2.13.17" ]]; then
+ echo_r "Scala 2.13 is only supported with Spark 4.x, will exit."
+ exit 1
+fi
+
+# Spark 4.x requires JDK 17. JAVA_VER is the class-file major version from
env.sh (52=JDK8, 61=JDK17).
+if [[ $SPARK_VERSION =~ ^4.* && ${JAVA_VER:-0} -lt 61 ]]; then
+ echo_r "Spark 4.x requires JDK 17 or later, will exit."
+ exit 1
+fi
+
# extract major version:
# eg: 3.1.2 -> 3.1
SCALA_MAJOR_VERSION=0
@@ -218,7 +244,7 @@ echo_g " scala version: ${SCALA_VERSION}, major version:
${SCALA_MAJOR_VERSION}"
echo_g " spark version: ${SPARK_VERSION}, major version:
${SPARK_MAJOR_VERSION}"
echo_g " build starting..."
-if [[ $SPARK_VERSION =~ ^3.* ]]; then
+if [[ $SPARK_VERSION =~ ^3.* || $SPARK_VERSION =~ ^4.* ]]; then
profile_name="spark-${SPARK_MAJOR_VERSION}"
module_suffix=${SPARK_MAJOR_VERSION}
else
diff --git a/spark-doris-connector/pom.xml b/spark-doris-connector/pom.xml
index 70edde1..873c3ef 100644
--- a/spark-doris-connector/pom.xml
+++ b/spark-doris-connector/pom.xml
@@ -49,6 +49,8 @@
<module>spark-doris-connector-spark-3.3</module>
<module>spark-doris-connector-spark-3.4</module>
<module>spark-doris-connector-spark-3.5</module>
+ <module>spark-doris-connector-spark-4-base</module>
+ <module>spark-doris-connector-spark-4.1</module>
</modules>
<scm>
@@ -86,6 +88,9 @@
<scala.major.version>2.11</scala.major.version>
<libthrift.version>0.16.0</libthrift.version>
<arrow.version>15.0.2</arrow.version>
+ <adbc.version>0.14.0</adbc.version>
+ <grpc.version>1.60.0</grpc.version>
+ <protobuf.version>3.22.3</protobuf.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.scm.id>github</project.scm.id>
<netty.version>4.1.110.Final</netty.version>
@@ -139,6 +144,16 @@
<artifactId>spark-doris-connector-spark-3.5</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>spark-doris-connector-spark-4-base</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>spark-doris-connector-spark-4.1</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.doris</groupId>
<artifactId>thrift-service</artifactId>
@@ -233,7 +248,7 @@
<dependency>
<groupId>org.apache.arrow.adbc</groupId>
<artifactId>adbc-driver-flight-sql</artifactId>
- <version>0.14.0</version>
+ <version>${adbc.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.arrow</groupId>
@@ -270,10 +285,22 @@
</exclusions>
</dependency>
+ <!-- Keep the whole gRPC stack on one version. grpc-netty pulls
grpc-api/core/util at
+ ${grpc.version}, but adbc -> arrow-flight pulls
grpc-stub/grpc-protobuf transitively
+ (e.g. flight 18 -> grpc 1.65 while grpc-netty is 1.69 under
Spark 4.1). Importing the
+ BOM aligns every io.grpc artifact, avoiding a mixed-version
gRPC frozen into the
+ shaded jar. The explicit grpc-netty entry below still applies
its netty exclusions. -->
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-bom</artifactId>
+ <version>${grpc.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
- <version>1.60.0</version>
+ <version>${grpc.version}</version>
<exclusions>
<exclusion>
<artifactId>netty-codec-http2</artifactId>
@@ -313,7 +340,7 @@
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
- <version>3.22.3</version>
+ <version>${protobuf.version}</version>
</dependency>
<dependency>
@@ -403,6 +430,15 @@
<version>${fasterxml.jackson.version}</version>
</dependency>
+ <!-- Pin jackson-datatype-jsr310 to the connector's jackson
version. Spark/Arrow pull a
+ newer jsr310 transitively (e.g. 2.20.0 on Spark 4.1), which
would otherwise be
+ shaded next to the connector's databind and break on
java.time (de)serialization. -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <version>${fasterxml.jackson.version}</version>
+ </dependency>
+
<!--
https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
@@ -509,7 +545,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
- <version>3.4.1</version>
+ <!-- 3.6.0 bundles an ASM that can process Java 21
multi-release classes, e.g.
+ bcprov-jdk18on (pulled transitively under Spark 4.1's
grpc 1.69 / netty 4.2). -->
+ <version>3.6.0</version>
<configuration>
<filters>
<filter>
@@ -879,6 +917,26 @@
<scala.major.version>2.12</scala.major.version>
</properties>
</profile>
+ <profile>
+ <id>spark-4.1</id>
+ <properties>
+ <spark.version>4.1.0</spark.version>
+ <spark.major.version>4.1</spark.major.version>
+ <scala.version>2.13.17</scala.version>
+ <scala.major.version>2.13</scala.major.version>
+ <!-- Match Spark 4.1's Arrow and Netty so the connector and
Spark agree on one copy
+ of each on the (unshaded) integration-test classpath;
bump adbc/flight/grpc in
+ lockstep (grpc 1.69 supports Netty 4.2). -->
+ <arrow.version>18.3.0</arrow.version>
+ <adbc.version>0.18.0</adbc.version>
+ <grpc.version>1.69.0</grpc.version>
+ <protobuf.version>3.25.1</protobuf.version>
+ <netty.version>4.2.7.Final</netty.version>
+ <!-- Spark 4 on JDK 17 needs these module opens (Arrow
off-heap, Spark date/time,
+ Catalyst codegen, etc.) — mirrors Spark's own
JavaModuleOptions. -->
+ <argLine>-Xmx512m --add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
--add-opens=java.base/java.io=ALL-UNNAMED
--add-opens=java.base/java.net=ALL-UNNAMED
--add-opens=java.base/java.nio=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED
--add-opens=java.base/ [...]
+ </properties>
+ </profile>
</profiles>
</project>
diff --git a/spark-doris-connector/spark-doris-connector-base/pom.xml
b/spark-doris-connector/spark-doris-connector-base/pom.xml
index 67ab8c3..e3480ca 100644
--- a/spark-doris-connector/spark-doris-connector-base/pom.xml
+++ b/spark-doris-connector/spark-doris-connector-base/pom.xml
@@ -54,6 +54,17 @@
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
+ <exclusions>
+ <!-- netty-all 4.2.x (Spark 4.1) is an aggregator that drags
BouncyCastle in at
+ compile scope; it is not relocated by the shade plugin,
so it would ship an
+ unrelocated org.bouncycastle crypto provider that can
clash with the one Spark
+ puts on the classpath. The connector does not use
BouncyCastle, so exclude it.
+ (No-op on netty-all 4.1.x, which does not pull it.) -->
+ <exclusion>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk18on</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<dependency>
@@ -103,6 +114,15 @@
<scope>runtime</scope>
</dependency>
+ <!-- Pin arrow-memory-core to the connector's Arrow version so the
test classpath stays
+ internally consistent. Spark 4.x ships a newer Arrow (provided),
which otherwise
+ leaks a mismatched arrow-memory-core and breaks the
allocation-manager lookup. -->
+ <dependency>
+ <groupId>org.apache.arrow</groupId>
+ <artifactId>arrow-memory-core</artifactId>
+ <version>${arrow.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.apache.arrow.adbc</groupId>
<artifactId>adbc-driver-flight-sql</artifactId>
diff --git
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/StreamLoadProcessor.java
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/StreamLoadProcessor.java
index a67ab28..e109ac8 100644
---
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/StreamLoadProcessor.java
+++
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/StreamLoadProcessor.java
@@ -38,9 +38,19 @@ import org.apache.spark.sql.util.DorisArrowUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
public class StreamLoadProcessor extends
AbstractStreamLoadProcessor<InternalRow> {
+ /**
+ * Monotonic per-JVM sequence appended to every generated label. The label
already carries
+ * {@code currentTimeMillis()}, but consecutive committed batches of the
same task (e.g.
+ * {@code batch.size=1} with a zero batch interval) can fall in the same
millisecond and collide;
+ * a colliding fresh label is rejected as "Label Already Exists". The
sequence guarantees each
+ * label is distinct so a retry (which mints a new label) never clashes
with a prior batch.
+ */
+ private static final AtomicLong LABEL_SEQ = new AtomicLong();
+
private StructType schema;
public StreamLoadProcessor(DorisConfig config) throws Exception {
@@ -110,7 +120,8 @@ public class StreamLoadProcessor extends
AbstractStreamLoadProcessor<InternalRow
long taskAttemptId = taskContext.taskAttemptId();
int partitionId = taskContext.partitionId();
String prefix = config.getValue(DorisOptions.DORIS_SINK_LABEL_PREFIX);
- return String.format("%s-%d-%d-%d-%d", prefix, stageId, taskAttemptId,
partitionId, System.currentTimeMillis());
+ return String.format("%s-%d-%d-%d-%d-%d", prefix, stageId,
taskAttemptId, partitionId,
+ System.currentTimeMillis(), LABEL_SEQ.getAndIncrement());
}
public void setSchema(StructType schema) {
diff --git
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisConfig.java
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisConfig.java
index 79ed053..fd0bbd8 100644
---
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisConfig.java
+++
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisConfig.java
@@ -17,7 +17,6 @@
package org.apache.doris.spark.config;
-import org.apache.commons.collections.MapUtils;
import org.apache.doris.spark.exception.OptionRequiredException;
import java.io.Serializable;
@@ -184,7 +183,7 @@ public class DorisConfig implements Serializable {
public static DorisConfig fromMap(Map<String, String> sparkConfMap,
Map<String, String> options, Boolean ignoreTableCheck) throws
OptionRequiredException {
Map<String, String> map = new HashMap<>(sparkConfMap);
- if (MapUtils.isNotEmpty(options)) {
+ if (options != null && !options.isEmpty()) {
map.putAll(options);
}
return new DorisConfig(map, ignoreTableCheck);
diff --git
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/DorisRow.scala
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/DorisRow.scala
index 818af05..756574a 100644
---
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/DorisRow.scala
+++
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/DorisRow.scala
@@ -54,6 +54,4 @@ private[spark] class DorisRow(rowOrder: Seq[String]) extends
Row {
override def getString(i: Int): String = get(i).toString
override def copy(): Row = this
-
- override def toSeq: Seq[Any] = values
}
diff --git
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/sources/DorisRelation.scala
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/sources/DorisRelation.scala
index 6fd7af5..084b821 100644
---
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/sources/DorisRelation.scala
+++
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/sources/DorisRelation.scala
@@ -48,7 +48,7 @@ private[sql] class DorisRelation(
val arrayNativeType =
cfg.getValue(DorisOptions.DORIS_READ_ARRAY_NATIVE_TYPE)
StructType(dorisSchema.getProperties.asScala.map(field => {
StructField(field.getName,
SchemaConvertors.toCatalystType(field.getType, field.getPrecision,
field.getScale, arrayNativeType))
- }))
+ }).toArray)
}
diff --git
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/testcase/TestStreamLoadForArrowType.scala
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/testcase/TestStreamLoadForArrowType.scala
index f1e1977..c6b8e03 100644
---
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/testcase/TestStreamLoadForArrowType.scala
+++
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/testcase/TestStreamLoadForArrowType.scala
@@ -137,7 +137,7 @@ object TestStreamLoadForArrowType {
inputList.append(row)
}
- val rdd = spark.sparkContext.parallelize(inputList, 1)
+ val rdd = spark.sparkContext.parallelize(inputList.toList, 1)
val df = spark.createDataFrame(rdd, schema).toDF()
df.write
@@ -242,7 +242,7 @@ object TestStreamLoadForArrowType {
inputList.append(row)
}
- val rdd = spark.sparkContext.parallelize(inputList, 1)
+ val rdd = spark.sparkContext.parallelize(inputList.toList, 1)
val df = spark.createDataFrame(rdd, schema).toDF()
df.write
@@ -347,7 +347,7 @@ object TestStreamLoadForArrowType {
inputList.append(row)
}
- val rdd = spark.sparkContext.parallelize(inputList, 1)
+ val rdd = spark.sparkContext.parallelize(inputList.toList, 1)
val df = spark.createDataFrame(rdd, schema).toDF()
df.write
@@ -444,7 +444,7 @@ CREATE TABLE `spark_connector_struct` (
inputList.append(row)
}
- val rdd = spark.sparkContext.parallelize(inputList, 1)
+ val rdd = spark.sparkContext.parallelize(inputList.toList, 1)
val df = spark.createDataFrame(rdd, schema).toDF()
df.write
diff --git
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/util/Retry.scala
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/util/Retry.scala
index 24f8c13..10c5955 100644
---
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/util/Retry.scala
+++
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/util/Retry.scala
@@ -20,7 +20,7 @@ package org.apache.doris.spark.util
import org.slf4j.Logger
import java.time.Duration
-import java.util.concurrent.locks.LockSupport
+import java.util.concurrent.TimeUnit
import scala.annotation.tailrec
import scala.reflect.ClassTag
import scala.util.{Failure, Success, Try}
@@ -38,11 +38,40 @@ object Retry {
case Failure(exception: T) if retryTimes > 0 =>
logger.warn("Execution failed caused by: {}", exception.getMessage)
logger.warn(s"$retryTimes times retry remaining, the next attempt will
be in ${interval.toMillis} ms")
- LockSupport.parkNanos(interval.toNanos)
+ sleepBeforeRetry(interval)
h
exec(retryTimes - 1, interval, logger)(f)(h)
case Failure(exception) => Failure(exception)
}
}
+ /**
+ * Wait for the full interval before the next retry.
+ *
+ * [[java.util.concurrent.locks.LockSupport.parkNanos]] may return before
the requested time on a
+ * spurious wakeup or when another thread unparks this one. The stream-load
processor interrupts
+ * the task thread on an async load failure (to unblock a blocking pipe
write), and an interrupt
+ * also unparks a parked thread, so a single `parkNanos` for the backoff is
repeatedly cut short
+ * and the configured retry interval collapses to a few milliseconds — most
visibly under Spark
+ * 4.x. That both hammers the backend with retries and breaks failover that
relies on the interval.
+ *
+ * Use [[Thread.sleep]] (via [[TimeUnit]]), which is not affected by
`unpark`, and loop until the
+ * full interval has elapsed. A spurious interrupt is absorbed (the
interrupt status is restored
+ * afterwards) so the interval is still honored.
+ */
+ private def sleepBeforeRetry(interval: Duration): Unit = {
+ val deadlineNanos = System.nanoTime() + interval.toNanos
+ var remainingNanos = interval.toNanos
+ var interrupted = false
+ while (remainingNanos > 0L) {
+ try {
+ TimeUnit.NANOSECONDS.sleep(remainingNanos)
+ } catch {
+ case _: InterruptedException => interrupted = true
+ }
+ remainingNanos = deadlineNanos - System.nanoTime()
+ }
+ if (interrupted) Thread.currentThread().interrupt()
+ }
+
}
diff --git
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/spark/sql/util/DorisArrowUtils.scala
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/spark/sql/util/DorisArrowUtils.scala
index 3879321..4f81227 100644
---
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/spark/sql/util/DorisArrowUtils.scala
+++
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/spark/sql/util/DorisArrowUtils.scala
@@ -106,7 +106,7 @@ object DorisArrowUtils {
val dt = fromArrowField(child)
StructField(child.getName, dt, child.isNullable)
}
- StructType(fields)
+ StructType(fields.toArray)
case arrowType => fromArrowType(arrowType)
}
}
diff --git a/spark-doris-connector/spark-doris-connector-it/pom.xml
b/spark-doris-connector/spark-doris-connector-it/pom.xml
index 067a9b2..b61946a 100644
--- a/spark-doris-connector/spark-doris-connector-it/pom.xml
+++ b/spark-doris-connector/spark-doris-connector-it/pom.xml
@@ -82,12 +82,6 @@
<version>8.0.33</version>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- <version>1.7.25</version>
- <scope>test</scope>
- </dependency>
</dependencies>
<profiles>
@@ -99,12 +93,37 @@
<properties>
<spark.doris.connector.artifactId>spark-doris-connector-spark-2</spark.doris.connector.artifactId>
</properties>
+ <dependencies>
+ <!-- Spark 2.x / 3.x use slf4j 1.7 + log4j 1.2 -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>1.7.25</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
</profile>
<profile>
<id>spark-3-it</id>
<properties>
<spark.doris.connector.artifactId>spark-doris-connector-spark-${spark.major.version}</spark.doris.connector.artifactId>
</properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>1.7.25</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ </profile>
+ <profile>
+ <!-- Spark 4.x brings its own slf4j 2.x + log4j 2.x binding; the
slf4j 1.7
+ binding above would clash (NoSuchFieldError: mdc). -->
+ <id>spark-4-it</id>
+ <properties>
+
<spark.doris.connector.artifactId>spark-doris-connector-spark-${spark.major.version}</spark.doris.connector.artifactId>
+ </properties>
</profile>
</profiles>
diff --git
a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
index 9c38eed..4381d4e 100644
---
a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
+++
b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
@@ -61,7 +61,10 @@ class DorisWriterFailoverITCase extends
AbstractContainerTestBase {
@Test
def testFailoverForRetry(): Unit = {
LOG.info("start to test testFailoverForRetry.")
- initializeTable(TABLE_WRITE_TBL_RETRY, DataModel.DUPLICATE)
+ // Use a UNIQUE (primary-key) table: without 2PC a retried batch may
re-load rows that already
+ // landed, and the unique key on `name` makes that re-load idempotent. The
three rows have
+ // distinct names, so dedup removes duplicates without dropping any
legitimate row.
+ initializeTable(TABLE_WRITE_TBL_RETRY, DataModel.UNIQUE)
val session = SparkSession.builder().master("local[1]").getOrCreate()
val df = session.createDataFrame(Seq(
("doris", "1234"),
diff --git a/spark-doris-connector/spark-doris-connector-spark-4-base/pom.xml
b/spark-doris-connector/spark-doris-connector-spark-4-base/pom.xml
new file mode 100644
index 0000000..61f1ee5
--- /dev/null
+++ b/spark-doris-connector/spark-doris-connector-spark-4-base/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>spark-doris-connector</artifactId>
+ <version>${revision}</version>
+ </parent>
+
+ <artifactId>spark-doris-connector-spark-4-base</artifactId>
+
+ <properties>
+ <maven.compiler.source>8</maven.compiler.source>
+ <maven.compiler.target>8</maven.compiler.target>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <maven.deploy.skip>true</maven.deploy.skip>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>spark-doris-connector-base</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.spark</groupId>
+ <artifactId>spark-sql_${scala.major.version}</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableBase.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableBase.scala
new file mode 100644
index 0000000..830aa5e
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableBase.scala
@@ -0,0 +1,82 @@
+// 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.doris.spark.catalog
+
+import org.apache.doris.spark.client.DorisFrontendClient
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.doris.spark.rest.models.Schema
+import org.apache.doris.spark.util.SchemaConvertors
+import org.apache.spark.sql.connector.catalog.TableCapability._
+import org.apache.spark.sql.connector.catalog.{Identifier, SupportsRead,
SupportsWrite, Table, TableCapability}
+import org.apache.spark.sql.connector.read.ScanBuilder
+import org.apache.spark.sql.connector.write.{LogicalWriteInfo, WriteBuilder}
+import org.apache.spark.sql.types.{StructField, StructType}
+import org.apache.spark.sql.util.CaseInsensitiveStringMap
+
+import java.util
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+
+abstract class DorisTableBase(identifier: Identifier, config: DorisConfig,
schema: Option[StructType]) extends Table with SupportsRead with SupportsWrite {
+
+ private lazy val frontend:DorisFrontendClient = new
DorisFrontendClient(config)
+
+ override def name(): String = identifier.toString
+
+ override def schema(): StructType = schema.getOrElse({
+ val dorisSchema = frontend.getTableSchema(identifier.namespace()(0),
identifier.name())
+ dorisSchemaToStructType(dorisSchema)
+ })
+
+ override def capabilities(): util.Set[TableCapability] = {
+ val capabilities = mutable.Set(BATCH_READ,
+ BATCH_WRITE,
+ STREAMING_WRITE,
+ TRUNCATE)
+ val properties = config.getSinkProperties
+ val partialColumnsEnabled =
properties.containsKey(DorisOptions.PARTIAL_COLUMNS) &&
"true".equalsIgnoreCase(properties.get(DorisOptions.PARTIAL_COLUMNS))
+ val schemaLessEnabled =
config.getValue(DorisOptions.DORIS_WRITE_SCHEMA_LESS)
+ if (partialColumnsEnabled || schemaLessEnabled) {
+ capabilities += ACCEPT_ANY_SCHEMA
+ }
+ capabilities.asJava
+ }
+
+ override def newScanBuilder(caseInsensitiveStringMap:
CaseInsensitiveStringMap): ScanBuilder = {
+ config.setProperty(DorisOptions.DORIS_TABLE_IDENTIFIER, name())
+ createScanBuilder(config, schema())
+ }
+
+ override def newWriteBuilder(logicalWriteInfo: LogicalWriteInfo):
WriteBuilder = {
+ config.setProperty(DorisOptions.DORIS_TABLE_IDENTIFIER, name())
+ createWriteBuilder(config, logicalWriteInfo.schema())
+ }
+
+ private def dorisSchemaToStructType(dorisSchema: Schema): StructType = {
+ val arrayNativeType =
config.getValue(DorisOptions.DORIS_READ_ARRAY_NATIVE_TYPE)
+ StructType(dorisSchema.getProperties.asScala.map(field => {
+ StructField(field.getName,
SchemaConvertors.toCatalystType(field.getType, field.getPrecision,
field.getScale, arrayNativeType))
+ }).toArray)
+ }
+
+ protected def createScanBuilder(config: DorisConfig, schema: StructType):
ScanBuilder
+
+ protected def createWriteBuilder(config: DorisConfig, schema: StructType):
WriteBuilder
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableCatalogBase.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableCatalogBase.scala
new file mode 100644
index 0000000..2c9ad0f
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableCatalogBase.scala
@@ -0,0 +1,108 @@
+// 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.doris.spark.catalog
+
+import org.apache.doris.spark.client.DorisFrontendClient
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException
+import org.apache.spark.sql.connector.catalog.{Identifier, NamespaceChange,
SupportsNamespaces, Table, TableCatalog, TableChange}
+import org.apache.spark.sql.connector.expressions.Transform
+import org.apache.spark.sql.types.StructType
+import org.apache.spark.sql.util.CaseInsensitiveStringMap
+
+import java.util
+import scala.collection.JavaConverters._
+
+trait DorisTableCatalogBase extends TableCatalog with SupportsNamespaces {
+
+ protected var catalogName: Option[String] = None
+
+ protected var dorisConfig: DorisConfig = _
+
+ protected var frontend: DorisFrontendClient = _
+
+ override def name(): String = {
+ require(catalogName.nonEmpty, "The Doris table catalog is not initialed")
+ catalogName.get
+ }
+
+ override def initialize(name: String, caseInsensitiveStringMap:
CaseInsensitiveStringMap): Unit = {
+ assert(catalogName.isEmpty, "The Doris table catalog is already initialed")
+ catalogName = Some(name)
+ dorisConfig = DorisConfig.fromMap(caseInsensitiveStringMap, true)
+ frontend = new DorisFrontendClient(dorisConfig)
+ }
+
+ override def listTables(namespace: Array[String]): Array[Identifier] = {
+ frontend.listTables(namespace).asScala.map(i => Identifier.of(i.getLeft,
i.getValue)).toArray
+ }
+
+ override def loadTable(identifier: Identifier): Table = {
+ checkIdentifier(identifier)
+ newTableInstance(identifier,
DorisConfig.fromMap((dorisConfig.toMap.asScala +
+ (DorisOptions.DORIS_TABLE_IDENTIFIER.getName ->
getFullTableName(identifier))).asJava, false), None)
+ }
+
+ override def createTable(identifier: Identifier, structType: StructType,
transforms: Array[Transform], map: util.Map[String, String]): Table = throw new
UnsupportedOperationException()
+
+ override def alterTable(identifier: Identifier, tableChanges: TableChange*):
Table = throw new UnsupportedOperationException()
+
+ override def dropTable(identifier: Identifier): Boolean = throw new
UnsupportedOperationException()
+
+ override def renameTable(identifier: Identifier, identifier1: Identifier):
Unit = throw new UnsupportedOperationException()
+
+ override def listNamespaces(): Array[Array[String]] = {
+ frontend.listDatabases().map(Array(_))
+ }
+
+ override def listNamespaces(namespace: Array[String]): Array[Array[String]]
= {
+ namespace match {
+ case Array() => listNamespaces()
+ case Array(_) if frontend.databaseExists(namespace(0)) => Array()
+ case _ => throw new NoSuchNamespaceException(namespace)
+ }
+ }
+
+ override def loadNamespaceMetadata(namespace: Array[String]):
util.Map[String, String] = {
+ namespace match {
+ case Array(database) =>
+ if (!frontend.databaseExists(database)) {
+ throw new NoSuchNamespaceException(namespace)
+ }
+ new util.HashMap[String, String]()
+ case _ => throw new NoSuchNamespaceException(namespace)
+ }
+ }
+
+ override def createNamespace(namespace: Array[String], metadata:
util.Map[String, String]): Unit = throw new UnsupportedOperationException()
+
+ override def alterNamespace(namespace: Array[String], changes:
NamespaceChange*): Unit = throw new UnsupportedOperationException()
+
+ private def checkIdentifier(identifier: Identifier): Unit = {
+ if (identifier.namespace().length > 1) {
+ throw new NoSuchNamespaceException(identifier.namespace())
+ }
+ }
+
+ private def getFullTableName(identifier: Identifier): String = {
+ (identifier.namespace() :+ identifier.name()).map(item =>
s"""`$item`""").mkString(".")
+ }
+
+ def newTableInstance(identifier: Identifier, config: DorisConfig, schema:
Option[StructType]): Table
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableProviderBase.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableProviderBase.scala
new file mode 100644
index 0000000..a03435d
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/catalog/DorisTableProviderBase.scala
@@ -0,0 +1,59 @@
+// 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.doris.spark.catalog
+
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.spark.sql.connector.catalog.{Identifier, Table,
TableProvider}
+import org.apache.spark.sql.connector.expressions.Transform
+import org.apache.spark.sql.types.StructType
+import org.apache.spark.sql.util.CaseInsensitiveStringMap
+
+import java.util
+
+abstract class DorisTableProviderBase extends TableProvider {
+
+ protected var t: Table = _
+
+ override def inferSchema(options: CaseInsensitiveStringMap): StructType = {
+ if (t == null) t = getTable(options)
+ t.schema()
+ }
+
+ override def getTable(schema: StructType, partitioning: Array[Transform],
properties: util.Map[String, String]): Table = {
+ if (t != null) t
+ else {
+ val dorisConfig = DorisConfig.fromMap(properties, false)
+ val tableIdentifier =
dorisConfig.getValue(DorisOptions.DORIS_TABLE_IDENTIFIER)
+ val tableIdentifierArr = tableIdentifier.split("\\.")
+ newTableInstance(Identifier.of(Array[String](tableIdentifierArr(0)),
tableIdentifierArr(1)), dorisConfig, Some(schema))
+ }
+ }
+
+ private def getTable(options: CaseInsensitiveStringMap): Table = {
+ if (t != null) t
+ else {
+ val dorisConfig = DorisConfig.fromMap(options, false)
+ val tableIdentifier =
dorisConfig.getValue(DorisOptions.DORIS_TABLE_IDENTIFIER)
+ val tableIdentifierArr = tableIdentifier.split("\\.")
+ newTableInstance(Identifier.of(Array[String](tableIdentifierArr(0)),
tableIdentifierArr(1)), dorisConfig, None)
+ }
+ }
+
+ def newTableInstance(identifier: Identifier, config: DorisConfig, schema:
Option[StructType]): Table;
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
new file mode 100644
index 0000000..4afcc01
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
@@ -0,0 +1,61 @@
+// 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.doris.spark.read
+
+import org.apache.doris.spark.client.entity.{Backend, DorisReaderPartition}
+import org.apache.doris.spark.client.read.ReaderPartitionGenerator
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.connector.read.{Batch, InputPartition,
PartitionReaderFactory, Scan}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.StructType
+
+import scala.language.implicitConversions
+
+abstract class AbstractDorisScan(config: DorisConfig, schema: StructType)
extends Scan with Batch with Logging {
+
+ private val scanMode =
ScanMode.valueOf(config.getValue(DorisOptions.READ_MODE).toUpperCase)
+
+ override def readSchema(): StructType = schema
+
+ override def toBatch: Batch = this
+
+ override def planInputPartitions(): Array[InputPartition] = {
+ ReaderPartitionGenerator.generatePartitions(config, schema.names,
compiledFilters(), getLimit,
+ SQLConf.get.datetimeJava8ApiEnabled)
+ .map(toInputPartition)
+ }
+
+
+ override def createReaderFactory(): PartitionReaderFactory = {
+ new DorisPartitionReaderFactory(readSchema(), scanMode, config)
+ }
+
+ private def toInputPartition(rp: DorisReaderPartition): DorisInputPartition =
+ DorisInputPartition(rp.getDatabase, rp.getTable, rp.getBackend,
rp.getTablets.map(_.toLong), rp.getOpaquedQueryPlan,
+ rp.getReadColumns, rp.getFilters, rp.getLimit,
rp.getDateTimeJava8APIEnabled)
+
+ protected def compiledFilters(): Array[String]
+
+ protected def getLimit: Int = -1
+
+}
+
+case class DorisInputPartition(database: String, table: String, backend:
Backend, tablets: Array[Long],
+ opaquedQueryPlan: String, readCols:
Array[String], predicates: Array[String],
+ limit: Int = -1, datetimeJava8ApiEnabled:
Boolean) extends InputPartition
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisPartitionReader.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisPartitionReader.scala
new file mode 100644
index 0000000..9cf2fd6
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisPartitionReader.scala
@@ -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.doris.spark.read
+
+import org.apache.doris.spark.client.entity.DorisReaderPartition
+import org.apache.doris.spark.client.read.{DorisFlightSqlReader, DorisReader,
DorisThriftReader}
+import org.apache.doris.spark.config.DorisConfig
+import org.apache.doris.spark.util.RowConvertors
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.GenericInternalRow
+import org.apache.spark.sql.connector.read.{InputPartition, PartitionReader}
+import org.apache.spark.sql.types.StructType
+
+import scala.language.implicitConversions
+
+class DorisPartitionReader(inputPartition: InputPartition, schema: StructType,
mode: ScanMode, config: DorisConfig)
+ extends PartitionReader[InternalRow] {
+
+ private implicit def toReaderPartition(inputPart: DorisInputPartition):
DorisReaderPartition = {
+ val tablets = inputPart.tablets.map(java.lang.Long.valueOf)
+ new DorisReaderPartition(inputPart.database, inputPart.table,
inputPart.backend, tablets,
+ inputPart.opaquedQueryPlan, inputPart.readCols, inputPart.predicates,
inputPart.limit, config, inputPart.datetimeJava8ApiEnabled)
+ }
+
+ private lazy val reader: DorisReader = {
+ mode match {
+ case ScanMode.THRIFT => new
DorisThriftReader(inputPartition.asInstanceOf[DorisInputPartition])
+ case ScanMode.ARROW => new
DorisFlightSqlReader(inputPartition.asInstanceOf[DorisInputPartition])
+ case _ => throw new UnsupportedOperationException()
+ }
+ }
+
+ private val datetimeJava8ApiEnabled: Boolean =
inputPartition.asInstanceOf[DorisInputPartition].datetimeJava8ApiEnabled
+
+ override def next(): Boolean = reader.hasNext
+
+ override def get(): InternalRow = {
+ val values = reader.next().asInstanceOf[Array[Any]]
+ val row = new GenericInternalRow(schema.length)
+ if (values.nonEmpty) {
+ values.zipWithIndex.foreach {
+ case (value, index) =>
+ if (value == null) row.setNullAt(index)
+ else {
+ val dataType = schema.fields(index).dataType
+ val catalystValue = RowConvertors.convertValue(value, dataType,
datetimeJava8ApiEnabled)
+ row.update(index, catalystValue)
+ }
+ }
+ }
+ row
+ }
+
+ override def close(): Unit = {
+ if (reader != null) {
+ reader.close()
+ }
+ }
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisPartitionReaderFactory.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisPartitionReaderFactory.scala
new file mode 100644
index 0000000..e91a9c6
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisPartitionReaderFactory.scala
@@ -0,0 +1,31 @@
+// 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.doris.spark.read
+
+import org.apache.doris.spark.config.DorisConfig
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.connector.read.{InputPartition, PartitionReader,
PartitionReaderFactory}
+import org.apache.spark.sql.types.StructType
+
+class DorisPartitionReaderFactory(schema: StructType, mode: ScanMode, config:
DorisConfig) extends PartitionReaderFactory {
+
+ override def createReader(inputPartition: InputPartition):
PartitionReader[InternalRow] = {
+ new DorisPartitionReader(inputPartition, schema, mode, config)
+ }
+
+}
\ No newline at end of file
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisScan.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisScan.scala
new file mode 100644
index 0000000..d52a82a
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisScan.scala
@@ -0,0 +1,33 @@
+// 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.doris.spark.read
+
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.doris.spark.util.DorisDialects
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.sources.Filter
+import org.apache.spark.sql.types.StructType
+
+import scala.language.implicitConversions
+
+class DorisScan(config: DorisConfig, schema: StructType, filters:
Array[Filter]) extends AbstractDorisScan(config, schema) with Logging {
+ override protected def compiledFilters(): Array[String] = {
+ val inValueLengthLimit =
config.getValue(DorisOptions.DORIS_FILTER_QUERY_IN_MAX_COUNT)
+ filters.map(DorisDialects.compileFilter(_,
inValueLengthLimit)).filter(_.isDefined).map(_.get)
+ }
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisScanBuilderBase.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisScanBuilderBase.scala
new file mode 100644
index 0000000..94367b2
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/DorisScanBuilderBase.scala
@@ -0,0 +1,52 @@
+// 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.doris.spark.read
+
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.spark.sql.connector.read.{ScanBuilder,
SupportsPushDownRequiredColumns}
+import org.apache.spark.sql.types.StructType
+
+protected[spark] abstract class DorisScanBuilderBase(config: DorisConfig,
schema: StructType) extends ScanBuilder
+ with SupportsPushDownRequiredColumns {
+
+ protected var readSchema: StructType = {
+ if (config.contains(DorisOptions.DORIS_READ_FIELDS)) {
+ val dorisReadFields =
config.getValue(DorisOptions.DORIS_READ_FIELDS).split(",").map(_.trim.replaceAll("`",
""))
+ doPruneColumns(schema, dorisReadFields)
+ } else {
+ schema
+ }
+ }
+
+ override def pruneColumns(requiredSchema: StructType): Unit = {
+ readSchema = doPruneColumns(readSchema, requiredSchema.fieldNames)
+ }
+
+ private def doPruneColumns(originSchema: StructType, requiredCols:
Array[String]): StructType = {
+ if (requiredCols.nonEmpty) {
+ val fields = originSchema.fields.filter(
+ field => requiredCols.contains(field.name)
+ )
+ if (fields.isEmpty) {
+ throw new IllegalArgumentException("No required columns found")
+ }
+ StructType(fields)
+ } else originSchema
+ }
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/ScanMode.java
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/ScanMode.java
new file mode 100644
index 0000000..430b71b
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/ScanMode.java
@@ -0,0 +1,22 @@
+// 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.doris.spark.read;
+
+public enum ScanMode {
+ THRIFT,ARROW
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/sql/sources/DorisSourceProvider.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/sql/sources/DorisSourceProvider.scala
new file mode 100644
index 0000000..5fab64c
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/sql/sources/DorisSourceProvider.scala
@@ -0,0 +1,29 @@
+// 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.doris.spark.sql.sources
+
+import org.apache.spark.sql.SQLContext
+import org.apache.spark.sql.sources.{BaseRelation, RelationProvider}
+
+trait DorisSourceProvider extends RelationProvider {
+
+ override def createRelation(sqlContext: SQLContext, parameters: Map[String,
String]): BaseRelation = {
+ new DorisRelation(sqlContext, parameters)
+ }
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisDataWriter.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisDataWriter.scala
new file mode 100644
index 0000000..0020c3d
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisDataWriter.scala
@@ -0,0 +1,123 @@
+// 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.doris.spark.write
+
+import org.apache.doris.spark.client.write.{CopyIntoProcessor, DorisCommitter,
DorisWriter, StreamLoadProcessor}
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.doris.spark.util.Retry
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.connector.write.{DataWriter, WriterCommitMessage}
+import org.apache.spark.sql.types.StructType
+
+import java.time.Duration
+import java.util.concurrent.locks.LockSupport
+import scala.collection.mutable
+import scala.util.{Failure, Success}
+
+class DorisDataWriter(config: DorisConfig, schema: StructType, partitionId:
Int, taskId: Long, epochId: Long = -1) extends DataWriter[InternalRow] with
Logging {
+
+ private val (writer: DorisWriter[InternalRow], committer: DorisCommitter) =
+ config.getValue(DorisOptions.LOAD_MODE) match {
+ case "stream_load" => (new StreamLoadProcessor(config, schema), new
StreamLoadProcessor(config, schema))
+ case "copy_into" => (new CopyIntoProcessor(config, schema), new
CopyIntoProcessor(config, schema))
+ case mode => throw new IllegalArgumentException("Unsupported load mode:
" + mode)
+ }
+
+ private val batchIntervalMs =
config.getValue(DorisOptions.DORIS_SINK_BATCH_INTERVAL_MS)
+
+ private val retries = config.getValue(DorisOptions.DORIS_SINK_MAX_RETRIES)
+
+ private val retryIntervalMs =
config.getValue(DorisOptions.DORIS_SINK_RETRY_INTERVAL_MS)
+
+ private val twoPhaseCommitEnabled =
config.getValue(DorisOptions.DORIS_SINK_ENABLE_2PC)
+
+ private val committedMessages = mutable.Buffer[String]()
+
+ private lazy val recordBuffer = mutable.Buffer[InternalRow]()
+
+ override def write(record: InternalRow): Unit = loadBatchWithRetries(record)
+
+ override def commit(): WriterCommitMessage = {
+ val txnId = Option(writer.stop())
+ if (twoPhaseCommitEnabled) {
+ if (txnId.isDefined) {
+ committedMessages += txnId.get
+ } else {
+ log.warn("No txn {} to commit batch", txnId)
+ }
+ }
+ DorisWriterCommitMessage(partitionId, taskId, epochId,
committedMessages.toArray)
+ }
+
+ override def abort(): Unit = {
+ if (committedMessages.nonEmpty) {
+ committedMessages.foreach(msg => committer.abort(msg))
+ }
+ close()
+ }
+
+ override def close(): Unit = {
+ if (writer != null) {
+ writer.close()
+ }
+ }
+
+ @throws[Exception]
+ private def loadBatchWithRetries(record: InternalRow): Unit = {
+ var isRetrying = false
+ Retry.exec[Unit, Exception](retries,
Duration.ofMillis(retryIntervalMs.toLong), log) {
+ if (isRetrying) {
+ // retrying, reload data from buffer
+ while (writer.getBatchCount < recordBuffer.size){
+ val idx = writer.getBatchCount
+ writer.load(recordBuffer(idx))
+ }
+ isRetrying = false
+ }
+ if (writer.endOfBatch()) {
+ // end of batch, stop batch write
+ val txnId = Option(writer.stop())
+ if (twoPhaseCommitEnabled) {
+ if (txnId.isDefined) {
+ committedMessages += txnId.get
+ } else {
+ throw new Exception("Failed to end batch write")
+ }
+ }
+ // clear buffer if retry is enabled
+ if (retries > 0) {
+ recordBuffer.clear()
+ }
+ writer.resetBatchCount()
+
LockSupport.parkNanos(Duration.ofMillis(batchIntervalMs.toLong).toNanos)
+ }
+ writer.load(record)
+ } {
+ // batch write failed, set retry flag and reset batch count
+ isRetrying = true
+ writer.resetBatchCount()
+ } match {
+ case Success(_) => if (retries > 0) recordBuffer += record
+ case Failure(exception) => throw new Exception(exception)
+ }
+ }
+
+}
+
+case class DorisWriterCommitMessage(partitionId: Int, taskId: Long, epochId:
Long, commitMessages: Array[String]) extends WriterCommitMessage
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisDataWriterFactory.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisDataWriterFactory.scala
new file mode 100644
index 0000000..2afd641
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisDataWriterFactory.scala
@@ -0,0 +1,37 @@
+// 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.doris.spark.write
+
+import org.apache.doris.spark.config.DorisConfig
+import org.apache.spark.sql.catalyst.InternalRow
+import
org.apache.spark.sql.connector.write.streaming.StreamingDataWriterFactory
+import org.apache.spark.sql.connector.write.{DataWriter, DataWriterFactory}
+import org.apache.spark.sql.types.StructType
+
+class DorisDataWriterFactory(config: DorisConfig, schema: StructType) extends
DataWriterFactory with StreamingDataWriterFactory {
+
+ // for batch write
+ override def createWriter(partitionId: Int, taskId: Long):
DataWriter[InternalRow] = {
+ new DorisDataWriter(config, schema, partitionId, taskId)
+ }
+
+ // for streaming write
+ override def createWriter(partitionId: Int, taskId: Long, epochId: Long):
DataWriter[InternalRow] = {
+ new DorisDataWriter(config, schema, partitionId, taskId, epochId)
+ }
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisWrite.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisWrite.scala
new file mode 100644
index 0000000..52233a7
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisWrite.scala
@@ -0,0 +1,91 @@
+// 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.doris.spark.write
+
+import org.apache.doris.spark.client.write.{CopyIntoProcessor, DorisCommitter,
StreamLoadProcessor}
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import
org.apache.spark.sql.connector.write.streaming.{StreamingDataWriterFactory,
StreamingWrite}
+import org.apache.spark.sql.connector.write.{BatchWrite, DataWriterFactory,
PhysicalWriteInfo, WriterCommitMessage}
+import org.apache.spark.sql.types.StructType
+import org.slf4j.LoggerFactory
+
+class DorisWrite(config: DorisConfig, schema: StructType) extends BatchWrite
with StreamingWrite {
+
+ private val LOG = LoggerFactory.getLogger(classOf[DorisWrite])
+
+ private val committer: DorisCommitter =
config.getValue(DorisOptions.LOAD_MODE) match {
+ case "stream_load" => new StreamLoadProcessor(config, schema)
+ case "copy_into" => new CopyIntoProcessor(config, schema)
+ case _ => throw new IllegalArgumentException()
+ }
+
+ private var lastCommittedEpoch: Option[Long] = None
+
+ private val committedEpochLock = new AnyRef
+
+ override def createBatchWriterFactory(physicalWriteInfo: PhysicalWriteInfo):
DataWriterFactory = {
+ new DorisDataWriterFactory(config, schema)
+ }
+
+ // for batch write
+ override def commit(writerCommitMessages: Array[WriterCommitMessage]): Unit
= {
+ if (writerCommitMessages != null && writerCommitMessages.nonEmpty) {
+ writerCommitMessages.filter(_ != null)
+
.foreach(_.asInstanceOf[DorisWriterCommitMessage].commitMessages.foreach(committer.commit))
+ }
+ }
+
+ // for batch write
+ override def abort(writerCommitMessages: Array[WriterCommitMessage]): Unit =
{
+ if (writerCommitMessages != null && writerCommitMessages.nonEmpty) {
+ LOG.info("writerCommitMessages size: " + writerCommitMessages.length)
+ writerCommitMessages.filter(_ != null)
+
.foreach(_.asInstanceOf[DorisWriterCommitMessage].commitMessages.foreach(committer.abort))
+ }
+ }
+
+ override def useCommitCoordinator(): Boolean = true
+
+ override def createStreamingWriterFactory(physicalWriteInfo:
PhysicalWriteInfo): StreamingDataWriterFactory = {
+ new DorisDataWriterFactory(config, schema)
+ }
+
+ // for streaming write
+ override def commit(epochId: Long, writerCommitMessages:
Array[WriterCommitMessage]): Unit = {
+ committedEpochLock.synchronized {
+ if ((lastCommittedEpoch.isEmpty || epochId > lastCommittedEpoch.get)
+ && writerCommitMessages != null && writerCommitMessages.exists(_ !=
null)) {
+ writerCommitMessages.filter(_ != null)
+
.foreach(_.asInstanceOf[DorisWriterCommitMessage].commitMessages.foreach(committer.commit))
+ lastCommittedEpoch = Some(epochId)
+ }
+ }
+ }
+
+ // for streaming write
+ override def abort(epochId: Long, writerCommitMessages:
Array[WriterCommitMessage]): Unit = {
+ committedEpochLock.synchronized {
+ if ((lastCommittedEpoch.isEmpty || epochId > lastCommittedEpoch.get)
+ && writerCommitMessages != null && writerCommitMessages.exists(_ !=
null)) {
+ writerCommitMessages.filter(_ != null)
+
.foreach(_.asInstanceOf[DorisWriterCommitMessage].commitMessages.foreach(committer.abort))
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisWriteBuilder.scala
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisWriteBuilder.scala
new file mode 100644
index 0000000..b74cc45
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/write/DorisWriteBuilder.scala
@@ -0,0 +1,47 @@
+// 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.doris.spark.write
+
+import org.apache.doris.spark.client.DorisFrontendClient
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.spark.sql.connector.write.streaming.StreamingWrite
+import org.apache.spark.sql.connector.write.{BatchWrite, SupportsTruncate,
WriteBuilder}
+import org.apache.spark.sql.types.StructType
+
+class DorisWriteBuilder(config: DorisConfig, schema: StructType) extends
WriteBuilder with SupportsTruncate {
+
+ private var isTruncate = false
+
+ override def buildForBatch(): BatchWrite = {
+ if (isTruncate) {
+ val client = new DorisFrontendClient(config)
+ val tableDb =
config.getValue(DorisOptions.DORIS_TABLE_IDENTIFIER).split("\\.")
+ client.truncateTable(tableDb(0), tableDb(1))
+ }
+ new DorisWrite(config, schema)
+ }
+
+ override def buildForStreaming(): StreamingWrite = {
+ new DorisWrite(config, schema)
+ }
+
+ override def truncate(): WriteBuilder = {
+ isTruncate = true
+ this
+ }
+}
diff --git a/spark-doris-connector/spark-doris-connector-spark-4.1/pom.xml
b/spark-doris-connector/spark-doris-connector-spark-4.1/pom.xml
new file mode 100644
index 0000000..4d8437b
--- /dev/null
+++ b/spark-doris-connector/spark-doris-connector-spark-4.1/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>spark-doris-connector</artifactId>
+ <version>${revision}</version>
+ </parent>
+
+ <artifactId>spark-doris-connector-spark-4.1</artifactId>
+
+ <properties>
+ <maven.compiler.source>17</maven.compiler.source>
+ <maven.compiler.target>17</maven.compiler.target>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <spark.version>4.1.0</spark.version>
+ <spark.major.version>4.1</spark.major.version>
+ <scala.version>2.13.17</scala.version>
+ <scala.major.version>2.13</scala.major.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>spark-doris-connector-spark-4-base</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.spark</groupId>
+ <artifactId>spark-sql_${scala.major.version}</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/resources/META-INF/services/org.apache.spark.sql.sources.DataSourceRegister
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/resources/META-INF/services/org.apache.spark.sql.sources.DataSourceRegister
new file mode 100644
index 0000000..986b253
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/resources/META-INF/services/org.apache.spark.sql.sources.DataSourceRegister
@@ -0,0 +1,18 @@
+# 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.
+
+org.apache.doris.spark.sql.sources.DorisDataSource
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/catalog/DorisTable.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/catalog/DorisTable.scala
new file mode 100644
index 0000000..be461af
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/catalog/DorisTable.scala
@@ -0,0 +1,34 @@
+// 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.doris.spark.catalog
+
+import org.apache.doris.spark.config.DorisConfig
+import org.apache.doris.spark.read.DorisScanBuilder
+import org.apache.doris.spark.write.DorisWriteBuilder
+import org.apache.spark.sql.connector.catalog.Identifier
+import org.apache.spark.sql.connector.read.ScanBuilder
+import org.apache.spark.sql.connector.write.WriteBuilder
+import org.apache.spark.sql.types.StructType
+
+class DorisTable(identifier: Identifier, config: DorisConfig, schema:
Option[StructType])
+ extends DorisTableBase(identifier, config, schema) {
+
+ override def createScanBuilder(config: DorisConfig, schema: StructType):
ScanBuilder = new DorisScanBuilder(config, schema)
+ override protected def createWriteBuilder(config: DorisConfig, schema:
StructType): WriteBuilder = new DorisWriteBuilder(config, schema)
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/catalog/DorisTableCatalog.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/catalog/DorisTableCatalog.scala
new file mode 100644
index 0000000..e3ffcc9
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/catalog/DorisTableCatalog.scala
@@ -0,0 +1,28 @@
+// 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.doris.spark.catalog
+
+import org.apache.doris.spark.config.DorisConfig
+import org.apache.spark.sql.connector.catalog.{Identifier, Table}
+import org.apache.spark.sql.types.StructType
+
+class DorisTableCatalog extends DorisTableCatalogBase {
+ override def dropNamespace(strings: Array[String], b: Boolean): Boolean =
throw new UnsupportedOperationException()
+ override def newTableInstance(identifier: Identifier, config: DorisConfig,
schema: Option[StructType]): Table =
+ new DorisTable(identifier, config, schema)
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/DorisScanBuilder.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/DorisScanBuilder.scala
new file mode 100644
index 0000000..6bdd256
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/DorisScanBuilder.scala
@@ -0,0 +1,53 @@
+// 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.doris.spark.read
+
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.doris.spark.read.expression.V2ExpressionBuilder
+import org.apache.spark.sql.connector.expressions.filter.Predicate
+import org.apache.spark.sql.connector.read.{Scan, SupportsPushDownLimit,
SupportsPushDownV2Filters}
+import org.apache.spark.sql.types.StructType
+
+class DorisScanBuilder(config: DorisConfig, schema: StructType) extends
DorisScanBuilderBase(config, schema)
+ with SupportsPushDownV2Filters
+ with SupportsPushDownLimit {
+
+ private var pushDownPredicates: Array[Predicate] = Array[Predicate]()
+
+ private val expressionBuilder = new
V2ExpressionBuilder(config.getValue(DorisOptions.DORIS_FILTER_QUERY_IN_MAX_COUNT))
+
+ private var limitSize: Int = -1
+
+ override def build(): Scan = new DorisScanV2(config, readSchema,
pushDownPredicates, limitSize)
+
+ override def pushPredicates(predicates: Array[Predicate]): Array[Predicate]
= {
+ val (pushed, unsupported) = predicates.partition(predicate => {
+ expressionBuilder.buildOpt(predicate).isDefined
+ })
+ this.pushDownPredicates = pushed
+ unsupported
+ }
+
+ override def pushedPredicates(): Array[Predicate] = pushDownPredicates
+
+ override def pushLimit(i: Int): Boolean = {
+ limitSize = i
+ true
+ }
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/DorisScanV2.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/DorisScanV2.scala
new file mode 100644
index 0000000..5e0104b
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/DorisScanV2.scala
@@ -0,0 +1,34 @@
+// 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.doris.spark.read
+
+import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.doris.spark.read.expression.V2ExpressionBuilder
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.connector.expressions.filter.Predicate
+import org.apache.spark.sql.types.StructType
+
+class DorisScanV2(config: DorisConfig, schema: StructType, filters:
Array[Predicate], limit: Int) extends AbstractDorisScan(config, schema) with
Logging {
+ override protected def compiledFilters(): Array[String] = {
+ val inValueLengthLimit =
config.getValue(DorisOptions.DORIS_FILTER_QUERY_IN_MAX_COUNT)
+ val v2ExpressionBuilder = new V2ExpressionBuilder(inValueLengthLimit)
+ filters.map(e =>
v2ExpressionBuilder.buildOpt(e)).filter(_.isDefined).map(_.get)
+ }
+
+ override protected def getLimit: Int = limit
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/expression/V2ExpressionBuilder.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/expression/V2ExpressionBuilder.scala
new file mode 100644
index 0000000..9a86a69
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/read/expression/V2ExpressionBuilder.scala
@@ -0,0 +1,120 @@
+// 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.doris.spark.read.expression
+
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.catalyst.util.DateTimeUtils
+import org.apache.spark.sql.connector.expressions.filter.{AlwaysFalse,
AlwaysTrue, And, Not, Or}
+import org.apache.spark.sql.connector.expressions.{Expression,
GeneralScalarExpression, Literal, NamedReference}
+import org.apache.spark.sql.types.{DateType, TimestampType}
+
+import scala.util.{Failure, Success, Try}
+
+class V2ExpressionBuilder(inValueLengthLimit: Int) extends Logging {
+
+ def buildOpt(predicate: Expression): Option[String] = {
+ Try {
+ Some(build(predicate))
+ } match {
+ case Success(value) => value
+ case Failure(exception) =>
+ logWarning(s"Failed to build expression: ${predicate.toString}, and
not support predicate push down, errMsg is ${exception.getMessage}")
+ None
+ }
+ }
+
+ def build(predicate: Expression): String = {
+ predicate match {
+ case and: And => s"(${build(and.left())} AND ${build(and.right())})"
+ case or: Or => s"(${build(or.left())} OR ${build(or.right())})"
+ case not: Not =>
+ not.child().name() match {
+ case "IS_NULL" => build(new GeneralScalarExpression("IS_NOT_NULL",
not.children()(0).children()))
+ case "=" => build(new GeneralScalarExpression("!=",
not.children()(0).children()))
+ case _ => s"NOT (${build(not.child())})"
+ }
+ case _: AlwaysTrue => "1=1"
+ case _: AlwaysFalse => "1=0"
+ case expr: Expression =>
+ expr match {
+ case literal: Literal[_] => visitLiteral(literal)
+ case namedRef: NamedReference => s"`${namedRef.toString}`"
+ case e: GeneralScalarExpression => e.name() match {
+ case "IN" =>
+ val expressions = e.children()
+ if (expressions.nonEmpty && expressions.length <=
inValueLengthLimit) {
+ s"""${build(expressions(0))} IN (${expressions.slice(1,
expressions.length).map(build).mkString(",")})"""
+ } else throw new IllegalArgumentException(s"exceeding limit of
IN values: actual size ${expressions.length}, limit size $inValueLengthLimit")
+ case "IS_NULL" => s"${build(e.children()(0))} IS NULL"
+ case "IS_NOT_NULL" => s"${build(e.children()(0))} IS NOT NULL"
+ case "STARTS_WITH" => visitStartWith(build(e.children()(0)),
build(e.children()(1)));
+ case "ENDS_WITH" => visitEndWith(build(e.children()(0)),
build(e.children()(1)));
+ case "CONTAINS" => visitContains(build(e.children()(0)),
build(e.children()(1)));
+ case "=" => s"${build(e.children()(0))} =
${build(e.children()(1))}"
+ case "!=" | "<>" => s"${build(e.children()(0))} !=
${build(e.children()(1))}"
+ case "<" => s"${build(e.children()(0))} <
${build(e.children()(1))}"
+ case "<=" => s"${build(e.children()(0))} <=
${build(e.children()(1))}"
+ case ">" => s"${build(e.children()(0))} >
${build(e.children()(1))}"
+ case ">=" => s"${build(e.children()(0))} >=
${build(e.children()(1))}"
+ case "CASE_WHEN" =>
+ val fragment = new StringBuilder("CASE ")
+ val expressions = e.children()
+
+ for(i<- 0 until expressions.size - 1 by 2){
+ fragment.append(s" WHEN ${build(expressions(i))} THEN
${build(expressions(i+1))} ")
+ }
+
+ if (expressions.length % 2 != 0) {
+ val last = expressions(expressions.length - 1)
+ fragment.append(s" ELSE ${build(last)} ")
+ }
+ fragment.append(" END")
+
+ fragment.mkString
+ case _ => throw new IllegalArgumentException(s"Unsupported
expression: ${e.name()}")
+ }
+ }
+ }
+ }
+
+ def visitLiteral(literal: Literal[_]): String = {
+ if (literal.value() == null) {
+ return "null"
+ }
+ literal.dataType() match {
+ case DateType =>
s"'${DateTimeUtils.toJavaDate(literal.value().asInstanceOf[Int]).toString}'"
+ case TimestampType =>
s"'${DateTimeUtils.toJavaTimestamp(literal.value().asInstanceOf[Long]).toString}'"
+ case _ => literal.toString
+ }
+ }
+ def visitStartWith(l: String, r: String): String = {
+ val value = r.substring(1, r.length - 1)
+ s"$l LIKE '$value%'"
+ }
+
+ def visitEndWith(l: String, r: String): String = {
+ val value = r.substring(1, r.length - 1)
+ s"$l LIKE '%$value'"
+ }
+
+ def visitContains(l: String, r: String): String = {
+ val value = r.substring(1, r.length - 1)
+ s"$l LIKE '%$value%'"
+ }
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/sql/sources/DorisDataSource.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/sql/sources/DorisDataSource.scala
new file mode 100644
index 0000000..beae22b
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/main/scala/org/apache/doris/spark/sql/sources/DorisDataSource.scala
@@ -0,0 +1,30 @@
+// 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.doris.spark.sql.sources
+
+import org.apache.doris.spark.catalog.{DorisTable, DorisTableProviderBase}
+import org.apache.doris.spark.config.DorisConfig
+import org.apache.spark.sql.connector.catalog.{Identifier, Table}
+import org.apache.spark.sql.types.StructType
+
+class DorisDataSource extends DorisTableProviderBase with
DorisSourceRegisterTrait with DorisSourceProvider with Serializable {
+
+ override def newTableInstance(identifier: Identifier, config: DorisConfig,
schema: Option[StructType]): Table =
+ new DorisTable(identifier, config, schema)
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/test/scala/org/apache/doris/spark/read/expression/V2ExpressionBuilderTest.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/test/scala/org/apache/doris/spark/read/expression/V2ExpressionBuilderTest.scala
new file mode 100644
index 0000000..7691b29
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/test/scala/org/apache/doris/spark/read/expression/V2ExpressionBuilderTest.scala
@@ -0,0 +1,65 @@
+package org.apache.doris.spark.read.expression
+
+// 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.
+
+import org.apache.spark.sql.ExpressionUtil
+import org.apache.spark.sql.sources._
+import org.junit.jupiter.api.{Assertions, Test}
+
+class V2ExpressionBuilderTest {
+
+ @Test
+ def buildTest(): Unit = {
+
+ val builder = new V2ExpressionBuilder(10)
+ Assertions.assertEquals(builder.build(EqualTo("c0", 1).toV2), "`c0` = 1")
+ Assertions.assertEquals(builder.build(Not(EqualTo("c1", 2)).toV2), "`c1`
!= 2")
+ Assertions.assertEquals(builder.build(GreaterThan("c2", 3.4).toV2), "`c2`
> 3.4")
+ Assertions.assertEquals(builder.build(GreaterThanOrEqual("c3", 5.6).toV2),
"`c3` >= 5.6")
+ Assertions.assertEquals(builder.build(LessThan("c4", 7.8).toV2), "`c4` <
7.8")
+ Assertions.assertEquals(builder.build(LessThanOrEqual("c5",
BigDecimal(9.1011)).toV2), "`c5` <= 9.1011")
+ Assertions.assertEquals(builder.build(StringStartsWith("c6","a").toV2),
"`c6` LIKE 'a%'")
+ Assertions.assertEquals(builder.build(StringEndsWith("c7", "b").toV2),
"`c7` LIKE '%b'")
+ Assertions.assertEquals(builder.build(StringContains("c8", "c").toV2),
"`c8` LIKE '%c%'")
+ Assertions.assertEquals(builder.build(In("c9", Array(12,13,14)).toV2),
"`c9` IN (12,13,14)")
+ Assertions.assertEquals(builder.build(IsNull("c10").toV2), "`c10` IS NULL")
+ Assertions.assertEquals(builder.build(Not(IsNull("c11")).toV2), "`c11` IS
NOT NULL")
+ Assertions.assertEquals(builder.build(And(EqualTo("c12", 15),
EqualTo("c13", 16)).toV2), "(`c12` = 15 AND `c13` = 16)")
+ Assertions.assertEquals(builder.build(Or(EqualTo("c14", 17),
EqualTo("c15", 18)).toV2), "(`c14` = 17 OR `c15` = 18)")
+ Assertions.assertEquals(builder.build(AlwaysTrue.toV2), "1=1")
+ Assertions.assertEquals(builder.build(AlwaysFalse.toV2), "1=0")
+ Assertions.assertEquals(builder.build(In("c19",
Array(19,20,21,22,23,24,25,26)).toV2), "`c19` IN (19,20,21,22,23,24,25,26)")
+ Assertions.assertEquals(builder.build(In("c19", Array("19","20")).toV2),
"`c19` IN ('19','20')")
+ val inException =
Assertions.assertThrows(classOf[IllegalArgumentException], () =>
builder.build(In("c19", Array(19,20,21,22,23,24,25,26,27,28,29)).toV2))
+ Assertions.assertEquals(inException.getMessage, "exceeding limit of IN
values: actual size 12, limit size 10")
+ val exception = Assertions.assertThrows(classOf[IllegalArgumentException],
() => builder.build(ExpressionUtil.buildCoalesceFilter()))
+ Assertions.assertEquals(exception.getMessage, "Unsupported expression:
COALESCE")
+
+ }
+
+ @Test
+ def buildOptTest() : Unit = {
+
+ val builder = new V2ExpressionBuilder(10)
+ Assertions.assertEquals(builder.buildOpt(EqualTo("c0", 1).toV2),
Some("`c0` = 1"))
+ Assertions.assertEquals(builder.buildOpt(Not(EqualTo("c1", 2)).toV2),
Some("`c1` != 2"))
+ Assertions.assertEquals(builder.buildOpt(GreaterThan("c2", 3.4).toV2),
Some("`c2` > 3.4"))
+
Assertions.assertEquals(builder.buildOpt(ExpressionUtil.buildCoalesceFilter()),
None)
+ }
+
+}
diff --git
a/spark-doris-connector/spark-doris-connector-spark-4.1/src/test/scala/org/apache/spark/sql/ExpressionUtil.scala
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/test/scala/org/apache/spark/sql/ExpressionUtil.scala
new file mode 100644
index 0000000..2da931a
--- /dev/null
+++
b/spark-doris-connector/spark-doris-connector-spark-4.1/src/test/scala/org/apache/spark/sql/ExpressionUtil.scala
@@ -0,0 +1,30 @@
+// 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.spark.sql
+
+import org.apache.spark.sql.connector.expressions.filter.Predicate
+import org.apache.spark.sql.connector.expressions.{Expression, FieldReference,
GeneralScalarExpression, LiteralValue}
+import org.apache.spark.sql.types.StringType
+
+object ExpressionUtil {
+
+ def buildCoalesceFilter(): Expression = {
+ val gse = new GeneralScalarExpression("COALESCE",
Array(FieldReference(Seq("A4")), LiteralValue("null", StringType)))
+ new Predicate("=", Array(gse, LiteralValue("1", StringType)))
+ }
+}
diff --git a/tools/releasing/deploy_staging_jars.sh
b/tools/releasing/deploy_staging_jars.sh
index 7d4b4b9..9eee7b9 100644
--- a/tools/releasing/deploy_staging_jars.sh
+++ b/tools/releasing/deploy_staging_jars.sh
@@ -17,7 +17,13 @@
# under the License.
##############################################################
-# This script is deploy stage jars to repository.apache.org
+# This script deploys the JDK 8 / Scala 2.11-2.12 stage jars
+# (Spark 2.4 - 3.5) to repository.apache.org.
+#
+# Spark 4.x requires JDK 17 and Scala 2.13, so it is deployed
+# by a separate script (deploy_staging_jars_spark4.sh). Keeping
+# the two toolchains in separate scripts lets either release
+# flow be rerun on its own if a JDK-switching issue occurs.
##############################################################
MVN=${MVN:-mvn}
@@ -61,5 +67,5 @@ ${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10
echo "Deploying spark3.5..."
${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-3.5 -pl spark-doris-connector-spark-3.5
-am
-echo "Deploy jar finished."
+echo "Deploy jar finished. Run deploy_staging_jars_spark4.sh (with JDK 17) to
deploy Spark 4.x."
cd ${CURR_DIR}
\ No newline at end of file
diff --git a/tools/releasing/deploy_staging_jars.sh
b/tools/releasing/deploy_staging_jars_spark4.sh
similarity index 52%
copy from tools/releasing/deploy_staging_jars.sh
copy to tools/releasing/deploy_staging_jars_spark4.sh
index 7d4b4b9..97f605f 100644
--- a/tools/releasing/deploy_staging_jars.sh
+++ b/tools/releasing/deploy_staging_jars_spark4.sh
@@ -17,7 +17,15 @@
# under the License.
##############################################################
-# This script is deploy stage jars to repository.apache.org
+# This script deploys the Spark 4.x stage jars (JDK 17,
+# Scala 2.13) to repository.apache.org.
+#
+# It is kept separate from deploy_staging_jars.sh (Spark 2.4 -
+# 3.5, JDK 8) so the two toolchains never share a run: either
+# release flow can be rerun on its own if a JDK-switching issue
+# occurs. This script fails fast when JDK 17 is not in effect
+# rather than silently falling back to JDK 8 and breaking the
+# build.
##############################################################
MVN=${MVN:-mvn}
@@ -27,6 +35,8 @@ CUSTOM_OPTIONS=${CUSTOM_OPTIONS:-}
set -o errexit
set -o nounset
+# PLACEHOLDER_BODY
+
CURR_DIR=`pwd`
BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
PROJECT_ROOT="${BASE_DIR}/../../"
@@ -37,29 +47,34 @@ if [ ! -f ${PROJECT_ROOT}/LICENSE.txt ]; then
exit 1
fi
+# Spark 4.x requires JDK 17. Prefer JAVA17_HOME when set; otherwise the
current JAVA_HOME must be
+# JDK 17. Fail fast on a wrong or missing toolchain instead of silently
building with JDK 8.
+if [ -n "${JAVA17_HOME:-}" ]; then
+ export JAVA_HOME="${JAVA17_HOME}"
+fi
+if [ -z "${JAVA_HOME:-}" ]; then
+ echo "JDK 17 is required to deploy Spark 4.x. Set JAVA17_HOME (or
JAVA_HOME) to a JDK 17 home."
+ exit 1
+fi
+JAVA_VERSION=$("${JAVA_HOME}/bin/java" -version 2>&1 | awk -F'"' '/version/
{print $2}')
+case "${JAVA_VERSION}" in
+ 17.*|18.*|19.*|2[0-9].*)
+ ;;
+ *)
+ echo "JDK 17 or later is required to deploy Spark 4.x, but JAVA_HOME
points to Java ${JAVA_VERSION}."
+ echo "Set JAVA17_HOME (or JAVA_HOME) to a JDK 17 home and rerun this
script."
+ exit 1
+ ;;
+esac
+
###########################
cd ${PROJECT_ROOT}/spark-doris-connector
-echo "Deploying to repository.apache.org"
-
-echo "Deploying spark2.4..."
-${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-2.4_2.11 -pl
spark-doris-connector-spark-2 -am
-
-echo "Deploying spark3.1..."
-${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-3.1 -pl spark-doris-connector-spark-3.1
-am
-
-echo "Deploying spark3.2..."
-${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-3.2 -pl spark-doris-connector-spark-3.2
-am
-
-echo "Deploying spark3.3..."
-${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-3.3 -pl spark-doris-connector-spark-3.3
-am
-
-echo "Deploying spark3.4..."
-${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-3.4 -pl spark-doris-connector-spark-3.4
-am
+echo "Deploying to repository.apache.org with JDK ${JAVA_VERSION}
(JAVA_HOME=${JAVA_HOME})"
-echo "Deploying spark3.5..."
-${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-3.5 -pl spark-doris-connector-spark-3.5
-am
+echo "Deploying spark4.1..."
+${MVN} clean deploy -Papache-release -DskipTests
-DretryFailedDeploymentCount=10 -Pspark-4.1 -pl spark-doris-connector-spark-4.1
-am
echo "Deploy jar finished."
-cd ${CURR_DIR}
\ No newline at end of file
+cd ${CURR_DIR}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]