diff --git a/.gitignore b/.gitignore
index f883fa99..2d3b91cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,21 @@ dependency-reduced-pom.xml
target
.flattened-pom.xml
+spark-load/.idea/
+spark-load/target
+spark-load/spark-load-core/dependency-reduced-pom.xml
+spark-load/spark-load-core/output/
+spark-load/spark-load-core/target/
+spark-load/spark-load-core/.idea/
+spark-load/spark-load-dist/dependency-reduced-pom.xml
+spark-load/spark-load-dist/target/
+spark-load/spark-load-dpp/dependency-reduced-pom.xml
+spark-load/spark-load-dpp/.flattened-pom.xml
+spark-load/spark-load-dpp/target/
+spark-load/spark-load-common/dependency-reduced-pom.xml
+spark-load/spark-load-common/target/
+
+
### Java template
# Compiled class file
*.class
diff --git a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/TestSparkConnector.scala b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/TestSparkConnector.scala
index 1242a9ba..a5e756c1 100644
--- a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/TestSparkConnector.scala
+++ b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/TestSparkConnector.scala
@@ -19,7 +19,8 @@ package org.apache.doris.spark.sql
import org.apache.spark.sql.SparkSession
import org.apache.spark.{SparkConf, SparkContext}
-import org.junit.{Ignore, Test}
+import org.junit.Ignore
+import org.junit.Test
// This test need real connect info to run.
// Set the connect info before comment out this @Ignore
diff --git a/spark-load/build.sh b/spark-load/build.sh
new file mode 100755
index 00000000..a8ca1c73
--- /dev/null
+++ b/spark-load/build.sh
@@ -0,0 +1,175 @@
+#!/usr/bin/env bash
+# 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.
+
+##############################################################
+# This script is used to compile Spark-Load
+# Usage:
+# sh build.sh
+#
+##############################################################
+
+# Bugzilla 37848: When no TTY is available, don't output to console
+have_tty=0
+# shellcheck disable=SC2006
+if [[ "`tty`" != "not a tty" ]]; then
+ have_tty=1
+fi
+
+# Bugzilla 37848: When no TTY is available, don't output to console
+have_tty=0
+# shellcheck disable=SC2006
+if [[ "`tty`" != "not a tty" ]]; then
+ have_tty=1
+fi
+
+ # Only use colors if connected to a terminal
+if [[ ${have_tty} -eq 1 ]]; then
+ PRIMARY=$(printf '\033[38;5;082m')
+ RED=$(printf '\033[31m')
+ GREEN=$(printf '\033[32m')
+ YELLOW=$(printf '\033[33m')
+ BLUE=$(printf '\033[34m')
+ BOLD=$(printf '\033[1m')
+ RESET=$(printf '\033[0m')
+else
+ PRIMARY=""
+ RED=""
+ GREEN=""
+ YELLOW=""
+ BLUE=""
+ BOLD=""
+ RESET=""
+fi
+
+echo_r () {
+ # Color red: Error, Failed
+ [[ $# -ne 1 ]] && return 1
+ # shellcheck disable=SC2059
+ printf "[%sDoris%s] %s$1%s\n" $BLUE $RESET $RED $RESET
+}
+
+echo_g () {
+ # Color green: Success
+ [[ $# -ne 1 ]] && return 1
+ # shellcheck disable=SC2059
+ printf "[%sDoris%s] %s$1%s\n" $BLUE $RESET $GREEN $RESET
+}
+
+echo_y () {
+ # Color yellow: Warning
+ [[ $# -ne 1 ]] && return 1
+ # shellcheck disable=SC2059
+ printf "[%sDoris%s] %s$1%s\n" $BLUE $RESET $YELLOW $RESET
+}
+
+echo_w () {
+ # Color yellow: White
+ [[ $# -ne 1 ]] && return 1
+ # shellcheck disable=SC2059
+ printf "[%sDoris%s] %s$1%s\n" $BLUE $RESET $WHITE $RESET
+}
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false
+os400=false
+# shellcheck disable=SC2006
+case "`uname`" in
+CYGWIN*) cygwin=true;;
+OS400*) os400=true;;
+esac
+
+# resolve links - $0 may be a softlink
+PRG="$0"
+
+while [[ -h "$PRG" ]]; do
+ # shellcheck disable=SC2006
+ ls=`ls -ld "$PRG"`
+ # shellcheck disable=SC2006
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ # shellcheck disable=SC2006
+ PRG=`dirname "$PRG"`/"$link"
+ fi
+done
+
+# Get standard environment variables
+# shellcheck disable=SC2006
+ROOT=$(cd "$(dirname "$PRG")" &>/dev/null && pwd)
+export DORIS_HOME=$(cd "$ROOT/../" &>/dev/null && pwd)
+
+. "${DORIS_HOME}"/env.sh
+
+# include custom environment variables
+if [[ -f ${DORIS_HOME}/custom_env.sh ]]; then
+ . "${DORIS_HOME}"/custom_env.sh
+fi
+
+selectSpark() {
+ echo 'Spark-Load supports multiple versions of spark. Which version do you need ?'
+ select spark in "2.x" "3.x" "other"
+ do
+ case $spark in
+ "2.x")
+ return 1
+ ;;
+ "3.x")
+ return 2
+ ;;
+ *)
+ echo "invalid selected, exit.."
+ exit 1
+ ;;
+ esac
+ done
+}
+
+SPARK_VERSION=0
+selectSpark
+SparkVer=$?
+if [ ${SparkVer} -eq 1 ]; then
+ SPARK_VERSION="spark2"
+ SCALA_VERSION="scala_2.11"
+elif [ ${SparkVer} -eq 2 ]; then
+ SPARK_VERSION="spark3"
+ SCALA_VERSION="scala_2.12"
+fi
+
+echo_g " spark load run based on : ${SPARK_VERSION} and ${SCALA_VERSION}"
+echo_g " build starting..."
+
+${MVN_BIN} clean package -P${SPARK_VERSION},${SCALA_VERSION} "$@"
+
+EXIT_CODE=$?
+if [ $EXIT_CODE -eq 0 ]; then
+ DIST_DIR=${DORIS_HOME}/dist
+ [ ! -d "$DIST_DIR" ] && mkdir "$DIST_DIR"
+ dist_jar=$(ls "${ROOT}"/target | grep "spark-load-")
+ rm -rf "${DIST_DIR}"/"${dist_jar}"
+ cp "${ROOT}"/target/"${dist_jar}" "$DIST_DIR"
+
+ echo_g "*****************************************************************"
+ echo_g "Successfully build Spark-Load"
+ echo_g "dist: $DIST_DIR/$dist_jar "
+ echo_g "*****************************************************************"
+ exit 0;
+else
+ echo_r "Failed build Spark-Load"
+ exit $EXIT_CODE;
+fi
diff --git a/spark-load/pom.xml b/spark-load/pom.xml
new file mode 100644
index 00000000..480d4f92
--- /dev/null
+++ b/spark-load/pom.xml
@@ -0,0 +1,418 @@
+
+
+
+ 4.0.0
+
+ org.apache.doris
+ spark-load
+ ${revision}
+ pom
+
+ spark-load-common
+ spark-load-core
+ spark-load-dpp
+ spark-load-dist
+
+
+
+ 1.8
+ 1.8
+ UTF-8
+ 24.0.0-SNAPSHOT
+ 1.13
+ 3.9
+ 3.3.6
+ 4.1.104.Final
+ 1.13.1
+ 3.2.2
+ 4.0.2
+ 32.1.2-jre
+ 2.14.2
+ 1.18.30
+ 1.4
+ 4.5.13
+ 5.8.2
+ 1.49
+ 2.17.1
+ 2.0.7
+ 1.2
+ 1.12.669
+ 0.8.13
+ 2.9.1
+
+
+
+
+
+
+ commons-codec
+ commons-codec
+ ${commons-codec.version}
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
+
+
+
+
+
+
+
+
+ org.apache.spark
+ spark-core_${scala.major.version}
+ ${spark.version}
+ provided
+
+
+ org.apache.logging.log4j
+ log4j-1.2-api
+
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+ org.apache.logging.log4j
+ log4j-core
+
+
+ commons-logging
+ commons-logging
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+ io.netty
+ netty-all
+ ${netty-all.version}
+
+
+
+
+ org.apache.spark
+ spark-sql_${scala.major.version}
+ ${spark.version}
+ provided
+
+
+ org.apache.hadoop
+ hadoop-common
+ ${hadoop.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.hadoop
+ hadoop-client
+ ${hadoop.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.hadoop
+ hadoop-aws
+ ${hadoop.version}
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+ log4j
+ log4j
+
+
+ servlet-api
+ javax.servlet
+
+
+
+ com.amazonaws
+ aws-java-sdk-s3
+
+
+ com.amazonaws
+ aws-java-sdk-bundle
+
+
+
+
+ com.amazonaws
+ aws-java-sdk-s3
+ ${aws-java-sdk.version}
+
+
+ com.amazonaws
+ aws-java-sdk-glue
+ ${aws-java-sdk.version}
+
+
+ com.amazonaws
+ aws-java-sdk-dynamodb
+ ${aws-java-sdk.version}
+
+
+
+ com.amazonaws
+ aws-java-sdk-logs
+ ${aws-java-sdk.version}
+
+
+ org.apache.parquet
+ parquet-column
+ ${parquet.version}
+
+
+ org.apache.parquet
+ parquet-hadoop
+ ${parquet.version}
+
+
+ org.apache.parquet
+ parquet-common
+ ${parquet.version}
+
+
+ commons-collections
+ commons-collections
+ ${commons-collections.version}
+
+
+ org.scala-lang
+ scala-library
+ ${scala.version}
+ provided
+
+
+ com.esotericsoftware
+ kryo-shaded
+ ${kryo.version}
+
+
+ org.apache.spark
+ spark-catalyst_${scala.major.version}
+ ${spark.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+ provided
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.veresion}
+ provided
+
+
+
+ commons-cli
+ commons-cli
+ ${commons-cli.version}
+
+
+ org.apache.spark
+ spark-launcher_${scala.major.version}
+ ${spark.version}
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ ${httpclient.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+
+
+
+ org.jmockit
+ jmockit
+ ${jmockit.version}
+ test
+
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ ${log4j.version}
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ ${log4j.version}
+
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ ${log4j.version}
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+
+ commons-logging
+ commons-logging
+ ${commons-logging.version}
+
+
+ org.roaringbitmap
+ RoaringBitmap
+ ${RoaringBitmap.version}
+
+
+
+
+ com.google.code.gson
+ gson
+ ${gson.version}
+
+
+ ${project.groupId}
+ spark-load-common
+ ${project.version}
+
+
+
+
+
+
+ spark2
+
+ false
+
+
+ 2.4.8
+
+
+
+ spark3
+
+ true
+
+
+ 3.4.1
+
+
+
+ scala_2.11
+
+ false
+
+
+ 2.11.8
+ 2.11
+
+
+
+ scala_2.12
+
+ true
+
+
+ 2.12.10
+ 2.12
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.1.1
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.1
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.4.1
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spark-load/spark-load-common/pom.xml b/spark-load/spark-load-common/pom.xml
new file mode 100644
index 00000000..4a0e96b7
--- /dev/null
+++ b/spark-load/spark-load-common/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+
+ 4.0.0
+
+ org.apache.doris
+ spark-load
+ ${revision}
+
+
+ spark-load-common
+ jar
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.google.code.gson
+ gson
+
+
+ com.google.guava
+ guava
+
+
+ org.roaringbitmap
+ RoaringBitmap
+
+
+ commons-codec
+ commons-codec
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
\ No newline at end of file
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/DppResult.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/DppResult.java
new file mode 100644
index 00000000..7a2a9cb4
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/DppResult.java
@@ -0,0 +1,87 @@
+// 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.common;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+
+/**
+ * Copied from Apache Doris org.apache.doris.sparkdpp.DppResult
+ */
+public class DppResult implements Serializable {
+
+ public boolean isSuccess;
+
+ public String failedReason;
+
+ public long scannedRows;
+
+ public long fileNumber;
+
+ public long fileSize;
+
+ public long normalRows;
+
+ public long abnormalRows;
+
+ public long unselectRows;
+
+ // only part of abnormal rows will be returned
+ public String partialAbnormalRows;
+
+ public long scannedBytes;
+
+ public DppResult() {
+ isSuccess = true;
+ failedReason = "";
+ scannedRows = 0;
+ fileNumber = 0;
+ fileSize = 0;
+ normalRows = 0;
+ abnormalRows = 0;
+ unselectRows = 0;
+ partialAbnormalRows = "";
+ scannedBytes = 0;
+ }
+
+ @JsonCreator
+ public DppResult(@JsonProperty(value = "is_success", required = true) boolean isSuccess,
+ @JsonProperty(value = "failed_reason", required = true) String failedReason,
+ @JsonProperty(value = "scanned_rows", required = true) long scannedRows,
+ @JsonProperty(value = "file_number", required = true) long fileNumber,
+ @JsonProperty(value = "file_size", required = true) long fileSize,
+ @JsonProperty(value = "normal_rows", required = true) long normalRows,
+ @JsonProperty(value = "abnormal_rows", required = true) long abnormalRows,
+ @JsonProperty(value = "unselect_rows", required = true) long unselectRows,
+ @JsonProperty("partial_abnormal_rows") String partialAbnormalRows,
+ @JsonProperty("scanned_bytes") long scannedBytes) {
+ this.isSuccess = isSuccess;
+ this.failedReason = failedReason;
+ this.scannedRows = scannedRows;
+ this.fileNumber = fileNumber;
+ this.fileSize = fileSize;
+ this.normalRows = normalRows;
+ this.abnormalRows = abnormalRows;
+ this.unselectRows = unselectRows;
+ this.partialAbnormalRows = partialAbnormalRows;
+ this.scannedBytes = scannedBytes;
+ }
+
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/BitmapValue.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/BitmapValue.java
new file mode 100644
index 00000000..db4a65c2
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/BitmapValue.java
@@ -0,0 +1,423 @@
+// 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.common.io;
+
+import org.roaringbitmap.Util;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+/**
+ * Copied from Apache Doris
+ */
+public class BitmapValue {
+
+ public static final int EMPTY = 0;
+ public static final int SINGLE32 = 1;
+ public static final int BITMAP32 = 2;
+ public static final int SINGLE64 = 3;
+ public static final int BITMAP64 = 4;
+
+ public static final int SINGLE_VALUE = 1;
+ public static final int BITMAP_VALUE = 2;
+
+ public static final long UNSIGNED_32BIT_INT_MAX_VALUE = 4294967295L;
+
+ private int bitmapType;
+ private long singleValue;
+ private Roaring64Map bitmap;
+
+ public BitmapValue() {
+ bitmapType = EMPTY;
+ }
+
+ public void add(int value) {
+ add(Util.toUnsignedLong(value));
+ }
+
+ public void add(long value) {
+ switch (bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ singleValue = value;
+ bitmapType = SINGLE_VALUE;
+ break;
+ case SINGLE_VALUE:
+ if (this.singleValue != value) {
+ bitmap = new Roaring64Map();
+ bitmap.add(value);
+ bitmap.add(singleValue);
+ bitmapType = BITMAP_VALUE;
+ }
+ break;
+ case BITMAP_VALUE:
+ bitmap.addLong(value);
+ break;
+ }
+ }
+
+ public boolean contains(int value) {
+ return contains(Util.toUnsignedLong(value));
+ }
+
+ public boolean contains(long value) {
+ switch (bitmapType) {
+ case EMPTY:
+ return false;
+ case SINGLE_VALUE:
+ return singleValue == value;
+ case BITMAP_VALUE:
+ return bitmap.contains(value);
+ default:
+ return false;
+ }
+ }
+
+ public long cardinality() {
+ switch (bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ return 0;
+ case SINGLE_VALUE:
+ return 1;
+ case BITMAP_VALUE:
+ return bitmap.getLongCardinality();
+ }
+ return 0;
+ }
+
+ public void serialize(DataOutput output) throws IOException {
+ switch (bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ output.writeByte(EMPTY);
+ break;
+ case SINGLE_VALUE:
+ // is 32-bit enough
+ // FE is big end but BE is little end.
+ if (isLongValue32bitEnough(singleValue)) {
+ output.write(SINGLE32);
+ output.writeInt(Integer.reverseBytes((int) singleValue));
+ } else {
+ output.writeByte(SINGLE64);
+ output.writeLong(Long.reverseBytes(singleValue));
+ }
+ break;
+ case BITMAP_VALUE:
+ bitmap.serialize(output);
+ break;
+ }
+ }
+
+ public void deserialize(DataInput input) throws IOException {
+ clear();
+ int bitmapType = input.readByte();
+ switch (bitmapType) {
+ case EMPTY:
+ break;
+ case SINGLE32:
+ singleValue = Util.toUnsignedLong(Integer.reverseBytes(input.readInt()));
+ this.bitmapType = SINGLE_VALUE;
+ break;
+ case SINGLE64:
+ singleValue = Long.reverseBytes(input.readLong());
+ this.bitmapType = SINGLE_VALUE;
+ break;
+ case BITMAP32:
+ case BITMAP64:
+ bitmap = bitmap == null ? new Roaring64Map() : bitmap;
+ bitmap.deserialize(input, bitmapType);
+ this.bitmapType = BITMAP_VALUE;
+ break;
+ default:
+ throw new RuntimeException(String.format("unknown bitmap type %s ", bitmapType));
+ }
+ }
+
+ // In-place bitwise AND (intersection) operation. The current bitmap is modified.
+ public void and(BitmapValue other) {
+ switch (other.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ clear();
+ break;
+ case SINGLE_VALUE:
+ switch (this.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ if (this.singleValue != other.singleValue) {
+ clear();
+ }
+ break;
+ case BITMAP_VALUE:
+ if (!this.bitmap.contains(other.singleValue)) {
+ clear();
+ } else {
+ clear();
+ this.singleValue = other.singleValue;
+ this.bitmapType = SINGLE_VALUE;
+ }
+ break;
+ }
+ break;
+ case BITMAP_VALUE:
+ switch (this.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ if (!other.bitmap.contains(this.singleValue)) {
+ clear();
+ }
+ break;
+ case BITMAP_VALUE:
+ this.bitmap.and(other.bitmap);
+ convertToSmallerType();
+ break;
+ }
+ break;
+ }
+ }
+
+ // In-place bitwise OR (union) operation. The current bitmap is modified.
+ public void or(BitmapValue other) {
+ switch (other.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ add(other.singleValue);
+ break;
+ case BITMAP_VALUE:
+ switch (this.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ // deep copy the bitmap in case of multi-rollups update the bitmap repeatedly
+ this.bitmap = new Roaring64Map();
+ this.bitmap.or(other.bitmap);
+ this.bitmapType = BITMAP_VALUE;
+ break;
+ case SINGLE_VALUE:
+ this.bitmap = new Roaring64Map();
+ this.bitmap.or(other.bitmap);
+ this.bitmap.add(this.singleValue);
+ this.bitmapType = BITMAP_VALUE;
+ break;
+ case BITMAP_VALUE:
+ this.bitmap.or(other.bitmap);
+ break;
+ }
+ break;
+ }
+ }
+
+ public void remove(long value) {
+ switch (this.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ if (this.singleValue == value) {
+ clear();
+ }
+ break;
+ case BITMAP_VALUE:
+ this.bitmap.removeLong(value);
+ convertToSmallerType();
+ break;
+ }
+ }
+
+ // In-place bitwise ANDNOT (difference) operation. The current bitmap is modified
+ public void not(BitmapValue other) {
+ switch (other.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ remove(other.singleValue);
+ break;
+ case BITMAP_VALUE:
+ switch (this.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ if (other.bitmap.contains(this.singleValue)) {
+ clear();
+ }
+ break;
+ case BITMAP_VALUE:
+ this.bitmap.andNot(other.bitmap);
+ convertToSmallerType();
+ break;
+ }
+ break;
+ }
+ }
+
+ // In-place bitwise XOR (symmetric difference) operation. The current bitmap is modified
+ public void xor(BitmapValue other) {
+ switch (other.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ switch (this.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ add(other.singleValue);
+ break;
+ case SINGLE_VALUE:
+ if (this.singleValue != other.singleValue) {
+ add(other.singleValue);
+ } else {
+ clear();
+ }
+ break;
+ case BITMAP_VALUE:
+ if (!this.bitmap.contains(other.singleValue)) {
+ this.bitmap.add(other.singleValue);
+ } else {
+ this.bitmap.removeLong(other.singleValue);
+ convertToSmallerType();
+ }
+ break;
+ }
+ break;
+ case BITMAP_VALUE:
+ switch (this.bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ this.bitmap = other.bitmap;
+ this.bitmapType = BITMAP_VALUE;
+ break;
+ case SINGLE_VALUE:
+ this.bitmap = other.bitmap;
+ this.bitmapType = BITMAP_VALUE;
+ if (this.bitmap.contains(this.singleValue)) {
+ this.bitmap.removeLong(this.singleValue);
+ } else {
+ this.bitmap.add(this.bitmapType);
+ }
+ break;
+ case BITMAP_VALUE:
+ this.bitmap.xor(other.bitmap);
+ convertToSmallerType();
+ break;
+ }
+ break;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null || !(other instanceof BitmapValue)) {
+ return false;
+ }
+ boolean ret = false;
+ if (this.bitmapType != ((BitmapValue) other).bitmapType) {
+ return false;
+ }
+ switch (((BitmapValue) other).bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ ret = true;
+ break;
+ case SINGLE_VALUE:
+ ret = this.singleValue == ((BitmapValue) other).singleValue;
+ break;
+ case BITMAP_VALUE:
+ ret = bitmap.equals(((BitmapValue) other).bitmap);
+ }
+ return ret;
+ }
+
+ /**
+ * usage note:
+ * now getSizeInBytes is different from be' impl
+ * The reason is that java's roaring didn't implement method #shrinkToFit but be's getSizeInBytes need it
+ * Implementing java's shrinkToFit means refactor roaring whose fields are all unaccess in Doris Fe's package
+ * That would be an another big project
+ */
+ // TODO(wb): keep getSizeInBytes consistent with be and refactor roaring
+ public long getSizeInBytes() {
+ long size = 0;
+ switch (bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ size = 1;
+ break;
+ case SINGLE_VALUE:
+ if (isLongValue32bitEnough(singleValue)) {
+ size = 1 + 4;
+ } else {
+ size = 1 + 8;
+ }
+ break;
+ case BITMAP_VALUE:
+ size = 1 + bitmap.getSizeInBytes();
+ }
+ return size;
+ }
+
+ @Override
+ public String toString() {
+ String toStringStr = "{}";
+ switch (bitmapType) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case EMPTY:
+ break;
+ case SINGLE_VALUE:
+ toStringStr = String.format("{%s}", singleValue);
+ break;
+ case BITMAP_VALUE:
+ toStringStr = this.bitmap.toString();
+ break;
+ }
+ return toStringStr;
+ }
+
+ public void clear() {
+ this.bitmapType = EMPTY;
+ this.singleValue = -1;
+ this.bitmap = null;
+ }
+
+ private void convertToSmallerType() {
+ if (bitmapType == BITMAP_VALUE) {
+ if (bitmap.getLongCardinality() == 0) {
+ this.bitmap = null;
+ this.bitmapType = EMPTY;
+ } else if (bitmap.getLongCardinality() == 1) {
+ this.singleValue = bitmap.select(0);
+ this.bitmapType = SINGLE_VALUE;
+ this.bitmap = null;
+ }
+ }
+ }
+
+ private boolean isLongValue32bitEnough(long value) {
+ return value <= UNSIGNED_32BIT_INT_MAX_VALUE;
+ }
+
+ // just for ut
+ public int getBitmapType() {
+ return bitmapType;
+ }
+
+ // just for ut
+ public boolean is32BitsEnough() {
+ switch (bitmapType) {
+ case EMPTY:
+ return true;
+ case SINGLE_VALUE:
+ return isLongValue32bitEnough(singleValue);
+ case BITMAP_VALUE:
+ return bitmap.is32BitsEnough();
+ default:
+ return false;
+ }
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Codec.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Codec.java
new file mode 100644
index 00000000..3c57a0f1
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Codec.java
@@ -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.common.io;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+/**
+ * Copied from Apache Doris
+ */
+public class Codec {
+
+ // not support encode negative value now
+ public static void encodeVarint64(long source, DataOutput out) throws IOException {
+ assert source >= 0;
+ short B = 128; // CHECKSTYLE IGNORE THIS LINE
+
+ while (source >= B) {
+ out.write((int) (source & (B - 1) | B));
+ source = source >> 7;
+ }
+ out.write((int) (source & (B - 1)));
+ }
+
+ // not support decode negative value now
+ public static long decodeVarint64(DataInput in) throws IOException {
+ long result = 0;
+ int shift = 0;
+ short B = 128; // CHECKSTYLE IGNORE THIS LINE
+
+ while (true) {
+ int oneByte = in.readUnsignedByte();
+ boolean isEnd = (oneByte & B) == 0;
+ result = result | ((long) (oneByte & B - 1) << (shift * 7));
+ if (isEnd) {
+ break;
+ }
+ shift++;
+ }
+
+ return result;
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Hll.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Hll.java
new file mode 100644
index 00000000..a28ea1d8
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Hll.java
@@ -0,0 +1,394 @@
+// 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.common.io;
+
+import org.apache.commons.codec.binary.StringUtils;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Copied from Apache Doris
+ */
+public class Hll {
+
+ public static final byte HLL_DATA_EMPTY = 0;
+ public static final byte HLL_DATA_EXPLICIT = 1;
+ public static final byte HLL_DATA_SPARSE = 2;
+ public static final byte HLL_DATA_FULL = 3;
+
+ public static final int HLL_COLUMN_PRECISION = 14;
+ public static final int HLL_ZERO_COUNT_BITS = (64 - HLL_COLUMN_PRECISION);
+ public static final int HLL_EXPLICIT_INT64_NUM = 160;
+ public static final int HLL_SPARSE_THRESHOLD = 4096;
+ public static final int HLL_REGISTERS_COUNT = 16 * 1024;
+ public static final long M64 = 0xc6a4a7935bd1e995L;
+ public static final int R64 = 47;
+ public static final int SEED = 0xadc83b19;
+ private int type;
+ private Set hashSet;
+ private byte[] registers;
+
+ public Hll() {
+ type = HLL_DATA_EMPTY;
+ this.hashSet = new HashSet<>();
+ }
+
+ public static byte getLongTailZeroNum(long hashValue) {
+ if (hashValue == 0) {
+ return 0;
+ }
+ long value = 1L;
+ byte idx = 0;
+ for (; ; idx++) {
+ if ((value & hashValue) != 0) {
+ return idx;
+ }
+ value = value << 1;
+ if (idx == 62) {
+ break;
+ }
+ }
+ return idx;
+ }
+
+ private static long getLittleEndianLong(final byte[] data, final int index) {
+ return (((long) data[index] & 0xff))
+ | (((long) data[index + 1] & 0xff) << 8)
+ | (((long) data[index + 2] & 0xff) << 16)
+ | (((long) data[index + 3] & 0xff) << 24)
+ | (((long) data[index + 4] & 0xff) << 32)
+ | (((long) data[index + 5] & 0xff) << 40)
+ | (((long) data[index + 6] & 0xff) << 48)
+ | (((long) data[index + 7] & 0xff) << 56);
+ }
+
+ public static long hash64(final byte[] data, final int length, final int seed) {
+ long h = (seed & 0xffffffffL) ^ (length * M64);
+ final int nblocks = length >> 3;
+
+ // body
+ for (int i = 0; i < nblocks; i++) {
+ final int index = (i << 3);
+ long k = getLittleEndianLong(data, index);
+
+ k *= M64;
+ k ^= k >>> R64;
+ k *= M64;
+
+ h ^= k;
+ h *= M64;
+ }
+
+ final int index = (nblocks << 3);
+ switch (length - index) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case 7:
+ h ^= ((long) data[index + 6] & 0xff) << 48;
+ case 6: // CHECKSTYLE IGNORE THIS LINE: fall through
+ h ^= ((long) data[index + 5] & 0xff) << 40;
+ case 5: // CHECKSTYLE IGNORE THIS LINE: fall through
+ h ^= ((long) data[index + 4] & 0xff) << 32;
+ case 4: // CHECKSTYLE IGNORE THIS LINE: fall through
+ h ^= ((long) data[index + 3] & 0xff) << 24;
+ case 3: // CHECKSTYLE IGNORE THIS LINE: fall through
+ h ^= ((long) data[index + 2] & 0xff) << 16;
+ case 2: // CHECKSTYLE IGNORE THIS LINE: fall through
+ h ^= ((long) data[index + 1] & 0xff) << 8;
+ case 1: // CHECKSTYLE IGNORE THIS LINE: fall through
+ h ^= ((long) data[index] & 0xff);
+ h *= M64;
+ }
+
+ h ^= h >>> R64;
+ h *= M64;
+ h ^= h >>> R64;
+
+ return h;
+ }
+
+ private void convertExplicitToRegister() {
+ assert this.type == HLL_DATA_EXPLICIT;
+ registers = new byte[HLL_REGISTERS_COUNT];
+ for (Long value : hashSet) {
+ updateRegisters(value);
+ }
+ hashSet.clear();
+ }
+
+ private void updateRegisters(long hashValue) {
+ int idx;
+ // hash value less than zero means we get a unsigned long
+ // so need to transfer to BigInter to mod
+ if (hashValue < 0) {
+ BigInteger unint64HashValue = new BigInteger(Long.toUnsignedString(hashValue));
+ unint64HashValue = unint64HashValue.mod(new BigInteger(Long.toUnsignedString(HLL_REGISTERS_COUNT)));
+ idx = unint64HashValue.intValue();
+ } else {
+ idx = (int) (hashValue % HLL_REGISTERS_COUNT);
+ }
+
+ hashValue >>>= HLL_COLUMN_PRECISION;
+ hashValue |= (1L << HLL_ZERO_COUNT_BITS);
+ byte firstOneBit = (byte) (getLongTailZeroNum(hashValue) + 1);
+ registers[idx] = registers[idx] > firstOneBit ? registers[idx] : firstOneBit;
+ }
+
+ private void mergeRegisters(byte[] other) {
+ for (int i = 0; i < HLL_REGISTERS_COUNT; i++) {
+ this.registers[i] = this.registers[i] > other[i] ? this.registers[i] : other[i];
+ }
+ }
+
+ public void updateWithHash(Object value) {
+ byte[] v = StringUtils.getBytesUtf8(String.valueOf(value));
+ update(hash64(v, v.length, SEED));
+ }
+
+ public void update(long hashValue) {
+ switch (this.type) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case HLL_DATA_EMPTY:
+ hashSet.add(hashValue);
+ type = HLL_DATA_EXPLICIT;
+ break;
+ case HLL_DATA_EXPLICIT:
+ if (hashSet.size() < HLL_EXPLICIT_INT64_NUM) {
+ hashSet.add(hashValue);
+ break;
+ }
+ convertExplicitToRegister();
+ type = HLL_DATA_FULL;
+ case HLL_DATA_SPARSE: // CHECKSTYLE IGNORE THIS LINE: fall through
+ case HLL_DATA_FULL:
+ updateRegisters(hashValue);
+ break;
+ }
+ }
+
+ public void merge(Hll other) {
+ if (other.type == HLL_DATA_EMPTY) {
+ return;
+ }
+ switch (this.type) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case HLL_DATA_EMPTY:
+ this.type = other.type;
+ switch (other.type) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case HLL_DATA_EXPLICIT:
+ this.hashSet.addAll(other.hashSet);
+ break;
+ case HLL_DATA_SPARSE:
+ case HLL_DATA_FULL:
+ this.registers = new byte[HLL_REGISTERS_COUNT];
+ System.arraycopy(other.registers, 0, this.registers, 0, HLL_REGISTERS_COUNT);
+ break;
+ }
+ break;
+ case HLL_DATA_EXPLICIT:
+ switch (other.type) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case HLL_DATA_EXPLICIT:
+ this.hashSet.addAll(other.hashSet);
+ if (this.hashSet.size() > HLL_EXPLICIT_INT64_NUM) {
+ convertExplicitToRegister();
+ this.type = HLL_DATA_FULL;
+ }
+ break;
+ case HLL_DATA_SPARSE:
+ case HLL_DATA_FULL:
+ convertExplicitToRegister();
+ mergeRegisters(other.registers);
+ this.type = HLL_DATA_FULL;
+ break;
+ }
+ break;
+ case HLL_DATA_SPARSE:
+ case HLL_DATA_FULL:
+ switch (other.type) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case HLL_DATA_EXPLICIT:
+ for (long value : other.hashSet) {
+ update(value);
+ }
+ break;
+ case HLL_DATA_SPARSE:
+ case HLL_DATA_FULL:
+ mergeRegisters(other.registers);
+ break;
+ }
+ break;
+ }
+ }
+
+ public void serialize(DataOutput output) throws IOException {
+ switch (type) { // CHECKSTYLE IGNORE THIS LINE: missing switch default
+ case HLL_DATA_EMPTY:
+ output.writeByte(type);
+ break;
+ case HLL_DATA_EXPLICIT:
+ output.writeByte(type);
+ output.writeByte(hashSet.size());
+ for (long value : hashSet) {
+ output.writeLong(Long.reverseBytes(value));
+ }
+ break;
+ case HLL_DATA_SPARSE:
+ case HLL_DATA_FULL:
+ int nonZeroRegisterNum = 0;
+ for (int i = 0; i < HLL_REGISTERS_COUNT; i++) {
+ if (registers[i] != 0) {
+ nonZeroRegisterNum++;
+ }
+ }
+ if (nonZeroRegisterNum > HLL_SPARSE_THRESHOLD) {
+ output.writeByte(HLL_DATA_FULL);
+ for (byte value : registers) {
+ output.writeByte(value);
+ }
+ } else {
+ output.writeByte(HLL_DATA_SPARSE);
+ output.writeInt(Integer.reverseBytes(nonZeroRegisterNum));
+ for (int i = 0; i < HLL_REGISTERS_COUNT; i++) {
+ if (registers[i] != 0) {
+ output.writeShort(Short.reverseBytes((short) i));
+ output.writeByte(registers[i]);
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ public boolean deserialize(DataInput input) throws IOException {
+ assert type == HLL_DATA_EMPTY;
+
+ if (input == null) {
+ return false;
+ }
+
+ this.type = input.readByte();
+ switch (this.type) {
+ case HLL_DATA_EMPTY:
+ break;
+ case HLL_DATA_EXPLICIT:
+ int hashSetSize = input.readUnsignedByte();
+ for (int i = 0; i < hashSetSize; i++) {
+ update(Long.reverseBytes(input.readLong()));
+ }
+ assert this.type == HLL_DATA_EXPLICIT;
+ break;
+ case HLL_DATA_SPARSE:
+ int sparseDataSize = Integer.reverseBytes(input.readInt());
+ this.registers = new byte[HLL_REGISTERS_COUNT];
+ for (int i = 0; i < sparseDataSize; i++) {
+ int idx = Short.reverseBytes(input.readShort());
+ byte value = input.readByte();
+ registers[idx] = value;
+ }
+ break;
+ case HLL_DATA_FULL:
+ this.registers = new byte[HLL_REGISTERS_COUNT];
+ for (int i = 0; i < HLL_REGISTERS_COUNT; i++) {
+ registers[i] = input.readByte();
+ }
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ // use strictfp to force java follow IEEE 754 to deal float point strictly
+ public strictfp long estimateCardinality() {
+ if (type == HLL_DATA_EMPTY) {
+ return 0;
+ }
+ if (type == HLL_DATA_EXPLICIT) {
+ return hashSet.size();
+ }
+
+ int numStreams = HLL_REGISTERS_COUNT;
+ float alpha = 0;
+
+ if (numStreams == 16) {
+ alpha = 0.673f;
+ } else if (numStreams == 32) {
+ alpha = 0.697f;
+ } else if (numStreams == 64) {
+ alpha = 0.709f;
+ } else {
+ alpha = 0.7213f / (1 + 1.079f / numStreams);
+ }
+
+ float harmonicMean = 0;
+ int numZeroRegisters = 0;
+
+ for (int i = 0; i < HLL_REGISTERS_COUNT; i++) {
+ harmonicMean += Math.pow(2.0f, -registers[i]);
+
+ if (registers[i] == 0) {
+ numZeroRegisters++;
+ }
+ }
+
+ harmonicMean = 1.0f / harmonicMean;
+ double estimate = alpha * numStreams * numStreams * harmonicMean;
+
+ if (estimate <= numStreams * 2.5 && numZeroRegisters != 0) {
+ estimate = numStreams * Math.log(((float) numStreams) / ((float) numZeroRegisters));
+ } else if (numStreams == 16384 && estimate < 72000) {
+ double bias = 5.9119 * 1.0e-18 * (estimate * estimate * estimate * estimate)
+ - 1.4253 * 1.0e-12 * (estimate * estimate * estimate)
+ + 1.2940 * 1.0e-7 * (estimate * estimate)
+ - 5.2921 * 1.0e-3 * estimate
+ + 83.3216;
+ estimate -= estimate * (bias / 100);
+ }
+
+ return (long) (estimate + 0.5);
+ }
+
+ public int maxSerializedSize() {
+ switch (type) {
+ case HLL_DATA_EMPTY:
+ default:
+ return 1;
+ case HLL_DATA_EXPLICIT:
+ return 2 + hashSet.size() * 8;
+ case HLL_DATA_SPARSE:
+ case HLL_DATA_FULL:
+ return 1 + HLL_REGISTERS_COUNT;
+ }
+ }
+
+ // just for ut
+ public int getType() {
+ return type;
+ }
+
+ // For convert to statistics used Hll128
+ public byte[] getRegisters() {
+ return registers;
+ }
+
+ // For convert to statistics used Hll128
+ public Set getHashSet() {
+ return hashSet;
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Roaring64Map.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Roaring64Map.java
new file mode 100644
index 00000000..33237983
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/io/Roaring64Map.java
@@ -0,0 +1,1432 @@
+// 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.common.io;
+
+import org.roaringbitmap.BitmapDataProvider;
+import org.roaringbitmap.BitmapDataProviderSupplier;
+import org.roaringbitmap.IntConsumer;
+import org.roaringbitmap.IntIterator;
+import org.roaringbitmap.InvalidRoaringFormat;
+import org.roaringbitmap.RoaringBitmap;
+import org.roaringbitmap.RoaringBitmapSupplier;
+import org.roaringbitmap.Util;
+import org.roaringbitmap.buffer.MutableRoaringBitmap;
+import org.roaringbitmap.longlong.ImmutableLongBitmapDataProvider;
+import org.roaringbitmap.longlong.LongConsumer;
+import org.roaringbitmap.longlong.LongIterator;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Copied from Apache Doris
+ */
+public class Roaring64Map {
+
+ private static final boolean DEFAULT_ORDER_IS_SIGNED = false;
+ private static final boolean DEFAULT_CARDINALITIES_ARE_CACHED = true;
+ /**
+ * the constant 2^64
+ */
+ private static final BigInteger TWO_64 = BigInteger.ONE.shiftLeft(64);
+ // Not final to enable initialization in Externalizable.readObject
+ private NavigableMap highToBitmap;
+ // If true, we handle longs a plain java longs: -1 if right before 0
+ // If false, we handle longs as unsigned longs: 0 has no predecessor and Long.MAX_VALUE + 1L is
+ // expressed as a
+ // negative long
+ private boolean signedLongs = false;
+ private BitmapDataProviderSupplier supplier;
+ // By default, we cache cardinalities
+ private transient boolean doCacheCardinalities = true;
+ // Prevent recomputing all cardinalities when requesting consecutive ranks
+ private transient int firstHighNotValid = highestHigh() + 1;
+ // This boolean needs firstHighNotValid == Integer.MAX_VALUE to be allowed to be true
+ // If false, it means nearly all cumulated cardinalities are valid, except high=Integer.MAX_VALUE
+ // If true, it means all cumulated cardinalities are valid, even high=Integer.MAX_VALUE
+ private transient boolean allValid = false;
+ // TODO: I would prefer not managing arrays myself
+ private transient long[] sortedCumulatedCardinality = new long[0];
+ private transient int[] sortedHighs = new int[0];
+ // We guess consecutive .addLong will be on proximate longs: we remember the bitmap attached to
+ // this bucket in order
+ // to skip the indirection
+ private transient Map.Entry latestAddedHigh = null;
+
+ /**
+ * By default, we consider longs are unsigned longs: normal longs: 0 is the lowest possible long.
+ * Long.MAX_VALUE is followed by Long.MIN_VALUE. -1L is the highest possible value
+ */
+ public Roaring64Map() {
+ this(DEFAULT_ORDER_IS_SIGNED);
+ }
+
+ /**
+ * By default, use RoaringBitmap as underlyings {@link BitmapDataProvider}
+ *
+ * @param signedLongs true if longs has to be ordered as plain java longs. False to handle them as
+ * unsigned 64bits long (as RoaringBitmap with unsigned integers)
+ */
+ public Roaring64Map(boolean signedLongs) {
+ this(signedLongs, DEFAULT_CARDINALITIES_ARE_CACHED);
+ }
+
+ /**
+ * By default, use RoaringBitmap as underlyings {@link BitmapDataProvider}
+ *
+ * @param signedLongs true if longs has to be ordered as plain java longs. False to handle them as
+ * unsigned 64bits long (as RoaringBitmap with unsigned integers)
+ * @param cacheCardinalities true if cardinalities have to be cached. It will prevent many
+ * iteration along the NavigableMap
+ */
+ public Roaring64Map(boolean signedLongs, boolean cacheCardinalities) {
+ this(signedLongs, cacheCardinalities, new RoaringBitmapSupplier());
+ }
+
+ /**
+ * By default, longs are managed as unsigned longs and cardinalities are cached.
+ *
+ * @param supplier provide the logic to instantiate new {@link BitmapDataProvider}, typically
+ * instantiated once per high.
+ */
+ public Roaring64Map(BitmapDataProviderSupplier supplier) {
+ this(DEFAULT_ORDER_IS_SIGNED, DEFAULT_CARDINALITIES_ARE_CACHED, supplier);
+ }
+
+ /**
+ * By default, we activating cardinalities caching.
+ *
+ * @param signedLongs true if longs has to be ordered as plain java longs. False to handle them as
+ * unsigned 64bits long (as RoaringBitmap with unsigned integers)
+ * @param supplier provide the logic to instantiate new {@link BitmapDataProvider}, typically
+ * instantiated once per high.
+ */
+ public Roaring64Map(boolean signedLongs, BitmapDataProviderSupplier supplier) {
+ this(signedLongs, DEFAULT_CARDINALITIES_ARE_CACHED, supplier);
+ }
+
+ /**
+ * @param signedLongs true if longs has to be ordered as plain java longs. False to handle them as
+ * unsigned 64bits long (as RoaringBitmap with unsigned integers)
+ * @param cacheCardinalities true if cardinalities have to be cached. It will prevent many
+ * iteration along the NavigableMap
+ * @param supplier provide the logic to instantiate new {@link BitmapDataProvider}, typically
+ * instantiated once per high.
+ */
+ public Roaring64Map(boolean signedLongs, boolean cacheCardinalities,
+ BitmapDataProviderSupplier supplier) {
+ this.signedLongs = signedLongs;
+ this.supplier = supplier;
+
+ if (signedLongs) {
+ highToBitmap = new TreeMap<>();
+ } else {
+ highToBitmap = new TreeMap<>(unsignedComparator());
+ }
+
+ this.doCacheCardinalities = cacheCardinalities;
+ resetPerfHelpers();
+ }
+
+ // From Arrays.binarySearch (Comparator). Check with org.roaringbitmap.Util.unsignedBinarySearch
+ private static int unsignedBinarySearch(int[] a, int fromIndex, int toIndex, int key,
+ Comparator super Integer> c) {
+ int low = fromIndex;
+ int high = toIndex - 1;
+
+ while (low <= high) {
+ int mid = (low + high) >>> 1;
+ int midVal = a[mid];
+ int cmp = c.compare(midVal, key);
+ if (cmp < 0) {
+ low = mid + 1;
+ } else if (cmp > 0) {
+ high = mid - 1;
+ } else {
+ return mid; // key found
+ }
+ }
+ return -(low + 1); // key not found.
+ }
+
+ /**
+ * Generate a bitmap with the specified values set to true. The provided longs values don't have
+ * to be in sorted order, but it may be preferable to sort them from a performance point of view.
+ *
+ * @param dat set values
+ * @return a new bitmap
+ */
+ public static Roaring64Map bitmapOf(final long... dat) {
+ final Roaring64Map ans = new Roaring64Map();
+ ans.add(dat);
+ return ans;
+ }
+
+ /**
+ * @param id any long, positive or negative
+ * @return an int holding the 32 highest order bits of information of the input long
+ */
+ public static int high(long id) {
+ return (int) (id >> 32);
+ }
+
+ /**
+ * @param id any long, positive or negative
+ * @return an int holding the 32 lowest order bits of information of the input long
+ */
+ public static int low(long id) {
+ return (int) id;
+ }
+
+ /**
+ * @param high an integer representing the highest order bits of the output long
+ * @param low an integer representing the lowest order bits of the output long
+ * @return a long packing together the integers as computed by
+ * {@link #high(long)} and {@link #low(long)}
+ */
+ // https://stackoverflow.com/questions/12772939/java-storing-two-ints-in-a-long
+ public static long pack(int high, int low) {
+ return (((long) high) << 32) | (low & 0xffffffffL);
+ }
+
+ /**
+ * @param signedLongs true if long put in a {@link Roaring64Map} should be considered as
+ * signed long.
+ * @return the int representing the highest value which can be set as high value in a
+ */
+ public static int highestHigh(boolean signedLongs) {
+ if (signedLongs) {
+ return Integer.MAX_VALUE;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * @return A comparator for unsigned longs: a negative long is a long greater than Long.MAX_VALUE
+ */
+ public static Comparator unsignedComparator() {
+ return new Comparator() {
+
+ @Override
+ public int compare(Integer o1, Integer o2) {
+ return compareUnsigned(o1, o2);
+ }
+ };
+ }
+
+ /**
+ * Compares two {@code int} values numerically treating the values as unsigned.
+ *
+ * @param x the first {@code int} to compare
+ * @param y the second {@code int} to compare
+ * @return the value {@code 0} if {@code x == y}; a value less than {@code 0} if {@code x < y} as
+ * unsigned values; and a value greater than {@code 0} if {@code x > y} as unsigned values
+ * @since 1.8
+ */
+ // Duplicated from jdk8 Integer.compareUnsigned
+ public static int compareUnsigned(int x, int y) {
+ return Integer.compare(x + Integer.MIN_VALUE, y + Integer.MIN_VALUE);
+ }
+
+ /**
+ * JDK8 Long.toUnsignedString was too complex to backport. Go for a slow version relying on
+ * BigInteger
+ */
+ // https://stackoverflow.com/questions/7031198/java-signed-long-to-unsigned-long-string
+ static String toUnsignedString(long l) {
+ BigInteger b = BigInteger.valueOf(l);
+ if (b.signum() < 0) {
+ b = b.add(TWO_64);
+ }
+ return b.toString();
+ }
+
+ private void resetPerfHelpers() {
+ firstHighNotValid = highestHigh(signedLongs) + 1;
+ allValid = false;
+
+ sortedCumulatedCardinality = new long[0];
+ sortedHighs = new int[0];
+
+ latestAddedHigh = null;
+ }
+
+ // Package-friendly: for the sake of unit-testing
+ // @VisibleForTesting
+ NavigableMap getHighToBitmap() {
+ return highToBitmap;
+ }
+
+ // Package-friendly: for the sake of unit-testing
+ // @VisibleForTesting
+ int getLowestInvalidHigh() {
+ return firstHighNotValid;
+ }
+
+ // Package-friendly: for the sake of unit-testing
+ // @VisibleForTesting
+ long[] getSortedCumulatedCardinality() {
+ return sortedCumulatedCardinality;
+ }
+
+ /**
+ * Add the value to the container (set the value to "true"), whether it already appears or not.
+ *
+ * Java lacks native unsigned longs but the x argument is considered to be unsigned. Within
+ * bitmaps, numbers are ordered according to {@link Long#compareUnsigned}. We order the numbers
+ * like 0, 1, ..., 9223372036854775807, -9223372036854775808, -9223372036854775807,..., -1.
+ *
+ * @param x long value
+ */
+ public void addLong(long x) {
+ int high = high(x);
+ int low = low(x);
+
+ // Copy the reference to prevent race-condition
+ Map.Entry local = latestAddedHigh;
+
+ BitmapDataProvider bitmap;
+ if (local != null && local.getKey().intValue() == high) {
+ bitmap = local.getValue();
+ } else {
+ bitmap = highToBitmap.get(high);
+ if (bitmap == null) {
+ bitmap = newRoaringBitmap();
+ pushBitmapForHigh(high, bitmap);
+ }
+ latestAddedHigh = new AbstractMap.SimpleImmutableEntry<>(high, bitmap);
+ }
+ bitmap.add(low);
+
+ invalidateAboveHigh(high);
+ }
+
+ /**
+ * Add the integer value to the container (set the value to "true"), whether it already appears or
+ * not.
+ *
+ * Javac lacks native unsigned integers but the x argument is considered to be unsigned. Within
+ * bitmaps, numbers are ordered according to {@link Integer#compareUnsigned}. We order the numbers
+ * like 0, 1, ..., 2147483647, -2147483648, -2147483647,..., -1.
+ *
+ * @param x integer value
+ */
+ public void addInt(int x) {
+ addLong(Util.toUnsignedLong(x));
+ }
+
+ private BitmapDataProvider newRoaringBitmap() {
+ return supplier.newEmpty();
+ }
+
+ private void invalidateAboveHigh(int high) {
+ // The cardinalities after this bucket may not be valid anymore
+ if (compare(firstHighNotValid, high) > 0) {
+ // High was valid up to now
+ firstHighNotValid = high;
+
+ int indexNotValid = binarySearch(sortedHighs, firstHighNotValid);
+
+ final int indexAfterWhichToReset;
+ if (indexNotValid >= 0) {
+ indexAfterWhichToReset = indexNotValid;
+ } else {
+ // We have invalidate a high not already present: added a value for a brand new high
+ indexAfterWhichToReset = -indexNotValid - 1;
+ }
+
+ // This way, sortedHighs remains sorted, without making a new/shorter array
+ Arrays.fill(sortedHighs, indexAfterWhichToReset, sortedHighs.length, highestHigh());
+ }
+ allValid = false;
+ }
+
+ private int compare(int x, int y) {
+ if (signedLongs) {
+ return Integer.compare(x, y);
+ } else {
+ return compareUnsigned(x, y);
+ }
+ }
+
+ private void pushBitmapForHigh(int high, BitmapDataProvider bitmap) {
+ // TODO .size is too slow
+ // int nbHighBefore = highToBitmap.headMap(high).size();
+
+ BitmapDataProvider previous = highToBitmap.put(high, bitmap);
+ assert previous == null : "Should push only not-existing high";
+ }
+
+ /**
+ * Returns the number of distinct integers added to the bitmap (e.g., number of bits set).
+ *
+ * @return the cardinality
+ */
+ public long getLongCardinality() {
+ if (doCacheCardinalities) {
+ if (highToBitmap.isEmpty()) {
+ return 0L;
+ }
+ int indexOk = ensureCumulatives(highestHigh());
+
+ // ensureCumulatives may have removed empty bitmaps
+ if (highToBitmap.isEmpty()) {
+ return 0L;
+ }
+
+
+ return sortedCumulatedCardinality[indexOk - 1];
+ } else {
+ long cardinality = 0L;
+ for (BitmapDataProvider bitmap : highToBitmap.values()) {
+ cardinality += bitmap.getLongCardinality();
+ }
+ return cardinality;
+ }
+ }
+
+ /**
+ * @return the cardinality as an int
+ * @throws UnsupportedOperationException if the cardinality does not fit in an int
+ */
+ public int getIntCardinality() throws UnsupportedOperationException {
+ long cardinality = getLongCardinality();
+
+ if (cardinality > Integer.MAX_VALUE) {
+ // TODO: we should handle cardinality fitting in an unsigned int
+ throw new UnsupportedOperationException(
+ "Can not call .getIntCardinality as the cardinality is bigger than Integer.MAX_VALUE");
+ }
+
+ return (int) cardinality;
+ }
+
+ /**
+ * Return the jth value stored in this bitmap.
+ *
+ * @param j index of the value
+ * @return the value
+ * @throws IllegalArgumentException if j is out of the bounds of the bitmap cardinality
+ */
+ public long select(final long j) throws IllegalArgumentException {
+ if (!doCacheCardinalities) {
+ return selectNoCache(j);
+ }
+
+ // Ensure all cumulatives as we we have straightforward way to know in advance the high of the
+ // j-th value
+ int indexOk = ensureCumulatives(highestHigh());
+
+ if (highToBitmap.isEmpty()) {
+ return throwSelectInvalidIndex(j);
+ }
+
+ // Use normal binarySearch as cardinality does not depends on considering longs signed or
+ // unsigned
+ // We need sortedCumulatedCardinality not to contain duplicated, else binarySearch may return
+ // any of the duplicates: we need to ensure it holds no high associated to an empty bitmap
+ int position = Arrays.binarySearch(sortedCumulatedCardinality, 0, indexOk, j);
+
+ if (position >= 0) {
+ if (position == indexOk - 1) {
+ // .select has been called on this.getCardinality
+ return throwSelectInvalidIndex(j);
+ }
+
+ // There is a bucket leading to this cardinality: the j-th element is the first element of
+ // next bucket
+ int high = sortedHighs[position + 1];
+ BitmapDataProvider nextBitmap = highToBitmap.get(high);
+ return pack(high, nextBitmap.select(0));
+ } else {
+ // There is no bucket with this cardinality
+ int insertionPoint = -position - 1;
+
+ final long previousBucketCardinality;
+ if (insertionPoint == 0) {
+ previousBucketCardinality = 0L;
+ } else if (insertionPoint >= indexOk) {
+ return throwSelectInvalidIndex(j);
+ } else {
+ previousBucketCardinality = sortedCumulatedCardinality[insertionPoint - 1];
+ }
+
+ // We get a 'select' query for a single bitmap: should fit in an int
+ final int givenBitmapSelect = (int) (j - previousBucketCardinality);
+
+ int high = sortedHighs[insertionPoint];
+ BitmapDataProvider lowBitmap = highToBitmap.get(high);
+ int low = lowBitmap.select(givenBitmapSelect);
+
+ return pack(high, low);
+ }
+ }
+
+ // For benchmarks: compute without using cardinalities cache
+ // https://github.com/RoaringBitmap/CRoaring/blob/master/cpp/roaring64map.hh
+ private long selectNoCache(long j) {
+ long left = j;
+
+ for (Map.Entry entry : highToBitmap.entrySet()) {
+ long lowCardinality = entry.getValue().getCardinality();
+
+ if (left >= lowCardinality) {
+ left -= lowCardinality;
+ } else {
+ // It is legit for left to be negative
+ int leftAsUnsignedInt = (int) left;
+ return pack(entry.getKey(), entry.getValue().select(leftAsUnsignedInt));
+ }
+ }
+
+ return throwSelectInvalidIndex(j);
+ }
+
+ private long throwSelectInvalidIndex(long j) {
+ // see org.roaringbitmap.buffer.ImmutableRoaringBitmap.select(int)
+ throw new IllegalArgumentException(
+ "select " + j + " when the cardinality is " + this.getLongCardinality());
+ }
+
+ /**
+ * For better performance, consider the Use the {@link #forEach forEach} method.
+ *
+ * @return a custom iterator over set bits, the bits are traversed in ascending sorted order
+ */
+ public Iterator iterator() {
+ final LongIterator it = getLongIterator();
+
+ return new Iterator() {
+
+ @Override
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ @Override
+ public Long next() {
+ return it.next();
+ }
+
+ @Override
+ public void remove() {
+ // TODO?
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ public void forEach(final LongConsumer lc) {
+ for (final Map.Entry highEntry : highToBitmap.entrySet()) {
+ highEntry.getValue().forEach(new IntConsumer() {
+
+ @Override
+ public void accept(int low) {
+ lc.accept(pack(highEntry.getKey(), low));
+ }
+ });
+ }
+ }
+
+ public long rankLong(long id) {
+ int high = high(id);
+ int low = low(id);
+
+ if (!doCacheCardinalities) {
+ return rankLongNoCache(high, low);
+ }
+
+ int indexOk = ensureCumulatives(high);
+
+ int highPosition = binarySearch(sortedHighs, 0, indexOk, high);
+
+ if (highPosition >= 0) {
+ // There is a bucket holding this item
+
+ final long previousBucketCardinality;
+ if (highPosition == 0) {
+ previousBucketCardinality = 0;
+ } else {
+ previousBucketCardinality = sortedCumulatedCardinality[highPosition - 1];
+ }
+
+ BitmapDataProvider lowBitmap = highToBitmap.get(sortedHighs[highPosition]);
+
+ // Rank is previous cardinality plus rank in current bitmap
+ return previousBucketCardinality + lowBitmap.rankLong(low);
+ } else {
+ // There is no bucket holding this item: insertionPoint is previous bitmap
+ int insertionPoint = -highPosition - 1;
+
+ if (insertionPoint == 0) {
+ // this key is before all inserted keys
+ return 0;
+ } else {
+ // The rank is the cardinality of this previous bitmap
+ return sortedCumulatedCardinality[insertionPoint - 1];
+ }
+ }
+ }
+
+ // https://github.com/RoaringBitmap/CRoaring/blob/master/cpp/roaring64map.hh
+ private long rankLongNoCache(int high, int low) {
+ long result = 0L;
+
+ BitmapDataProvider lastBitmap = highToBitmap.get(high);
+ if (lastBitmap == null) {
+ // There is no value with same high: the rank is a sum of cardinalities
+ for (Map.Entry bitmap : highToBitmap.entrySet()) {
+ if (bitmap.getKey().intValue() > high) {
+ break;
+ } else {
+ result += bitmap.getValue().getLongCardinality();
+ }
+ }
+ } else {
+ for (BitmapDataProvider bitmap : highToBitmap.values()) {
+ if (bitmap == lastBitmap) {
+ result += bitmap.rankLong(low);
+ break;
+ } else {
+ result += bitmap.getLongCardinality();
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * @param high for which high bucket should we compute the cardinality
+ * @return the highest validatedIndex
+ */
+ protected int ensureCumulatives(int high) {
+ if (allValid) {
+ // the whole array is valid (up-to its actual length, not its capacity)
+ return highToBitmap.size();
+ } else if (compare(high, firstHighNotValid) < 0) {
+ // The high is strictly below the first not valid: it is valid
+
+ // sortedHighs may have only a subset of valid values on the right. However, these invalid
+ // values have been set to maxValue, and we are here as high < firstHighNotValid ==> high <
+ // maxHigh()
+ int position = binarySearch(sortedHighs, high);
+
+ if (position >= 0) {
+ // This high has a bitmap: +1 as this index will be used as right (excluded) bound in a
+ // binary-search
+ return position + 1;
+ } else {
+ // This high has no bitmap: it could be between 2 highs with bitmaps
+ int insertionPosition = -position - 1;
+ return insertionPosition;
+ }
+ } else {
+
+ // For each deprecated buckets
+ SortedMap tailMap =
+ highToBitmap.tailMap(firstHighNotValid, true);
+
+ // TODO .size on tailMap make an iterator: arg
+ int indexOk = highToBitmap.size() - tailMap.size();
+
+ // TODO: It should be possible to compute indexOk based on sortedHighs array
+ // assert indexOk == binarySearch(sortedHighs, firstHighNotValid);
+
+ Iterator> it = tailMap.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry e = it.next();
+ int currentHigh = e.getKey();
+
+ if (compare(currentHigh, high) > 0) {
+ // No need to compute more than needed
+ break;
+ } else if (e.getValue().isEmpty()) {
+ // highToBitmap can not be modified as we iterate over it
+ if (latestAddedHigh != null && latestAddedHigh.getKey().intValue() == currentHigh) {
+ // Dismiss the cached bitmap as it is removed from the NavigableMap
+ latestAddedHigh = null;
+ }
+ it.remove();
+ } else {
+ ensureOne(e, currentHigh, indexOk);
+
+ // We have added one valid cardinality
+ indexOk++;
+ }
+
+ }
+
+ if (highToBitmap.isEmpty() || indexOk == highToBitmap.size()) {
+ // We have compute all cardinalities
+ allValid = true;
+ }
+
+ return indexOk;
+ }
+ }
+
+ private int binarySearch(int[] array, int key) {
+ if (signedLongs) {
+ return Arrays.binarySearch(array, key);
+ } else {
+ return unsignedBinarySearch(array, 0, array.length, key,
+ unsignedComparator());
+ }
+ }
+
+ private int binarySearch(int[] array, int from, int to, int key) {
+ if (signedLongs) {
+ return Arrays.binarySearch(array, from, to, key);
+ } else {
+ return unsignedBinarySearch(array, from, to, key, unsignedComparator());
+ }
+ }
+
+ private void ensureOne(Map.Entry e, int currentHigh, int indexOk) {
+ // sortedHighs are valid only up to some index
+ assert indexOk <= sortedHighs.length : indexOk + " is bigger than " + sortedHighs.length;
+
+ final int index;
+ if (indexOk == 0) {
+ if (sortedHighs.length == 0) {
+ index = -1;
+ // } else if (sortedHighs[0] == currentHigh) {
+ // index = 0;
+ } else {
+ index = -1;
+ }
+ } else if (indexOk < sortedHighs.length) {
+ index = -indexOk - 1;
+ } else {
+ index = -sortedHighs.length - 1;
+ }
+ assert index == binarySearch(sortedHighs, 0, indexOk, currentHigh) : "Computed " + index
+ + " differs from dummy binary-search index: "
+ + binarySearch(sortedHighs, 0, indexOk, currentHigh);
+
+ if (index >= 0) {
+ // This would mean calling .ensureOne is useless: should never got here at the first time
+ throw new IllegalStateException("Unexpectedly found " + currentHigh + " in "
+ + Arrays.toString(sortedHighs) + " strictly before index" + indexOk);
+ } else {
+ int insertionPosition = -index - 1;
+
+ // This is a new key
+ if (insertionPosition >= sortedHighs.length) {
+ int previousSize = sortedHighs.length;
+
+ // TODO softer growing factor
+ int newSize = Math.min(Integer.MAX_VALUE, sortedHighs.length * 2 + 1);
+
+ // Insertion at the end
+ sortedHighs = Arrays.copyOf(sortedHighs, newSize);
+ sortedCumulatedCardinality = Arrays.copyOf(sortedCumulatedCardinality, newSize);
+
+ // Not actually needed. But simplify the reading of array content
+ Arrays.fill(sortedHighs, previousSize, sortedHighs.length, highestHigh());
+ Arrays.fill(sortedCumulatedCardinality, previousSize, sortedHighs.length, Long.MAX_VALUE);
+ }
+ sortedHighs[insertionPosition] = currentHigh;
+
+ final long previousCardinality;
+ if (insertionPosition >= 1) {
+ previousCardinality = sortedCumulatedCardinality[insertionPosition - 1];
+ } else {
+ previousCardinality = 0;
+ }
+
+ sortedCumulatedCardinality[insertionPosition] =
+ previousCardinality + e.getValue().getLongCardinality();
+
+ if (currentHigh == highestHigh()) {
+ // We are already on the highest high. Do not set allValid as it is set anyway out of the
+ // loop
+ firstHighNotValid = currentHigh;
+ } else {
+ // The first not valid is the next high
+ // TODO: The entry comes from a NavigableMap: it may be quite cheap to know the next high
+ firstHighNotValid = currentHigh + 1;
+ }
+ }
+ }
+
+ private int highestHigh() {
+ return highestHigh(signedLongs);
+ }
+
+ /**
+ * In-place bitwise OR (union) operation. The current bitmap is modified.
+ *
+ * @param x2 other bitmap
+ */
+ public void or(final Roaring64Map x2) {
+ boolean firstBucket = true;
+
+ for (Map.Entry e2 : x2.highToBitmap.entrySet()) {
+ // Keep object to prevent auto-boxing
+ Integer high = e2.getKey();
+
+ BitmapDataProvider lowBitmap1 = this.highToBitmap.get(high);
+
+ BitmapDataProvider lowBitmap2 = e2.getValue();
+
+ // TODO Reviewers: is it a good idea to rely on BitmapDataProvider except in methods
+ // expecting an actual MutableRoaringBitmap?
+ // TODO This code may lead to closing a buffer Bitmap in current Navigable even if current is
+ // not on buffer
+ if ((lowBitmap1 == null || lowBitmap1 instanceof RoaringBitmap)
+ && lowBitmap2 instanceof RoaringBitmap) {
+ if (lowBitmap1 == null) {
+ // Clone to prevent future modification of this modifying the input Bitmap
+ RoaringBitmap lowBitmap2Clone = ((RoaringBitmap) lowBitmap2).clone();
+
+ pushBitmapForHigh(high, lowBitmap2Clone);
+ } else {
+ ((RoaringBitmap) lowBitmap1).or((RoaringBitmap) lowBitmap2);
+ }
+ } else if ((lowBitmap1 == null || lowBitmap1 instanceof MutableRoaringBitmap)
+ && lowBitmap2 instanceof MutableRoaringBitmap) {
+ if (lowBitmap1 == null) {
+ // Clone to prevent future modification of this modifying the input Bitmap
+ BitmapDataProvider lowBitmap2Clone = ((MutableRoaringBitmap) lowBitmap2).clone();
+ pushBitmapForHigh(high, lowBitmap2Clone);
+ } else {
+ ((MutableRoaringBitmap) lowBitmap1).or((MutableRoaringBitmap) lowBitmap2);
+ }
+ } else {
+ throw new UnsupportedOperationException(
+ ".or is not between " + this.getClass() + " and " + lowBitmap2.getClass());
+ }
+
+ if (firstBucket) {
+ firstBucket = false;
+
+ // Invalidate the lowest high as lowest not valid
+ firstHighNotValid = Math.min(firstHighNotValid, high);
+ allValid = false;
+ }
+ }
+ }
+
+ /**
+ * In-place bitwise XOR (symmetric difference) operation. The current bitmap is modified.
+ *
+ * @param x2 other bitmap
+ */
+ public void xor(final Roaring64Map x2) {
+ boolean firstBucket = true;
+
+ for (Map.Entry e2 : x2.highToBitmap.entrySet()) {
+ // Keep object to prevent auto-boxing
+ Integer high = e2.getKey();
+
+ BitmapDataProvider lowBitmap1 = this.highToBitmap.get(high);
+
+ BitmapDataProvider lowBitmap2 = e2.getValue();
+
+ // TODO Reviewers: is it a good idea to rely on BitmapDataProvider except in methods
+ // expecting an actual MutableRoaringBitmap?
+ // TODO This code may lead to closing a buffer Bitmap in current Navigable even if current is
+ // not on buffer
+ if ((lowBitmap1 == null || lowBitmap1 instanceof RoaringBitmap)
+ && lowBitmap2 instanceof RoaringBitmap) {
+ if (lowBitmap1 == null) {
+ // Clone to prevent future modification of this modifying the input Bitmap
+ RoaringBitmap lowBitmap2Clone = ((RoaringBitmap) lowBitmap2).clone();
+
+ pushBitmapForHigh(high, lowBitmap2Clone);
+ } else {
+ ((RoaringBitmap) lowBitmap1).xor((RoaringBitmap) lowBitmap2);
+ }
+ } else if ((lowBitmap1 == null || lowBitmap1 instanceof MutableRoaringBitmap)
+ && lowBitmap2 instanceof MutableRoaringBitmap) {
+ if (lowBitmap1 == null) {
+ // Clone to prevent future modification of this modifying the input Bitmap
+ BitmapDataProvider lowBitmap2Clone = ((MutableRoaringBitmap) lowBitmap2).clone();
+
+ pushBitmapForHigh(high, lowBitmap2Clone);
+ } else {
+ ((MutableRoaringBitmap) lowBitmap1).xor((MutableRoaringBitmap) lowBitmap2);
+ }
+ } else {
+ throw new UnsupportedOperationException(
+ ".or is not between " + this.getClass() + " and " + lowBitmap2.getClass());
+ }
+
+ if (firstBucket) {
+ firstBucket = false;
+
+ // Invalidate the lowest high as lowest not valid
+ firstHighNotValid = Math.min(firstHighNotValid, high);
+ allValid = false;
+ }
+ }
+ }
+
+ /**
+ * In-place bitwise AND (intersection) operation. The current bitmap is modified.
+ *
+ * @param x2 other bitmap
+ */
+ public void and(final Roaring64Map x2) {
+ boolean firstBucket = true;
+
+ Iterator> thisIterator = highToBitmap.entrySet().iterator();
+ while (thisIterator.hasNext()) {
+ Map.Entry e1 = thisIterator.next();
+
+ // Keep object to prevent auto-boxing
+ Integer high = e1.getKey();
+
+ BitmapDataProvider lowBitmap2 = x2.highToBitmap.get(high);
+
+ if (lowBitmap2 == null) {
+ // None of given high values are present in x2
+ thisIterator.remove();
+ } else {
+ BitmapDataProvider lowBitmap1 = e1.getValue();
+
+ if (lowBitmap2 instanceof RoaringBitmap && lowBitmap1 instanceof RoaringBitmap) {
+ ((RoaringBitmap) lowBitmap1).and((RoaringBitmap) lowBitmap2);
+ } else if (lowBitmap2 instanceof MutableRoaringBitmap
+ && lowBitmap1 instanceof MutableRoaringBitmap) {
+ ((MutableRoaringBitmap) lowBitmap1).and((MutableRoaringBitmap) lowBitmap2);
+ } else {
+ throw new UnsupportedOperationException(
+ ".and is not between " + this.getClass() + " and " + lowBitmap1.getClass());
+ }
+ }
+
+ if (firstBucket) {
+ firstBucket = false;
+
+ // Invalidate the lowest high as lowest not valid
+ firstHighNotValid = Math.min(firstHighNotValid, high);
+ allValid = false;
+ }
+ }
+ }
+
+ /**
+ * In-place bitwise ANDNOT (difference) operation. The current bitmap is modified.
+ *
+ * @param x2 other bitmap
+ */
+ public void andNot(final Roaring64Map x2) {
+ boolean firstBucket = true;
+
+ Iterator> thisIterator = highToBitmap.entrySet().iterator();
+ while (thisIterator.hasNext()) {
+ Map.Entry e1 = thisIterator.next();
+
+ // Keep object to prevent auto-boxing
+ Integer high = e1.getKey();
+
+ BitmapDataProvider lowBitmap2 = x2.highToBitmap.get(high);
+
+ if (lowBitmap2 != null) {
+ BitmapDataProvider lowBitmap1 = e1.getValue();
+
+ if (lowBitmap2 instanceof RoaringBitmap && lowBitmap1 instanceof RoaringBitmap) {
+ ((RoaringBitmap) lowBitmap1).andNot((RoaringBitmap) lowBitmap2);
+ } else if (lowBitmap2 instanceof MutableRoaringBitmap
+ && lowBitmap1 instanceof MutableRoaringBitmap) {
+ ((MutableRoaringBitmap) lowBitmap1).andNot((MutableRoaringBitmap) lowBitmap2);
+ } else {
+ throw new UnsupportedOperationException(
+ ".and is not between " + this.getClass() + " and " + lowBitmap1.getClass());
+ }
+ }
+
+ if (firstBucket) {
+ firstBucket = false;
+
+ // Invalidate the lowest high as lowest not valid
+ firstHighNotValid = Math.min(firstHighNotValid, high);
+ allValid = false;
+ }
+ }
+ }
+
+ /**
+ * A string describing the bitmap.
+ *
+ * @return the string
+ */
+ @Override
+ public String toString() {
+ final StringBuilder answer = new StringBuilder();
+ final LongIterator i = this.getLongIterator();
+ answer.append("{");
+ if (i.hasNext()) {
+ if (signedLongs) {
+ answer.append(i.next());
+ } else {
+ answer.append(toUnsignedString(i.next()));
+ }
+ }
+ while (i.hasNext()) {
+ answer.append(",");
+ // to avoid using too much memory, we limit the size
+ if (answer.length() > 0x80000) {
+ answer.append("...");
+ break;
+ }
+ if (signedLongs) {
+ answer.append(i.next());
+ } else {
+ answer.append(toUnsignedString(i.next()));
+ }
+
+ }
+ answer.append("}");
+ return answer.toString();
+ }
+
+ /**
+ * For better performance, consider the Use the {@link #forEach forEach} method.
+ *
+ * @return a custom iterator over set bits, the bits are traversed in ascending sorted order
+ */
+ public LongIterator getLongIterator() {
+ final Iterator> it = highToBitmap.entrySet().iterator();
+
+ return toIterator(it, false);
+ }
+
+ protected LongIterator toIterator(final Iterator> it,
+ final boolean reversed) {
+ return new LongIterator() {
+
+ protected int currentKey;
+ protected IntIterator currentIt;
+
+ @Override
+ public boolean hasNext() {
+ if (currentIt == null) {
+ // Were initially empty
+ if (!moveToNextEntry(it)) {
+ return false;
+ }
+ }
+
+ while (true) {
+ if (currentIt.hasNext()) {
+ return true;
+ } else {
+ if (!moveToNextEntry(it)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * @param it the underlying iterator which has to be moved to next long
+ * @return true if we MAY have more entries. false if there is definitely nothing more
+ */
+ private boolean moveToNextEntry(Iterator> it) {
+ if (it.hasNext()) {
+ Map.Entry next = it.next();
+ currentKey = next.getKey();
+ if (reversed) {
+ currentIt = next.getValue().getReverseIntIterator();
+ } else {
+ currentIt = next.getValue().getIntIterator();
+ }
+
+ // We may have more long
+ return true;
+ } else {
+ // We know there is nothing more
+ return false;
+ }
+ }
+
+ @Override
+ public long next() {
+ if (hasNext()) {
+ return pack(currentKey, currentIt.next());
+ } else {
+ throw new IllegalStateException("empty");
+ }
+ }
+
+ @Override
+ public LongIterator clone() {
+ throw new UnsupportedOperationException("TODO");
+ }
+ };
+ }
+
+ public boolean contains(long x) {
+ int high = high(x);
+ BitmapDataProvider lowBitmap = highToBitmap.get(high);
+ if (lowBitmap == null) {
+ return false;
+ }
+
+ int low = low(x);
+ return lowBitmap.contains(low);
+ }
+
+ public int getSizeInBytes() {
+ return (int) getLongSizeInBytes();
+ }
+
+ public long getLongSizeInBytes() {
+ long size = 8;
+
+ // Size of containers
+ size += highToBitmap.values().stream().mapToLong(p -> p.getLongSizeInBytes()).sum();
+
+ // Size of Map data-structure: we consider each TreeMap entry costs 40 bytes
+ // http://java-performance.info/memory-consumption-of-java-data-types-2/
+ size += 8 + 40 * highToBitmap.size();
+
+ // Size of (boxed) Integers used as keys
+ size += 16 * highToBitmap.size();
+
+ // The cache impacts the size in heap
+ size += 8 * sortedCumulatedCardinality.length;
+ size += 4 * sortedHighs.length;
+
+ return size;
+ }
+
+ public boolean isEmpty() {
+ return getLongCardinality() == 0L;
+ }
+
+ public ImmutableLongBitmapDataProvider limit(long x) {
+ throw new UnsupportedOperationException("TODO");
+ }
+
+ /**
+ * Use a run-length encoding where it is estimated as more space efficient
+ *
+ * @return whether a change was applied
+ */
+ public boolean runOptimize() {
+ boolean hasChanged = false;
+ for (BitmapDataProvider lowBitmap : highToBitmap.values()) {
+ if (lowBitmap instanceof RoaringBitmap) {
+ hasChanged |= ((RoaringBitmap) lowBitmap).runOptimize();
+ } else if (lowBitmap instanceof MutableRoaringBitmap) {
+ hasChanged |= ((MutableRoaringBitmap) lowBitmap).runOptimize();
+ }
+ }
+ return hasChanged;
+ }
+
+ public long serializedSizeInBytes() {
+ long nbBytes = 0L;
+
+ // .writeBoolean for signedLongs boolean
+ nbBytes += 1;
+
+ // .writeInt for number of different high values
+ nbBytes += 4;
+
+ for (Map.Entry entry : highToBitmap.entrySet()) {
+ // .writeInt for high
+ nbBytes += 4;
+
+ // The low bitmap size in bytes
+ nbBytes += entry.getValue().serializedSizeInBytes();
+ }
+
+ return nbBytes;
+ }
+
+ /**
+ * reset to an empty bitmap; result occupies as much space a newly created bitmap.
+ */
+ public void clear() {
+ this.highToBitmap.clear();
+ resetPerfHelpers();
+ }
+
+ /**
+ * Return the set values as an array, if the cardinality is smaller than 2147483648. The long
+ * values are in sorted order.
+ *
+ * @return array representing the set values.
+ */
+ public long[] toArray() {
+ long cardinality = this.getLongCardinality();
+ if (cardinality > Integer.MAX_VALUE) {
+ throw new IllegalStateException("The cardinality does not fit in an array");
+ }
+
+ final long[] array = new long[(int) cardinality];
+
+ int pos = 0;
+ LongIterator it = getLongIterator();
+
+ while (it.hasNext()) {
+ array[pos++] = it.next();
+ }
+ return array;
+ }
+
+ /* ------------------ method below from Roaring64NavigableMap and being overwritten ----------------------------- */
+
+ /**
+ * Set all the specified values to true. This can be expected to be slightly faster than calling
+ * "add" repeatedly. The provided integers values don't have to be in sorted order, but it may be
+ * preferable to sort them from a performance point of view.
+ *
+ * @param dat set values
+ */
+ public void add(long... dat) {
+ for (long oneLong : dat) {
+ addLong(oneLong);
+ }
+ }
+
+ /**
+ * Add to the current bitmap all longs in [rangeStart,rangeEnd).
+ *
+ * @param rangeStart inclusive beginning of range
+ * @param rangeEnd exclusive ending of range
+ */
+ public void add(final long rangeStart, final long rangeEnd) {
+ int startHigh = high(rangeStart);
+ int startLow = low(rangeStart);
+
+ int endHigh = high(rangeEnd);
+ int endLow = low(rangeEnd);
+
+ for (int high = startHigh; high <= endHigh; high++) {
+ final int currentStartLow;
+ if (startHigh == high) {
+ // The whole range starts in this bucket
+ currentStartLow = startLow;
+ } else {
+ // Add the bucket from the beginning
+ currentStartLow = 0;
+ }
+
+ long startLowAsLong = Util.toUnsignedLong(currentStartLow);
+
+ final long endLowAsLong;
+ if (endHigh == high) {
+ // The whole range ends in this bucket
+ endLowAsLong = Util.toUnsignedLong(endLow);
+ } else {
+ // Add the bucket until the end: we have a +1 as, in RoaringBitmap.add(long,long), the end
+ // is excluded
+ endLowAsLong = Util.toUnsignedLong(-1) + 1;
+ }
+
+ if (endLowAsLong > startLowAsLong) {
+ // Initialize the bitmap only if there is access data to write
+ BitmapDataProvider bitmap = highToBitmap.get(high);
+ if (bitmap == null) {
+ bitmap = new MutableRoaringBitmap();
+ pushBitmapForHigh(high, bitmap);
+ }
+
+ if (bitmap instanceof RoaringBitmap) {
+ ((RoaringBitmap) bitmap).add(startLowAsLong, endLowAsLong);
+ } else if (bitmap instanceof MutableRoaringBitmap) {
+ ((MutableRoaringBitmap) bitmap).add(startLowAsLong, endLowAsLong);
+ } else {
+ throw new UnsupportedOperationException("TODO. Not for " + bitmap.getClass());
+ }
+ }
+ }
+
+ invalidateAboveHigh(startHigh);
+ }
+
+
+
+ /*---------------------------- method below is new written for doris's own bitmap --------------------------------*/
+
+ public LongIterator getReverseLongIterator() {
+ return toIterator(highToBitmap.descendingMap().entrySet().iterator(), true);
+ }
+
+ /*--------------- method below fetched from org.roaringbitmap.longlong RoaringIntPacking -----------------------*/
+
+ public void removeLong(long x) {
+ int high = high(x);
+
+ BitmapDataProvider bitmap = highToBitmap.get(high);
+
+ if (bitmap != null) {
+ int low = low(x);
+ bitmap.remove(low);
+
+ // Invalidate only if actually modified
+ invalidateAboveHigh(high);
+ }
+
+ }
+
+ public void trim() {
+ for (BitmapDataProvider bitmap : highToBitmap.values()) {
+ bitmap.trim();
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return highToBitmap.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Roaring64Map other = (Roaring64Map) obj;
+ return Objects.equals(highToBitmap, other.highToBitmap);
+ }
+
+ /**
+ * Add the value if it is not already present, otherwise remove it.
+ *
+ * @param x long value
+ */
+ public void flip(final long x) {
+ int high = high(x);
+ BitmapDataProvider lowBitmap = highToBitmap.get(high);
+ if (lowBitmap == null) {
+ // The value is not added: add it without any flip specific code
+ addLong(x);
+ } else {
+ int low = low(x);
+
+ // .flip is not in BitmapDataProvider contract
+ // TODO Is it relevant to calling .flip with a cast?
+ if (lowBitmap instanceof RoaringBitmap) {
+ ((RoaringBitmap) lowBitmap).flip(low);
+ } else if (lowBitmap instanceof MutableRoaringBitmap) {
+ ((MutableRoaringBitmap) lowBitmap).flip(low);
+ } else {
+ // Fallback to a manual flip
+ if (lowBitmap.contains(low)) {
+ lowBitmap.remove(low);
+ } else {
+ lowBitmap.add(low);
+ }
+ }
+ }
+
+ invalidateAboveHigh(high);
+ }
+
+ /**
+ * Serialize this bitmap.
+ *
+ * Unlike RoaringBitmap, there is no specification for now: it may change from one java version
+ * to another, and from one RoaringBitmap version to another.
+ *
+ * Consider calling {@link #runOptimize} before serialization to improve compression.
+ *
+ * The current bitmap is not modified.
+ *
+ * @param out the DataOutput stream
+ * @throws IOException Signals that an I/O exception has occurred.
+ */
+ public void serialize(DataOutput out) throws IOException {
+ if (highToBitmap.size() == 0) {
+ return;
+ }
+ if (is32BitsEnough()) {
+ out.write(BitmapValue.BITMAP32);
+ highToBitmap.get(0).serialize(out);
+ return;
+ }
+
+ out.write(BitmapValue.BITMAP64);
+ Codec.encodeVarint64(highToBitmap.size(), out);
+
+ for (Map.Entry entry : highToBitmap.entrySet()) {
+ // serialized in little end for BE cpp read in case of bugs when the value is larger than 32bits
+ out.writeInt(Integer.reverseBytes(entry.getKey().intValue()));
+ entry.getValue().serialize(out);
+ }
+ }
+
+ /**
+ * Deserialize (retrieve) this bitmap.
+ *
+ * Unlike RoaringBitmap, there is no specification for now: it may change from one java version to
+ * another, and from one RoaringBitmap version to another.
+ *
+ * The current bitmap is overwritten.
+ *
+ * @param in the DataInput stream
+ * @throws IOException Signals that an I/O exception has occurred.
+ */
+ public void deserialize(DataInput in, int bitmapType) throws IOException {
+ this.clear();
+ highToBitmap = new TreeMap<>();
+
+ if (bitmapType == BitmapValue.BITMAP32) {
+ RoaringBitmap provider = new RoaringBitmap();
+ provider.deserialize(in);
+ highToBitmap.put(0, provider);
+ return;
+ }
+
+ if (bitmapType != BitmapValue.BITMAP64) {
+ throw new InvalidRoaringFormat("invalid bitmap type");
+ }
+
+ long nbHighs = Codec.decodeVarint64(in);
+ for (int i = 0; i < nbHighs; i++) {
+ // keep the same behavior with little-end serialize
+ int high = Integer.reverseBytes(in.readInt());
+ RoaringBitmap provider = new RoaringBitmap();
+ provider.deserialize(in);
+ highToBitmap.put(high, provider);
+ }
+
+ resetPerfHelpers();
+ }
+
+ public boolean is32BitsEnough() {
+ return highToBitmap.size() == 1 && highToBitmap.get(0) != null;
+ }
+
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/AutoType.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/AutoType.java
new file mode 100644
index 00000000..f65a9fdf
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/AutoType.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Helper class to convert type between Java's wrapper type and primitive type
+ * There are 8 wrapper/primitive types in Java:
+ * |Wrapped Type |Primitive Type
+ * --------------------------------------
+ * |Boolean |boolean
+ * |Character |char
+ * |Byte |byte
+ * |Short |short
+ * |Integer |int
+ * |Float |float
+ * |Long |longFieldReflection
+ * |Double |double
+ *
+ * Copied from Apache Doris
+ */
+public class AutoType {
+ private static final Map, Class>> PRIMITIVE_TO_WRAPPER = new HashMap();
+ private static final Map, Class>> WRAPPER_TO_PRIMITIVE = new HashMap();
+
+ static {
+ WRAPPER_TO_PRIMITIVE.put(Boolean.class, Boolean.TYPE);
+ WRAPPER_TO_PRIMITIVE.put(Character.class, Character.TYPE);
+ WRAPPER_TO_PRIMITIVE.put(Byte.class, Byte.TYPE);
+ WRAPPER_TO_PRIMITIVE.put(Short.class, Short.TYPE);
+ WRAPPER_TO_PRIMITIVE.put(Integer.class, Integer.TYPE);
+ WRAPPER_TO_PRIMITIVE.put(Float.class, Float.TYPE);
+ WRAPPER_TO_PRIMITIVE.put(Long.class, Long.TYPE);
+ WRAPPER_TO_PRIMITIVE.put(Double.class, Double.TYPE);
+
+ PRIMITIVE_TO_WRAPPER.put(Boolean.TYPE, Boolean.class);
+ PRIMITIVE_TO_WRAPPER.put(Character.TYPE, Character.class);
+ PRIMITIVE_TO_WRAPPER.put(Byte.TYPE, Byte.class);
+ PRIMITIVE_TO_WRAPPER.put(Short.TYPE, Short.class);
+ PRIMITIVE_TO_WRAPPER.put(Integer.TYPE, Integer.class);
+ PRIMITIVE_TO_WRAPPER.put(Float.TYPE, Float.class);
+ PRIMITIVE_TO_WRAPPER.put(Long.TYPE, Long.class);
+ PRIMITIVE_TO_WRAPPER.put(Double.TYPE, Double.class);
+ }
+
+ public static boolean isWrapperOfPrimitiveType(Class> type) {
+ return WRAPPER_TO_PRIMITIVE.containsKey(type);
+ }
+
+ public static Class> getPrimitiveType(Class> wrapperType) {
+ return WRAPPER_TO_PRIMITIVE.get(wrapperType);
+ }
+
+ public static Class> getWrapperType(Class> primitiveType) {
+ return PRIMITIVE_TO_WRAPPER.get(primitiveType);
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ConstructorReflection.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ConstructorReflection.java
new file mode 100644
index 00000000..4b437ce4
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ConstructorReflection.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Modify from mockit.internal.util.ConstructorReflection JMockit v1.13
+ * Util class to invoke constructor of specified class.
+ *
+ * Copied from Apache Doris
+ */
+public final class ConstructorReflection {
+
+ private ConstructorReflection() {
+ }
+
+ /**
+ * invoke the {@constructor} with parameters {@initArgs}.
+ */
+ public static T invoke(Constructor constructor, Object... initArgs) {
+ if (constructor == null || initArgs == null) {
+ throw new IllegalArgumentException();
+ }
+ makeAccessible(constructor);
+
+ try {
+ return constructor.newInstance(initArgs);
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Error) {
+ throw (Error) cause;
+ } else if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else {
+ throw new IllegalStateException("Should never get here", cause);
+ }
+ }
+ }
+
+ /**
+ * invoke the constructor with parameters {@nonNullArgs Object...}.
+ */
+ public static T newInstance(Class extends T> aClass, Object... nonNullArgs) {
+ if (aClass == null || nonNullArgs == null) {
+ throw new IllegalArgumentException();
+ } else {
+ Class>[] argTypes = ParameterReflection.getArgumentTypesFromArgumentValues(nonNullArgs);
+ Constructor constructor = findCompatibleConstructor(aClass, argTypes);
+ return invoke(constructor, nonNullArgs);
+ }
+ }
+
+ /**
+ * invoke the constructor with no parameters of {@aClass Class}.
+ */
+ private static T newInstance(Class aClass) {
+ return (T) newInstance((Class) aClass, ParameterReflection.NO_PARAMETERS);
+ }
+
+ /**
+ * invoke the default constructor of {@aClass Class}.
+ * if the default constructor is not available, try to invoke the one constructor with no parameters.
+ */
+ public static T newInstanceUsingDefaultConstructor(Class aClass) {
+ if (aClass == null) {
+ throw new IllegalArgumentException();
+ }
+ try {
+ return aClass.newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ return newInstance(aClass);
+ }
+ }
+
+ /**
+ * invoke the default constructor of {@aClass Class}.
+ */
+ public static T newInstanceUsingDefaultConstructorIfAvailable(Class aClass) {
+ if (aClass == null) {
+ throw new IllegalArgumentException();
+ }
+ try {
+ return aClass.newInstance();
+ } catch (InstantiationException e) {
+ return null;
+ } catch (IllegalAccessException e) {
+ return null;
+ }
+ }
+
+ /**
+ * invoke inner-class constructor with outer-class instance {@outerInstance} and parameters {@nonNullArgs}.
+ */
+ public static T newInnerInstance(Class extends T> innerClass, Object outerInstance, Object... nonNullArgs) {
+ if (innerClass == null || outerInstance == null || nonNullArgs == null) {
+ throw new IllegalArgumentException();
+ } else {
+ Object[] initArgs = ParameterReflection.argumentsWithExtraFirstValue(nonNullArgs, outerInstance);
+ return newInstance(innerClass, initArgs);
+ }
+ }
+
+ /**
+ * Get non-inner-class constructor with {@argTypes Class>[]}.
+ * if more than one constructor was found, choose the more specific one. (i.e. constructor with parameters that have more concrete types is more specific)
+ * if no constructor was found, will check if {@theClass} is a inner class. Then a IllegalArgumentException exception will be thrown.
+ */
+ private static Constructor findCompatibleConstructor(Class> theClass, Class>[] argTypes) {
+ if (theClass == null || argTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ Constructor found = null;
+ Class>[] foundParameters = null;
+ Constructor>[] declaredConstructors = theClass.getDeclaredConstructors();
+ Constructor[] declaredConstructorsArray = declaredConstructors;
+
+ for (Constructor> declaredConstructor : declaredConstructorsArray) {
+ Class>[] declaredParamTypes = declaredConstructor.getParameterTypes();
+ int gap = declaredParamTypes.length - argTypes.length;
+ if (gap == 0 && (ParameterReflection.matchesParameterTypes(declaredParamTypes, argTypes)
+ || ParameterReflection.acceptsArgumentTypes(declaredParamTypes, argTypes))
+ &&
+ (found == null || ParameterReflection.hasMoreSpecificTypes(declaredParamTypes, foundParameters))) {
+ found = (Constructor) declaredConstructor;
+ foundParameters = declaredParamTypes;
+ }
+ }
+
+ if (found != null) {
+ return found;
+ } else {
+ Class> declaringClass = theClass.getDeclaringClass();
+ Class>[] paramTypes = declaredConstructors[0].getParameterTypes();
+ // check if this constructor is belong to a inner class
+ // the parameter[0] of inner class's constructor is a instance of outer class
+ if (paramTypes[0] == declaringClass && paramTypes.length > argTypes.length) {
+ throw new IllegalArgumentException(
+ "Invalid instantiation of inner class; use newInnerInstance instead");
+ } else {
+ String argTypesDesc = ParameterReflection.getParameterTypesDescription(argTypes);
+ throw new IllegalArgumentException(
+ "No compatible constructor found: " + theClass.getSimpleName() + argTypesDesc);
+ }
+ }
+ }
+
+ // ensure that field is accessible
+ public static void makeAccessible(AccessibleObject classMember) {
+ if (!classMember.isAccessible()) {
+ classMember.setAccessible(true);
+ }
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/Deencapsulation.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/Deencapsulation.java
new file mode 100644
index 00000000..74362e0c
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/Deencapsulation.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+/**
+ * Modify from mockit.internal.util.Deencapsulation JMockit ver1.13
+ *
+ * Copied from Apache Doris
+ */
+public final class Deencapsulation {
+ private Deencapsulation() {
+ }
+
+ public static T getField(Object objectWithField, String fieldName) {
+ return FieldReflection.getField(objectWithField.getClass(), fieldName, objectWithField);
+ }
+
+ public static T getField(Object objectWithField, Class fieldType) {
+ return FieldReflection.getField(objectWithField.getClass(), fieldType, objectWithField);
+ }
+
+ public static T getField(Class> classWithStaticField, String fieldName) {
+ return FieldReflection.getField(classWithStaticField, fieldName, null);
+ }
+
+ public static T getField(Class> classWithStaticField, Class fieldType) {
+ return FieldReflection.getField(classWithStaticField, fieldType, null);
+ }
+
+ public static void setField(Object objectWithField, String fieldName, Object fieldValue) {
+ FieldReflection.setField(objectWithField.getClass(), objectWithField, fieldName, fieldValue);
+ }
+
+ public static void setField(Object objectWithField, Object fieldValue) {
+ FieldReflection.setField(objectWithField.getClass(), objectWithField, null, fieldValue);
+ }
+
+ public static void setField(Class> classWithStaticField, String fieldName, Object fieldValue) {
+ FieldReflection.setField(classWithStaticField, null, fieldName, fieldValue);
+ }
+
+ public static void setField(Class> classWithStaticField, Object fieldValue) {
+ FieldReflection.setField(classWithStaticField, null, null, fieldValue);
+ }
+
+ public static T invoke(Object objectWithMethod, String methodName, Object... nonNullArgs) {
+ Class> theClass = objectWithMethod.getClass();
+ return MethodReflection.invoke(theClass, objectWithMethod, methodName, nonNullArgs);
+ }
+
+ public static T invoke(Class> classWithStaticMethod, String methodName, Object... nonNullArgs) {
+ return MethodReflection.invoke(classWithStaticMethod, null, methodName, nonNullArgs);
+ }
+
+ public static T newInstance(Class extends T> classToInstantiate, Object... nonNullArgs) {
+ return ConstructorReflection.newInstance(classToInstantiate, nonNullArgs);
+ }
+
+ public static T newInnerInstance(Class extends T> innerClassToInstantiate, Object outerClassInstance, Object... nonNullArgs) {
+ return ConstructorReflection.newInnerInstance(innerClassToInstantiate, outerClassInstance, nonNullArgs);
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/FieldReflection.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/FieldReflection.java
new file mode 100644
index 00000000..04c6d9cd
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/FieldReflection.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+/**
+ * Modify from mockit.internal.util.FieldReflection JMockit v1.13
+ * Util class to set and get the value of specified field.
+ *
+ * Copied from Apache Doris
+ */
+public final class FieldReflection {
+ private FieldReflection() {
+ }
+
+ /**
+ * Get field's value with field's name.
+ */
+ public static T getField(Class> theClass, String fieldName, Object targetObject) {
+ if (theClass == null || fieldName == null || targetObject == null) {
+ throw new IllegalStateException();
+ }
+ Field field = getDeclaredField(theClass, fieldName, targetObject != null);
+ return getFieldValue(field, targetObject);
+ }
+
+ /**
+ * Get field's value with field's type.
+ */
+ public static T getField(Class> theClass, Class fieldType, Object targetObject) {
+ if (theClass == null || fieldType == null) {
+ throw new IllegalStateException();
+ }
+ Field field = getDeclaredField(theClass, fieldType, targetObject != null, false);
+ return getFieldValue(field, targetObject);
+ }
+
+ /**
+ * Get field's value with field's type.
+ */
+ public static T getField(Class> theClass, Type fieldType, Object targetObject) {
+ if (theClass == null || fieldType == null) {
+ throw new IllegalStateException();
+ }
+ Field field = getDeclaredField(theClass, fieldType, targetObject != null, false);
+ return getFieldValue(field, targetObject);
+ }
+
+ /**
+ * Modify field's value in targetObject.
+ * If {@fieldName String} is null, will try to set field with field's type.
+ */
+ public static Field setField(Class> theClass, Object targetObject, String fieldName, Object fieldValue) {
+ if (theClass == null) {
+ throw new IllegalArgumentException();
+ }
+ boolean instanceField = targetObject != null;
+ Field field;
+ if (fieldName != null) {
+ field = getDeclaredField(theClass, fieldName, instanceField);
+ } else {
+ if (fieldValue == null) {
+ throw new IllegalArgumentException("Missing field value when setting field by type");
+ }
+
+ field = getDeclaredField(theClass, fieldValue.getClass(), instanceField, true);
+ }
+
+ setFieldValue(field, targetObject, fieldValue);
+ return field;
+ }
+
+ /**
+ * Get field by field's name.
+ * If no field is found in this class, it will continue to look up its super class.
+ * If {@instanceField boolean} is true, will only search for the non-static field.
+ */
+ private static Field getDeclaredField(Class> theClass, String fieldName, boolean instanceField) {
+ if (theClass == null || fieldName == null) {
+ throw new IllegalStateException();
+ }
+ try {
+ return theClass.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ Class> superClass = theClass.getSuperclass();
+ if (superClass != null && superClass != Object.class) {
+ return getDeclaredField(superClass, fieldName, instanceField);
+ } else {
+ String kind = instanceField ? "instance" : "static";
+ throw new IllegalArgumentException("No " + kind + " field of name \"" + fieldName + "\" found in " + theClass);
+ }
+ }
+ }
+
+ /**
+ * Get field by field's type.
+ * If no field is found in this class, it will continue to look up its super class.
+ * If {@instanceField boolean} is true, will only search for the non-static field.
+ * If {@forAssignment boolean} is true, will compare its super type with desiredType.
+ */
+ private static Field getDeclaredField(Class> theClass, Type desiredType, boolean instanceField, boolean forAssignment) {
+ if (theClass == null || desiredType == null) {
+ throw new IllegalStateException();
+ }
+ Field found = getDeclaredFieldInSingleClass(theClass, desiredType, instanceField, forAssignment);
+ if (found == null) {
+ Class> superClass = theClass.getSuperclass();
+ if (superClass != null && superClass != Object.class) {
+ return getDeclaredField(superClass, desiredType, instanceField, forAssignment);
+ } else {
+ StringBuilder errorMsg = new StringBuilder(instanceField ? "Instance" : "Static");
+ String typeName = getTypeName(desiredType);
+ errorMsg.append(" field of type ").append(typeName).append(" not found in ").append(theClass);
+ throw new IllegalArgumentException(errorMsg.toString());
+ }
+ } else {
+ return found;
+ }
+ }
+
+ /**
+ * Get field by field's type.
+ * There is only one field is expected to be found in a single class.
+ * If {@instanceField boolean} is true, will only search for the non-static field.
+ * If {@forAssignment boolean} is true, will compare its super type with desiredType.
+ * If more than one field are found, a IllegalArgumentException will be thrown.
+ */
+ private static Field getDeclaredFieldInSingleClass(Class> theClass, Type desiredType, boolean instanceField, boolean forAssignment) {
+ if (theClass == null || desiredType == null) {
+ throw new IllegalStateException();
+ }
+ Field found = null;
+ Field[] fields = theClass.getDeclaredFields();
+
+ for (Field field : fields) {
+ if (!field.isSynthetic()) {
+ Type fieldType = field.getGenericType();
+ if (instanceField != Modifier.isStatic(field.getModifiers()) && isCompatibleFieldType(fieldType, desiredType, forAssignment)) {
+ if (found != null) {
+ String message = errorMessageForMoreThanOneFieldFound(desiredType, instanceField, forAssignment, found, field);
+ throw new IllegalArgumentException(message);
+ }
+
+ found = field;
+ }
+ }
+ }
+
+ return found;
+ }
+
+ /**
+ * return true if the {@fieldType} is compatible with {@desiredType}.
+ * If {@forAssignment} is true, will compare its super type with desiredType.
+ * If {@forAssignment} is false, will also compare it with desiredType's super type.
+ */
+ private static boolean isCompatibleFieldType(Type fieldType, Type desiredType, boolean forAssignment) {
+ if (fieldType == null || desiredType == null) {
+ throw new IllegalStateException();
+ }
+ Class> fieldClass = getClassType(fieldType);
+ Class> desiredClass = getClassType(desiredType);
+ if (isSameType(desiredClass, fieldClass)) {
+ return true;
+ } else if (forAssignment) {
+ return fieldClass.isAssignableFrom(desiredClass);
+ } else {
+ return desiredClass.isAssignableFrom(fieldClass) || fieldClass.isAssignableFrom(desiredClass);
+ }
+ }
+
+ private static String errorMessageForMoreThanOneFieldFound(Type desiredFieldType, boolean instanceField, boolean forAssignment, Field firstField, Field secondField) {
+ return "More than one " + (instanceField ? "instance" : "static") + " field " + (forAssignment ? "to" : "from")
+ + " which a value of type "
+ + getTypeName(desiredFieldType) + (forAssignment ? " can be assigned" : " can be read") + " exists in "
+ + secondField.getDeclaringClass() + ": " + firstField.getName() + ", " + secondField.getName();
+ }
+
+ private static String getTypeName(Type type) {
+ if (type == null) {
+ throw new IllegalStateException();
+ }
+ Class> classType = getClassType(type);
+ Class> primitiveType = AutoType.getPrimitiveType(classType);
+ if (primitiveType != null) {
+ return primitiveType + " or " + classType.getSimpleName();
+ } else {
+ String name = classType.getName();
+ return name.startsWith("java.lang.") ? name.substring(10) : name;
+ }
+ }
+
+ /**
+ * Get field in {@targetObject Object}.
+ */
+ private static T getFieldValue(Field field, Object targetObject) {
+ if (field == null) {
+ throw new IllegalStateException();
+ }
+ makeAccessible(field);
+
+ try {
+ return (T) field.get(targetObject);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Modify field with value in {@targetObject Object}.
+ */
+ public static void setFieldValue(Field field, Object targetObject, Object value) {
+ if (field == null) {
+ throw new IllegalStateException();
+ }
+ try {
+ if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
+ throw new IllegalArgumentException("Do not allow to set static final field");
+ } else {
+ makeAccessible(field);
+ field.set(targetObject, value);
+ }
+
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /*
+ private static void setStaticFinalField(Field field, Object value) throws IllegalAccessException {
+ if (field == null) {
+ throw new IllegalStateException();
+ }
+ Field modifiersField;
+ try {
+ modifiersField = Field.class.getDeclaredField("modifiers");
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+
+ modifiersField.setAccessible(true);
+ int nonFinalModifiers = modifiersField.getInt(field) - 16;
+ modifiersField.setInt(field, nonFinalModifiers);
+ FieldAccessor accessor = ReflectionFactory.getReflectionFactory().newFieldAccessor(field, false);
+ accessor.set((Object)null, value);
+ }
+ */
+
+ public static Class> getClassType(Type declaredType) {
+ while (!(declaredType instanceof Class)) {
+ if (declaredType instanceof ParameterizedType) {
+ return (Class) ((ParameterizedType) declaredType).getRawType();
+ }
+
+ if (!(declaredType instanceof TypeVariable)) {
+ throw new IllegalArgumentException("Type of unexpected kind: " + declaredType);
+ }
+
+ declaredType = ((TypeVariable) declaredType).getBounds()[0];
+ }
+
+ return (Class) declaredType;
+ }
+
+ // ensure that field is accessible
+ public static void makeAccessible(AccessibleObject classMember) {
+ if (!classMember.isAccessible()) {
+ classMember.setAccessible(true);
+ }
+ }
+
+ // return true if the two types are same type.
+ private static boolean isSameType(Class> firstType, Class> secondType) {
+ return firstType == secondType
+ || firstType.isPrimitive() && firstType == AutoType.getPrimitiveType(secondType)
+ || secondType.isPrimitive() && secondType == AutoType.getPrimitiveType(firstType);
+ }
+
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/GeneratedClasses.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/GeneratedClasses.java
new file mode 100644
index 00000000..1281f4ed
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/GeneratedClasses.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+import java.lang.reflect.Proxy;
+
+/**
+ * Modify from mockit.internal.util.GeneratedClasses JMockit v1.13
+ * Helper class to return type of mocked-object
+ *
+ * Copied from Apache Doris
+ */
+public final class GeneratedClasses {
+ private static final String IMPLCLASS_PREFIX = "$Impl_";
+ private static final String SUBCLASS_PREFIX = "$Subclass_";
+
+ private GeneratedClasses() {
+ }
+
+ static boolean isGeneratedImplementationClass(Class> mockedType) {
+ return isGeneratedImplementationClass(mockedType.getName());
+ }
+
+ static boolean isGeneratedImplementationClass(String className) {
+ return className.contains(IMPLCLASS_PREFIX);
+ }
+
+ static boolean isGeneratedSubclass(String className) {
+ return className.contains(SUBCLASS_PREFIX);
+ }
+
+ static boolean isGeneratedClass(String className) {
+ return isGeneratedSubclass(className) || isGeneratedImplementationClass(className);
+ }
+
+ static Class> getMockedClassOrInterfaceType(Class> aClass) {
+ if (!Proxy.isProxyClass(aClass) && !isGeneratedImplementationClass(aClass)) {
+ return isGeneratedSubclass(aClass.getName()) ? aClass.getSuperclass() : aClass;
+ } else {
+ return aClass.getInterfaces()[0];
+ }
+ }
+
+ static Class> getMockedClass(Object mock) {
+ return getMockedClassOrInterfaceType(mock.getClass());
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/MethodReflection.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/MethodReflection.java
new file mode 100644
index 00000000..293e9816
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/MethodReflection.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Modify from mockit.internal.util.MethodReflection JMockit v1.13
+ * Util class to get and invoke method from specified class.
+ *
+ * Copied from Apache Doris
+ */
+public final class MethodReflection {
+ private MethodReflection() {
+ }
+
+ public static T invoke(Class> theClass, Object targetInstance, String methodName, Object... methodArgs) {
+ if (theClass == null || methodName == null) {
+ throw new IllegalArgumentException();
+ }
+ boolean staticMethod = targetInstance == null;
+ Class>[] argTypes = ParameterReflection.getArgumentTypesFromArgumentValues(methodArgs);
+ Method method = staticMethod ? findCompatibleStaticMethod(theClass, methodName, argTypes) :
+ findCompatibleMethod(theClass, methodName, argTypes);
+ if (staticMethod && !Modifier.isStatic(method.getModifiers())) {
+ throw new IllegalArgumentException(
+ "Attempted to invoke non-static method without an instance to invoke it on");
+ } else {
+ T result = invoke(targetInstance, method, methodArgs);
+ return result;
+ }
+ }
+
+ public static T invoke(Object targetInstance, Method method, Object... methodArgs) {
+ if (method == null || methodArgs == null) {
+ throw new IllegalArgumentException();
+ }
+ makeAccessible(method);
+
+ try {
+ return (T) method.invoke(targetInstance, methodArgs);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Failure to invoke method: " + method, e);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Error) {
+ throw (Error) cause;
+ } else if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else {
+ ThrowOfCheckedException.doThrow((Exception) cause);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Get a static method with {@methodName String} and {@argTypes Class>[]}.
+ * If no method was found, a IllegalArgumentException will be thrown.
+ */
+ private static Method findCompatibleStaticMethod(Class> theClass, String methodName, Class>[] argTypes) {
+ if (theClass == null || methodName == null || argTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ Method methodFound = findCompatibleMethodInClass(theClass, methodName, argTypes);
+ if (methodFound != null) {
+ return methodFound;
+ } else {
+ String argTypesDesc = ParameterReflection.getParameterTypesDescription(argTypes);
+ throw new IllegalArgumentException("No compatible static method found: " + methodName + argTypesDesc);
+ }
+ }
+
+ /**
+ * Get a non-static method with {@methodName String} and {@argTypes Class>[]}.
+ */
+ public static Method findCompatibleMethod(Class> theClass, String methodName, Class>[] argTypes) {
+ if (theClass == null || methodName == null || argTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ Method methodFound = findCompatibleMethodIfAvailable(theClass, methodName, argTypes);
+ if (methodFound != null) {
+ return methodFound;
+ } else {
+ String argTypesDesc = ParameterReflection.getParameterTypesDescription(argTypes);
+ throw new IllegalArgumentException("No compatible method found: " + methodName + argTypesDesc);
+ }
+ }
+
+ /**
+ * Get method with {@methodName String} and {@argTypes Class>[]} from {@theClass Class>}.
+ * If more than one method is found, choose the more specific one. (i.e. method with parameters that have more concrete types is more specific)
+ */
+ private static Method findCompatibleMethodInClass(Class> theClass, String methodName, Class>[] argTypes) {
+ if (theClass == null || methodName == null || argTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ Method found = null;
+ Class>[] foundParamTypes = null;
+ Method[] methods = theClass.getDeclaredMethods();
+
+ for (Method declaredMethod : methods) {
+ if (declaredMethod.getName().equals(methodName)) {
+ Class>[] declaredParamTypes = declaredMethod.getParameterTypes();
+ int gap = declaredParamTypes.length - argTypes.length;
+ if (gap == 0 && (ParameterReflection.matchesParameterTypes(declaredParamTypes, argTypes)
+ || ParameterReflection.acceptsArgumentTypes(declaredParamTypes, argTypes))
+ && (foundParamTypes == null
+ || ParameterReflection.hasMoreSpecificTypes(declaredParamTypes, foundParamTypes))) {
+ found = declaredMethod;
+ foundParamTypes = declaredParamTypes;
+ }
+ }
+ }
+
+ return found;
+ }
+
+ /**
+ * Get method with {@methodName String} and {@argTypes Class>[]} from {@theClass Class>} as well as its super class.
+ * If more than one method is found, choose the more specify one. (i.e. choose the method with parameters that have more concrete types)
+ */
+ private static Method findCompatibleMethodIfAvailable(Class> theClass, String methodName, Class>[] argTypes) {
+ if (theClass == null || methodName == null || argTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ Method methodFound = null;
+
+ while (true) {
+ Method compatibleMethod = findCompatibleMethodInClass(theClass, methodName, argTypes);
+ if (compatibleMethod != null && (methodFound == null ||
+ ParameterReflection.hasMoreSpecificTypes(compatibleMethod.getParameterTypes(),
+ methodFound.getParameterTypes()))) {
+ methodFound = compatibleMethod;
+ }
+
+ Class> superClass = theClass.getSuperclass();
+ if (superClass == null || superClass == Object.class) {
+ return methodFound;
+ }
+
+ theClass = superClass;
+ }
+ }
+
+
+ // ensure that field is accessible
+ public static void makeAccessible(AccessibleObject classMember) {
+ if (!classMember.isAccessible()) {
+ classMember.setAccessible(true);
+ }
+ }
+
+ // return true if the two types are same type.
+ private static boolean isSameType(Class> firstType, Class> secondType) {
+ return firstType == secondType
+ || firstType.isPrimitive() && firstType == AutoType.getPrimitiveType(secondType)
+ || secondType.isPrimitive() && secondType == AutoType.getPrimitiveType(firstType);
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ParameterReflection.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ParameterReflection.java
new file mode 100644
index 00000000..6a6efc11
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ParameterReflection.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+import java.util.regex.Pattern;
+
+/**
+ * Modify from mockit.internal.util.ParameterReflection JMockit v1.13
+ * Util class to verify parameter of methods.
+ *
+ * Copied from Apache Doris
+ */
+public final class ParameterReflection {
+ public static final Class>[] NO_PARAMETERS = new Class[0];
+
+ public static final Pattern JAVA_LANG = Pattern.compile("java.lang.", 16);
+
+ private ParameterReflection() {
+ }
+
+ /**
+ * check if every member in {@declaredTypes} is completely equal to the corresponding member {@specifiedTypes}.
+ */
+ static boolean matchesParameterTypes(Class>[] declaredTypes, Class>[] specifiedTypes) {
+ if (declaredTypes == null || specifiedTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ for (int i = 0; i < declaredTypes.length; ++i) {
+ Class> declaredType = declaredTypes[i];
+ Class> specifiedType = specifiedTypes[i];
+ if (!isSameType(declaredType, specifiedType)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * check if every member in {@paramTypes} is acceptable to the corresponding member in {@argTypes}.
+ */
+ static boolean acceptsArgumentTypes(Class>[] paramTypes, Class>[] argTypes) {
+ if (paramTypes == null || argTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ for (int i = 0; i < paramTypes.length; ++i) {
+ Class> parType = paramTypes[i];
+ Class> argType = argTypes[i];
+ if (!isSameType(parType, argType) && !parType.isAssignableFrom(argType)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get all types from objects {@args}.
+ */
+ static Class>[] getArgumentTypesFromArgumentValues(Object... args) {
+ if (args == null) {
+ throw new IllegalArgumentException();
+ }
+ if (args.length == 0) {
+ return NO_PARAMETERS;
+ } else {
+ Class>[] argTypes = new Class[args.length];
+
+ for (int i = 0; i < args.length; ++i) {
+ argTypes[i] = getArgumentTypeFromArgumentValue(i, args);
+ }
+
+ return argTypes;
+ }
+ }
+
+ /**
+ * Get type from {@args} by index.
+ */
+ static Class> getArgumentTypeFromArgumentValue(int i, Object[] args) {
+ Object arg = args[i];
+ if (arg == null) {
+ throw new IllegalArgumentException("Invalid null value passed as argument " + i);
+ } else {
+ Class argType;
+ if (arg instanceof Class) {
+ argType = (Class) arg;
+ args[i] = null;
+ } else {
+ argType = GeneratedClasses.getMockedClass(arg);
+ }
+
+ return argType;
+ }
+ }
+
+ /**
+ * return true if {@currentTypes} is more specific than {@previousTypes}.
+ */
+ static boolean hasMoreSpecificTypes(Class>[] currentTypes, Class>[] previousTypes) {
+ if (currentTypes == null || previousTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ for (int i = 0; i < currentTypes.length; ++i) {
+ Class> current = wrappedIfPrimitive(currentTypes[i]);
+ Class> previous = wrappedIfPrimitive(previousTypes[i]);
+ if (current != previous && previous.isAssignableFrom(current)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * return the type names of {@paramTypes} wrapped in brackets.
+ */
+ static String getParameterTypesDescription(Class>[] paramTypes) {
+ if (paramTypes == null) {
+ throw new IllegalArgumentException();
+ }
+ StringBuilder paramTypesDesc = new StringBuilder(200);
+ paramTypesDesc.append('(');
+ String sep = "";
+
+ for (Class paramType : paramTypes) {
+ String typeName = JAVA_LANG.matcher(paramType.getCanonicalName()).replaceAll("");
+ paramTypesDesc.append(sep).append(typeName);
+ sep = ", ";
+ }
+
+ paramTypesDesc.append(')');
+ return paramTypesDesc.toString();
+ }
+
+ /**
+ * return real parameters array of inner-class belong to the outer-class instance {@firstValue Object}.
+ * the parameter[0] of a inner-class constructor is always the instance of its outer-class.
+ */
+ static Object[] argumentsWithExtraFirstValue(Object[] args, Object firstValue) {
+ Object[] args2 = new Object[1 + args.length];
+ args2[0] = firstValue;
+ System.arraycopy(args, 0, args2, 1, args.length);
+ return args2;
+ }
+
+ // return wrapped type if its type is primitive.
+ private static Class> wrappedIfPrimitive(Class> parameterType) {
+ if (parameterType.isPrimitive()) {
+ Class> wrapperType = AutoType.getWrapperType(parameterType);
+
+ assert wrapperType != null;
+
+ return wrapperType;
+ } else {
+ return parameterType;
+ }
+ }
+
+ // return true if the two types are same type.
+ private static boolean isSameType(Class> firstType, Class> secondType) {
+ return firstType == secondType
+ || firstType.isPrimitive() && firstType == AutoType.getPrimitiveType(secondType)
+ || secondType.isPrimitive() && secondType == AutoType.getPrimitiveType(firstType);
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ThrowOfCheckedException.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ThrowOfCheckedException.java
new file mode 100644
index 00000000..4dfc44ae
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/common/jmockit/ThrowOfCheckedException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2006 JMockit developers
+ * This file is subject to the terms of the MIT license (see LICENSE.txt).
+ */
+
+package org.apache.doris.common.jmockit;
+
+/**
+ * Modify from mockit.internal.reflection.ThrowOfCheckedException JMockit v1.13
+ */
+public final class ThrowOfCheckedException {
+ private static Exception exceptionToThrow;
+
+ ThrowOfCheckedException() throws Exception {
+ throw exceptionToThrow;
+ }
+
+ public static synchronized void doThrow(Exception checkedException) {
+ exceptionToThrow = checkedException;
+ ConstructorReflection.newInstanceUsingDefaultConstructor(ThrowOfCheckedException.class);
+ }
+}
diff --git a/spark-load/spark-load-common/src/main/java/org/apache/doris/config/EtlJobConfig.java b/spark-load/spark-load-common/src/main/java/org/apache/doris/config/EtlJobConfig.java
new file mode 100644
index 00000000..9cca8650
--- /dev/null
+++ b/spark-load/spark-load-common/src/main/java/org/apache/doris/config/EtlJobConfig.java
@@ -0,0 +1,513 @@
+// 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.config;
+
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+
+import java.io.Serializable;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Copied from Apache Doris org.apache.doris.sparkdpp.EtlJobConfig
+ */
+public class EtlJobConfig implements Serializable {
+ // global dict
+ public static final String GLOBAL_DICT_TABLE_NAME = "doris_global_dict_table_%d";
+ public static final String DISTINCT_KEY_TABLE_NAME = "doris_distinct_key_table_%d_%s";
+ public static final String DORIS_INTERMEDIATE_HIVE_TABLE_NAME = "doris_intermediate_hive_table_%d_%s";
+ // tableId.partitionId.indexId.bucket.schemaHash
+ public static final String TABLET_META_FORMAT = "%d.%d.%d.%d.%d";
+ public static final String ETL_OUTPUT_FILE_FORMAT = "parquet";
+ // dpp result
+ public static final String DPP_RESULT_NAME = "dpp_result.json";
+ // hdfsEtlPath/jobs/dbId/loadLabel/PendingTaskSignature
+ private static final String ETL_OUTPUT_PATH_FORMAT = "%s/jobs/%d/%s/%d";
+ private static final String ETL_OUTPUT_FILE_NAME_DESC_V1 =
+ "version.label.tableId.partitionId.indexId.bucket.schemaHash.parquet";
+ @SerializedName(value = "tables")
+ public Map tables;
+ @SerializedName(value = "outputPath")
+ public String outputPath;
+ @SerializedName(value = "outputFilePattern")
+ public String outputFilePattern;
+ @SerializedName(value = "label")
+ public String label;
+ @SerializedName(value = "properties")
+ public EtlJobProperty properties;
+ @SerializedName(value = "configVersion")
+ public ConfigVersion configVersion;
+
+ /**
+ * for json deserialize
+ */
+ public EtlJobConfig() {
+ }
+
+ public EtlJobConfig(Map tables, String outputFilePattern, String label, EtlJobProperty properties) {
+ this.tables = tables;
+ // set outputPath when submit etl job
+ this.outputPath = null;
+ this.outputFilePattern = outputFilePattern;
+ this.label = label;
+ this.properties = properties;
+ this.configVersion = ConfigVersion.V1;
+ }
+
+ public static String getOutputPath(String hdfsEtlPath, long dbId, String loadLabel, long taskSignature) {
+ return String.format(ETL_OUTPUT_PATH_FORMAT, hdfsEtlPath, dbId, loadLabel, taskSignature);
+ }
+
+ public static String getOutputFilePattern(String loadLabel, FilePatternVersion filePatternVersion) {
+ return String.format("%s.%s.%s.%s", filePatternVersion.name(), loadLabel, TABLET_META_FORMAT,
+ ETL_OUTPUT_FILE_FORMAT);
+ }
+
+ public static String getDppResultFilePath(String outputPath) {
+ return outputPath + "/" + DPP_RESULT_NAME;
+ }
+
+ public static String getTabletMetaStr(String filePath) throws Exception {
+ String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
+ String[] fileNameArr = fileName.split("\\.");
+ // check file version
+ switch (FilePatternVersion.valueOf(fileNameArr[0])) {
+ case V1:
+ // version.label.tableId.partitionId.indexId.bucket.schemaHash.parquet
+ if (fileNameArr.length != ETL_OUTPUT_FILE_NAME_DESC_V1.split("\\.").length) {
+ throw new Exception(
+ "etl output file name error, format: " + ETL_OUTPUT_FILE_NAME_DESC_V1 + ", name: "
+ + fileName);
+ }
+ long tableId = Long.parseLong(fileNameArr[2]);
+ long partitionId = Long.parseLong(fileNameArr[3]);
+ long indexId = Long.parseLong(fileNameArr[4]);
+ int bucket = Integer.parseInt(fileNameArr[5]);
+ int schemaHash = Integer.parseInt(fileNameArr[6]);
+ // tableId.partitionId.indexId.bucket.schemaHash
+ return String.format(TABLET_META_FORMAT, tableId, partitionId, indexId, bucket, schemaHash);
+ default:
+ throw new Exception("etl output file version error. version: " + fileNameArr[0]);
+ }
+ }
+
+ public static EtlJobConfig configFromJson(String jsonConfig) {
+ return new Gson().fromJson(jsonConfig, EtlJobConfig.class);
+ }
+
+ public String configToJson() {
+ Gson gson =
+ new GsonBuilder().addDeserializationExclusionStrategy(new HiddenAnnotationExclusionStrategy()).create();
+ return gson.toJson(this);
+ }
+
+ @Override
+ public String toString() {
+ return "EtlJobConfig{" + "tables=" + tables + ", outputPath='" + outputPath + '\'' + ", outputFilePattern='"
+ + outputFilePattern + '\'' + ", label='" + label + '\'' + ", properties=" + properties + ", version="
+ + configVersion + '}';
+ }
+
+ public String getOutputPath() {
+ return outputPath;
+ }
+
+ public enum ConfigVersion {
+ V1
+ }
+
+ public enum FilePatternVersion {
+ V1
+ }
+
+ public enum SourceType {
+ FILE, HIVE
+ }
+
+ public static class EtlJobProperty implements Serializable {
+ @SerializedName(value = "strictMode")
+ public boolean strictMode;
+ @SerializedName(value = "timezone")
+ public String timezone;
+
+ @Override
+ public String toString() {
+ return "EtlJobProperty{" + "strictMode=" + strictMode + ", timezone='" + timezone + '\'' + '}';
+ }
+ }
+
+ public static class EtlTable implements Serializable {
+ @SerializedName(value = "indexes")
+ public List indexes;
+ @SerializedName(value = "partitionInfo")
+ public EtlPartitionInfo partitionInfo;
+ @SerializedName(value = "fileGroups")
+ public List fileGroups;
+
+ /**
+ * for json deserialize
+ */
+ public EtlTable() {
+ }
+
+ public EtlTable(List etlIndexes, EtlPartitionInfo etlPartitionInfo) {
+ this.indexes = etlIndexes;
+ this.partitionInfo = etlPartitionInfo;
+ this.fileGroups = Lists.newArrayList();
+ }
+
+ public void addFileGroup(EtlFileGroup etlFileGroup) {
+ fileGroups.add(etlFileGroup);
+ }
+
+ @Override
+ public String toString() {
+ return "EtlTable{" + "indexes=" + indexes + ", partitionInfo=" + partitionInfo + ", fileGroups="
+ + fileGroups + '}';
+ }
+ }
+
+ public static class EtlColumn implements Serializable {
+ @SerializedName(value = "columnName")
+ public String columnName;
+ @SerializedName(value = "columnType")
+ public String columnType;
+ @SerializedName(value = "isAllowNull")
+ public boolean isAllowNull;
+ @SerializedName(value = "isKey")
+ public boolean isKey;
+ @SerializedName(value = "aggregationType")
+ public String aggregationType;
+ @SerializedName(value = "defaultValue")
+ public String defaultValue;
+ @SerializedName(value = "stringLength")
+ public int stringLength;
+ @SerializedName(value = "precision")
+ public int precision;
+ @SerializedName(value = "scale")
+ public int scale;
+ @SerializedName(value = "defineExpr")
+ public String defineExpr;
+
+ // for unit test
+ public EtlColumn() {
+ }
+
+ public EtlColumn(String columnName, String columnType, boolean isAllowNull, boolean isKey,
+ String aggregationType, String defaultValue, int stringLength, int precision, int scale) {
+ this.columnName = columnName;
+ this.columnType = columnType;
+ this.isAllowNull = isAllowNull;
+ this.isKey = isKey;
+ this.aggregationType = aggregationType;
+ this.defaultValue = defaultValue;
+ this.stringLength = stringLength;
+ this.precision = precision;
+ this.scale = scale;
+ this.defineExpr = null;
+ }
+
+ @Override
+ public String toString() {
+ return "EtlColumn{" + "columnName='" + columnName + '\'' + ", columnType='" + columnType + '\''
+ + ", isAllowNull=" + isAllowNull + ", isKey=" + isKey + ", aggregationType='" + aggregationType
+ + '\'' + ", defaultValue='" + defaultValue + '\'' + ", stringLength=" + stringLength
+ + ", precision=" + precision + ", scale=" + scale + ", defineExpr='" + defineExpr + '\'' + '}';
+ }
+ }
+
+ public static class EtlIndexComparator implements Comparator {
+ @Override
+ public int compare(EtlIndex a, EtlIndex b) {
+ int diff = a.columns.size() - b.columns.size();
+ if (diff == 0) {
+ return 0;
+ } else if (diff > 0) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ public static class EtlIndex implements Serializable {
+ @SerializedName(value = "indexId")
+ public long indexId;
+ @SerializedName(value = "columns")
+ public List columns;
+ @SerializedName(value = "schemaHash")
+ public int schemaHash;
+ @SerializedName(value = "indexType")
+ public String indexType;
+ @SerializedName(value = "isBaseIndex")
+ public boolean isBaseIndex;
+ @SerializedName(value = "schemaVersion")
+ public int schemaVersion;
+
+ /**
+ * for json deserialize
+ */
+ public EtlIndex() {
+ }
+
+ public EtlIndex(long indexId, List etlColumns, int schemaHash, String indexType, boolean isBaseIndex,
+ int schemaVersion) {
+ this.indexId = indexId;
+ this.columns = etlColumns;
+ this.schemaHash = schemaHash;
+ this.indexType = indexType;
+ this.isBaseIndex = isBaseIndex;
+ this.schemaVersion = schemaVersion;
+ }
+
+ public EtlColumn getColumn(String name) {
+ for (EtlColumn column : columns) {
+ if (column.columnName.equals(name)) {
+ return column;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "EtlIndex{" + "indexId=" + indexId + ", columns=" + columns + ", schemaHash=" + schemaHash
+ + ", indexType='" + indexType + '\'' + ", isBaseIndex=" + isBaseIndex + ", schemaVersion="
+ + schemaVersion + '}';
+ }
+ }
+
+ public static class EtlPartitionInfo implements Serializable {
+ @SerializedName(value = "partitionType")
+ public String partitionType;
+ @SerializedName(value = "partitionColumnRefs")
+ public List partitionColumnRefs;
+ @SerializedName(value = "distributionColumnRefs")
+ public List distributionColumnRefs;
+ @SerializedName(value = "partitions")
+ public List partitions;
+
+ /**
+ * for json deserialize
+ */
+ public EtlPartitionInfo() {
+ }
+
+ public EtlPartitionInfo(String partitionType, List partitionColumnRefs,
+ List distributionColumnRefs, List etlPartitions) {
+ this.partitionType = partitionType;
+ this.partitionColumnRefs = partitionColumnRefs;
+ this.distributionColumnRefs = distributionColumnRefs;
+ this.partitions = etlPartitions;
+ }
+
+ @Override
+ public String toString() {
+ return "EtlPartitionInfo{" + "partitionType='" + partitionType + '\'' + ", partitionColumnRefs="
+ + partitionColumnRefs + ", distributionColumnRefs=" + distributionColumnRefs + ", partitions="
+ + partitions + '}';
+ }
+ }
+
+ public static class EtlPartition implements Serializable {
+ @SerializedName(value = "partitionId")
+ public long partitionId;
+ @SerializedName(value = "startKeys")
+ public List