From fded46674d7d27f887ecafdd66d92c640ae7c961 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 15 Oct 2024 16:16:38 +0800 Subject: [PATCH 01/72] [Improve] Update snapshot version to 2.3.9 (#7841) --- bin/install-plugin.cmd | 4 +-- bin/install-plugin.sh | 4 +-- .../en/seatunnel-engine/download-seatunnel.md | 6 ++-- docs/en/start-v2/docker/docker.md | 6 ++-- docs/en/start-v2/kubernetes/kubernetes.mdx | 36 +++++++++---------- docs/en/start-v2/locally/deployment.md | 8 ++--- .../zh/seatunnel-engine/download-seatunnel.md | 6 ++-- docs/zh/start-v2/locally/deployment.md | 6 ++-- pom.xml | 2 +- tools/dependencies/known-dependencies.txt | 10 +++--- 10 files changed, 44 insertions(+), 44 deletions(-) diff --git a/bin/install-plugin.cmd b/bin/install-plugin.cmd index be82e001bd8..2fe2a340f9a 100644 --- a/bin/install-plugin.cmd +++ b/bin/install-plugin.cmd @@ -22,8 +22,8 @@ REM Get seatunnel home set "SEATUNNEL_HOME=%~dp0..\" echo Set SEATUNNEL_HOME to [%SEATUNNEL_HOME%] -REM Connector default version is 2.3.8, you can also choose a custom version. eg: 2.3.8: install-plugin.bat 2.3.8 -set "version=2.3.8" +REM Connector default version is 2.3.9, you can also choose a custom version. eg: 2.3.9: install-plugin.bat 2.3.9 +set "version=2.3.9" if not "%~1"=="" set "version=%~1" REM Create the lib directory diff --git a/bin/install-plugin.sh b/bin/install-plugin.sh index 1938caf30c3..51afda5ad8a 100755 --- a/bin/install-plugin.sh +++ b/bin/install-plugin.sh @@ -23,8 +23,8 @@ # get seatunnel home SEATUNNEL_HOME=$(cd $(dirname $0);cd ../;pwd) -# connector default version is 2.3.8, you can also choose a custom version. eg: 2.3.8: sh install-plugin.sh 2.3.8 -version=2.3.8 +# connector default version is 2.3.9, you can also choose a custom version. eg: 2.3.9: sh install-plugin.sh 2.3.9 +version=2.3.9 if [ -n "$1" ]; then version="$1" diff --git a/docs/en/seatunnel-engine/download-seatunnel.md b/docs/en/seatunnel-engine/download-seatunnel.md index 48b5ed63a54..12b169e482c 100644 --- a/docs/en/seatunnel-engine/download-seatunnel.md +++ b/docs/en/seatunnel-engine/download-seatunnel.md @@ -20,7 +20,7 @@ Go to the [Seatunnel Download Page](https://seatunnel.apache.org/download) to do Or you can also download it through the terminal. ```shell -export version="2.3.8" +export version="2.3.9" wget "https://archive.apache.org/dist/seatunnel/${version}/apache-seatunnel-${version}-bin.tar.gz" tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" ``` @@ -33,10 +33,10 @@ Starting from the 2.2.0-beta version, the binary package no longer provides the sh bin/install-plugin.sh ``` -If you need a specific connector version, taking 2.3.8 as an example, you need to execute the following command. +If you need a specific connector version, taking 2.3.9 as an example, you need to execute the following command. ```bash -sh bin/install-plugin.sh 2.3.8 +sh bin/install-plugin.sh 2.3.9 ``` Usually you don't need all the connector plugins, so you can specify the plugins you need through configuring `config/plugin_config`, for example, if you only need the `connector-console` plugin, then you can modify the plugin.properties configuration file as follows. diff --git a/docs/en/start-v2/docker/docker.md b/docs/en/start-v2/docker/docker.md index 8c3c620fb1c..2c2c7824f4f 100644 --- a/docs/en/start-v2/docker/docker.md +++ b/docs/en/start-v2/docker/docker.md @@ -40,7 +40,7 @@ You can download the source code from the [download page](https://seatunnel.apac ```shell cd seatunnel # Use already sett maven profile -sh ./mvnw -B clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dlicense.skipAddThirdParty=true -D"docker.build.skip"=false -D"docker.verify.skip"=false -D"docker.push.skip"=true -D"docker.tag"=2.3.8 -Dmaven.deploy.skip -D"skip.spotless"=true --no-snapshot-updates -Pdocker,seatunnel +sh ./mvnw -B clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dlicense.skipAddThirdParty=true -D"docker.build.skip"=false -D"docker.verify.skip"=false -D"docker.push.skip"=true -D"docker.tag"=2.3.9 -Dmaven.deploy.skip -D"skip.spotless"=true --no-snapshot-updates -Pdocker,seatunnel # Check the docker image docker images | grep apache/seatunnel @@ -53,10 +53,10 @@ sh ./mvnw clean package -DskipTests -Dskip.spotless=true # Build docker image cd seatunnel-dist -docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.8 -t apache/seatunnel:2.3.8 . +docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.9 -t apache/seatunnel:2.3.9 . # If you build from dev branch, you should add SNAPSHOT suffix to the version -docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.8-SNAPSHOT -t apache/seatunnel:2.3.8-SNAPSHOT . +docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.9-SNAPSHOT -t apache/seatunnel:2.3.9-SNAPSHOT . # Check the docker image docker images | grep apache/seatunnel diff --git a/docs/en/start-v2/kubernetes/kubernetes.mdx b/docs/en/start-v2/kubernetes/kubernetes.mdx index eb231850514..ce996c09b2a 100644 --- a/docs/en/start-v2/kubernetes/kubernetes.mdx +++ b/docs/en/start-v2/kubernetes/kubernetes.mdx @@ -44,7 +44,7 @@ To run the image with SeaTunnel, first create a `Dockerfile`: ```Dockerfile FROM flink:1.13 -ENV SEATUNNEL_VERSION="2.3.8" +ENV SEATUNNEL_VERSION="2.3.9" ENV SEATUNNEL_HOME="/opt/seatunnel" RUN wget https://dlcdn.apache.org/seatunnel/${SEATUNNEL_VERSION}/apache-seatunnel-${SEATUNNEL_VERSION}-bin.tar.gz @@ -56,13 +56,13 @@ RUN cd ${SEATUNNEL_HOME} && sh bin/install-plugin.sh ${SEATUNNEL_VERSION} Then run the following commands to build the image: ```bash -docker build -t seatunnel:2.3.8-flink-1.13 -f Dockerfile . +docker build -t seatunnel:2.3.9-flink-1.13 -f Dockerfile . ``` -Image `seatunnel:2.3.8-flink-1.13` needs to be present in the host (minikube) so that the deployment can take place. +Image `seatunnel:2.3.9-flink-1.13` needs to be present in the host (minikube) so that the deployment can take place. Load image to minikube via: ```bash -minikube image load seatunnel:2.3.8-flink-1.13 +minikube image load seatunnel:2.3.9-flink-1.13 ``` @@ -72,7 +72,7 @@ minikube image load seatunnel:2.3.8-flink-1.13 ```Dockerfile FROM openjdk:8 -ENV SEATUNNEL_VERSION="2.3.8" +ENV SEATUNNEL_VERSION="2.3.9" ENV SEATUNNEL_HOME="/opt/seatunnel" RUN wget https://dlcdn.apache.org/seatunnel/${SEATUNNEL_VERSION}/apache-seatunnel-${SEATUNNEL_VERSION}-bin.tar.gz @@ -84,13 +84,13 @@ RUN cd ${SEATUNNEL_HOME} && sh bin/install-plugin.sh ${SEATUNNEL_VERSION} Then run the following commands to build the image: ```bash -docker build -t seatunnel:2.3.8 -f Dockerfile . +docker build -t seatunnel:2.3.9 -f Dockerfile . ``` -Image `seatunnel:2.3.8` need to be present in the host (minikube) so that the deployment can take place. +Image `seatunnel:2.3.9` need to be present in the host (minikube) so that the deployment can take place. Load image to minikube via: ```bash -minikube image load seatunnel:2.3.8 +minikube image load seatunnel:2.3.9 ``` @@ -100,7 +100,7 @@ minikube image load seatunnel:2.3.8 ```Dockerfile FROM openjdk:8 -ENV SEATUNNEL_VERSION="2.3.8" +ENV SEATUNNEL_VERSION="2.3.9" ENV SEATUNNEL_HOME="/opt/seatunnel" RUN wget https://dlcdn.apache.org/seatunnel/${SEATUNNEL_VERSION}/apache-seatunnel-${SEATUNNEL_VERSION}-bin.tar.gz @@ -112,13 +112,13 @@ RUN cd ${SEATUNNEL_HOME} && sh bin/install-plugin.sh ${SEATUNNEL_VERSION} Then run the following commands to build the image: ```bash -docker build -t seatunnel:2.3.8 -f Dockerfile . +docker build -t seatunnel:2.3.9 -f Dockerfile . ``` -Image `seatunnel:2.3.8` needs to be present in the host (minikube) so that the deployment can take place. +Image `seatunnel:2.3.9` needs to be present in the host (minikube) so that the deployment can take place. Load image to minikube via: ```bash -minikube image load seatunnel:2.3.8 +minikube image load seatunnel:2.3.9 ``` @@ -191,7 +191,7 @@ none ]}> -In this guide we will use [seatunnel.streaming.conf](https://github.com/apache/seatunnel/blob/2.3.8-release/config/v2.streaming.conf.template): +In this guide we will use [seatunnel.streaming.conf](https://github.com/apache/seatunnel/blob/2.3.9-release/config/v2.streaming.conf.template): ```conf env { @@ -245,7 +245,7 @@ kind: FlinkDeployment metadata: name: seatunnel-flink-streaming-example spec: - image: seatunnel:2.3.8-flink-1.13 + image: seatunnel:2.3.9-flink-1.13 flinkVersion: v1_13 flinkConfiguration: taskmanager.numberOfTaskSlots: "2" @@ -291,7 +291,7 @@ kubectl apply -f seatunnel-flink.yaml -In this guide we will use [seatunnel.streaming.conf](https://github.com/apache/seatunnel/blob/2.3.8-release/config/v2.streaming.conf.template): +In this guide we will use [seatunnel.streaming.conf](https://github.com/apache/seatunnel/blob/2.3.9-release/config/v2.streaming.conf.template): ```conf env { @@ -334,7 +334,7 @@ metadata: spec: containers: - name: seatunnel - image: seatunnel:2.3.8 + image: seatunnel:2.3.9 command: ["/bin/sh","-c","/opt/seatunnel/bin/seatunnel.sh --config /data/seatunnel.streaming.conf -e local"] resources: limits: @@ -366,7 +366,7 @@ kubectl apply -f seatunnel.yaml -In this guide we will use [seatunnel.streaming.conf](https://github.com/apache/seatunnel/blob/2.3.8-release/config/v2.streaming.conf.template): +In this guide we will use [seatunnel.streaming.conf](https://github.com/apache/seatunnel/blob/2.3.9-release/config/v2.streaming.conf.template): ```conf env { @@ -524,7 +524,7 @@ spec: spec: containers: - name: seatunnel - image: seatunnel:2.3.8 + image: seatunnel:2.3.9 imagePullPolicy: IfNotPresent ports: - containerPort: 5801 diff --git a/docs/en/start-v2/locally/deployment.md b/docs/en/start-v2/locally/deployment.md index 8555c097f36..4684871acb0 100644 --- a/docs/en/start-v2/locally/deployment.md +++ b/docs/en/start-v2/locally/deployment.md @@ -22,7 +22,7 @@ Visit the [SeaTunnel Download Page](https://seatunnel.apache.org/download) to do Or you can also download it through the terminal: ```shell -export version="2.3.8" +export version="2.3.9" wget "https://archive.apache.org/dist/seatunnel/${version}/apache-seatunnel-${version}-bin.tar.gz" tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" ``` @@ -35,10 +35,10 @@ Starting from version 2.2.0-beta, the binary package no longer provides connecto sh bin/install-plugin.sh ``` -If you need a specific connector version, taking 2.3.8 as an example, you need to execute the following command: +If you need a specific connector version, taking 2.3.9 as an example, you need to execute the following command: ```bash -sh bin/install-plugin.sh 2.3.8 +sh bin/install-plugin.sh 2.3.9 ``` Typically, you do not need all the connector plugins. You can specify the required plugins by configuring `config/plugin_config`. For example, if you want the sample application to work properly, you will need the `connector-console` and `connector-fake` plugins. You can modify the `plugin_config` configuration file as follows: @@ -71,7 +71,7 @@ You can download the source code from the [download page](https://seatunnel.apac cd seatunnel sh ./mvnw clean install -DskipTests -Dskip.spotless=true # get the binary package -cp seatunnel-dist/target/apache-seatunnel-2.3.8-bin.tar.gz /The-Path-You-Want-To-Copy +cp seatunnel-dist/target/apache-seatunnel-2.3.9-bin.tar.gz /The-Path-You-Want-To-Copy cd /The-Path-You-Want-To-Copy tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" diff --git a/docs/zh/seatunnel-engine/download-seatunnel.md b/docs/zh/seatunnel-engine/download-seatunnel.md index 18b8cc68db5..d8d0b449137 100644 --- a/docs/zh/seatunnel-engine/download-seatunnel.md +++ b/docs/zh/seatunnel-engine/download-seatunnel.md @@ -20,7 +20,7 @@ import TabItem from '@theme/TabItem'; 或者您也可以通过终端下载 ```shell -export version="2.3.8" +export version="2.3.9" wget "https://archive.apache.org/dist/seatunnel/${version}/apache-seatunnel-${version}-bin.tar.gz" tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" ``` @@ -30,13 +30,13 @@ tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" 从2.2.0-beta版本开始,二进制包不再默认提供连接器依赖,因此在第一次使用它时,您需要执行以下命令来安装连接器:(当然,您也可以从 [Apache Maven Repository](https://repo.maven.apache.org/maven2/org/apache/seatunnel/) 手动下载连接器,然后将其移动至`connectors/seatunnel`目录下)。 ```bash -sh bin/install-plugin.sh 2.3.8 +sh bin/install-plugin.sh 2.3.9 ``` 如果您需要指定的连接器版本,以2.3.7为例,您需要执行如下命令 ```bash -sh bin/install-plugin.sh 2.3.8 +sh bin/install-plugin.sh 2.3.9 ``` 通常您并不需要所有的连接器插件,所以您可以通过配置`config/plugin_config`来指定您所需要的插件,例如,您只需要`connector-console`插件,那么您可以修改plugin.properties配置文件如下 diff --git a/docs/zh/start-v2/locally/deployment.md b/docs/zh/start-v2/locally/deployment.md index ce17e773319..275c0f2b92a 100644 --- a/docs/zh/start-v2/locally/deployment.md +++ b/docs/zh/start-v2/locally/deployment.md @@ -22,7 +22,7 @@ import TabItem from '@theme/TabItem'; 或者您也可以通过终端下载: ```shell -export version="2.3.8" +export version="2.3.9" wget "https://archive.apache.org/dist/seatunnel/${version}/apache-seatunnel-${version}-bin.tar.gz" tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" ``` @@ -38,7 +38,7 @@ sh bin/install-plugin.sh 如果您需要指定的连接器版本,以2.3.7为例,您需要执行如下命令: ```bash -sh bin/install-plugin.sh 2.3.8 +sh bin/install-plugin.sh 2.3.9 ``` 通常情况下,你不需要所有的连接器插件。你可以通过配置`config/plugin_config`来指定所需的插件。例如,如果你想让示例应用程序正常工作,你将需要`connector-console`和`connector-fake`插件。你可以修改`plugin_config`配置文件,如下所示: @@ -71,7 +71,7 @@ connector-console cd seatunnel sh ./mvnw clean install -DskipTests -Dskip.spotless=true # 获取构建好的二进制包 -cp seatunnel-dist/target/apache-seatunnel-2.3.8-bin.tar.gz /The-Path-You-Want-To-Copy +cp seatunnel-dist/target/apache-seatunnel-2.3.9-bin.tar.gz /The-Path-You-Want-To-Copy cd /The-Path-You-Want-To-Copy tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" diff --git a/pom.xml b/pom.xml index e80dae90247..f9d59c79ab0 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 2.3.8-SNAPSHOT + 2.3.9-SNAPSHOT 2.1.1 UTF-8 1.8 diff --git a/tools/dependencies/known-dependencies.txt b/tools/dependencies/known-dependencies.txt index 72b2ef03f6b..1f49332ef92 100755 --- a/tools/dependencies/known-dependencies.txt +++ b/tools/dependencies/known-dependencies.txt @@ -26,9 +26,9 @@ protostuff-collectionschema-1.8.0.jar protostuff-core-1.8.0.jar protostuff-runtime-1.8.0.jar scala-library-2.12.15.jar -seatunnel-jackson-2.3.8-SNAPSHOT-optional.jar -seatunnel-guava-2.3.8-SNAPSHOT-optional.jar -seatunnel-hazelcast-shade-2.3.8-SNAPSHOT-optional.jar +seatunnel-jackson-2.3.9-SNAPSHOT-optional.jar +seatunnel-guava-2.3.9-SNAPSHOT-optional.jar +seatunnel-hazelcast-shade-2.3.9-SNAPSHOT-optional.jar slf4j-api-1.7.25.jar jsqlparser-4.5.jar animal-sniffer-annotations-1.17.jar @@ -46,7 +46,7 @@ accessors-smart-2.4.7.jar asm-9.1.jar avro-1.11.1.jar groovy-4.0.16.jar -seatunnel-janino-2.3.8-SNAPSHOT-optional.jar +seatunnel-janino-2.3.9-SNAPSHOT-optional.jar protobuf-java-util-3.25.3.jar protobuf-java-3.25.3.jar protoc-jar-3.11.4.jar @@ -69,4 +69,4 @@ jetty-util-9.4.20.v20190813.jar jetty-util-9.4.56.v20240826.jar jetty-util-ajax-9.4.56.v20240826.jar javax.servlet-api-3.1.0.jar -seatunnel-jetty9-9.4.56-2.3.8-SNAPSHOT-optional.jar +seatunnel-jetty9-9.4.56-2.3.9-SNAPSHOT-optional.jar From c7a384af2b8bff37861ec249bff6670100bda9b2 Mon Sep 17 00:00:00 2001 From: dailai <837833280@qq.com> Date: Tue, 15 Oct 2024 17:26:51 +0800 Subject: [PATCH 02/72] [Improve][Connector-v2] Use checkpointId as the commit's identifier instead of the hash for streaming write of paimon sink (#7835) --- .../apache/seatunnel/api/sink/SinkWriter.java | 15 +++++++ .../multitablesink/MultiTableSinkWriter.java | 9 +++- .../MultiTableSinkWriterTest.java | 2 +- .../paimon/sink/PaimonSinkWriter.java | 12 ++++-- .../commit/PaimonAggregatedCommitInfo.java | 4 +- .../commit/PaimonAggregatedCommitter.java | 42 +++++++++++++------ .../paimon/sink/commit/PaimonCommitInfo.java | 2 + .../server/task/flow/SinkFlowLifeCycle.java | 2 +- .../flink/sink/FlinkSinkWriter.java | 2 +- .../spark/sink/writer/SparkDataWriter.java | 2 +- .../sink/write/SeaTunnelSparkDataWriter.java | 2 +- 11 files changed, 72 insertions(+), 22 deletions(-) diff --git a/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/SinkWriter.java b/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/SinkWriter.java index 4567e98cbfe..330580b980f 100644 --- a/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/SinkWriter.java +++ b/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/SinkWriter.java @@ -62,8 +62,23 @@ default void applySchemaChange(SchemaChangeEvent event) throws IOException {} * * @return the commit info need to commit */ + @Deprecated Optional prepareCommit() throws IOException; + /** + * prepare the commit, will be called before {@link #snapshotState(long checkpointId)}. If you + * need to use 2pc, you can return the commit info in this method, and receive the commit info + * in {@link SinkCommitter#commit(List)}. If this method failed (by throw exception), **Only** + * Spark engine will call {@link #abortPrepare()} + * + * @param checkpointId checkpointId + * @return the commit info need to commit + * @throws IOException If fail to prepareCommit + */ + default Optional prepareCommit(long checkpointId) throws IOException { + return prepareCommit(); + } + /** * @return The writer's state. * @throws IOException if fail to snapshot writer's state. diff --git a/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriter.java b/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriter.java index f01c3d65dcf..f5b30be5370 100644 --- a/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriter.java +++ b/seatunnel-api/src/main/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriter.java @@ -220,6 +220,11 @@ public List snapshotState(long checkpointId) throws IOException @Override public Optional prepareCommit() throws IOException { + return Optional.empty(); + } + + @Override + public Optional prepareCommit(long checkpointId) throws IOException { checkQueueRemain(); subSinkErrorCheck(); MultiTableCommitInfo multiTableCommitInfo = @@ -238,7 +243,9 @@ public Optional prepareCommit() throws IOException { .entrySet()) { Optional commit; try { - commit = sinkWriterEntry.getValue().prepareCommit(); + SinkWriter sinkWriter = + sinkWriterEntry.getValue(); + commit = sinkWriter.prepareCommit(checkpointId); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/seatunnel-api/src/test/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriterTest.java b/seatunnel-api/src/test/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriterTest.java index 66e0ff0d4ef..86722eb2466 100644 --- a/seatunnel-api/src/test/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriterTest.java +++ b/seatunnel-api/src/test/java/org/apache/seatunnel/api/sink/multitablesink/MultiTableSinkWriterTest.java @@ -57,7 +57,7 @@ public void testPrepareCommitState() throws IOException { DefaultSerializer defaultSerializer = new DefaultSerializer<>(); for (int i = 0; i < 100; i++) { - byte[] bytes = defaultSerializer.serialize(multiTableSinkWriter.prepareCommit().get()); + byte[] bytes = defaultSerializer.serialize(multiTableSinkWriter.prepareCommit(i).get()); defaultSerializer.deserialize(bytes); } } diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java index 7a3fe6d0336..8cc6d0d485f 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java @@ -130,6 +130,7 @@ public PaimonSinkWriter( return; } this.commitUser = states.get(0).getCommitUser(); + long checkpointId = states.get(0).getCheckpointId(); try (TableCommit tableCommit = tableWriteBuilder.newCommit()) { List commitables = states.stream() @@ -142,7 +143,7 @@ public PaimonSinkWriter( ((BatchTableCommit) tableCommit).commit(commitables); } else { log.debug("Trying to recommit states streaming mode"); - ((StreamTableCommit) tableCommit).commit(Objects.hash(commitables), commitables); + ((StreamTableCommit) tableCommit).commit(checkpointId, commitables); } } catch (Exception e) { throw new PaimonConnectorException( @@ -174,16 +175,21 @@ public void write(SeaTunnelRow element) throws IOException { @Override public Optional prepareCommit() throws IOException { + return Optional.empty(); + } + + @Override + public Optional prepareCommit(long checkpointId) throws IOException { try { List fileCommittables; if (JobContextUtil.isBatchJob(jobContext)) { fileCommittables = ((BatchTableWrite) tableWrite).prepareCommit(); } else { fileCommittables = - ((StreamTableWrite) tableWrite).prepareCommit(false, committables.size()); + ((StreamTableWrite) tableWrite).prepareCommit(false, checkpointId); } committables.addAll(fileCommittables); - return Optional.of(new PaimonCommitInfo(fileCommittables)); + return Optional.of(new PaimonCommitInfo(fileCommittables, checkpointId)); } catch (Exception e) { throw new PaimonConnectorException( PaimonConnectorErrorCode.TABLE_PRE_COMMIT_FAILED, diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitInfo.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitInfo.java index 8a7ad84a2e8..83ed71f6151 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitInfo.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitInfo.java @@ -24,6 +24,7 @@ import java.io.Serializable; import java.util.List; +import java.util.Map; /** Paimon connector aggregate commit information class. */ @Data @@ -32,5 +33,6 @@ public class PaimonAggregatedCommitInfo implements Serializable { private static final long serialVersionUID = 1; - private List> committables; + // key: checkpointId value: Paimon commit message List + private Map> committablesMap; } diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java index 5c3f68f3365..a3e457907e0 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java @@ -36,10 +36,11 @@ import lombok.extern.slf4j.Slf4j; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; -import java.util.Objects; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; /** Paimon connector aggregated committer class */ @@ -70,21 +71,32 @@ public PaimonAggregatedCommitter( public List commit( List aggregatedCommitInfo) throws IOException { try (TableCommit tableCommit = tableWriteBuilder.newCommit()) { - List fileCommittables = - aggregatedCommitInfo.stream() - .map(PaimonAggregatedCommitInfo::getCommittables) - .flatMap(List::stream) - .flatMap(List::stream) - .collect(Collectors.toList()); PaimonSecurityContext.runSecured( () -> { if (JobContextUtil.isBatchJob(jobContext)) { log.debug("Trying to commit states batch mode"); + List fileCommittables = + aggregatedCommitInfo.stream() + .flatMap( + info -> + info.getCommittablesMap().values() + .stream()) + .flatMap(List::stream) + .collect(Collectors.toList()); ((BatchTableCommit) tableCommit).commit(fileCommittables); } else { log.debug("Trying to commit states streaming mode"); - ((StreamTableCommit) tableCommit) - .commit(Objects.hash(fileCommittables), fileCommittables); + aggregatedCommitInfo.stream() + .flatMap( + paimonAggregatedCommitInfo -> + paimonAggregatedCommitInfo.getCommittablesMap() + .entrySet().stream()) + .forEach( + entry -> + ((StreamTableCommit) tableCommit) + .commit( + entry.getKey(), + entry.getValue())); } return null; }); @@ -99,8 +111,14 @@ public List commit( @Override public PaimonAggregatedCommitInfo combine(List commitInfos) { - List> committables = new ArrayList<>(); - commitInfos.forEach(commitInfo -> committables.add(commitInfo.getCommittables())); + Map> committables = new HashMap<>(); + commitInfos.forEach( + commitInfo -> + committables + .computeIfAbsent( + commitInfo.getCheckpointId(), + id -> new CopyOnWriteArrayList<>()) + .addAll(commitInfo.getCommittables())); return new PaimonAggregatedCommitInfo(committables); } diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonCommitInfo.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonCommitInfo.java index 9927973821c..1d9844103fc 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonCommitInfo.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonCommitInfo.java @@ -32,4 +32,6 @@ public class PaimonCommitInfo implements Serializable { private static final long serialVersionUID = 1L; List committables; + + Long checkpointId; } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java index bce6e9f6376..5970d9a745c 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java @@ -185,7 +185,7 @@ public void received(Record record) { } if (barrier.snapshot()) { try { - lastCommitInfo = writer.prepareCommit(); + lastCommitInfo = writer.prepareCommit(barrier.getId()); } catch (Exception e) { writer.abortPrepare(); throw e; diff --git a/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/sink/FlinkSinkWriter.java b/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/sink/FlinkSinkWriter.java index 7a47052c01d..3ee6e3533cc 100644 --- a/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/sink/FlinkSinkWriter.java +++ b/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/sink/FlinkSinkWriter.java @@ -101,7 +101,7 @@ public void write(InputT element, SinkWriter.Context context) throws IOException @Override public List> prepareCommit(boolean flush) throws IOException { - Optional commTOptional = sinkWriter.prepareCommit(); + Optional commTOptional = sinkWriter.prepareCommit(checkpointId); return commTOptional .map(CommitWrapper::new) .map(Collections::singletonList) diff --git a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/sink/writer/SparkDataWriter.java b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/sink/writer/SparkDataWriter.java index a9eac500629..c34a0783eb1 100644 --- a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/sink/writer/SparkDataWriter.java +++ b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/sink/writer/SparkDataWriter.java @@ -87,7 +87,7 @@ public WriterCommitMessage commit() throws IOException { // 2. commit fails // 2.1. We have the commit info, we need to execute the sinkCommitter#abort to rollback // the transaction. - Optional commitInfoTOptional = sinkWriter.prepareCommit(); + Optional commitInfoTOptional = sinkWriter.prepareCommit(epochId); commitInfoTOptional.ifPresent(commitInfoT -> latestCommitInfoT = commitInfoT); sinkWriter.snapshotState(epochId++); if (sinkCommitter != null) { diff --git a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/sink/write/SeaTunnelSparkDataWriter.java b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/sink/write/SeaTunnelSparkDataWriter.java index c2c24aa9147..1a97f6e618b 100644 --- a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/sink/write/SeaTunnelSparkDataWriter.java +++ b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/sink/write/SeaTunnelSparkDataWriter.java @@ -79,7 +79,7 @@ protected void initResourceManger() { @Override public WriterCommitMessage commit() throws IOException { - Optional commitInfoTOptional = sinkWriter.prepareCommit(); + Optional commitInfoTOptional = sinkWriter.prepareCommit(epochId); commitInfoTOptional.ifPresent(commitInfoT -> latestCommitInfoT = commitInfoT); sinkWriter.snapshotState(epochId++); if (sinkCommitter != null) { From 8871f52fdec82ed1845f27b56fda0e08a62cd7f1 Mon Sep 17 00:00:00 2001 From: Guangdong Liu <804167098@qq.com> Date: Tue, 15 Oct 2024 19:50:03 +0800 Subject: [PATCH 03/72] [bugfix][core] Fix the problem of incorrect association between metrics and nodes (#7799) --- docs/en/seatunnel-engine/rest-api-v1.md | 18 +- docs/en/seatunnel-engine/rest-api-v2.md | 18 +- docs/zh/seatunnel-engine/rest-api-v1.md | 21 ++- docs/zh/seatunnel-engine/rest-api-v2.md | 20 ++- .../seatunnel/engine/e2e/RestApiIT.java | 160 ++++++++++++++++++ .../seatunnel/engine/core/job/JobDAGInfo.java | 4 + .../seatunnel/engine/server/dag/DAGUtils.java | 10 +- .../metrics/TaskMetricsCalcContext.java | 3 +- .../rest/RestHttpGetCommandProcessor.java | 13 +- .../server/rest/servlet/BaseServlet.java | 31 +++- .../server/rest/servlet/JobInfoServlet.java | 14 +- .../server/task/SeaTunnelSourceCollector.java | 2 +- .../server/task/flow/SinkFlowLifeCycle.java | 58 ++++++- 13 files changed, 334 insertions(+), 38 deletions(-) diff --git a/docs/en/seatunnel-engine/rest-api-v1.md b/docs/en/seatunnel-engine/rest-api-v1.md index ec9d8f13b9b..aaefcbc5faf 100644 --- a/docs/en/seatunnel-engine/rest-api-v1.md +++ b/docs/en/seatunnel-engine/rest-api-v1.md @@ -121,10 +121,19 @@ network: }, "createTime": "", "jobDag": { - "vertices": [ + "jobId": "", + "envOptions": [], + "vertexInfoMap": [ + { + "vertexId": 1, + "type": "", + "vertexName": "", + "tablePaths": [ + "" + ] + } ], - "edges": [ - ] + "pipelineEdges": {} }, "pluginJarsUrls": [ ], @@ -162,6 +171,7 @@ network: "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -227,6 +237,7 @@ This API has been deprecated, please use /hazelcast/rest/maps/job-info/:jobId in "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -307,6 +318,7 @@ When we can't get the job info, the response will be: "finishTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, diff --git a/docs/en/seatunnel-engine/rest-api-v2.md b/docs/en/seatunnel-engine/rest-api-v2.md index e5b9d5d718d..1e7cf10d4e6 100644 --- a/docs/en/seatunnel-engine/rest-api-v2.md +++ b/docs/en/seatunnel-engine/rest-api-v2.md @@ -88,10 +88,19 @@ seatunnel: }, "createTime": "", "jobDag": { - "vertices": [ + "jobId": "", + "envOptions": [], + "vertexInfoMap": [ + { + "vertexId": 1, + "type": "", + "vertexName": "", + "tablePaths": [ + "" + ] + } ], - "edges": [ - ] + "pipelineEdges": {} }, "pluginJarsUrls": [ ], @@ -129,6 +138,7 @@ seatunnel: "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -194,6 +204,7 @@ This API has been deprecated, please use /job-info/:jobId instead "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -274,6 +285,7 @@ When we can't get the job info, the response will be: "finishTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, diff --git a/docs/zh/seatunnel-engine/rest-api-v1.md b/docs/zh/seatunnel-engine/rest-api-v1.md index 5154922ec07..a59c6bbde5f 100644 --- a/docs/zh/seatunnel-engine/rest-api-v1.md +++ b/docs/zh/seatunnel-engine/rest-api-v1.md @@ -119,10 +119,19 @@ network: }, "createTime": "", "jobDag": { - "vertices": [ + "jobId": "", + "envOptions": [], + "vertexInfoMap": [ + { + "vertexId": 1, + "type": "", + "vertexName": "", + "tablePaths": [ + "" + ] + } ], - "edges": [ - ] + "pipelineEdges": {} }, "pluginJarsUrls": [ ], @@ -160,6 +169,7 @@ network: "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -239,6 +249,7 @@ network: "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -305,6 +316,7 @@ network: "finishTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -316,7 +328,8 @@ network: } ], "pipelineEdges": {} - }, "metrics": "" + }, + "metrics": "" } ] ``` diff --git a/docs/zh/seatunnel-engine/rest-api-v2.md b/docs/zh/seatunnel-engine/rest-api-v2.md index df884fa18ec..75a03f93fdb 100644 --- a/docs/zh/seatunnel-engine/rest-api-v2.md +++ b/docs/zh/seatunnel-engine/rest-api-v2.md @@ -80,14 +80,21 @@ seatunnel: "jobId": "", "jobName": "", "jobStatus": "", - "envOptions": { - }, "createTime": "", "jobDag": { - "vertices": [ + "jobId": "", + "envOptions": [], + "vertexInfoMap": [ + { + "vertexId": 1, + "type": "", + "vertexName": "", + "tablePaths": [ + "" + ] + } ], - "edges": [ - ] + "pipelineEdges": {} }, "pluginJarsUrls": [ ], @@ -125,6 +132,7 @@ seatunnel: "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -204,6 +212,7 @@ seatunnel: "createTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, @@ -270,6 +279,7 @@ seatunnel: "finishTime": "", "jobDag": { "jobId": "", + "envOptions": [], "vertexInfoMap": [ { "vertexId": 1, diff --git a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java index 8e5b15cc3d2..0dd90edddad 100644 --- a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java +++ b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java @@ -280,6 +280,55 @@ public void testGetRunningJobs() { + RestConstant.RUNNING_JOBS_URL) .then() .statusCode(200) + .body( + "[0].jobDag.jobId", + equalTo( + Long.toString( + clientJobProxy + .getJobId()))) + .body("[0].jobDag.pipelineEdges", hasKey("1")) + .body( + "[0].jobDag.pipelineEdges['1']", + hasSize(1)) + .body( + "[0].jobDag.pipelineEdges['1'][0].inputVertexId", + equalTo("1")) + .body( + "[0].jobDag.pipelineEdges['1'][0].targetVertexId", + equalTo("2")) + .body("[0].jobDag.vertexInfoMap", hasSize(2)) + .body( + "[0].jobDag.vertexInfoMap[0].vertexId", + equalTo(1)) + .body( + "[0].jobDag.vertexInfoMap[0].type", + equalTo("source")) + .body( + "[0].jobDag.vertexInfoMap[0].vertexName", + equalTo( + "pipeline-1 [Source[0]-FakeSource]")) + .body( + "[0].jobDag.vertexInfoMap[0].tablePaths[0]", + equalTo("fake")) + .body( + "[0].jobDag.vertexInfoMap[1].vertexId", + equalTo(2)) + .body( + "[0].jobDag.vertexInfoMap[1].type", + equalTo("sink")) + .body( + "[0].jobDag.vertexInfoMap[1].vertexName", + equalTo( + "pipeline-1 [Sink[0]-LocalFile-MultiTableSink]")) + .body( + "[0].jobDag.vertexInfoMap[1].tablePaths[0]", + equalTo("fake")) + .body( + "[0].jobDag.envOptions.'job.mode'", + equalTo("STREAMING")) + .body( + "[0].jobDag.envOptions.'checkpoint.interval'", + equalTo("5000")) .body("[0].jobName", equalTo("fake_to_file")) .body("[0].jobStatus", equalTo("RUNNING")); @@ -293,6 +342,55 @@ public void testGetRunningJobs() { + RestConstant.RUNNING_JOBS_URL) .then() .statusCode(200) + .body( + "[0].jobDag.jobId", + equalTo( + Long.toString( + clientJobProxy + .getJobId()))) + .body("[0].jobDag.pipelineEdges", hasKey("1")) + .body( + "[0].jobDag.pipelineEdges['1']", + hasSize(1)) + .body( + "[0].jobDag.pipelineEdges['1'][0].inputVertexId", + equalTo("1")) + .body( + "[0].jobDag.pipelineEdges['1'][0].targetVertexId", + equalTo("2")) + .body("[0].jobDag.vertexInfoMap", hasSize(2)) + .body( + "[0].jobDag.vertexInfoMap[0].vertexId", + equalTo(1)) + .body( + "[0].jobDag.vertexInfoMap[0].type", + equalTo("source")) + .body( + "[0].jobDag.vertexInfoMap[0].vertexName", + equalTo( + "pipeline-1 [Source[0]-FakeSource]")) + .body( + "[0].jobDag.vertexInfoMap[0].tablePaths[0]", + equalTo("fake")) + .body( + "[0].jobDag.vertexInfoMap[1].vertexId", + equalTo(2)) + .body( + "[0].jobDag.vertexInfoMap[1].type", + equalTo("sink")) + .body( + "[0].jobDag.vertexInfoMap[1].vertexName", + equalTo( + "pipeline-1 [Sink[0]-LocalFile-MultiTableSink]")) + .body( + "[0].jobDag.vertexInfoMap[1].tablePaths[0]", + equalTo("fake")) + .body( + "[0].jobDag.envOptions.'job.mode'", + equalTo("STREAMING")) + .body( + "[0].jobDag.envOptions.'checkpoint.interval'", + equalTo("5000")) .body("[0].jobName", equalTo("fake_to_file")) .body("[0].jobStatus", equalTo("RUNNING")); })); @@ -314,6 +412,57 @@ public void testGetJobInfoByJobId() { + batchJobProxy.getJobId()) .then() .statusCode(200) + .body( + "jobDag.jobId", + equalTo( + Long.toString( + batchJobProxy.getJobId()))) + .body("jobDag.pipelineEdges", hasKey("1")) + .body("jobDag.pipelineEdges['1']", hasSize(1)) + .body( + "jobDag.pipelineEdges['1'][0].inputVertexId", + equalTo("1")) + .body( + "jobDag.pipelineEdges['1'][0].targetVertexId", + equalTo("2")) + .body("jobDag.vertexInfoMap", hasSize(2)) + .body( + "jobDag.vertexInfoMap[0].vertexId", + equalTo(1)) + .body( + "jobDag.vertexInfoMap[0].type", + equalTo("source")) + .body( + "jobDag.vertexInfoMap[0].vertexName", + equalTo( + "pipeline-1 [Source[0]-FakeSource]")) + .body( + "jobDag.vertexInfoMap[0].tablePaths[0]", + equalTo("fake")) + .body( + "jobDag.vertexInfoMap[1].vertexId", + equalTo(2)) + .body( + "jobDag.vertexInfoMap[1].type", + equalTo("sink")) + .body( + "jobDag.vertexInfoMap[1].vertexName", + equalTo( + "pipeline-1 [Sink[0]-console-MultiTableSink]")) + .body( + "jobDag.vertexInfoMap[1].tablePaths[0]", + equalTo("fake")) + .body( + "metrics.TableSourceReceivedCount.fake", + equalTo("5")) + .body( + "metrics.TableSinkWriteCount.fake", + equalTo("5")) + .body("metrics.SinkWriteCount", equalTo("5")) + .body("metrics.SourceReceivedCount", equalTo("5")) + .body( + "jobDag.envOptions.'job.mode'", + equalTo("BATCH")) .body("jobName", equalTo("fake_to_console")) .body("jobStatus", equalTo("FINISHED")); @@ -369,6 +518,17 @@ public void testGetJobInfoByJobId() { .body( "jobDag.vertexInfoMap[1].tablePaths[0]", equalTo("fake")) + .body( + "metrics.TableSourceReceivedCount.fake", + equalTo("5")) + .body( + "metrics.TableSinkWriteCount.fake", + equalTo("5")) + .body("metrics.SinkWriteCount", equalTo("5")) + .body("metrics.SourceReceivedCount", equalTo("5")) + .body( + "jobDag.envOptions.'job.mode'", + equalTo("BATCH")) .body("jobName", equalTo("fake_to_console")) .body("jobStatus", equalTo("FINISHED")); }); diff --git a/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobDAGInfo.java b/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobDAGInfo.java index ee6326acbde..aea57beef33 100644 --- a/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobDAGInfo.java +++ b/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobDAGInfo.java @@ -21,6 +21,7 @@ import com.hazelcast.internal.json.JsonArray; import com.hazelcast.internal.json.JsonObject; +import com.hazelcast.internal.util.JsonUtil; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -34,6 +35,7 @@ @Data public class JobDAGInfo implements Serializable { Long jobId; + Map envOptions; Map> pipelineEdges; Map vertexInfoMap; @@ -54,6 +56,8 @@ public JsonObject toJsonObject() { JsonObject jsonObject = new JsonObject(); jsonObject.add("jobId", jobId.toString()); jsonObject.add("pipelineEdges", pipelineEdgesJsonObject); + jsonObject.add("envOptions", JsonUtil.toJsonObject(envOptions)); + JsonArray vertexInfoMapString = new JsonArray(); for (Map.Entry entry : vertexInfoMap.entrySet()) { JsonObject vertexInfoJsonObj = new JsonObject(); diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/DAGUtils.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/DAGUtils.java index e7b41de73d9..bbe6d77e00f 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/DAGUtils.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/DAGUtils.java @@ -86,7 +86,10 @@ public static JobDAGInfo getJobDAGInfo( }); }); return new JobDAGInfo( - jobImmutableInformation.getJobId(), pipelineWithEdges, vertexInfoMap); + jobImmutableInformation.getJobId(), + logicalDag.getJobConfig().getEnvOptions(), + pipelineWithEdges, + vertexInfoMap); } else { // Generate LogicalPlan DAG List edges = @@ -130,7 +133,10 @@ public static JobDAGInfo getJobDAGInfo( }, Collectors.toList())); return new JobDAGInfo( - jobImmutableInformation.getJobId(), pipelineWithEdges, vertexInfoMap); + jobImmutableInformation.getJobId(), + logicalDag.getJobConfig().getEnvOptions(), + pipelineWithEdges, + vertexInfoMap); } } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/metrics/TaskMetricsCalcContext.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/metrics/TaskMetricsCalcContext.java index eab9ecbd348..6890421f9f1 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/metrics/TaskMetricsCalcContext.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/metrics/TaskMetricsCalcContext.java @@ -122,14 +122,13 @@ private void initializeMetrics( } } - public void updateMetrics(Object data) { + public void updateMetrics(Object data, String tableId) { count.inc(); QPS.markEvent(); if (data instanceof SeaTunnelRow) { SeaTunnelRow row = (SeaTunnelRow) data; bytes.inc(row.getBytesSize()); bytesPerSeconds.markEvent(row.getBytesSize()); - String tableId = row.getTableId(); if (StringUtils.isNotBlank(tableId)) { String tableName = TablePath.of(tableId).getFullName(); diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java index d052629f2e0..b860dbc1c74 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java @@ -35,6 +35,7 @@ import org.apache.seatunnel.engine.core.job.JobInfo; import org.apache.seatunnel.engine.core.job.JobStatus; import org.apache.seatunnel.engine.server.SeaTunnelServer; +import org.apache.seatunnel.engine.server.dag.DAGUtils; import org.apache.seatunnel.engine.server.log.Log4j2HttpGetCommandProcessor; import org.apache.seatunnel.engine.server.master.JobHistoryService.JobState; import org.apache.seatunnel.engine.server.operation.GetClusterHealthMetricsOperation; @@ -692,19 +693,23 @@ private JsonObject convertToJson(JobInfo jobInfo, long jobId) { jobStatus = seaTunnelServer.getCoordinatorService().getJobStatus(jobId); } + JobDAGInfo jobDAGInfo = + DAGUtils.getJobDAGInfo( + logicalDag, + jobImmutableInformation, + getSeaTunnelServer(false).getSeaTunnelConfig().getEngineConfig(), + true); + jobInfoJson .add(RestConstant.JOB_ID, String.valueOf(jobId)) .add(RestConstant.JOB_NAME, logicalDag.getJobConfig().getName()) .add(RestConstant.JOB_STATUS, jobStatus.toString()) - .add( - RestConstant.ENV_OPTIONS, - JsonUtil.toJsonObject(logicalDag.getJobConfig().getEnvOptions())) .add( RestConstant.CREATE_TIME, DateTimeUtils.toString( jobImmutableInformation.getCreateTime(), DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS)) - .add(RestConstant.JOB_DAG, logicalDag.getLogicalDagAsJson()) + .add(RestConstant.JOB_DAG, jobDAGInfo.toJsonObject()) .add( RestConstant.PLUGIN_JARS_URLS, (JsonValue) diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java index ce5fc74d3c8..5553e2c85ec 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java @@ -37,6 +37,7 @@ import org.apache.seatunnel.engine.core.job.JobStatus; import org.apache.seatunnel.engine.server.CoordinatorService; import org.apache.seatunnel.engine.server.SeaTunnelServer; +import org.apache.seatunnel.engine.server.dag.DAGUtils; import org.apache.seatunnel.engine.server.master.JobHistoryService; import org.apache.seatunnel.engine.server.operation.CancelJobOperation; import org.apache.seatunnel.engine.server.operation.GetJobMetricsOperation; @@ -57,6 +58,7 @@ import com.hazelcast.internal.json.JsonObject; import com.hazelcast.internal.json.JsonValue; import com.hazelcast.internal.serialization.Data; +import com.hazelcast.internal.util.JsonUtil; import com.hazelcast.jet.impl.execution.init.CustomClassLoadedObject; import com.hazelcast.spi.impl.NodeEngineImpl; @@ -74,7 +76,6 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static com.hazelcast.internal.util.JsonUtil.toJsonObject; import static org.apache.seatunnel.api.common.metrics.MetricNames.SINK_WRITE_BYTES; import static org.apache.seatunnel.api.common.metrics.MetricNames.SINK_WRITE_BYTES_PER_SECONDS; import static org.apache.seatunnel.api.common.metrics.MetricNames.SINK_WRITE_COUNT; @@ -182,19 +183,26 @@ nodeEngine, new GetJobStatusOperation(jobId)) jobStatus = seaTunnelServer.getCoordinatorService().getJobStatus(jobId); } + JobDAGInfo jobDAGInfo = + DAGUtils.getJobDAGInfo( + logicalDag, + jobImmutableInformation, + getSeaTunnelServer(false).getSeaTunnelConfig().getEngineConfig(), + true); + jobInfoJson .add(RestConstant.JOB_ID, String.valueOf(jobId)) .add(RestConstant.JOB_NAME, logicalDag.getJobConfig().getName()) .add(RestConstant.JOB_STATUS, jobStatus.toString()) .add( RestConstant.ENV_OPTIONS, - toJsonObject(logicalDag.getJobConfig().getEnvOptions())) + JsonUtil.toJsonObject(logicalDag.getJobConfig().getEnvOptions())) .add( RestConstant.CREATE_TIME, DateTimeUtils.toString( jobImmutableInformation.getCreateTime(), DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS)) - .add(RestConstant.JOB_DAG, logicalDag.getLogicalDagAsJson()) + .add(RestConstant.JOB_DAG, jobDAGInfo.toJsonObject()) .add( RestConstant.PLUGIN_JARS_URLS, (JsonValue) @@ -210,7 +218,7 @@ nodeEngine, new GetJobStatusOperation(jobId)) .add( RestConstant.IS_START_WITH_SAVE_POINT, jobImmutableInformation.isStartWithSavePoint()) - .add(RestConstant.METRICS, toJsonObject(getJobMetrics(jobMetrics))); + .add(RestConstant.METRICS, metricsToJsonObject(getJobMetrics(jobMetrics))); return jobInfoJson; } @@ -288,7 +296,7 @@ protected JsonObject getJobInfoJson( DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS)) .add(RestConstant.JOB_DAG, jobDAGInfo.toJsonObject()) .add(RestConstant.PLUGIN_JARS_URLS, new JsonArray()) - .add(RestConstant.METRICS, toJsonObject(getJobMetrics(jobMetrics))); + .add(RestConstant.METRICS, metricsToJsonObject(getJobMetrics(jobMetrics))); } private Map getJobMetrics(String jobMetrics) { @@ -575,4 +583,17 @@ private void submitJob( jobImmutableInformation.isStartWithSavePoint()); voidPassiveCompletableFuture.join(); } + + private JsonObject metricsToJsonObject(Map jobMetrics) { + JsonObject members = new JsonObject(); + jobMetrics.forEach( + (key, value) -> { + if (value instanceof Map) { + members.add(key, metricsToJsonObject((Map) value)); + } else { + members.add(key, value.toString()); + } + }); + return members; + } } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/JobInfoServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/JobInfoServlet.java index 16683e68265..d41635a9eb2 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/JobInfoServlet.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/JobInfoServlet.java @@ -48,18 +48,18 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) if (jobId != null && jobId.length() > 1) { jobId = jobId.substring(1); } else { - jobId = ""; + throw new IllegalArgumentException("The jobId must not be empty."); } IMap jobInfoMap = nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_RUNNING_JOB_INFO); - JobInfo jobInfo = (JobInfo) jobInfoMap.get(Long.valueOf(jobId)); + Object jobInfo = jobInfoMap.get(Long.valueOf(jobId)); IMap finishedJobStateMap = nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_FINISHED_JOB_STATE); - JobState finishedJobState = (JobState) finishedJobStateMap.get(Long.valueOf(jobId)); - if (!jobId.isEmpty() && jobInfo != null) { - writeJson(resp, convertToJson(jobInfo, Long.parseLong(jobId))); - } else if (!jobId.isEmpty() && finishedJobState != null) { + Object finishedJobState = finishedJobStateMap.get(Long.valueOf(jobId)); + if (jobInfo != null) { + writeJson(resp, convertToJson((JobInfo) jobInfo, Long.parseLong(jobId))); + } else if (finishedJobState != null) { JobMetrics finishedJobMetrics = (JobMetrics) nodeEngine @@ -75,7 +75,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) writeJson( resp, getJobInfoJson( - finishedJobState, + (JobState) finishedJobState, finishedJobMetrics.toJsonString(), finishedJobDAGInfo)); } else { diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SeaTunnelSourceCollector.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SeaTunnelSourceCollector.java index 7f2e34bdcb8..53d206874a5 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SeaTunnelSourceCollector.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SeaTunnelSourceCollector.java @@ -107,7 +107,7 @@ public void collect(T row) { "Unsupported row type: " + rowType.getClass().getName()); } flowControlGate.audit((SeaTunnelRow) row); - taskMetricsCalcContext.updateMetrics(row); + taskMetricsCalcContext.updateMetrics(row, tableId); } sendRecordToNext(new Record<>(row)); emptyThisPollNext = false; diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java index 5970d9a745c..295328d8210 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/flow/SinkFlowLifeCycle.java @@ -23,12 +23,15 @@ import org.apache.seatunnel.api.sink.MultiTableResourceManager; import org.apache.seatunnel.api.sink.SinkCommitter; import org.apache.seatunnel.api.sink.SinkWriter; +import org.apache.seatunnel.api.sink.SinkWriter.Context; import org.apache.seatunnel.api.sink.SupportResourceShare; import org.apache.seatunnel.api.sink.event.WriterCloseEvent; import org.apache.seatunnel.api.sink.multitablesink.MultiTableSink; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.event.SchemaChangeEvent; import org.apache.seatunnel.api.table.type.Record; +import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.common.constants.PluginType; import org.apache.seatunnel.engine.core.checkpoint.InternalCheckpointListener; import org.apache.seatunnel.engine.core.dag.actions.SinkAction; @@ -53,7 +56,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -70,7 +75,7 @@ public class SinkFlowLifeCycle sinkAction; private SinkWriter writer; - private SinkWriter.Context writerContext; + private Context writerContext; private transient Optional> commitInfoSerializer; private transient Optional> writerStateSerializer; @@ -97,6 +102,9 @@ public class SinkFlowLifeCycle tablesMaps = new HashMap<>(); + public SinkFlowLifeCycle( SinkAction sinkAction, TaskLocation taskLocation, @@ -118,6 +126,21 @@ public SinkFlowLifeCycle( boolean isMulti = sinkAction.getSink() instanceof MultiTableSink; if (isMulti) { sinkTables = ((MultiTableSink) sinkAction.getSink()).getSinkTables(); + String[] upstreamTablePaths = + ((MultiTableSink) sinkAction.getSink()) + .getSinks() + .keySet() + .toArray(new String[0]); + for (int i = 0; i < ((MultiTableSink) sinkAction.getSink()).getSinks().size(); i++) { + tablesMaps.put(TablePath.of(upstreamTablePaths[i]), sinkTables.get(i)); + } + } else { + Optional catalogTable = sinkAction.getSink().getWriteCatalogTable(); + if (catalogTable.isPresent()) { + sinkTables.add(catalogTable.get().getTablePath()); + } else { + sinkTables.add(TablePath.DEFAULT); + } } this.taskMetricsCalcContext = new TaskMetricsCalcContext(metricsContext, PluginType.SINK, isMulti, sinkTables); @@ -246,8 +269,39 @@ public void received(Record record) { if (prepareClose) { return; } + String tableId = ""; writer.write((T) record.getData()); - taskMetricsCalcContext.updateMetrics(record.getData()); + if (record.getData() instanceof SeaTunnelRow) { + if (this.sinkAction.getSink() instanceof MultiTableSink) { + if (((SeaTunnelRow) record.getData()).getTableId() == null + || ((SeaTunnelRow) record.getData()).getTableId().isEmpty()) { + tableId = ((SeaTunnelRow) record.getData()).getTableId(); + } else { + + TablePath tablePath = + tablesMaps.get( + TablePath.of( + ((SeaTunnelRow) record.getData()) + .getTableId())); + tableId = + tablePath != null + ? tablePath.getFullName() + : TablePath.DEFAULT.getFullName(); + } + + } else { + Optional writeCatalogTable = + this.sinkAction.getSink().getWriteCatalogTable(); + tableId = + writeCatalogTable + .map( + catalogTable -> + catalogTable.getTablePath().getFullName()) + .orElseGet(TablePath.DEFAULT::getFullName); + } + + taskMetricsCalcContext.updateMetrics(record.getData(), tableId); + } } } catch (Exception e) { throw new RuntimeException(e); From 69086e0dba1195822658f7618ac4c53d26d9ba45 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 16 Oct 2024 10:01:33 +0800 Subject: [PATCH 04/72] [Improve] Update snapshot version to 2.3.9 (#7849) --- README.md | 3 ++- docs/zh/seatunnel-engine/download-seatunnel.md | 2 +- docs/zh/start-v2/docker/docker.md | 6 +++--- docs/zh/start-v2/locally/deployment.md | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2f15fd2209e..1404587b0b0 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,8 @@ Companies and organizations worldwide use SeaTunnel for research, production, an ### 1. How do I install SeaTunnel? -Follow the [Installation Guide](https://seatunnel.apache.org/docs/2.3.3/start-v2/locally/deployment/) on our website to get started. +Follow the [Installation Guide](https://seatunnel.apache.org/docs/start-v2/locally/deployment/) on our website to get +started. ### 2. How can I contribute to SeaTunnel? diff --git a/docs/zh/seatunnel-engine/download-seatunnel.md b/docs/zh/seatunnel-engine/download-seatunnel.md index d8d0b449137..8d06a2e4f78 100644 --- a/docs/zh/seatunnel-engine/download-seatunnel.md +++ b/docs/zh/seatunnel-engine/download-seatunnel.md @@ -33,7 +33,7 @@ tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" sh bin/install-plugin.sh 2.3.9 ``` -如果您需要指定的连接器版本,以2.3.7为例,您需要执行如下命令 +如果您需要指定的连接器版本,以2.3.9为例,您需要执行如下命令 ```bash sh bin/install-plugin.sh 2.3.9 diff --git a/docs/zh/start-v2/docker/docker.md b/docs/zh/start-v2/docker/docker.md index 548b318598d..1c4bc5d4b10 100644 --- a/docs/zh/start-v2/docker/docker.md +++ b/docs/zh/start-v2/docker/docker.md @@ -40,7 +40,7 @@ docker run --rm -it -v /tmp/job/:/config apache/seatunnel: ./bin/se ```shell cd seatunnel # Use already sett maven profile -mvn -B clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dlicense.skipAddThirdParty=true -D"docker.build.skip"=false -D"docker.verify.skip"=false -D"docker.push.skip"=true -D"docker.tag"=2.3.8 -Dmaven.deploy.skip -D"skip.spotless"=true --no-snapshot-updates -Pdocker,seatunnel +mvn -B clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dlicense.skipAddThirdParty=true -D"docker.build.skip"=false -D"docker.verify.skip"=false -D"docker.push.skip"=true -D"docker.tag"=2.3.9 -Dmaven.deploy.skip -D"skip.spotless"=true --no-snapshot-updates -Pdocker,seatunnel # Check the docker image docker images | grep apache/seatunnel @@ -53,10 +53,10 @@ mvn clean package -DskipTests -Dskip.spotless=true # Build docker image cd seatunnel-dist -docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.8 -t apache/seatunnel:2.3.8 . +docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.9 -t apache/seatunnel:2.3.9 . # If you build from dev branch, you should add SNAPSHOT suffix to the version -docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.8-SNAPSHOT -t apache/seatunnel:2.3.8-SNAPSHOT . +docker build -f src/main/docker/Dockerfile --build-arg VERSION=2.3.9-SNAPSHOT -t apache/seatunnel:2.3.9-SNAPSHOT . # Check the docker image docker images | grep apache/seatunnel diff --git a/docs/zh/start-v2/locally/deployment.md b/docs/zh/start-v2/locally/deployment.md index 275c0f2b92a..927f5476ece 100644 --- a/docs/zh/start-v2/locally/deployment.md +++ b/docs/zh/start-v2/locally/deployment.md @@ -35,7 +35,7 @@ tar -xzvf "apache-seatunnel-${version}-bin.tar.gz" sh bin/install-plugin.sh ``` -如果您需要指定的连接器版本,以2.3.7为例,您需要执行如下命令: +如果您需要指定的连接器版本,以2.3.9为例,您需要执行如下命令: ```bash sh bin/install-plugin.sh 2.3.9 From d06d9cd6529ba30af4bcde8e1152febf682af0ea Mon Sep 17 00:00:00 2001 From: Tyrantlucifer Date: Wed, 16 Oct 2024 17:30:53 +0800 Subject: [PATCH 05/72] [Hotfix][Core][Flink] SeaTunnel flink engine support application mode on yarn (#7762) --- .github/workflows/backend.yml | 2 +- .../seatunnel/common/config/Common.java | 6 +++++- .../start-seatunnel-flink-13-connector-v2.sh | 19 +++++++++++++++++++ .../core/starter/flink/FlinkStarter.java | 17 +++++++++++++++++ .../start-seatunnel-flink-15-connector-v2.sh | 19 +++++++++++++++++++ .../core/starter/flink/FlinkStarter.java | 17 +++++++++++++++++ 6 files changed, 78 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 6afc981bed0..a5165c85baa 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -683,7 +683,7 @@ jobs: matrix: java: [ '8', '11' ] os: [ 'ubuntu-latest' ] - timeout-minutes: 150 + timeout-minutes: 180 steps: - uses: actions/checkout@v2 - name: Set up JDK ${{ matrix.java }} diff --git a/seatunnel-common/src/main/java/org/apache/seatunnel/common/config/Common.java b/seatunnel-common/src/main/java/org/apache/seatunnel/common/config/Common.java index 95928d1e4cc..0ebdc341fac 100644 --- a/seatunnel-common/src/main/java/org/apache/seatunnel/common/config/Common.java +++ b/seatunnel-common/src/main/java/org/apache/seatunnel/common/config/Common.java @@ -39,6 +39,8 @@ public class Common { + private static final String FLINK_YARN_APPLICATION_PATH = "runtime.tar.gz"; + private Common() { throw new IllegalStateException("Utility class"); } @@ -113,8 +115,10 @@ public static Path appRootDir() { } catch (URISyntaxException e) { throw new RuntimeException(e); } - } else if (DeployMode.CLUSTER == MODE || DeployMode.RUN_APPLICATION == MODE) { + } else if (DeployMode.CLUSTER == MODE) { return Paths.get(""); + } else if (DeployMode.RUN_APPLICATION == MODE) { + return Paths.get(FLINK_YARN_APPLICATION_PATH); } else { throw new IllegalStateException("deploy mode not support : " + MODE); } diff --git a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/bin/start-seatunnel-flink-13-connector-v2.sh b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/bin/start-seatunnel-flink-13-connector-v2.sh index f2c61f2193f..4bd9905354e 100755 --- a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/bin/start-seatunnel-flink-13-connector-v2.sh +++ b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/bin/start-seatunnel-flink-13-connector-v2.sh @@ -43,6 +43,25 @@ if [ -f "${CONF_DIR}/seatunnel-env.sh" ]; then . "${CONF_DIR}/seatunnel-env.sh" fi +if [ ! -f "${APP_DIR}/runtime.tar.gz" ];then + + directories=("connectors" "lib" "plugins") + + existing_dirs=() + + for dir in "${directories[@]}"; do + if [ -d "$dir" ]; then + existing_dirs+=("$dir") + fi + done + + if [ ${#existing_dirs[@]} -eq 0 ]; then + echo "[connectors,lib,plugins] not existed, skip generate runtime.tar.gz" + else + tar -zcvf runtime.tar.gz "${existing_dirs[@]}" + fi +fi + if [ $# == 0 ] then args="-h" diff --git a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java index c244f2ff333..e9d0ba7df28 100644 --- a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java +++ b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-13-starter/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java @@ -20,6 +20,7 @@ import org.apache.seatunnel.common.config.Common; import org.apache.seatunnel.core.starter.Starter; import org.apache.seatunnel.core.starter.enums.EngineType; +import org.apache.seatunnel.core.starter.enums.MasterType; import org.apache.seatunnel.core.starter.flink.args.FlinkCommandArgs; import org.apache.seatunnel.core.starter.utils.CommandLineUtils; @@ -32,6 +33,7 @@ public class FlinkStarter implements Starter { private static final String APP_NAME = SeaTunnelFlink.class.getName(); public static final String APP_JAR_NAME = EngineType.FLINK13.getStarterJarName(); public static final String SHELL_NAME = EngineType.FLINK13.getStarterShellName(); + public static final String RUNTIME_FILE = "runtime.tar.gz"; private final FlinkCommandArgs flinkCommandArgs; private final String appJar; @@ -61,6 +63,18 @@ public List buildCommands() { command.add("--target"); command.add(flinkCommandArgs.getMasterType().getMaster()); } + // set yarn application mode parameters + if (flinkCommandArgs.getMasterType() == MasterType.YARN_APPLICATION) { + command.add( + String.format("-Dyarn.ship-files=\"%s\"", flinkCommandArgs.getConfigFile())); + command.add(String.format("-Dyarn.ship-archives=%s", RUNTIME_FILE)); + } + // set yarn application name + if (flinkCommandArgs.getMasterType() == MasterType.YARN_APPLICATION + || flinkCommandArgs.getMasterType() == MasterType.YARN_PER_JOB + || flinkCommandArgs.getMasterType() == MasterType.YARN_SESSION) { + command.add(String.format("-Dyarn.application.name=%s", flinkCommandArgs.getJobName())); + } // set flink original parameters command.addAll(flinkCommandArgs.getOriginalParameters()); // set main class name @@ -86,6 +100,9 @@ public List buildCommands() { if (flinkCommandArgs.isDecrypt()) { command.add("--decrypt"); } + // set deploy mode + command.add("--deploy-mode"); + command.add(flinkCommandArgs.getDeployMode().getDeployMode()); // set extra system properties flinkCommandArgs.getVariables().stream() .filter(Objects::nonNull) diff --git a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-15-starter/src/main/bin/start-seatunnel-flink-15-connector-v2.sh b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-15-starter/src/main/bin/start-seatunnel-flink-15-connector-v2.sh index 137b8c043b1..5698a340dab 100755 --- a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-15-starter/src/main/bin/start-seatunnel-flink-15-connector-v2.sh +++ b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-15-starter/src/main/bin/start-seatunnel-flink-15-connector-v2.sh @@ -43,6 +43,25 @@ if [ -f "${CONF_DIR}/seatunnel-env.sh" ]; then . "${CONF_DIR}/seatunnel-env.sh" fi +if [ ! -f "${APP_DIR}/runtime.tar.gz" ];then + + directories=("connectors" "lib" "plugins") + + existing_dirs=() + + for dir in "${directories[@]}"; do + if [ -d "$dir" ]; then + existing_dirs+=("$dir") + fi + done + + if [ ${#existing_dirs[@]} -eq 0 ]; then + echo "[connectors,lib,plugins] not existed, skip generate runtime.tar.gz" + else + tar -zcvf runtime.tar.gz "${existing_dirs[@]}" + fi +fi + if [ $# == 0 ] then args="-h" diff --git a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java index e74bbd402fc..06cfd5f4495 100644 --- a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java +++ b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/FlinkStarter.java @@ -20,6 +20,7 @@ import org.apache.seatunnel.common.config.Common; import org.apache.seatunnel.core.starter.Starter; import org.apache.seatunnel.core.starter.enums.EngineType; +import org.apache.seatunnel.core.starter.enums.MasterType; import org.apache.seatunnel.core.starter.flink.args.FlinkCommandArgs; import org.apache.seatunnel.core.starter.utils.CommandLineUtils; @@ -32,6 +33,7 @@ public class FlinkStarter implements Starter { private static final String APP_NAME = SeaTunnelFlink.class.getName(); public static final String APP_JAR_NAME = EngineType.FLINK15.getStarterJarName(); public static final String SHELL_NAME = EngineType.FLINK15.getStarterShellName(); + public static final String RUNTIME_FILE = "runtime.tar.gz"; private final FlinkCommandArgs flinkCommandArgs; private final String appJar; @@ -61,6 +63,18 @@ public List buildCommands() { command.add("--target"); command.add(flinkCommandArgs.getMasterType().getMaster()); } + // set yarn application mode parameters + if (flinkCommandArgs.getMasterType() == MasterType.YARN_APPLICATION) { + command.add( + String.format("-Dyarn.ship-files=\"%s\"", flinkCommandArgs.getConfigFile())); + command.add(String.format("-Dyarn.ship-archives=%s", RUNTIME_FILE)); + } + // set yarn application name + if (flinkCommandArgs.getMasterType() == MasterType.YARN_APPLICATION + || flinkCommandArgs.getMasterType() == MasterType.YARN_PER_JOB + || flinkCommandArgs.getMasterType() == MasterType.YARN_SESSION) { + command.add(String.format("-Dyarn.application.name=%s", flinkCommandArgs.getJobName())); + } // set flink original parameters command.addAll(flinkCommandArgs.getOriginalParameters()); // set main class name @@ -86,6 +100,9 @@ public List buildCommands() { if (flinkCommandArgs.isDecrypt()) { command.add("--decrypt"); } + // set deploy mode + command.add("--deploy-mode"); + command.add(flinkCommandArgs.getDeployMode().getDeployMode()); // set extra system properties flinkCommandArgs.getVariables().stream() .filter(Objects::nonNull) From bbf643772e399b287e152613c482dfa6430f58aa Mon Sep 17 00:00:00 2001 From: luckyLJY <79346217+luckyLJY@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:49:23 +0800 Subject: [PATCH 06/72] [Feature][connector-v2]Support opengauss jdbc connnector using opengauss driver. (#7622) Co-authored-by: lucky_ljy --- docs/en/connector-v2/sink/Jdbc.md | 5 +- docs/en/connector-v2/source/Jdbc.md | 5 +- docs/zh/connector-v2/sink/Jdbc.md | 41 +-- .../connector-jdbc/pom.xml | 12 +- .../catalog/opengauss/OpenGaussCatalog.java | 44 +++ .../opengauss/OpenGaussCatalogFactory.java | 62 ++++ .../internal/dialect/DatabaseIdentifier.java | 1 + .../opengauss/OpenGaussDialectFactory.java | 31 ++ .../seatunnel/jdbc/JdbcOpenGaussIT.java | 332 ++++++++++++++++++ .../jdbc_opengauss_source_and_sink.conf | 67 ++++ 10 files changed, 574 insertions(+), 26 deletions(-) create mode 100644 seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalog.java create mode 100644 seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java create mode 100644 seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/opengauss/OpenGaussDialectFactory.java create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOpenGaussIT.java create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/resources/jdbc_opengauss_source_and_sink.conf diff --git a/docs/en/connector-v2/sink/Jdbc.md b/docs/en/connector-v2/sink/Jdbc.md index 1ddbdd507d9..8df5f12bfab 100644 --- a/docs/en/connector-v2/sink/Jdbc.md +++ b/docs/en/connector-v2/sink/Jdbc.md @@ -226,7 +226,7 @@ In the case of is_exactly_once = "true", Xa transactions are used. This requires there are some reference value for params above. -| datasource | driver | url | xa_data_source_class_name | maven | +| datasource | driver | url | xa_data_source_class_name | maven | |-------------------|----------------------------------------------|--------------------------------------------------------------------|----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------| | MySQL | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | com.mysql.cj.jdbc.MysqlXADataSource | https://mvnrepository.com/artifact/mysql/mysql-connector-java | | PostgreSQL | org.postgresql.Driver | jdbc:postgresql://localhost:5432/postgres | org.postgresql.xa.PGXADataSource | https://mvnrepository.com/artifact/org.postgresql/postgresql | @@ -235,7 +235,7 @@ there are some reference value for params above. | SQL Server | com.microsoft.sqlserver.jdbc.SQLServerDriver | jdbc:sqlserver://localhost:1433 | com.microsoft.sqlserver.jdbc.SQLServerXADataSource | https://mvnrepository.com/artifact/com.microsoft.sqlserver/mssql-jdbc | | Oracle | oracle.jdbc.OracleDriver | jdbc:oracle:thin:@localhost:1521/xepdb1 | oracle.jdbc.xa.OracleXADataSource | https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 | | sqlite | org.sqlite.JDBC | jdbc:sqlite:test.db | / | https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc | -| GBase8a | com.gbase.jdbc.Driver | jdbc:gbase://e2e_gbase8aDb:5258/test | / | https://cdn.gbase.cn/products/30/p5CiVwXBKQYIUGN8ecHvk/gbase-connector-java-9.5.0.7-build1-bin.jar | +| GBase8a | com.gbase.jdbc.Driver | jdbc:gbase://e2e_gbase8aDb:5258/test | / | https://cdn.gbase.cn/products/30/p5CiVwXBKQYIUGN8ecHvk/gbase-connector-java-9.5.0.7-build1-bin.jar | | StarRocks | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | / | https://mvnrepository.com/artifact/mysql/mysql-connector-java | | db2 | com.ibm.db2.jcc.DB2Driver | jdbc:db2://localhost:50000/testdb | com.ibm.db2.jcc.DB2XADataSource | https://mvnrepository.com/artifact/com.ibm.db2.jcc/db2jcc/db2jcc4 | | saphana | com.sap.db.jdbc.Driver | jdbc:sap://localhost:39015 | / | https://mvnrepository.com/artifact/com.sap.cloud.db.jdbc/ngdbc | @@ -248,6 +248,7 @@ there are some reference value for params above. | OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | / | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar | | xugu | com.xugu.cloudjdbc.Driver | jdbc:xugu://localhost:5138 | / | https://repo1.maven.org/maven2/com/xugudb/xugu-jdbc/12.2.0/xugu-jdbc-12.2.0.jar | | InterSystems IRIS | com.intersystems.jdbc.IRISDriver | jdbc:IRIS://localhost:1972/%SYS | / | https://raw.githubusercontent.com/intersystems-community/iris-driver-distribution/main/JDBC/JDK18/intersystems-jdbc-3.8.4.jar | +| opengauss | org.opengauss.Driver | jdbc:opengauss://localhost:5432/postgres | / | https://repo1.maven.org/maven2/org/opengauss/opengauss-jdbc/5.1.0-og/opengauss-jdbc-5.1.0-og.jar | ## Example diff --git a/docs/en/connector-v2/source/Jdbc.md b/docs/en/connector-v2/source/Jdbc.md index 27b3d875580..f2e7486e247 100644 --- a/docs/en/connector-v2/source/Jdbc.md +++ b/docs/en/connector-v2/source/Jdbc.md @@ -113,7 +113,7 @@ The JDBC Source connector supports parallel reading of data from tables. SeaTunn there are some reference value for params above. -| datasource | driver | url | maven | +| datasource | driver | url | maven | |-------------------|-----------------------------------------------------|------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------| | mysql | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | https://mvnrepository.com/artifact/mysql/mysql-connector-java | | postgresql | org.postgresql.Driver | jdbc:postgresql://localhost:5432/postgres | https://mvnrepository.com/artifact/org.postgresql/postgresql | @@ -122,7 +122,7 @@ there are some reference value for params above. | sqlserver | com.microsoft.sqlserver.jdbc.SQLServerDriver | jdbc:sqlserver://localhost:1433 | https://mvnrepository.com/artifact/com.microsoft.sqlserver/mssql-jdbc | | oracle | oracle.jdbc.OracleDriver | jdbc:oracle:thin:@localhost:1521/xepdb1 | https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 | | sqlite | org.sqlite.JDBC | jdbc:sqlite:test.db | https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc | -| gbase8a | com.gbase.jdbc.Driver | jdbc:gbase://e2e_gbase8aDb:5258/test | https://cdn.gbase.cn/products/30/p5CiVwXBKQYIUGN8ecHvk/gbase-connector-java-9.5.0.7-build1-bin.jar | +| gbase8a | com.gbase.jdbc.Driver | jdbc:gbase://e2e_gbase8aDb:5258/test | https://cdn.gbase.cn/products/30/p5CiVwXBKQYIUGN8ecHvk/gbase-connector-java-9.5.0.7-build1-bin.jar | | starrocks | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | https://mvnrepository.com/artifact/mysql/mysql-connector-java | | db2 | com.ibm.db2.jcc.DB2Driver | jdbc:db2://localhost:50000/testdb | https://mvnrepository.com/artifact/com.ibm.db2.jcc/db2jcc/db2jcc4 | | tablestore | com.alicloud.openservices.tablestore.jdbc.OTSDriver | "jdbc:ots:http s://myinstance.cn-hangzhou.ots.aliyuncs.com/myinstance" | https://mvnrepository.com/artifact/com.aliyun.openservices/tablestore-jdbc | @@ -137,6 +137,7 @@ there are some reference value for params above. | Hive | org.apache.hive.jdbc.HiveDriver | jdbc:hive2://localhost:10000 | https://repo1.maven.org/maven2/org/apache/hive/hive-jdbc/3.1.3/hive-jdbc-3.1.3-standalone.jar | | xugu | com.xugu.cloudjdbc.Driver | jdbc:xugu://localhost:5138 | https://repo1.maven.org/maven2/com/xugudb/xugu-jdbc/12.2.0/xugu-jdbc-12.2.0.jar | | InterSystems IRIS | com.intersystems.jdbc.IRISDriver | jdbc:IRIS://localhost:1972/%SYS | https://raw.githubusercontent.com/intersystems-community/iris-driver-distribution/main/JDBC/JDK18/intersystems-jdbc-3.8.4.jar | +| opengauss | org.opengauss.Driver | jdbc:opengauss://localhost:5432/postgres | https://repo1.maven.org/maven2/org/opengauss/opengauss-jdbc/5.1.0-og/opengauss-jdbc-5.1.0-og.jar | ## Example diff --git a/docs/zh/connector-v2/sink/Jdbc.md b/docs/zh/connector-v2/sink/Jdbc.md index e1ab422952e..6a7253da61c 100644 --- a/docs/zh/connector-v2/sink/Jdbc.md +++ b/docs/zh/connector-v2/sink/Jdbc.md @@ -216,26 +216,27 @@ Sink插件常用参数,请参考 [Sink常用选项](../sink-common-options.md) 附录参数仅提供参考 -| 数据源 | driver | url | xa_data_source_class_name | maven | -|------------|----------------------------------------------|--------------------------------------------------------------------|----------------------------------------------------|-------------------------------------------------------------------------------------------------------------| -| MySQL | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | com.mysql.cj.jdbc.MysqlXADataSource | https://mvnrepository.com/artifact/mysql/mysql-connector-java | -| PostgreSQL | org.postgresql.Driver | jdbc:postgresql://localhost:5432/postgres | org.postgresql.xa.PGXADataSource | https://mvnrepository.com/artifact/org.postgresql/postgresql | -| DM | dm.jdbc.driver.DmDriver | jdbc:dm://localhost:5236 | dm.jdbc.driver.DmdbXADataSource | https://mvnrepository.com/artifact/com.dameng/DmJdbcDriver18 | -| Phoenix | org.apache.phoenix.queryserver.client.Driver | jdbc:phoenix:thin:url=http://localhost:8765;serialization=PROTOBUF | / | https://mvnrepository.com/artifact/com.aliyun.phoenix/ali-phoenix-shaded-thin-client | -| SQL Server | com.microsoft.sqlserver.jdbc.SQLServerDriver | jdbc:sqlserver://localhost:1433 | com.microsoft.sqlserver.jdbc.SQLServerXADataSource | https://mvnrepository.com/artifact/com.microsoft.sqlserver/mssql-jdbc | -| Oracle | oracle.jdbc.OracleDriver | jdbc:oracle:thin:@localhost:1521/xepdb1 | oracle.jdbc.xa.OracleXADataSource | https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 | -| sqlite | org.sqlite.JDBC | jdbc:sqlite:test.db | / | https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc | -| GBase8a | com.gbase.jdbc.Driver | jdbc:gbase://e2e_gbase8aDb:5258/test | / | https://cdn.gbase.cn/products/30/p5CiVwXBKQYIUGN8ecHvk/gbase-connector-java-9.5.0.7-build1-bin.jar | -| StarRocks | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | / | https://mvnrepository.com/artifact/mysql/mysql-connector-java | -| db2 | com.ibm.db2.jcc.DB2Driver | jdbc:db2://localhost:50000/testdb | com.ibm.db2.jcc.DB2XADataSource | https://mvnrepository.com/artifact/com.ibm.db2.jcc/db2jcc/db2jcc4 | -| saphana | com.sap.db.jdbc.Driver | jdbc:sap://localhost:39015 | / | https://mvnrepository.com/artifact/com.sap.cloud.db.jdbc/ngdbc | -| Doris | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | / | https://mvnrepository.com/artifact/mysql/mysql-connector-java | -| teradata | com.teradata.jdbc.TeraDriver | jdbc:teradata://localhost/DBS_PORT=1025,DATABASE=test | / | https://mvnrepository.com/artifact/com.teradata.jdbc/terajdbc | -| Redshift | com.amazon.redshift.jdbc42.Driver | jdbc:redshift://localhost:5439/testdb | com.amazon.redshift.xa.RedshiftXADataSource | https://mvnrepository.com/artifact/com.amazon.redshift/redshift-jdbc42 | -| Snowflake | net.snowflake.client.jdbc.SnowflakeDriver | jdbc:snowflake://.snowflakecomputing.com | / | https://mvnrepository.com/artifact/net.snowflake/snowflake-jdbc | -| Vertica | com.vertica.jdbc.Driver | jdbc:vertica://localhost:5433 | / | https://repo1.maven.org/maven2/com/vertica/jdbc/vertica-jdbc/12.0.3-0/vertica-jdbc-12.0.3-0.jar | -| Kingbase | com.kingbase8.Driver | jdbc:kingbase8://localhost:54321/db_test | / | https://repo1.maven.org/maven2/cn/com/kingbase/kingbase8/8.6.0/kingbase8-8.6.0.jar | -| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | / | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar | +| 数据源 | driver | url | xa_data_source_class_name | maven | +|------------|----------------------------------------------|--------------------------------------------------------------------|----------------------------------------------------|------------------------------------------------------------------------------------------------------| +| MySQL | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | com.mysql.cj.jdbc.MysqlXADataSource | https://mvnrepository.com/artifact/mysql/mysql-connector-java | +| PostgreSQL | org.postgresql.Driver | jdbc:postgresql://localhost:5432/postgres | org.postgresql.xa.PGXADataSource | https://mvnrepository.com/artifact/org.postgresql/postgresql | +| DM | dm.jdbc.driver.DmDriver | jdbc:dm://localhost:5236 | dm.jdbc.driver.DmdbXADataSource | https://mvnrepository.com/artifact/com.dameng/DmJdbcDriver18 | +| Phoenix | org.apache.phoenix.queryserver.client.Driver | jdbc:phoenix:thin:url=http://localhost:8765;serialization=PROTOBUF | / | https://mvnrepository.com/artifact/com.aliyun.phoenix/ali-phoenix-shaded-thin-client | +| SQL Server | com.microsoft.sqlserver.jdbc.SQLServerDriver | jdbc:sqlserver://localhost:1433 | com.microsoft.sqlserver.jdbc.SQLServerXADataSource | https://mvnrepository.com/artifact/com.microsoft.sqlserver/mssql-jdbc | +| Oracle | oracle.jdbc.OracleDriver | jdbc:oracle:thin:@localhost:1521/xepdb1 | oracle.jdbc.xa.OracleXADataSource | https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 | +| sqlite | org.sqlite.JDBC | jdbc:sqlite:test.db | / | https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc | +| GBase8a | com.gbase.jdbc.Driver | jdbc:gbase://e2e_gbase8aDb:5258/test | / | https://cdn.gbase.cn/products/30/p5CiVwXBKQYIUGN8ecHvk/gbase-connector-java-9.5.0.7-build1-bin.jar | +| StarRocks | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | / | https://mvnrepository.com/artifact/mysql/mysql-connector-java | +| db2 | com.ibm.db2.jcc.DB2Driver | jdbc:db2://localhost:50000/testdb | com.ibm.db2.jcc.DB2XADataSource | https://mvnrepository.com/artifact/com.ibm.db2.jcc/db2jcc/db2jcc4 | +| saphana | com.sap.db.jdbc.Driver | jdbc:sap://localhost:39015 | / | https://mvnrepository.com/artifact/com.sap.cloud.db.jdbc/ngdbc | +| Doris | com.mysql.cj.jdbc.Driver | jdbc:mysql://localhost:3306/test | / | https://mvnrepository.com/artifact/mysql/mysql-connector-java | +| teradata | com.teradata.jdbc.TeraDriver | jdbc:teradata://localhost/DBS_PORT=1025,DATABASE=test | / | https://mvnrepository.com/artifact/com.teradata.jdbc/terajdbc | +| Redshift | com.amazon.redshift.jdbc42.Driver | jdbc:redshift://localhost:5439/testdb | com.amazon.redshift.xa.RedshiftXADataSource | https://mvnrepository.com/artifact/com.amazon.redshift/redshift-jdbc42 | +| Snowflake | net.snowflake.client.jdbc.SnowflakeDriver | jdbc:snowflake://.snowflakecomputing.com | / | https://mvnrepository.com/artifact/net.snowflake/snowflake-jdbc | +| Vertica | com.vertica.jdbc.Driver | jdbc:vertica://localhost:5433 | / | https://repo1.maven.org/maven2/com/vertica/jdbc/vertica-jdbc/12.0.3-0/vertica-jdbc-12.0.3-0.jar | +| Kingbase | com.kingbase8.Driver | jdbc:kingbase8://localhost:54321/db_test | / | https://repo1.maven.org/maven2/cn/com/kingbase/kingbase8/8.6.0/kingbase8-8.6.0.jar | +| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | / | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar | +| opengauss | org.opengauss.Driver | jdbc:opengauss://localhost:5432/postgres | / | https://repo1.maven.org/maven2/org/opengauss/opengauss-jdbc/5.1.0-og/opengauss-jdbc-5.1.0-og.jar | ## 示例 diff --git a/seatunnel-connectors-v2/connector-jdbc/pom.xml b/seatunnel-connectors-v2/connector-jdbc/pom.xml index 7b4199c462f..a6be0dd03ba 100644 --- a/seatunnel-connectors-v2/connector-jdbc/pom.xml +++ b/seatunnel-connectors-v2/connector-jdbc/pom.xml @@ -53,6 +53,7 @@ 12.2.0 3.0.0 3.2.0 + 5.1.0-og @@ -203,11 +204,15 @@ ${iris.jdbc.version} provided - org.tikv tikv-client-java ${tikv.version} + + + org.opengauss + opengauss-jdbc + ${opengauss.jdbc.version} provided @@ -316,11 +321,14 @@ com.intersystems intersystems-jdbc - org.tikv tikv-client-java + + org.opengauss + opengauss-jdbc + diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalog.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalog.java new file mode 100644 index 00000000000..6a8eab50107 --- /dev/null +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalog.java @@ -0,0 +1,44 @@ +/* + * 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.seatunnel.connectors.seatunnel.jdbc.catalog.opengauss; + +import org.apache.seatunnel.common.utils.JdbcUrlUtil; +import org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.psql.PostgresCatalog; + +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.sql.Connection; + +@Slf4j +public class OpenGaussCatalog extends PostgresCatalog { + + public OpenGaussCatalog( + String catalogName, + String username, + String pwd, + JdbcUrlUtil.UrlInfo urlInfo, + String defaultSchema) { + super(catalogName, username, pwd, urlInfo, defaultSchema); + } + + @VisibleForTesting + public void setConnection(String url, Connection connection) { + this.connectionMap.put(url, connection); + } +} diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java new file mode 100644 index 00000000000..bff96ff6d30 --- /dev/null +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java @@ -0,0 +1,62 @@ +/* + * 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.seatunnel.connectors.seatunnel.jdbc.catalog.opengauss; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.configuration.util.OptionValidationException; +import org.apache.seatunnel.api.table.catalog.Catalog; +import org.apache.seatunnel.api.table.factory.CatalogFactory; +import org.apache.seatunnel.api.table.factory.Factory; +import org.apache.seatunnel.common.utils.JdbcUrlUtil; +import org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.JdbcCatalogOptions; +import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseIdentifier; + +import com.google.auto.service.AutoService; + +import java.util.Optional; + +@AutoService(Factory.class) +public class OpenGaussCatalogFactory implements CatalogFactory { + + @Override + public String factoryIdentifier() { + return DatabaseIdentifier.OPENGAUSS; + } + + @Override + public Catalog createCatalog(String catalogName, ReadonlyConfig options) { + String urlWithDatabase = options.get(JdbcCatalogOptions.BASE_URL); + JdbcUrlUtil.UrlInfo urlInfo = JdbcUrlUtil.getUrlInfo(urlWithDatabase); + Optional defaultDatabase = urlInfo.getDefaultDatabase(); + if (!defaultDatabase.isPresent()) { + throw new OptionValidationException(JdbcCatalogOptions.BASE_URL); + } + return new OpenGaussCatalog( + catalogName, + options.get(JdbcCatalogOptions.USERNAME), + options.get(JdbcCatalogOptions.PASSWORD), + urlInfo, + options.get(JdbcCatalogOptions.SCHEMA)); + } + + @Override + public OptionRule optionRule() { + return JdbcCatalogOptions.BASE_RULE.build(); + } +} diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/DatabaseIdentifier.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/DatabaseIdentifier.java index 45f849c28bd..e2a32b4f3f0 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/DatabaseIdentifier.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/DatabaseIdentifier.java @@ -42,4 +42,5 @@ public class DatabaseIdentifier { public static final String XUGU = "XUGU"; public static final String IRIS = "IRIS"; public static final String INCEPTOR = "Inceptor"; + public static final String OPENGAUSS = "OpenGauss"; } diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/opengauss/OpenGaussDialectFactory.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/opengauss/OpenGaussDialectFactory.java new file mode 100644 index 00000000000..b1ceed51e9b --- /dev/null +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/opengauss/OpenGaussDialectFactory.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.opengauss; + +import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.JdbcDialectFactory; +import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.psql.PostgresDialectFactory; + +import com.google.auto.service.AutoService; + +@AutoService(JdbcDialectFactory.class) +public class OpenGaussDialectFactory extends PostgresDialectFactory { + + @Override + public boolean acceptsURL(String url) { + return url.startsWith("jdbc:opengauss:"); + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOpenGaussIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOpenGaussIT.java new file mode 100644 index 00000000000..5d2b8b19dc7 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOpenGaussIT.java @@ -0,0 +1,332 @@ +/* + * 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.seatunnel.connectors.seatunnel.jdbc; + +import org.apache.seatunnel.api.table.catalog.Catalog; +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.ConstraintKey; +import org.apache.seatunnel.api.table.catalog.PrimaryKey; +import org.apache.seatunnel.api.table.catalog.TablePath; +import org.apache.seatunnel.api.table.catalog.TableSchema; +import org.apache.seatunnel.api.table.type.SeaTunnelRow; +import org.apache.seatunnel.common.utils.JdbcUrlUtil; +import org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.opengauss.OpenGaussCatalog; +import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseIdentifier; +import org.apache.seatunnel.e2e.common.container.ContainerExtendedFactory; +import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerLoggerFactory; + +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Slf4j +public class JdbcOpenGaussIT extends AbstractJdbcIT { + protected static final String OPENGAUSS_IMAGE = "opengauss/opengauss:5.0.0"; + + private static final String OPEN_GAUSS_ALIASES = "e2e_OpenGauss"; + private static final String DRIVER_CLASS = "org.opengauss.Driver"; + private static final int OPEN_GAUSS_PORT = 5432; + private static final String OPEN_GAUSS_URL = "jdbc:opengauss://" + HOST + ":%s/%s"; + private static final String USERNAME = "gaussdb"; + private static final String PASSWORD = "openGauss@123"; + private static final String DATABASE = "postgres"; + private static final String SCHEMA = "public"; + private static final String SOURCE_TABLE = "gs_e2e_source_table"; + private static final String SINK_TABLE = "gs_e2e_sink_table"; + private static final String CATALOG_TABLE = "e2e_table_catalog"; + private static final Integer GEN_ROWS = 100; + private static final List CONFIG_FILE = + Lists.newArrayList("/jdbc_opengauss_source_and_sink.conf"); + + private static final String CREATE_SQL = + "CREATE TABLE IF NOT EXISTS %s (\n" + + " gid SERIAL PRIMARY KEY,\n" + + " uuid_col UUID,\n" + + " text_col TEXT,\n" + + " varchar_col VARCHAR(255),\n" + + " char_col CHAR(10),\n" + + " boolean_col bool,\n" + + " smallint_col int2,\n" + + " integer_col int4,\n" + + " bigint_col BIGINT,\n" + + " decimal_col DECIMAL(10, 2),\n" + + " numeric_col NUMERIC(8, 4),\n" + + " real_col float4,\n" + + " double_precision_col float8,\n" + + " smallserial_col SMALLSERIAL,\n" + + " bigserial_col BIGSERIAL,\n" + + " date_col DATE,\n" + + " timestamp_col TIMESTAMP,\n" + + " bpchar_col BPCHAR(10),\n" + + " age INT NOT null\n" + + ");"; + + private static final String[] fieldNames = + new String[] { + "gid", + "uuid_col", + "text_col", + "varchar_col", + "char_col", + "boolean_col", + "smallint_col", + "integer_col", + "bigint_col", + "decimal_col", + "numeric_col", + "real_col", + "double_precision_col", + "smallserial_col", + "bigserial_col", + "date_col", + "timestamp_col", + "bpchar_col", + "age" + }; + + @TestContainerExtension + protected final ContainerExtendedFactory extendedFactory = + container -> { + Container.ExecResult extraCommands = + container.execInContainer( + "bash", + "-c", + "mkdir -p /tmp/seatunnel/plugins/Jdbc/lib && cd /tmp/seatunnel/plugins/Jdbc/lib && curl -O " + + driverUrl()); + Assertions.assertEquals(0, extraCommands.getExitCode(), extraCommands.getStderr()); + }; + + @Test + @Override + public void testCatalog() { + if (catalog == null) { + return; + } + TablePath sourceTablePath = + new TablePath( + jdbcCase.getDatabase(), jdbcCase.getSchema(), jdbcCase.getSourceTable()); + TablePath targetTablePath = + new TablePath( + jdbcCase.getCatalogDatabase(), + jdbcCase.getCatalogSchema(), + jdbcCase.getCatalogTable()); + + CatalogTable catalogTable = catalog.getTable(sourceTablePath); + catalog.createTable(targetTablePath, catalogTable, false); + Assertions.assertTrue(catalog.tableExists(targetTablePath)); + + catalog.dropTable(targetTablePath, false); + Assertions.assertFalse(catalog.tableExists(targetTablePath)); + } + + @Test + public void testCreateIndex() { + String schema = "public"; + String databaseName = jdbcCase.getDatabase(); + TablePath sourceTablePath = TablePath.of(databaseName, "public", "gs_e2e_source_table"); + TablePath targetTablePath = TablePath.of(databaseName, "public", "gs_ide_sink_table_2"); + OpenGaussCatalog openGaussCatalog = (OpenGaussCatalog) catalog; + CatalogTable catalogTable = openGaussCatalog.getTable(sourceTablePath); + dropTableWithAssert(openGaussCatalog, targetTablePath, true); + // not create index + createIndexOrNot(openGaussCatalog, targetTablePath, catalogTable, false); + Assertions.assertFalse(hasIndex(openGaussCatalog, targetTablePath)); + + dropTableWithAssert(openGaussCatalog, targetTablePath, true); + // create index + createIndexOrNot(openGaussCatalog, targetTablePath, catalogTable, true); + Assertions.assertTrue(hasIndex(openGaussCatalog, targetTablePath)); + + dropTableWithAssert(openGaussCatalog, targetTablePath, true); + } + + protected boolean hasIndex(Catalog catalog, TablePath targetTablePath) { + TableSchema tableSchema = catalog.getTable(targetTablePath).getTableSchema(); + PrimaryKey primaryKey = tableSchema.getPrimaryKey(); + List constraintKeys = tableSchema.getConstraintKeys(); + if (primaryKey != null && StringUtils.isNotBlank(primaryKey.getPrimaryKey())) { + return true; + } + if (!constraintKeys.isEmpty()) { + return true; + } + return false; + } + + private void dropTableWithAssert( + OpenGaussCatalog openGaussCatalog, + TablePath targetTablePath, + boolean ignoreIfNotExists) { + openGaussCatalog.dropTable(targetTablePath, ignoreIfNotExists); + Assertions.assertFalse(openGaussCatalog.tableExists(targetTablePath)); + } + + private void createIndexOrNot( + OpenGaussCatalog openGaussCatalog, + TablePath targetTablePath, + CatalogTable catalogTable, + boolean createIndex) { + openGaussCatalog.createTable(targetTablePath, catalogTable, false, createIndex); + Assertions.assertTrue(openGaussCatalog.tableExists(targetTablePath)); + } + + @Override + JdbcCase getJdbcCase() { + Map containerEnv = new HashMap<>(); + containerEnv.put("OPEN_GAUSS_PASSWORD", PASSWORD); + containerEnv.put("APP_USER", USERNAME); + containerEnv.put("APP_USER_PASSWORD", PASSWORD); + String jdbcUrl = String.format(OPEN_GAUSS_URL, OPEN_GAUSS_PORT, DATABASE); + Pair> testDataSet = initTestData(); + String[] fieldNames = testDataSet.getKey(); + + String insertSql = insertTable(SCHEMA, SOURCE_TABLE, fieldNames); + + return JdbcCase.builder() + .dockerImage(OPENGAUSS_IMAGE) + .networkAliases(OPEN_GAUSS_ALIASES) + .containerEnv(containerEnv) + .driverClass(DRIVER_CLASS) + .host(HOST) + .port(OPEN_GAUSS_PORT) + .localPort(OPEN_GAUSS_PORT) + .jdbcTemplate(OPEN_GAUSS_URL) + .jdbcUrl(jdbcUrl) + .userName(USERNAME) + .password(PASSWORD) + .database(DATABASE) + .schema(SCHEMA) + .sourceTable(SOURCE_TABLE) + .sinkTable(SINK_TABLE) + .catalogDatabase(DATABASE) + .catalogSchema(SCHEMA) + .catalogTable(CATALOG_TABLE) + .createSql(CREATE_SQL) + .configFile(CONFIG_FILE) + .insertSql(insertSql) + .testData(testDataSet) + .build(); + } + + @Override + String driverUrl() { + return "https://repo1.maven.org/maven2/org/opengauss/opengauss-jdbc/5.1.0-og/opengauss-jdbc-5.1.0-og.jar"; + } + + @Override + protected Class loadDriverClass() { + return super.loadDriverClassFromUrl(); + } + + @Override + Pair> initTestData() { + List rows = new ArrayList<>(); + for (Integer i = 0; i < GEN_ROWS; i++) { + SeaTunnelRow row = + new SeaTunnelRow( + new Object[] { + i, + UUID.randomUUID(), + String.valueOf(i), + String.valueOf(i), + String.valueOf(i), + i % 2 == 0, + i, + i, + Long.valueOf(i), + BigDecimal.valueOf(i * 10.0), + BigDecimal.valueOf(i * 0.01), + Float.parseFloat("1.1"), + Double.parseDouble("1.111"), + i, + Long.valueOf(i), + LocalDate.of(2022, 1, 1).atStartOfDay(), + LocalDateTime.of(2022, 1, 1, 10, 0), + "Testing", + i + }); + rows.add(row); + } + + return Pair.of(fieldNames, rows); + } + + @Override + public String quoteIdentifier(String field) { + return "\"" + field + "\""; + } + + @Override + protected void clearTable(String database, String schema, String table) { + clearTable(schema, table); + } + + @Override + protected String buildTableInfoWithSchema(String database, String schema, String table) { + return buildTableInfoWithSchema(schema, table); + } + + @Override + GenericContainer initContainer() { + GenericContainer container = + new GenericContainer<>(OPENGAUSS_IMAGE) + .withNetwork(NETWORK) + .withNetworkAliases(OPEN_GAUSS_ALIASES) + .withEnv("GS_PASSWORD", PASSWORD) + .withLogConsumer( + new Slf4jLogConsumer( + DockerLoggerFactory.getLogger(OPENGAUSS_IMAGE))); + container.setPortBindings( + Lists.newArrayList(String.format("%s:%s", OPEN_GAUSS_PORT, OPEN_GAUSS_PORT))); + + return container; + } + + @Override + protected void initCatalog() { + String jdbcUrl = jdbcCase.getJdbcUrl().replace(HOST, dbServer.getHost()); + catalog = + new OpenGaussCatalog( + DatabaseIdentifier.OPENGAUSS, + jdbcCase.getUserName(), + jdbcCase.getPassword(), + JdbcUrlUtil.getUrlInfo(jdbcUrl), + SCHEMA); + // set connection + ((OpenGaussCatalog) catalog).setConnection(jdbcUrl, connection); + catalog.open(); + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/resources/jdbc_opengauss_source_and_sink.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/resources/jdbc_opengauss_source_and_sink.conf new file mode 100644 index 00000000000..224fcb24037 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/resources/jdbc_opengauss_source_and_sink.conf @@ -0,0 +1,67 @@ +# +# 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. +# + +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" +} + +source { + jdbc { + url = "jdbc:opengauss://e2e_OpenGauss:5432/postgres?loggerLevel=OFF" + driver = "org.opengauss.Driver" + connection_check_timeout_sec = 100 + user = "gaussdb" + password = "openGauss@123" + table_path = "postgres.public.gs_e2e_source_table" + query = "select * from public.gs_e2e_source_table" + split.size = 10 + } +} + +transform { +} + +sink { + jdbc { + url = "jdbc:opengauss://e2e_OpenGauss:5432/postgres?loggerLevel=OFF&stringtype=unspecified" + driver = "org.opengauss.Driver" + user = "gaussdb" + password = "openGauss@123" + database = "postgres" + table = "public.gs_e2e_sink_table" + compatible_mode = "postgresLow" + generate_sink_sql = true + } +} From a941b91628c6463c8c4039a81e387a32640da774 Mon Sep 17 00:00:00 2001 From: CosmosNi <40288034+CosmosNi@users.noreply.github.com> Date: Wed, 16 Oct 2024 22:52:36 +0800 Subject: [PATCH 07/72] [Fix][Connector-V2] Fix AbstractSingleSplitReader lock useless when do checkpoint (#7764) Co-authored-by: njh_cmss --- .github/workflows/backend.yml | 10 +++++----- .../common/source/AbstractSingleSplitReader.java | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a5165c85baa..f98ac8c80a6 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -50,7 +50,7 @@ jobs: dead-link: name: Dead links runs-on: ubuntu-latest - timeout-minutes: 120 + timeout-minutes: 150 steps: - uses: actions/checkout@v2 - run: sudo npm install -g markdown-link-check@3.8.7 @@ -330,7 +330,7 @@ jobs: matrix: java: [ '8', '11' ] os: [ 'ubuntu-latest' ] - timeout-minutes: 120 + timeout-minutes: 150 steps: - uses: actions/checkout@v2 - name: Set up JDK ${{ matrix.java }} @@ -351,7 +351,7 @@ jobs: echo "sub modules is empty, skipping" fi env: - MAVEN_OPTS: -Xmx2048m + MAVEN_OPTS: -Xmx4096m updated-modules-integration-test-part-3: needs: [ changes, sanity-check ] @@ -392,7 +392,7 @@ jobs: matrix: java: [ '8', '11' ] os: [ 'ubuntu-latest' ] - timeout-minutes: 120 + timeout-minutes: 150 steps: - uses: actions/checkout@v2 - name: Set up JDK ${{ matrix.java }} @@ -413,7 +413,7 @@ jobs: echo "sub modules is empty, skipping" fi env: - MAVEN_OPTS: -Xmx2048m + MAVEN_OPTS: -Xmx4096m updated-modules-integration-test-part-5: needs: [ changes, sanity-check ] if: needs.changes.outputs.api == 'false' && needs.changes.outputs.it-modules != '' diff --git a/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/AbstractSingleSplitReader.java b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/AbstractSingleSplitReader.java index 31385d0d470..d8dd6fae1e7 100644 --- a/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/AbstractSingleSplitReader.java +++ b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/AbstractSingleSplitReader.java @@ -26,13 +26,11 @@ public abstract class AbstractSingleSplitReader implements SourceReader { - protected final Object lock = new Object(); - protected volatile boolean noMoreSplits = false; @Override public void pollNext(Collector output) throws Exception { - synchronized (lock) { + synchronized (output.getCheckpointLock()) { if (noMoreSplits) { return; } From 207b8c16fd71540409d9493d8106dff7894917f8 Mon Sep 17 00:00:00 2001 From: Jia Fan Date: Wed, 16 Oct 2024 22:53:52 +0800 Subject: [PATCH 08/72] [Improve][Connector-V2] Add doris/starrocks create table with comment (#7847) --- .../seatunnel/connectors/doris/util/DorisCatalogUtil.java | 7 +++++-- .../connectors/doris/catalog/DorisCreateTableTest.java | 6 ++++-- .../seatunnel/starrocks/sink/StarRocksSaveModeUtil.java | 7 +++++-- .../starrocks/catalog/StarRocksCreateTableTest.java | 6 ++++-- .../seatunnel/e2e/connector/doris/DorisCatalogIT.java | 3 +++ 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java index 5025caed21c..e4f8804be02 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java @@ -252,9 +252,12 @@ private static String columnToDorisType( Column column, TypeConverter typeConverter) { checkNotNull(column, "The column is required."); return String.format( - "`%s` %s %s ", + "`%s` %s %s %s", column.getName(), typeConverter.reconvert(column).getColumnType(), - column.isNullable() ? "NULL" : "NOT NULL"); + column.isNullable() ? "NULL" : "NOT NULL", + StringUtils.isEmpty(column.getComment()) + ? "" + : "COMMENT '" + column.getComment() + "'"); } } diff --git a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java index 09a5b6a3293..02b3c5478f4 100644 --- a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java +++ b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java @@ -57,7 +57,9 @@ public void test() { columns.add(PhysicalColumn.of("id", BasicType.LONG_TYPE, (Long) null, true, null, "")); columns.add(PhysicalColumn.of("name", BasicType.STRING_TYPE, (Long) null, true, null, "")); - columns.add(PhysicalColumn.of("age", BasicType.INT_TYPE, (Long) null, true, null, "")); + columns.add( + PhysicalColumn.of( + "age", BasicType.INT_TYPE, (Long) null, true, null, "test comment")); columns.add(PhysicalColumn.of("score", BasicType.INT_TYPE, (Long) null, true, null, "")); columns.add(PhysicalColumn.of("gender", BasicType.BYTE_TYPE, (Long) null, true, null, "")); columns.add( @@ -122,7 +124,7 @@ public void test() { Assertions.assertEquals( result, "CREATE TABLE IF NOT EXISTS `test1`.`test2` ( \n" - + "`id` BIGINT NULL ,`age` INT NULL , \n" + + "`id` BIGINT NULL ,`age` INT NULL COMMENT 'test comment' , \n" + "`name` STRING NULL ,`score` INT NULL , \n" + "`create_time` DATETIME NOT NULL , \n" + "`gender` TINYINT NULL \n" diff --git a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java index 7fd3af17e72..f2eb56adc8a 100644 --- a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java +++ b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java @@ -108,12 +108,15 @@ public static String getCreateTableSql( private static String columnToStarrocksType(Column column) { checkNotNull(column, "The column is required."); return String.format( - "`%s` %s %s ", + "`%s` %s %s %s", column.getName(), dataTypeToStarrocksType( column.getDataType(), column.getColumnLength() == null ? 0 : column.getColumnLength()), - column.isNullable() ? "NULL" : "NOT NULL"); + column.isNullable() ? "NULL" : "NOT NULL", + StringUtils.isEmpty(column.getComment()) + ? "" + : "COMMENT '" + column.getComment() + "'"); } private static String mergeColumnInTemplate( diff --git a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java index fc3d15c4b4a..763413335aa 100644 --- a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java +++ b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java @@ -55,7 +55,9 @@ public void test() { List columns = new ArrayList<>(); columns.add(PhysicalColumn.of("id", BasicType.LONG_TYPE, (Long) null, true, null, "")); - columns.add(PhysicalColumn.of("name", BasicType.STRING_TYPE, (Long) null, true, null, "")); + columns.add( + PhysicalColumn.of( + "name", BasicType.STRING_TYPE, (Long) null, true, null, "test comment")); columns.add(PhysicalColumn.of("age", BasicType.INT_TYPE, (Long) null, true, null, "")); columns.add(PhysicalColumn.of("score", BasicType.INT_TYPE, (Long) null, true, null, "")); columns.add(PhysicalColumn.of("gender", BasicType.BYTE_TYPE, (Long) null, true, null, "")); @@ -112,7 +114,7 @@ public void test() { Assertions.assertEquals( "CREATE TABLE IF NOT EXISTS `test1`.`test2` ( \n" + "`id` BIGINT NULL ,`age` INT NULL , \n" - + "`name` STRING NULL ,`score` INT NULL , \n" + + "`name` STRING NULL COMMENT 'test comment',`score` INT NULL , \n" + "`create_time` DATETIME NOT NULL , \n" + "`gender` TINYINT NULL \n" + ") ENGINE=OLAP \n" diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java index b1d299004f3..07fab009083 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java @@ -260,6 +260,9 @@ private CatalogTable assertCreateTable( createdTable.getTableSchema().getColumns().stream() .map(Column::getName) .collect(Collectors.toList())); + Assertions.assertEquals( + "k1", createdTable.getTableSchema().getColumns().get(0).getComment()); + ; return createdTable; } From f4393a02bf453b8bcb0bad5996ec14a58105756a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 22:56:34 +0800 Subject: [PATCH 09/72] Bump commons-io:commons-io from 2.11.0 to 2.14.0 in /seatunnel-connectors-v2/connector-clickhouse (#7784) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- seatunnel-connectors-v2/connector-clickhouse/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seatunnel-connectors-v2/connector-clickhouse/pom.xml b/seatunnel-connectors-v2/connector-clickhouse/pom.xml index 2a4b77a3f45..22d2565a63a 100644 --- a/seatunnel-connectors-v2/connector-clickhouse/pom.xml +++ b/seatunnel-connectors-v2/connector-clickhouse/pom.xml @@ -53,7 +53,7 @@ commons-io commons-io - 2.11.0 + 2.14.0 From 418759d1dbd479b31ade3daee0fc4c1363fc02ba Mon Sep 17 00:00:00 2001 From: hailin0 Date: Wed, 16 Oct 2024 23:30:34 +0800 Subject: [PATCH 10/72] [Improve][Formats] Support not primary-key table for debezium format (#7836) --- ...atibleDebeziumJsonSerializationSchema.java | 5 +++ ...atibleDebeziumJsonSerializationSchema.java | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 seatunnel-formats/seatunnel-format-compatible-debezium-json/src/test/java/org/apache/seatunnel/format/compatible/debezium/json/TestCompatibleDebeziumJsonSerializationSchema.java diff --git a/seatunnel-formats/seatunnel-format-compatible-debezium-json/src/main/java/org/apache/seatunnel/format/compatible/debezium/json/CompatibleDebeziumJsonSerializationSchema.java b/seatunnel-formats/seatunnel-format-compatible-debezium-json/src/main/java/org/apache/seatunnel/format/compatible/debezium/json/CompatibleDebeziumJsonSerializationSchema.java index 4d692663fee..b4b4f47cf5c 100644 --- a/seatunnel-formats/seatunnel-format-compatible-debezium-json/src/main/java/org/apache/seatunnel/format/compatible/debezium/json/CompatibleDebeziumJsonSerializationSchema.java +++ b/seatunnel-formats/seatunnel-format-compatible-debezium-json/src/main/java/org/apache/seatunnel/format/compatible/debezium/json/CompatibleDebeziumJsonSerializationSchema.java @@ -30,15 +30,20 @@ @RequiredArgsConstructor public class CompatibleDebeziumJsonSerializationSchema implements SerializationSchema { + private final boolean isKey; private final int index; public CompatibleDebeziumJsonSerializationSchema(SeaTunnelRowType rowType, boolean isKey) { + this.isKey = isKey; this.index = rowType.indexOf(isKey ? FIELD_KEY : FIELD_VALUE); } @Override public byte[] serialize(SeaTunnelRow row) { String field = (String) row.getField(index); + if (isKey && field == null) { + return null; + } return field.getBytes(); } } diff --git a/seatunnel-formats/seatunnel-format-compatible-debezium-json/src/test/java/org/apache/seatunnel/format/compatible/debezium/json/TestCompatibleDebeziumJsonSerializationSchema.java b/seatunnel-formats/seatunnel-format-compatible-debezium-json/src/test/java/org/apache/seatunnel/format/compatible/debezium/json/TestCompatibleDebeziumJsonSerializationSchema.java new file mode 100644 index 00000000000..35117080f06 --- /dev/null +++ b/seatunnel-formats/seatunnel-format-compatible-debezium-json/src/test/java/org/apache/seatunnel/format/compatible/debezium/json/TestCompatibleDebeziumJsonSerializationSchema.java @@ -0,0 +1,39 @@ +/* + * 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.seatunnel.format.compatible.debezium.json; + +import org.apache.seatunnel.api.table.type.SeaTunnelRow; +import org.apache.seatunnel.api.table.type.SeaTunnelRowType; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TestCompatibleDebeziumJsonSerializationSchema { + + @Test + public void testDebeziumSerializeKeyIsNull() { + SeaTunnelRowType rowType = + CompatibleDebeziumJsonDeserializationSchema.DEBEZIUM_DATA_ROW_TYPE; + SeaTunnelRow row = new SeaTunnelRow(new Object[] {"test_topic", null, "value"}); + + CompatibleDebeziumJsonSerializationSchema serializationSchema = + new CompatibleDebeziumJsonSerializationSchema(rowType, true); + Assertions.assertNull(serializationSchema.serialize(row)); + } +} From 6b5f74e5247b8b7464b740e0f3cffe0ab2080db9 Mon Sep 17 00:00:00 2001 From: Jia Fan Date: Wed, 16 Oct 2024 23:34:23 +0800 Subject: [PATCH 11/72] [Improve][Connector-V2] Change File Read/WriteStrategy `setSeaTunnelRowTypeInfo` to `setCatalogTable` (#7829) --- .../file/hdfs/source/BaseHdfsFileSource.java | 8 +++---- .../file/config/BaseFileSourceConfig.java | 2 +- .../seatunnel/file/sink/BaseFileSink.java | 6 +++++- .../file/sink/BaseMultipleTableFileSink.java | 2 +- .../sink/writer/AbstractWriteStrategy.java | 7 ++++--- .../file/sink/writer/BinaryWriteStrategy.java | 8 +++---- .../file/sink/writer/JsonWriteStrategy.java | 10 +++++---- .../file/sink/writer/TextWriteStrategy.java | 9 ++++---- .../file/sink/writer/WriteStrategy.java | 8 +++---- .../source/reader/AbstractReadStrategy.java | 7 ++++--- .../file/source/reader/ExcelReadStrategy.java | 17 ++++++++------- .../file/source/reader/JsonReadStrategy.java | 5 +++-- .../file/source/reader/ReadStrategy.java | 4 ++-- .../file/source/reader/TextReadStrategy.java | 16 +++++++------- .../file/source/reader/XmlReadStrategy.java | 21 ++++++++++--------- .../file/writer/ExcelReadStrategyTest.java | 7 +++---- .../file/writer/ParquetWriteStrategyTest.java | 4 +++- .../file/writer/ReadStrategyEncodingTest.java | 11 +++++----- .../file/writer/XmlReadStrategyTest.java | 7 +++---- .../file/cos/source/CosFileSource.java | 8 +++---- .../file/oss/jindo/source/OssFileSource.java | 8 +++---- .../file/obs/source/ObsFileSource.java | 8 +++---- .../seatunnel/hive/sink/HiveSink.java | 2 +- .../hive/source/config/HiveSourceConfig.java | 4 +++- 24 files changed, 102 insertions(+), 87 deletions(-) diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java b/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java index 9af2721e220..2b71980935b 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java @@ -21,9 +21,9 @@ import org.apache.seatunnel.api.common.PrepareFailException; import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.catalog.schema.TableSchemaOptions; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.config.CheckConfigUtil; import org.apache.seatunnel.common.config.CheckResult; import org.apache.seatunnel.common.constants.PluginType; @@ -109,9 +109,9 @@ public void prepare(Config pluginConfig) throws PrepareFailException { case JSON: case EXCEL: case XML: - SeaTunnelRowType userDefinedSchema = - CatalogTableUtil.buildWithConfig(pluginConfig).getSeaTunnelRowType(); - readStrategy.setSeaTunnelRowTypeInfo(userDefinedSchema); + CatalogTable userDefinedCatalogTable = + CatalogTableUtil.buildWithConfig(pluginConfig); + readStrategy.setCatalogTable(userDefinedCatalogTable); rowType = readStrategy.getActualSeaTunnelRowTypeInfo(); break; case ORC: diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/BaseFileSourceConfig.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/BaseFileSourceConfig.java index 373ada564a8..10b969b0086 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/BaseFileSourceConfig.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/BaseFileSourceConfig.java @@ -95,7 +95,7 @@ private CatalogTable parseCatalogTable(ReadonlyConfig readonlyConfig) { case JSON: case EXCEL: case XML: - readStrategy.setSeaTunnelRowTypeInfo(catalogTable.getSeaTunnelRowType()); + readStrategy.setCatalogTable(catalogTable); return newCatalogTable(catalogTable, readStrategy.getActualSeaTunnelRowTypeInfo()); case ORC: case PARQUET: diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseFileSink.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseFileSink.java index 6686da98806..af6003c79ce 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseFileSink.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseFileSink.java @@ -26,6 +26,8 @@ import org.apache.seatunnel.api.sink.SeaTunnelSink; import org.apache.seatunnel.api.sink.SinkAggregatedCommitter; import org.apache.seatunnel.api.sink.SinkWriter; +import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; +import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.connectors.seatunnel.file.config.HadoopConf; @@ -110,7 +112,9 @@ public void prepare(Config pluginConfig) throws PrepareFailException { protected WriteStrategy createWriteStrategy() { WriteStrategy writeStrategy = WriteStrategyFactory.of(fileSinkConfig.getFileFormat(), fileSinkConfig); - writeStrategy.setSeaTunnelRowTypeInfo(seaTunnelRowType); + writeStrategy.setCatalogTable( + CatalogTableUtil.getCatalogTable( + "file", null, null, TablePath.DEFAULT.getTableName(), seaTunnelRowType)); return writeStrategy; } } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseMultipleTableFileSink.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseMultipleTableFileSink.java index a48368be448..b35c113f8da 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseMultipleTableFileSink.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/BaseMultipleTableFileSink.java @@ -112,7 +112,7 @@ public Optional> getWriterStateSerializer() { protected WriteStrategy createWriteStrategy() { WriteStrategy writeStrategy = WriteStrategyFactory.of(fileSinkConfig.getFileFormat(), fileSinkConfig); - writeStrategy.setSeaTunnelRowTypeInfo(catalogTable.getSeaTunnelRowType()); + writeStrategy.setCatalogTable(catalogTable); return writeStrategy; } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/AbstractWriteStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/AbstractWriteStrategy.java index 68476488a55..dd49c7f2d0c 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/AbstractWriteStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/AbstractWriteStrategy.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.connectors.seatunnel.file.sink.writer; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; @@ -150,11 +151,11 @@ public Configuration getConfiguration(HadoopConf hadoopConf) { /** * set seaTunnelRowTypeInfo in writer * - * @param seaTunnelRowType seaTunnelRowType + * @param catalogTable seaTunnelRowType */ @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - this.seaTunnelRowType = seaTunnelRowType; + public void setCatalogTable(CatalogTable catalogTable) { + this.seaTunnelRowType = catalogTable.getSeaTunnelRowType(); } /** diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/BinaryWriteStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/BinaryWriteStrategy.java index 7f496b2927d..06d05d62505 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/BinaryWriteStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/BinaryWriteStrategy.java @@ -17,8 +17,8 @@ package org.apache.seatunnel.connectors.seatunnel.file.sink.writer; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.exception.CommonError; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; import org.apache.seatunnel.connectors.seatunnel.file.exception.FileConnectorErrorCode; @@ -46,9 +46,9 @@ public BinaryWriteStrategy(FileSinkConfig fileSinkConfig) { } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - super.setSeaTunnelRowTypeInfo(seaTunnelRowType); - if (!seaTunnelRowType.equals(BinaryReadStrategy.binaryRowType)) { + public void setCatalogTable(CatalogTable catalogTable) { + super.setCatalogTable(catalogTable); + if (!catalogTable.getSeaTunnelRowType().equals(BinaryReadStrategy.binaryRowType)) { throw new FileConnectorException( FileConnectorErrorCode.FORMAT_NOT_SUPPORT, "BinaryWriteStrategy only supports binary format, please read file with `BINARY` format, and do not change schema in the transform."); diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/JsonWriteStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/JsonWriteStrategy.java index f95973f4cfc..23fb7893a8f 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/JsonWriteStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/JsonWriteStrategy.java @@ -18,8 +18,8 @@ package org.apache.seatunnel.connectors.seatunnel.file.sink.writer; import org.apache.seatunnel.api.serialization.SerializationSchema; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.exception.CommonError; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; import org.apache.seatunnel.common.utils.EncodingUtils; @@ -55,11 +55,13 @@ public JsonWriteStrategy(FileSinkConfig textFileSinkConfig) { } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - super.setSeaTunnelRowTypeInfo(seaTunnelRowType); + public void setCatalogTable(CatalogTable catalogTable) { + super.setCatalogTable(catalogTable); this.serializationSchema = new JsonSerializationSchema( - buildSchemaWithRowType(seaTunnelRowType, sinkColumnsIndexInRow), charset); + buildSchemaWithRowType( + catalogTable.getSeaTunnelRowType(), sinkColumnsIndexInRow), + charset); } @Override diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/TextWriteStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/TextWriteStrategy.java index 621048fb39a..77e2eb5c5b0 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/TextWriteStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/TextWriteStrategy.java @@ -18,8 +18,8 @@ package org.apache.seatunnel.connectors.seatunnel.file.sink.writer; import org.apache.seatunnel.api.serialization.SerializationSchema; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.exception.CommonError; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; import org.apache.seatunnel.common.utils.DateTimeUtils; @@ -71,12 +71,13 @@ public TextWriteStrategy(FileSinkConfig fileSinkConfig) { } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - super.setSeaTunnelRowTypeInfo(seaTunnelRowType); + public void setCatalogTable(CatalogTable catalogTable) { + super.setCatalogTable(catalogTable); this.serializationSchema = TextSerializationSchema.builder() .seaTunnelRowType( - buildSchemaWithRowType(seaTunnelRowType, sinkColumnsIndexInRow)) + buildSchemaWithRowType( + catalogTable.getSeaTunnelRowType(), sinkColumnsIndexInRow)) .delimiter(fieldDelimiter) .dateFormatter(dateFormat) .dateTimeFormatter(dateTimeFormat) diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/WriteStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/WriteStrategy.java index 6a1b1840b4d..24b23c9bfc3 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/WriteStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/writer/WriteStrategy.java @@ -17,8 +17,8 @@ package org.apache.seatunnel.connectors.seatunnel.file.sink.writer; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.connectors.seatunnel.file.config.HadoopConf; import org.apache.seatunnel.connectors.seatunnel.file.exception.FileConnectorException; import org.apache.seatunnel.connectors.seatunnel.file.hadoop.HadoopFileSystemProxy; @@ -56,11 +56,11 @@ public interface WriteStrategy extends Transaction, Serializable, Closeable { void write(SeaTunnelRow seaTunnelRow) throws FileConnectorException; /** - * set seaTunnelRowTypeInfo in writer + * set catalog table to write strategy * - * @param seaTunnelRowType seaTunnelRowType + * @param catalogTable catalogTable */ - void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType); + void setCatalogTable(CatalogTable catalogTable); /** * use seaTunnelRow generate partition directory diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/AbstractReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/AbstractReadStrategy.java index 3e71a3b2932..00d90d84195 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/AbstractReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/AbstractReadStrategy.java @@ -20,6 +20,7 @@ import org.apache.seatunnel.shade.com.typesafe.config.Config; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.BasicType; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; @@ -92,10 +93,10 @@ public void init(HadoopConf conf) { } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - this.seaTunnelRowType = seaTunnelRowType; + public void setCatalogTable(CatalogTable catalogTable) { + this.seaTunnelRowType = catalogTable.getSeaTunnelRowType(); this.seaTunnelRowTypeWithPartition = - mergePartitionTypes(fileNames.get(0), seaTunnelRowType); + mergePartitionTypes(fileNames.get(0), catalogTable.getSeaTunnelRowType()); } boolean checkFileType(String path) { diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java index c90b6d6659b..d7dfe206ab5 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java @@ -21,6 +21,7 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; @@ -145,15 +146,15 @@ protected void readProcess( } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - if (isNullOrEmpty(seaTunnelRowType.getFieldNames()) - || isNullOrEmpty(seaTunnelRowType.getFieldTypes())) { + public void setCatalogTable(CatalogTable catalogTable) { + SeaTunnelRowType rowType = catalogTable.getSeaTunnelRowType(); + if (isNullOrEmpty(rowType.getFieldNames()) || isNullOrEmpty(rowType.getFieldTypes())) { throw new FileConnectorException( CommonErrorCodeDeprecated.UNSUPPORTED_OPERATION, "Schema information is not set or incorrect Schema settings"); } SeaTunnelRowType userDefinedRowTypeWithPartition = - mergePartitionTypes(fileNames.get(0), seaTunnelRowType); + mergePartitionTypes(fileNames.get(0), rowType); // column projection if (pluginConfig.hasPath(BaseSourceConfigOptions.READ_COLUMNS.key())) { // get the read column index from user-defined row type @@ -161,15 +162,15 @@ public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { String[] fields = new String[readColumns.size()]; SeaTunnelDataType[] types = new SeaTunnelDataType[readColumns.size()]; for (int i = 0; i < indexes.length; i++) { - indexes[i] = seaTunnelRowType.indexOf(readColumns.get(i)); - fields[i] = seaTunnelRowType.getFieldName(indexes[i]); - types[i] = seaTunnelRowType.getFieldType(indexes[i]); + indexes[i] = rowType.indexOf(readColumns.get(i)); + fields[i] = rowType.getFieldName(indexes[i]); + types[i] = rowType.getFieldType(indexes[i]); } this.seaTunnelRowType = new SeaTunnelRowType(fields, types); this.seaTunnelRowTypeWithPartition = mergePartitionTypes(fileNames.get(0), this.seaTunnelRowType); } else { - this.seaTunnelRowType = seaTunnelRowType; + this.seaTunnelRowType = rowType; this.seaTunnelRowTypeWithPartition = userDefinedRowTypeWithPartition; } } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/JsonReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/JsonReadStrategy.java index 982419266f5..dfd57363d9d 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/JsonReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/JsonReadStrategy.java @@ -20,6 +20,7 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.serialization.DeserializationSchema; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; @@ -62,8 +63,8 @@ public void init(HadoopConf conf) { } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - super.setSeaTunnelRowTypeInfo(seaTunnelRowType); + public void setCatalogTable(CatalogTable catalogTable) { + super.setCatalogTable(catalogTable); if (isMergePartition) { deserializationSchema = new JsonDeserializationSchema(false, false, this.seaTunnelRowTypeWithPartition); diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ReadStrategy.java index c5bdf281244..9389223814a 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ReadStrategy.java @@ -20,6 +20,7 @@ import org.apache.seatunnel.shade.com.typesafe.config.Config; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; @@ -56,8 +57,7 @@ default SeaTunnelRowType getSeaTunnelRowTypeInfoWithUserConfigRowType( return getSeaTunnelRowTypeInfo(path); } - // todo: use CatalogTable - void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType); + void setCatalogTable(CatalogTable catalogTable); List getFileNamesByPath(String path) throws IOException; diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/TextReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/TextReadStrategy.java index 2b722593770..1a7a7398a4f 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/TextReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/TextReadStrategy.java @@ -21,6 +21,7 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.serialization.DeserializationSchema; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; @@ -170,9 +171,10 @@ public SeaTunnelRowType getSeaTunnelRowTypeInfo(String path) { } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { + public void setCatalogTable(CatalogTable catalogTable) { + SeaTunnelRowType rowType = catalogTable.getSeaTunnelRowType(); SeaTunnelRowType userDefinedRowTypeWithPartition = - mergePartitionTypes(fileNames.get(0), seaTunnelRowType); + mergePartitionTypes(fileNames.get(0), rowType); Optional fieldDelimiterOptional = ReadonlyConfig.fromConfig(pluginConfig) .getOptional(BaseSourceConfigOptions.FIELD_DELIMITER); @@ -201,7 +203,7 @@ public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { deserializationSchema = builder.seaTunnelRowType(userDefinedRowTypeWithPartition).build(); } else { - deserializationSchema = builder.seaTunnelRowType(seaTunnelRowType).build(); + deserializationSchema = builder.seaTunnelRowType(rowType).build(); } // column projection if (pluginConfig.hasPath(BaseSourceConfigOptions.READ_COLUMNS.key())) { @@ -210,15 +212,15 @@ public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { String[] fields = new String[readColumns.size()]; SeaTunnelDataType[] types = new SeaTunnelDataType[readColumns.size()]; for (int i = 0; i < indexes.length; i++) { - indexes[i] = seaTunnelRowType.indexOf(readColumns.get(i)); - fields[i] = seaTunnelRowType.getFieldName(indexes[i]); - types[i] = seaTunnelRowType.getFieldType(indexes[i]); + indexes[i] = rowType.indexOf(readColumns.get(i)); + fields[i] = rowType.getFieldName(indexes[i]); + types[i] = rowType.getFieldType(indexes[i]); } this.seaTunnelRowType = new SeaTunnelRowType(fields, types); this.seaTunnelRowTypeWithPartition = mergePartitionTypes(fileNames.get(0), this.seaTunnelRowType); } else { - this.seaTunnelRowType = seaTunnelRowType; + this.seaTunnelRowType = rowType; this.seaTunnelRowTypeWithPartition = userDefinedRowTypeWithPartition; } } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/XmlReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/XmlReadStrategy.java index a553a4f9d06..e012c46bdf5 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/XmlReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/XmlReadStrategy.java @@ -23,6 +23,7 @@ import org.apache.seatunnel.api.configuration.Option; import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; @@ -173,20 +174,20 @@ public SeaTunnelRowType getSeaTunnelRowTypeInfo(String path) throws FileConnecto } @Override - public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { - if (ArrayUtils.isEmpty(seaTunnelRowType.getFieldNames()) - || ArrayUtils.isEmpty(seaTunnelRowType.getFieldTypes())) { + public void setCatalogTable(CatalogTable catalogTable) { + SeaTunnelRowType rowType = catalogTable.getSeaTunnelRowType(); + if (ArrayUtils.isEmpty(rowType.getFieldNames()) + || ArrayUtils.isEmpty(rowType.getFieldTypes())) { throw new FileConnectorException( CommonErrorCodeDeprecated.ILLEGAL_ARGUMENT, "Schema information is undefined or misconfigured, please check your configuration file."); } if (readColumns.isEmpty()) { - this.seaTunnelRowType = seaTunnelRowType; - this.seaTunnelRowTypeWithPartition = - mergePartitionTypes(fileNames.get(0), seaTunnelRowType); + this.seaTunnelRowType = rowType; + this.seaTunnelRowTypeWithPartition = mergePartitionTypes(fileNames.get(0), rowType); } else { - if (readColumns.retainAll(Arrays.asList(seaTunnelRowType.getFieldNames()))) { + if (readColumns.retainAll(Arrays.asList(rowType.getFieldNames()))) { log.warn( "The read columns configuration will be filtered by the schema configuration, this may cause the actual results to be inconsistent with expectations. This is due to read columns not being a subset of the schema, " + "maybe you should check the schema and read_columns!"); @@ -195,9 +196,9 @@ public void setSeaTunnelRowTypeInfo(SeaTunnelRowType seaTunnelRowType) { String[] fields = new String[readColumns.size()]; SeaTunnelDataType[] types = new SeaTunnelDataType[readColumns.size()]; for (int i = 0; i < readColumns.size(); i++) { - indexes[i] = seaTunnelRowType.indexOf(readColumns.get(i)); - fields[i] = seaTunnelRowType.getFieldName(indexes[i]); - types[i] = seaTunnelRowType.getFieldType(indexes[i]); + indexes[i] = rowType.indexOf(readColumns.get(i)); + fields[i] = rowType.getFieldName(indexes[i]); + types[i] = rowType.getFieldType(indexes[i]); } this.seaTunnelRowType = new SeaTunnelRowType(fields, types); this.seaTunnelRowTypeWithPartition = diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java index 8aa43a03bdc..149ee7648d5 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java @@ -21,9 +21,9 @@ import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.utils.DateTimeUtils; import org.apache.seatunnel.common.utils.DateUtils; import org.apache.seatunnel.common.utils.TimeUtils; @@ -72,9 +72,8 @@ private void testExcelRead(String filePath) throws IOException, URISyntaxExcepti excelReadStrategy.init(localConf); List fileNamesByPath = excelReadStrategy.getFileNamesByPath(excelFilePath); - SeaTunnelRowType userDefinedSchema = - CatalogTableUtil.buildWithConfig(pluginConfig).getSeaTunnelRowType(); - excelReadStrategy.setSeaTunnelRowTypeInfo(userDefinedSchema); + CatalogTable userDefinedCatalogTable = CatalogTableUtil.buildWithConfig(pluginConfig); + excelReadStrategy.setCatalogTable(userDefinedCatalogTable); TestCollector testCollector = new TestCollector(); excelReadStrategy.read(fileNamesByPath.get(0), "", testCollector); for (SeaTunnelRow seaTunnelRow : testCollector.getRows()) { diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ParquetWriteStrategyTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ParquetWriteStrategyTest.java index 236d6f5a037..e692d7294b7 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ParquetWriteStrategyTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ParquetWriteStrategyTest.java @@ -20,6 +20,7 @@ import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.type.BasicType; import org.apache.seatunnel.api.table.type.LocalTimeType; import org.apache.seatunnel.api.table.type.PrimitiveByteArrayType; @@ -82,7 +83,8 @@ public void testParquetWriteInt96() throws Exception { ParquetWriteStrategy writeStrategy = new ParquetWriteStrategy(writeSinkConfig); ParquetReadStrategyTest.LocalConf hadoopConf = new ParquetReadStrategyTest.LocalConf(FS_DEFAULT_NAME_DEFAULT); - writeStrategy.setSeaTunnelRowTypeInfo(writeRowType); + writeStrategy.setCatalogTable( + CatalogTableUtil.getCatalogTable("test", null, null, "test", writeRowType)); writeStrategy.init(hadoopConf, "test1", "test1", 0); writeStrategy.beginTransaction(1L); writeStrategy.write( diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ReadStrategyEncodingTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ReadStrategyEncodingTest.java index 736ae590963..ad23dd0186f 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ReadStrategyEncodingTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ReadStrategyEncodingTest.java @@ -21,9 +21,9 @@ import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.connectors.seatunnel.file.config.HadoopConf; import org.apache.seatunnel.connectors.seatunnel.file.source.reader.AbstractReadStrategy; import org.apache.seatunnel.connectors.seatunnel.file.source.reader.JsonReadStrategy; @@ -121,11 +121,10 @@ private static void testRead( readStrategy.init(localConf); readStrategy.getFileNamesByPath(sourceFilePath); testCollector = new TestCollector(); - SeaTunnelRowType seaTunnelRowTypeInfo = - CatalogTableUtil.buildWithConfig(pluginConfig).getSeaTunnelRowType(); - Assertions.assertNotNull(seaTunnelRowTypeInfo); - readStrategy.setSeaTunnelRowTypeInfo(seaTunnelRowTypeInfo); - log.info(seaTunnelRowTypeInfo.toString()); + CatalogTable catalogTable = CatalogTableUtil.buildWithConfig(pluginConfig); + Assertions.assertNotNull(catalogTable.getSeaTunnelRowType()); + readStrategy.setCatalogTable(catalogTable); + log.info(catalogTable.getSeaTunnelRowType().toString()); readStrategy.read(sourceFilePath, "", testCollector); assertRows(testCollector); } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/XmlReadStrategyTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/XmlReadStrategyTest.java index 8bb2e483896..fca8f68fd2c 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/XmlReadStrategyTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/XmlReadStrategyTest.java @@ -21,9 +21,9 @@ import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory; import org.apache.seatunnel.api.source.Collector; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.utils.DateTimeUtils; import org.apache.seatunnel.common.utils.DateUtils; import org.apache.seatunnel.common.utils.TimeUtils; @@ -66,9 +66,8 @@ public void testXmlRead() throws IOException, URISyntaxException { xmlReadStrategy.setPluginConfig(pluginConfig); xmlReadStrategy.init(localConf); List fileNamesByPath = xmlReadStrategy.getFileNamesByPath(xmlFilePath); - SeaTunnelRowType userDefinedSchema = - CatalogTableUtil.buildWithConfig(pluginConfig).getSeaTunnelRowType(); - xmlReadStrategy.setSeaTunnelRowTypeInfo(userDefinedSchema); + CatalogTable catalogTable = CatalogTableUtil.buildWithConfig(pluginConfig); + xmlReadStrategy.setCatalogTable(catalogTable); TestCollector testCollector = new TestCollector(); xmlReadStrategy.read(fileNamesByPath.get(0), "", testCollector); for (SeaTunnelRow seaTunnelRow : testCollector.getRows()) { diff --git a/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSource.java b/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSource.java index 0690b2acebb..bd8df0261cb 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSource.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSource.java @@ -22,9 +22,9 @@ import org.apache.seatunnel.api.common.PrepareFailException; import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode; import org.apache.seatunnel.api.source.SeaTunnelSource; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.catalog.schema.TableSchemaOptions; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.config.CheckConfigUtil; import org.apache.seatunnel.common.config.CheckResult; import org.apache.seatunnel.common.constants.PluginType; @@ -95,9 +95,9 @@ public void prepare(Config pluginConfig) throws PrepareFailException { case JSON: case EXCEL: case XML: - SeaTunnelRowType userDefinedSchema = - CatalogTableUtil.buildWithConfig(pluginConfig).getSeaTunnelRowType(); - readStrategy.setSeaTunnelRowTypeInfo(userDefinedSchema); + CatalogTable userDefinedCatalogTable = + CatalogTableUtil.buildWithConfig(pluginConfig); + readStrategy.setCatalogTable(userDefinedCatalogTable); rowType = readStrategy.getActualSeaTunnelRowTypeInfo(); break; case ORC: diff --git a/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSource.java b/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSource.java index 335e3967808..ed9807729f1 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSource.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSource.java @@ -22,9 +22,9 @@ import org.apache.seatunnel.api.common.PrepareFailException; import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode; import org.apache.seatunnel.api.source.SeaTunnelSource; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.catalog.schema.TableSchemaOptions; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.config.CheckConfigUtil; import org.apache.seatunnel.common.config.CheckResult; import org.apache.seatunnel.common.constants.PluginType; @@ -96,9 +96,9 @@ public void prepare(Config pluginConfig) throws PrepareFailException { case JSON: case EXCEL: case XML: - SeaTunnelRowType userDefinedSchema = - CatalogTableUtil.buildWithConfig(pluginConfig).getSeaTunnelRowType(); - readStrategy.setSeaTunnelRowTypeInfo(userDefinedSchema); + CatalogTable userDefinedCatalogTable = + CatalogTableUtil.buildWithConfig(pluginConfig); + readStrategy.setCatalogTable(userDefinedCatalogTable); rowType = readStrategy.getActualSeaTunnelRowTypeInfo(); break; case ORC: diff --git a/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSource.java b/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSource.java index cf3061a44a3..8d2ae3d90bb 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSource.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSource.java @@ -22,9 +22,9 @@ import org.apache.seatunnel.api.common.PrepareFailException; import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode; import org.apache.seatunnel.api.source.SeaTunnelSource; +import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.catalog.schema.TableSchemaOptions; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.config.CheckConfigUtil; import org.apache.seatunnel.common.config.CheckResult; import org.apache.seatunnel.common.constants.PluginType; @@ -91,9 +91,9 @@ public void prepare(Config pluginConfig) throws PrepareFailException { case TEXT: case JSON: case EXCEL: - SeaTunnelRowType userDefinedSchema = - CatalogTableUtil.buildWithConfig(pluginConfig).getSeaTunnelRowType(); - readStrategy.setSeaTunnelRowTypeInfo(userDefinedSchema); + CatalogTable userDefinedCatalogTable = + CatalogTableUtil.buildWithConfig(pluginConfig); + readStrategy.setCatalogTable(userDefinedCatalogTable); rowType = readStrategy.getActualSeaTunnelRowTypeInfo(); break; case ORC: diff --git a/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/sink/HiveSink.java b/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/sink/HiveSink.java index 997c42f9faf..6e91baf0013 100644 --- a/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/sink/HiveSink.java +++ b/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/sink/HiveSink.java @@ -240,7 +240,7 @@ private Table getTableInformation() { private WriteStrategy getWriteStrategy() { if (writeStrategy == null) { writeStrategy = WriteStrategyFactory.of(fileSinkConfig.getFileFormat(), fileSinkConfig); - writeStrategy.setSeaTunnelRowTypeInfo(catalogTable.getSeaTunnelRowType()); + writeStrategy.setCatalogTable(catalogTable); } return writeStrategy; } diff --git a/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/source/config/HiveSourceConfig.java b/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/source/config/HiveSourceConfig.java index e98143fcf0e..eba9b5a15be 100644 --- a/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/source/config/HiveSourceConfig.java +++ b/seatunnel-connectors-v2/connector-hive/src/main/java/org/apache/seatunnel/connectors/seatunnel/hive/source/config/HiveSourceConfig.java @@ -279,7 +279,9 @@ private CatalogTable parseCatalogTableFromTable( } SeaTunnelRowType seaTunnelRowType = new SeaTunnelRowType(fieldNames, fieldTypes); - readStrategy.setSeaTunnelRowTypeInfo(seaTunnelRowType); + readStrategy.setCatalogTable( + CatalogTableUtil.getCatalogTable( + "hive", table.getDbName(), null, table.getTableName(), seaTunnelRowType)); final SeaTunnelRowType finalSeatunnelRowType = readStrategy.getActualSeaTunnelRowTypeInfo(); CatalogTable catalogTable = buildEmptyCatalogTable(readonlyConfig, table); From 258f9317656c8afa2acf152a35f62508b1690b63 Mon Sep 17 00:00:00 2001 From: zhouyh Date: Thu, 17 Oct 2024 10:35:39 +0800 Subject: [PATCH 12/72] [Fix][Connector-V2]Oceanbase vector database is added as the source server (#7832) Co-authored-by: Jia Fan --- docs/en/connector-v2/sink/Jdbc.md | 2 +- docs/en/connector-v2/source/Jdbc.md | 2 +- docs/zh/connector-v2/sink/Jdbc.md | 2 +- .../connector-jdbc/pom.xml | 2 +- .../oceanbase/OceanBaseMySqlCatalog.java | 64 +++++++++-- .../OceanBaseMySqlTypeConverter.java | 20 ++++ .../OceanBaseMysqlJdbcRowConverter.java | 16 ++- .../seatunnel/jdbc/JdbcOceanBaseITBase.java | 2 +- .../seatunnel/jdbc/JdbcOceanBaseMilvusIT.java | 106 ++++++++++++++++-- ...jdbc_oceanbase_source_and_milvus_sink.conf | 43 +++++++ 10 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/resources/jdbc_oceanbase_source_and_milvus_sink.conf diff --git a/docs/en/connector-v2/sink/Jdbc.md b/docs/en/connector-v2/sink/Jdbc.md index 8df5f12bfab..9b86a27721d 100644 --- a/docs/en/connector-v2/sink/Jdbc.md +++ b/docs/en/connector-v2/sink/Jdbc.md @@ -245,7 +245,7 @@ there are some reference value for params above. | Snowflake | net.snowflake.client.jdbc.SnowflakeDriver | jdbc:snowflake://.snowflakecomputing.com | / | https://mvnrepository.com/artifact/net.snowflake/snowflake-jdbc | | Vertica | com.vertica.jdbc.Driver | jdbc:vertica://localhost:5433 | / | https://repo1.maven.org/maven2/com/vertica/jdbc/vertica-jdbc/12.0.3-0/vertica-jdbc-12.0.3-0.jar | | Kingbase | com.kingbase8.Driver | jdbc:kingbase8://localhost:54321/db_test | / | https://repo1.maven.org/maven2/cn/com/kingbase/kingbase8/8.6.0/kingbase8-8.6.0.jar | -| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | / | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar | +| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | / | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.12/oceanbase-client-2.4.12.jar | | xugu | com.xugu.cloudjdbc.Driver | jdbc:xugu://localhost:5138 | / | https://repo1.maven.org/maven2/com/xugudb/xugu-jdbc/12.2.0/xugu-jdbc-12.2.0.jar | | InterSystems IRIS | com.intersystems.jdbc.IRISDriver | jdbc:IRIS://localhost:1972/%SYS | / | https://raw.githubusercontent.com/intersystems-community/iris-driver-distribution/main/JDBC/JDK18/intersystems-jdbc-3.8.4.jar | | opengauss | org.opengauss.Driver | jdbc:opengauss://localhost:5432/postgres | / | https://repo1.maven.org/maven2/org/opengauss/opengauss-jdbc/5.1.0-og/opengauss-jdbc-5.1.0-og.jar | diff --git a/docs/en/connector-v2/source/Jdbc.md b/docs/en/connector-v2/source/Jdbc.md index f2e7486e247..2b5897cbaea 100644 --- a/docs/en/connector-v2/source/Jdbc.md +++ b/docs/en/connector-v2/source/Jdbc.md @@ -133,7 +133,7 @@ there are some reference value for params above. | Redshift | com.amazon.redshift.jdbc42.Driver | jdbc:redshift://localhost:5439/testdb?defaultRowFetchSize=1000 | https://mvnrepository.com/artifact/com.amazon.redshift/redshift-jdbc42 | | Vertica | com.vertica.jdbc.Driver | jdbc:vertica://localhost:5433 | https://repo1.maven.org/maven2/com/vertica/jdbc/vertica-jdbc/12.0.3-0/vertica-jdbc-12.0.3-0.jar | | Kingbase | com.kingbase8.Driver | jdbc:kingbase8://localhost:54321/db_test | https://repo1.maven.org/maven2/cn/com/kingbase/kingbase8/8.6.0/kingbase8-8.6.0.jar | -| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar | +| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.12/oceanbase-client-2.4.12.jar | | Hive | org.apache.hive.jdbc.HiveDriver | jdbc:hive2://localhost:10000 | https://repo1.maven.org/maven2/org/apache/hive/hive-jdbc/3.1.3/hive-jdbc-3.1.3-standalone.jar | | xugu | com.xugu.cloudjdbc.Driver | jdbc:xugu://localhost:5138 | https://repo1.maven.org/maven2/com/xugudb/xugu-jdbc/12.2.0/xugu-jdbc-12.2.0.jar | | InterSystems IRIS | com.intersystems.jdbc.IRISDriver | jdbc:IRIS://localhost:1972/%SYS | https://raw.githubusercontent.com/intersystems-community/iris-driver-distribution/main/JDBC/JDK18/intersystems-jdbc-3.8.4.jar | diff --git a/docs/zh/connector-v2/sink/Jdbc.md b/docs/zh/connector-v2/sink/Jdbc.md index 6a7253da61c..4370af20026 100644 --- a/docs/zh/connector-v2/sink/Jdbc.md +++ b/docs/zh/connector-v2/sink/Jdbc.md @@ -235,7 +235,7 @@ Sink插件常用参数,请参考 [Sink常用选项](../sink-common-options.md) | Snowflake | net.snowflake.client.jdbc.SnowflakeDriver | jdbc:snowflake://.snowflakecomputing.com | / | https://mvnrepository.com/artifact/net.snowflake/snowflake-jdbc | | Vertica | com.vertica.jdbc.Driver | jdbc:vertica://localhost:5433 | / | https://repo1.maven.org/maven2/com/vertica/jdbc/vertica-jdbc/12.0.3-0/vertica-jdbc-12.0.3-0.jar | | Kingbase | com.kingbase8.Driver | jdbc:kingbase8://localhost:54321/db_test | / | https://repo1.maven.org/maven2/cn/com/kingbase/kingbase8/8.6.0/kingbase8-8.6.0.jar | -| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | / | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar | +| OceanBase | com.oceanbase.jdbc.Driver | jdbc:oceanbase://localhost:2881 | / | https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.12/oceanbase-client-2.4.12.jar | | opengauss | org.opengauss.Driver | jdbc:opengauss://localhost:5432/postgres | / | https://repo1.maven.org/maven2/org/opengauss/opengauss-jdbc/5.1.0-og/opengauss-jdbc-5.1.0-og.jar | ## 示例 diff --git a/seatunnel-connectors-v2/connector-jdbc/pom.xml b/seatunnel-connectors-v2/connector-jdbc/pom.xml index a6be0dd03ba..60e324be4c3 100644 --- a/seatunnel-connectors-v2/connector-jdbc/pom.xml +++ b/seatunnel-connectors-v2/connector-jdbc/pom.xml @@ -49,7 +49,7 @@ 2.5.1 8.6.0 3.1.3 - 2.4.11 + 2.4.12 12.2.0 3.0.0 3.2.0 diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oceanbase/OceanBaseMySqlCatalog.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oceanbase/OceanBaseMySqlCatalog.java index 33aa2f8ccd4..046a16f01be 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oceanbase/OceanBaseMySqlCatalog.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oceanbase/OceanBaseMySqlCatalog.java @@ -20,26 +20,32 @@ import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.Column; import org.apache.seatunnel.api.table.catalog.ConstraintKey; +import org.apache.seatunnel.api.table.catalog.PrimaryKey; import org.apache.seatunnel.api.table.catalog.TableIdentifier; import org.apache.seatunnel.api.table.catalog.TablePath; +import org.apache.seatunnel.api.table.catalog.TableSchema; import org.apache.seatunnel.api.table.catalog.exception.CatalogException; import org.apache.seatunnel.api.table.converter.BasicTypeDefine; import org.apache.seatunnel.common.utils.JdbcUrlUtil; import org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.AbstractJdbcCatalog; -import org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.utils.CatalogUtils; import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.oceanbase.OceanBaseMySqlTypeConverter; -import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.oceanbase.OceanBaseMySqlTypeMapper; import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.oceanbase.OceanBaseMysqlType; +import org.apache.commons.lang.StringUtils; + import com.google.common.base.Preconditions; import lombok.extern.slf4j.Slf4j; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -197,12 +203,54 @@ protected String getDropDatabaseSql(String databaseName) { @Override public CatalogTable getTable(String sqlQuery) throws SQLException { - Connection defaultConnection = getConnection(defaultUrl); - try (Statement statement = defaultConnection.createStatement(); - ResultSet resultSet = statement.executeQuery(sqlQuery)) { - ResultSetMetaData metaData = resultSet.getMetaData(); - return CatalogUtils.getCatalogTable( - metaData, new OceanBaseMySqlTypeMapper(typeConverter), sqlQuery); + try (Connection connection = getConnection(defaultUrl)) { + String tableName = null; + String databaseName = null; + String schemaName = null; + String catalogName = "jdbc_catalog"; + TableSchema.Builder schemaBuilder = TableSchema.builder(); + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sqlQuery)) { + ResultSetMetaData metaData = resultSet.getMetaData(); + tableName = metaData.getTableName(1); + databaseName = metaData.getCatalogName(1); + schemaName = metaData.getSchemaName(1); + catalogName = metaData.getCatalogName(1); + } + databaseName = StringUtils.defaultIfBlank(databaseName, null); + schemaName = StringUtils.defaultIfBlank(schemaName, null); + + TablePath tablePath = + StringUtils.isBlank(tableName) + ? TablePath.DEFAULT + : TablePath.of(databaseName, schemaName, tableName); + + try (PreparedStatement ps = + connection.prepareStatement(getSelectColumnsSql(tablePath)); + ResultSet columnResultSet = ps.executeQuery(); + ResultSet primaryKeys = + connection + .getMetaData() + .getPrimaryKeys(catalogName, schemaName, tableName)) { + while (primaryKeys.next()) { + String primaryKeyColumnName = primaryKeys.getString("COLUMN_NAME"); + schemaBuilder.primaryKey( + PrimaryKey.of( + primaryKeyColumnName, + Collections.singletonList(primaryKeyColumnName))); + } + while (columnResultSet.next()) { + schemaBuilder.column(buildColumn(columnResultSet)); + } + } + return CatalogTable.of( + TableIdentifier.of(catalogName, tablePath), + schemaBuilder.build(), + new HashMap<>(), + new ArrayList<>(), + "", + catalogName); } } diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMySqlTypeConverter.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMySqlTypeConverter.java index 4e9fa04d0d3..78c8415a886 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMySqlTypeConverter.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMySqlTypeConverter.java @@ -25,6 +25,7 @@ import org.apache.seatunnel.api.table.type.DecimalType; import org.apache.seatunnel.api.table.type.LocalTimeType; import org.apache.seatunnel.api.table.type.PrimitiveByteArrayType; +import org.apache.seatunnel.api.table.type.VectorType; import org.apache.seatunnel.common.exception.CommonError; import org.apache.seatunnel.connectors.seatunnel.common.source.TypeDefineUtils; import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseIdentifier; @@ -100,6 +101,9 @@ public class OceanBaseMySqlTypeConverter public static final long POWER_2_32 = (long) Math.pow(2, 32); public static final long MAX_VARBINARY_LENGTH = POWER_2_16 - 4; + private static final String VECTOR_TYPE_NAME = ""; + private static final String VECTOR_NAME = "VECTOR"; + @Override public String identifier() { return DatabaseIdentifier.OCENABASE; @@ -289,6 +293,17 @@ public Column convert(BasicTypeDefine typeDefine) { builder.dataType(LocalTimeType.LOCAL_DATE_TIME_TYPE); builder.scale(typeDefine.getScale()); break; + case VECTOR_TYPE_NAME: + String columnType = typeDefine.getColumnType(); + if (columnType.startsWith("vector(") && columnType.endsWith(")")) { + Integer number = + Integer.parseInt( + columnType.substring( + columnType.indexOf("(") + 1, columnType.indexOf(")"))); + builder.dataType(VectorType.VECTOR_FLOAT_TYPE); + builder.scale(number); + } + break; default: throw CommonError.convertToSeaTunnelTypeError( DatabaseIdentifier.OCENABASE, mysqlDataType, typeDefine.getName()); @@ -501,6 +516,11 @@ public BasicTypeDefine reconvert(Column column) { builder.columnType(MYSQL_DATETIME); } break; + case FLOAT_VECTOR: + builder.nativeType(VECTOR_NAME); + builder.columnType(String.format("%s(%s)", VECTOR_NAME, column.getScale())); + builder.dataType(VECTOR_NAME); + break; default: throw CommonError.convertToConnectorTypeError( DatabaseIdentifier.OCENABASE, diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMysqlJdbcRowConverter.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMysqlJdbcRowConverter.java index a498879138d..0a52e6a90be 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMysqlJdbcRowConverter.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/oceanbase/OceanBaseMysqlJdbcRowConverter.java @@ -32,6 +32,8 @@ import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseIdentifier; import org.apache.seatunnel.connectors.seatunnel.jdbc.utils.JdbcFieldTypeUtils; +import org.apache.commons.lang3.StringUtils; + import java.math.BigDecimal; import java.nio.ByteBuffer; import java.sql.Date; @@ -89,12 +91,16 @@ public SeaTunnelRow toInternal(ResultSet rs, TableSchema tableSchema) throws SQL fields[fieldIndex] = JdbcFieldTypeUtils.getFloat(rs, resultSetIndex); break; case FLOAT_VECTOR: - Object[] objects = (Object[]) rs.getObject(fieldIndex); - Float[] arrays = new Float[objects.length]; - for (int i = 0; i < objects.length; i++) { - arrays[i] = Float.parseFloat(objects[i].toString()); + String result = JdbcFieldTypeUtils.getString(rs, resultSetIndex); + if (StringUtils.isNotBlank(result)) { + result = result.replace("[", "").replace("]", ""); + String[] stringArray = result.split(","); + Float[] arrays = new Float[stringArray.length]; + for (int i = 0; i < stringArray.length; i++) { + arrays[i] = Float.parseFloat(stringArray[i]); + } + fields[fieldIndex] = BufferUtils.toByteBuffer(arrays); } - fields[fieldIndex] = BufferUtils.toByteBuffer(arrays); break; case DOUBLE: fields[fieldIndex] = JdbcFieldTypeUtils.getDouble(rs, resultSetIndex); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseITBase.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseITBase.java index e9674c9f106..732dbb72b07 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseITBase.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseITBase.java @@ -87,6 +87,6 @@ void checkResult(String executeKey, TestContainer container, Container.ExecResul @Override String driverUrl() { - return "https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar"; + return "https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.12/oceanbase-client-2.4.12.jar"; } } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseMilvusIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseMilvusIT.java index a70852fc6d2..e91eaed2de1 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseMilvusIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcOceanBaseMilvusIT.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.connectors.seatunnel.jdbc; +import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException; import org.apache.seatunnel.common.utils.ExceptionUtils; import org.apache.seatunnel.e2e.common.TestResource; @@ -28,6 +29,7 @@ import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterAll; @@ -44,7 +46,6 @@ import org.testcontainers.oceanbase.OceanBaseCEContainer; import org.testcontainers.utility.DockerLoggerFactory; -import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.JsonObject; import io.milvus.client.MilvusServiceClient; @@ -74,7 +75,10 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Random; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; import java.util.stream.Stream; import static org.awaitility.Awaitility.given; @@ -127,7 +131,7 @@ public class JdbcOceanBaseMilvusIT extends TestSuiteBase implements TestResource }; String driverUrl() { - return "https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.11/oceanbase-client-2.4.11.jar"; + return "https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/2.4.12/oceanbase-client-2.4.12.jar"; } @BeforeAll @@ -263,7 +267,8 @@ public void tearDown() throws Exception { @TestTemplate public void testMilvusToOceanBase(TestContainer container) throws Exception { try { - Container.ExecResult execResult = container.executeJob(configFile().get(0)); + Container.ExecResult execResult = + container.executeJob("/jdbc_milvus_source_and_oceanbase_sink.conf"); Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); } finally { clearTable(jdbcCase.getDatabase(), jdbcCase.getSchema(), jdbcCase.getSinkTable()); @@ -274,13 +279,72 @@ public void testMilvusToOceanBase(TestContainer container) throws Exception { public void testFakeToOceanBase(TestContainer container) throws IOException, InterruptedException { try { - Container.ExecResult execResult = container.executeJob(configFile().get(1)); + Container.ExecResult execResult = + container.executeJob("/jdbc_fake_to_oceanbase_sink.conf"); Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); } finally { clearTable(jdbcCase.getDatabase(), jdbcCase.getSchema(), jdbcCase.getSinkTable()); } } + @TestTemplate + public void testOceanBaseToMilvus(TestContainer container) throws Exception { + try { + initOceanBaseTestData(); + Container.ExecResult execResult = + container.executeJob("/jdbc_oceanbase_source_and_milvus_sink.conf"); + Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); + } finally { + clearTable(jdbcCase.getDatabase(), jdbcCase.getSchema(), jdbcCase.getSinkTable()); + } + } + + private void initOceanBaseTestData() { + try (Statement statement = connection.createStatement()) { + statement.execute(insertTable()); + connection.commit(); + } catch (SQLException e) { + try { + connection.rollback(); + } catch (SQLException exception) { + throw new SeaTunnelRuntimeException(JdbcITErrorCode.CLEAR_TABLE_FAILED, exception); + } + throw new SeaTunnelRuntimeException(JdbcITErrorCode.CLEAR_TABLE_FAILED, e); + } + } + + public String insertTable() { + Pair> testDataSet = initTestData(); + String[] fieldNames = testDataSet.getKey(); + String columns = + Arrays.stream(fieldNames) + .map(this::quoteIdentifier) + .collect(Collectors.joining(", ")); + List fields = + testDataSet.getValue().stream() + .map(SeaTunnelRow::getFields) + .collect(Collectors.toList()); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder + .append("INSERT INTO ") + .append(buildTableInfoWithSchema(OCEANBASE_DATABASE, OCEANBASE_SINK)) + .append(" (") + .append(columns) + .append(") VALUES "); + + int valuesCount = fields.size(); + for (int i = 0; i < valuesCount; i++) { + String fieldData = Arrays.toString(fields.get(i)); + sqlBuilder.append("(").append(fieldData, 1, fieldData.length() - 1).append(")"); + + if (i < valuesCount - 1) { + sqlBuilder.append(", "); + } + } + return sqlBuilder.toString(); + } + private void clearTable(String database, String schema, String table) { clearTable(database, table); } @@ -322,11 +386,6 @@ JdbcCase getJdbcCase() { .build(); } - List configFile() { - return Lists.newArrayList( - "/jdbc_milvus_source_and_oceanbase_sink.conf", "/jdbc_fake_to_oceanbase_sink.conf"); - } - private void initializeJdbcConnection(String jdbcUrl) throws SQLException, InstantiationException, IllegalAccessException { Driver driver = (Driver) loadDriverClass().newInstance(); @@ -433,4 +492,33 @@ public String buildTableInfoWithSchema(String schema, String table) { return quoteIdentifier(table); } } + + private String[] getFieldNames() { + return new String[] { + "book_id", "book_intro", "book_title", + }; + } + + private Pair> initTestData() { + String[] fieldNames = getFieldNames(); + + List rows = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 100; i++) { + SeaTunnelRow row = + new SeaTunnelRow( + new Object[] { + i + 100, + "'" + + DoubleStream.generate(() -> random.nextDouble() * 10) + .limit(VECTOR_DIM) + .mapToObj(num -> String.format("%.4f", num)) + .collect(Collectors.joining(", ", "[", "]")) + + "'", + "\"" + "test" + i + "\"", + }); + rows.add(row); + } + return Pair.of(fieldNames, rows); + } } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/resources/jdbc_oceanbase_source_and_milvus_sink.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/resources/jdbc_oceanbase_source_and_milvus_sink.conf new file mode 100644 index 00000000000..6875a6974c7 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-2/src/test/resources/jdbc_oceanbase_source_and_milvus_sink.conf @@ -0,0 +1,43 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" +} + +source { + jdbc { + url = "jdbc:oceanbase://e2e_oceanbase_vector:2881/seatunnel" + driver = "com.oceanbase.jdbc.Driver" + user = "root@test" + password = "" + compatible_mode="mysql" + database = "seatunnel" + table = "simple_example" + query = "select * from simple_example" + } +} + +sink { + Milvus { + url = "http://milvus-e2e:19530" + token = "root:Milvus" + database = "default" + collection="simple_example" + } +} \ No newline at end of file From 63c7b4e9cc031bdea36ff6f68d2ad4ebde912f53 Mon Sep 17 00:00:00 2001 From: Jia Fan Date: Thu, 17 Oct 2024 10:36:14 +0800 Subject: [PATCH 13/72] [Fix][Connector-V2] Fix kafka `format_error_handle_way` not work (#7838) --- .../org/apache/seatunnel/common/Handover.java | 5 ++- .../apache/seatunnel/common/HandoverTest.java | 31 +++++++++++++++++++ .../kafka/source/KafkaRecordEmitter.java | 6 ++-- .../e2e/connector/kafka/KafkaIT.java | 6 ++-- ...rmat_error_handle_way_fail_to_console.conf | 1 + ...rmat_error_handle_way_skip_to_console.conf | 1 + .../reader/SeaTunnelInputPartitionReader.java | 6 +++- .../batch/ParallelBatchPartitionReader.java | 2 +- .../batch/ParallelBatchPartitionReader.java | 2 +- .../batch/SeaTunnelBatchPartitionReader.java | 6 +++- .../SeaTunnelMicroBatchPartitionReader.java | 6 +++- 11 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 seatunnel-common/src/test/java/org/apache/seatunnel/common/HandoverTest.java diff --git a/seatunnel-common/src/main/java/org/apache/seatunnel/common/Handover.java b/seatunnel-common/src/main/java/org/apache/seatunnel/common/Handover.java index 1686514a15b..3132d93a169 100644 --- a/seatunnel-common/src/main/java/org/apache/seatunnel/common/Handover.java +++ b/seatunnel-common/src/main/java/org/apache/seatunnel/common/Handover.java @@ -30,7 +30,10 @@ public final class Handover implements Closeable { new LinkedBlockingQueue<>(DEFAULT_QUEUE_SIZE); private Throwable error; - public boolean isEmpty() { + public boolean isEmpty() throws Exception { + if (error != null) { + rethrowException(error, error.getMessage()); + } return blockingQueue.isEmpty(); } diff --git a/seatunnel-common/src/test/java/org/apache/seatunnel/common/HandoverTest.java b/seatunnel-common/src/test/java/org/apache/seatunnel/common/HandoverTest.java new file mode 100644 index 00000000000..199a2d4e723 --- /dev/null +++ b/seatunnel-common/src/test/java/org/apache/seatunnel/common/HandoverTest.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.seatunnel.common; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class HandoverTest { + + @Test + public void testThrowExceptionWhenQueueIsEmtpy() { + Handover handover = new Handover<>(); + handover.reportError(new RuntimeException("test")); + Assertions.assertThrows(RuntimeException.class, handover::isEmpty); + } +} diff --git a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaRecordEmitter.java b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaRecordEmitter.java index 6593137aff7..87d2b7b7c9f 100644 --- a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaRecordEmitter.java +++ b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaRecordEmitter.java @@ -31,7 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.Map; public class KafkaRecordEmitter @@ -71,13 +70,14 @@ public void emitRecord( // consumerRecord.offset + 1 is the offset commit to Kafka and also the start offset // for the next run splitState.setCurrentOffset(consumerRecord.offset() + 1); - } catch (IOException e) { + } catch (Exception e) { if (this.messageFormatErrorHandleWay == MessageFormatErrorHandleWay.SKIP) { logger.warn( "Deserialize message failed, skip this message, message: {}", new String(consumerRecord.value())); + } else { + throw e; } - throw e; } } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java index ffc97f4dd33..4a57cbdbd35 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java @@ -291,7 +291,7 @@ public void testSourceKafkaJsonFormatErrorHandleWaySkipToConsole(TestContainer c DEFAULT_FORMAT, DEFAULT_FIELD_DELIMITER, null); - generateTestData(row -> serializer.serializeRow(row), 0, 100); + generateTestData(serializer::serializeRow, 0, 100); Container.ExecResult execResult = container.executeJob( "/kafka/kafkasource_format_error_handle_way_skip_to_console.conf"); @@ -308,11 +308,11 @@ public void testSourceKafkaJsonFormatErrorHandleWayFailToConsole(TestContainer c DEFAULT_FORMAT, DEFAULT_FIELD_DELIMITER, null); - generateTestData(row -> serializer.serializeRow(row), 0, 100); + generateTestData(serializer::serializeRow, 0, 100); Container.ExecResult execResult = container.executeJob( "/kafka/kafkasource_format_error_handle_way_fail_to_console.conf"); - Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); + Assertions.assertEquals(1, execResult.getExitCode(), execResult.getStderr()); } @TestTemplate diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_fail_to_console.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_fail_to_console.conf index d2a0f05354d..dd1390d1679 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_fail_to_console.conf +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_fail_to_console.conf @@ -37,6 +37,7 @@ source { result_table_name = "kafka_table" start_mode = "earliest" format_error_handle_way = fail + format = text schema = { fields { id = bigint diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_skip_to_console.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_skip_to_console.conf index 88b6098b5e5..a34856d31b1 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_skip_to_console.conf +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_format_error_handle_way_skip_to_console.conf @@ -37,6 +37,7 @@ source { result_table_name = "kafka_table" start_mode = "earliest" format_error_handle_way = skip + format = text schema = { fields { id = bigint diff --git a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/SeaTunnelInputPartitionReader.java b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/SeaTunnelInputPartitionReader.java index 26e554b7fdb..4b651ec0535 100644 --- a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/SeaTunnelInputPartitionReader.java +++ b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/SeaTunnelInputPartitionReader.java @@ -34,7 +34,11 @@ public SeaTunnelInputPartitionReader(ParallelBatchPartitionReader partitionReade @Override public boolean next() throws IOException { - return partitionReader.next(); + try { + return partitionReader.next(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override diff --git a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/batch/ParallelBatchPartitionReader.java b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/batch/ParallelBatchPartitionReader.java index 20240638702..e20dca09d51 100644 --- a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/batch/ParallelBatchPartitionReader.java +++ b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-2.4/src/main/java/org/apache/seatunnel/translation/spark/source/reader/batch/ParallelBatchPartitionReader.java @@ -84,7 +84,7 @@ protected String getEnumeratorThreadName() { return String.format("parallel-split-enumerator-executor-%s", subtaskId); } - public boolean next() throws IOException { + public boolean next() throws Exception { prepare(); while (running && handover.isEmpty()) { try { diff --git a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/ParallelBatchPartitionReader.java b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/ParallelBatchPartitionReader.java index 27ab9f42d2c..5ca32d6775a 100644 --- a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/ParallelBatchPartitionReader.java +++ b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/ParallelBatchPartitionReader.java @@ -84,7 +84,7 @@ protected String getEnumeratorThreadName() { return String.format("parallel-split-enumerator-executor-%s", subtaskId); } - public boolean next() throws IOException { + public boolean next() throws Exception { prepare(); while (running && handover.isEmpty()) { try { diff --git a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/SeaTunnelBatchPartitionReader.java b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/SeaTunnelBatchPartitionReader.java index e9c9268a463..9841a0dfbdd 100644 --- a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/SeaTunnelBatchPartitionReader.java +++ b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/batch/SeaTunnelBatchPartitionReader.java @@ -32,7 +32,11 @@ public SeaTunnelBatchPartitionReader(ParallelBatchPartitionReader partitionReade @Override public boolean next() throws IOException { - return partitionReader.next(); + try { + return partitionReader.next(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override diff --git a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/micro/SeaTunnelMicroBatchPartitionReader.java b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/micro/SeaTunnelMicroBatchPartitionReader.java index 61d466d946d..597be91d675 100644 --- a/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/micro/SeaTunnelMicroBatchPartitionReader.java +++ b/seatunnel-translation/seatunnel-translation-spark/seatunnel-translation-spark-3.3/src/main/java/org/apache/seatunnel/translation/spark/source/partition/micro/SeaTunnelMicroBatchPartitionReader.java @@ -34,7 +34,11 @@ public SeaTunnelMicroBatchPartitionReader(ParallelBatchPartitionReader partition @Override public boolean next() throws IOException { - return partitionReader.next(); + try { + return partitionReader.next(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override From 934434cc75afc747339182df6468f89a5d0fb1b2 Mon Sep 17 00:00:00 2001 From: happyboy1024 <137260654+happyboy1024@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:54:23 +0800 Subject: [PATCH 14/72] [Feature][Connector-V2] Support write cdc changelog event into hudi sink (#7845) Co-authored-by: happyboy1024 <296442618@qq.com> --- docs/en/connector-v2/sink/Hudi.md | 8 +- docs/zh/connector-v2/sink/Hudi.md | 8 +- .../connector-hudi/pom.xml | 23 + .../seatunnel/hudi/catalog/HudiCatalog.java | 11 +- .../seatunnel/hudi/config/HudiOptions.java | 6 - .../seatunnel/hudi/config/HudiSinkConfig.java | 4 - .../hudi/config/HudiTableConfig.java | 5 + .../hudi/config/HudiTableOptions.java | 9 +- .../seatunnel/hudi/sink/HudiSink.java | 21 +- .../seatunnel/hudi/sink/HudiSinkFactory.java | 8 +- .../commiter/HudiSinkAggregatedCommitter.java | 102 ---- .../sink/convert/AvroSchemaConverter.java | 23 +- .../sink/convert/HudiRecordConverter.java | 8 +- .../sink/convert/RowDataToAvroConverters.java | 8 +- .../sink/state/HudiAggregatedCommitInfo.java | 11 +- .../hudi/sink/state/HudiCommitInfo.java | 18 +- .../hudi/sink/writer/HudiRecordWriter.java | 178 +++---- .../hudi/sink/writer/HudiSinkWriter.java | 32 +- .../seatunnel/hudi/util/HudiUtil.java | 9 +- .../connectors/seatunnel/hudi/HudiTest.java | 30 +- .../hudi/catalog/HudiCatalogTest.java | 1 + .../connector-hudi-e2e/pom.xml | 32 ++ .../seatunnel/e2e/connector/hudi/HudiIT.java | 4 +- .../e2e/connector/hudi/HudiMultiTableIT.java | 3 +- .../hudi/HudiSeatunnelS3MultiTableIT.java | 4 +- .../e2e/connector/hudi/HudiSinkCDCIT.java | 453 ++++++++++++++++++ .../hudi/HudiSparkS3MultiTableIT.java | 4 +- .../src/test/resources/ddl/mysql_cdc.sql | 75 +++ .../test/resources/{ => hudi}/core-site.xml | 0 .../resources/{ => hudi}/fake_to_hudi.conf | 0 .../fake_to_hudi_with_omit_config_item.conf | 0 .../{ => hudi}/multi_fake_to_hudi.conf | 0 .../resources/hudi/mysql_cdc_to_hudi.conf | 56 +++ .../resources/{ => hudi}/s3_fake_to_hudi.conf | 0 .../test/resources/mysql/server-gtids/my.cnf | 65 +++ .../src/test/resources/mysql/setup.sql | 27 ++ 36 files changed, 927 insertions(+), 319 deletions(-) delete mode 100644 seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/commiter/HudiSinkAggregatedCommitter.java create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSinkCDCIT.java create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/ddl/mysql_cdc.sql rename seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/{ => hudi}/core-site.xml (100%) rename seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/{ => hudi}/fake_to_hudi.conf (100%) rename seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/{ => hudi}/fake_to_hudi_with_omit_config_item.conf (100%) rename seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/{ => hudi}/multi_fake_to_hudi.conf (100%) create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/mysql_cdc_to_hudi.conf rename seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/{ => hudi}/s3_fake_to_hudi.conf (100%) create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/server-gtids/my.cnf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/setup.sql diff --git a/docs/en/connector-v2/sink/Hudi.md b/docs/en/connector-v2/sink/Hudi.md index 6c424fde15e..ea4c066d2f8 100644 --- a/docs/en/connector-v2/sink/Hudi.md +++ b/docs/en/connector-v2/sink/Hudi.md @@ -8,7 +8,7 @@ Used to write data to Hudi. ## Key features -- [x] [exactly-once](../../concept/connector-v2-features.md) +- [ ] [exactly-once](../../concept/connector-v2-features.md) - [x] [cdc](../../concept/connector-v2-features.md) - [x] [support multiple table write](../../concept/connector-v2-features.md) @@ -21,7 +21,6 @@ Base configuration: | table_dfs_path | string | yes | - | | conf_files_path | string | no | - | | table_list | Array | no | - | -| auto_commit | boolean | no | true | | schema_save_mode | enum | no | CREATE_SCHEMA_WHEN_NOT_EXIST| | common-options | Config | no | - | @@ -44,6 +43,7 @@ Table list configuration: | index_type | enum | no | BLOOM | | index_class_name | string | no | - | | record_byte_size | Int | no | 1024 | +| cdc_enabled | boolean| no | false | Note: When this configuration corresponds to a single table, you can flatten the configuration items in table_list to the outer layer. @@ -115,9 +115,9 @@ Note: When this configuration corresponds to a single table, you can flatten the `max_commits_to_keep` The max commits to keep of hudi table. -### auto_commit [boolean] +### cdc_enabled [boolean] -`auto_commit` Automatic transaction commit is enabled by default. +`cdc_enabled` Whether to persist the CDC change log. When enable, persist the change data if necessary, and the table can be queried as a CDC query mode. ### schema_save_mode [Enum] diff --git a/docs/zh/connector-v2/sink/Hudi.md b/docs/zh/connector-v2/sink/Hudi.md index 2fbf0271358..7d8007f6b03 100644 --- a/docs/zh/connector-v2/sink/Hudi.md +++ b/docs/zh/connector-v2/sink/Hudi.md @@ -8,7 +8,7 @@ ## 主要特点 -- [x] [exactly-once](../../concept/connector-v2-features.md) +- [ ] [exactly-once](../../concept/connector-v2-features.md) - [x] [cdc](../../concept/connector-v2-features.md) - [x] [support multiple table write](../../concept/connector-v2-features.md) @@ -21,7 +21,6 @@ | table_dfs_path | string | 是 | - | | conf_files_path | string | 否 | - | | table_list | string | 否 | - | -| auto_commit | boolean| 否 | true | | schema_save_mode | enum | 否 | CREATE_SCHEMA_WHEN_NOT_EXIST | | common-options | config | 否 | - | @@ -44,6 +43,7 @@ | index_type | enum | no | BLOOM | | index_class_name | string | no | - | | record_byte_size | Int | no | 1024 | +| cdc_enabled | boolean| no | false | 注意: 当此配置对应于单个表时,您可以将table_list中的配置项展平到外层。 @@ -115,9 +115,9 @@ `max_commits_to_keep` Hudi 表保留的最多提交数。 -### auto_commit [boolean] +### cdc_enabled [boolean] -`auto_commit` 是否自动提交. +`cdc_enabled` 是否持久化Hudi表的CDC变更日志。启用后,在必要时持久化更改数据,表可以作为CDC模式进行查询. ### schema_save_mode [Enum] diff --git a/seatunnel-connectors-v2/connector-hudi/pom.xml b/seatunnel-connectors-v2/connector-hudi/pom.xml index 35fc0b0459a..1a11d34f47e 100644 --- a/seatunnel-connectors-v2/connector-hudi/pom.xml +++ b/seatunnel-connectors-v2/connector-hudi/pom.xml @@ -102,4 +102,27 @@ + + + + maven-shade-plugin + + + + shade + + package + + + + org.apache.avro + ${seatunnel.shade.package}.${connector.name}.org.apache.avro + + + + + + + + diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalog.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalog.java index e0a25bfd85b..0d238c193d8 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalog.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalog.java @@ -35,6 +35,7 @@ import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; +import org.apache.hudi.avro.AvroSchemaUtils; import org.apache.hudi.common.model.HoodieAvroPayload; import org.apache.hudi.common.model.HoodieTableType; import org.apache.hudi.common.table.HoodieTableConfig; @@ -53,6 +54,7 @@ import java.util.stream.Collectors; import static org.apache.hbase.thirdparty.com.google.common.base.Preconditions.checkNotNull; +import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.CDC_ENABLED; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.RECORD_KEY_FIELDS; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.TABLE_TYPE; import static org.apache.seatunnel.connectors.seatunnel.hudi.sink.convert.AvroSchemaConverter.convertToSchema; @@ -195,6 +197,7 @@ public CatalogTable getTable(TablePath tablePath) String.join(",", tableConfig.getRecordKeyFields().get())); } options.put(TABLE_TYPE.key(), tableType.name()); + options.put(CDC_ENABLED.key(), String.valueOf(tableConfig.isCDCEnabled())); return CatalogTable.of( TableIdentifier.of( catalogName, tablePath.getDatabaseName(), tablePath.getTableName()), @@ -218,10 +221,16 @@ public void createTable(TablePath tablePath, CatalogTable table, boolean ignoreI .setTableType(table.getOptions().get(TABLE_TYPE.key())) .setRecordKeyFields(table.getOptions().get(RECORD_KEY_FIELDS.key())) .setTableCreateSchema( - convertToSchema(table.getSeaTunnelRowType()).toString()) + convertToSchema( + table.getSeaTunnelRowType(), + AvroSchemaUtils.getAvroRecordQualifiedName( + table.getTableId().getTableName())) + .toString()) .setTableName(tablePath.getTableName()) .setPartitionFields(String.join(",", table.getPartitionKeys())) .setPayloadClassName(HoodieAvroPayload.class.getName()) + .setCDCEnabled( + Boolean.parseBoolean(table.getOptions().get(CDC_ENABLED.key()))) .initTable(new HadoopStorageConfiguration(hadoopConf), tablePathStr); } } catch (IOException e) { diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiOptions.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiOptions.java index 38450e2dfdd..745e78eaf93 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiOptions.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiOptions.java @@ -44,12 +44,6 @@ public interface HudiOptions { .noDefaultValue() .withDescription("table_list"); - Option AUTO_COMMIT = - Options.key("auto_commit") - .booleanType() - .defaultValue(true) - .withDescription("auto commit"); - Option SCHEMA_SAVE_MODE = Options.key("schema_save_mode") .enumType(SchemaSaveMode.class) diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiSinkConfig.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiSinkConfig.java index 06650e87c03..bcb4efe77b8 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiSinkConfig.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiSinkConfig.java @@ -40,15 +40,12 @@ public class HudiSinkConfig implements Serializable { private String confFilesPath; - private boolean autoCommit; - private SchemaSaveMode schemaSaveMode; private DataSaveMode dataSaveMode; public static HudiSinkConfig of(ReadonlyConfig config) { Builder builder = HudiSinkConfig.builder(); - Optional optionalAutoCommit = config.getOptional(HudiOptions.AUTO_COMMIT); Optional optionalSchemaSaveMode = config.getOptional(HudiOptions.SCHEMA_SAVE_MODE); Optional optionalDataSaveMode = @@ -58,7 +55,6 @@ public static HudiSinkConfig of(ReadonlyConfig config) { builder.confFilesPath(config.get(HudiOptions.CONF_FILES_PATH)); builder.tableList(HudiTableConfig.of(config)); - builder.autoCommit(optionalAutoCommit.orElseGet(HudiOptions.AUTO_COMMIT::defaultValue)); builder.schemaSaveMode( optionalSchemaSaveMode.orElseGet(HudiOptions.SCHEMA_SAVE_MODE::defaultValue)); builder.dataSaveMode( diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableConfig.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableConfig.java index ba0ae33efdb..1ae612c9cb5 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableConfig.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableConfig.java @@ -40,6 +40,7 @@ import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.BATCH_INTERVAL_MS; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.BATCH_SIZE; +import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.CDC_ENABLED; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.DATABASE; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.INDEX_CLASS_NAME; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.INDEX_TYPE; @@ -108,6 +109,9 @@ public HudiTableConfig() {} @JsonProperty("max_commits_to_keep") private int maxCommitsToKeep; + @JsonProperty("cdc_enabled") + private boolean cdcEnabled; + public static List of(ReadonlyConfig connectorConfig) { List tableList; if (connectorConfig.getOptional(HudiOptions.TABLE_LIST).isPresent()) { @@ -132,6 +136,7 @@ public static List of(ReadonlyConfig connectorConfig) { connectorConfig.get(UPSERT_SHUFFLE_PARALLELISM)) .minCommitsToKeep(connectorConfig.get(MIN_COMMITS_TO_KEEP)) .maxCommitsToKeep(connectorConfig.get(MAX_COMMITS_TO_KEEP)) + .cdcEnabled(connectorConfig.get(CDC_ENABLED)) .build(); tableList = Collections.singletonList(hudiTableConfig); } diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableOptions.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableOptions.java index e48ef7be56e..2a2c7e01b35 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableOptions.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/config/HudiTableOptions.java @@ -46,6 +46,13 @@ public interface HudiTableOptions { .defaultValue(HoodieTableType.COPY_ON_WRITE) .withDescription("hudi table type"); + Option CDC_ENABLED = + Options.key("cdc_enabled") + .booleanType() + .defaultValue(false) + .withDescription( + "When enable, persist the change data if necessary, and can be queried as a CDC query mode."); + Option RECORD_KEY_FIELDS = Options.key("record_key_fields") .stringType() @@ -76,7 +83,7 @@ public interface HudiTableOptions { Options.key("record_byte_size") .intType() .defaultValue(1024) - .withDescription("auto commit"); + .withDescription("The byte size of each record"); Option OP_TYPE = Options.key("op_type") diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSink.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSink.java index 13c245336aa..11a402ab101 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSink.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSink.java @@ -24,7 +24,6 @@ import org.apache.seatunnel.api.sink.DefaultSaveModeHandler; import org.apache.seatunnel.api.sink.SaveModeHandler; import org.apache.seatunnel.api.sink.SeaTunnelSink; -import org.apache.seatunnel.api.sink.SinkAggregatedCommitter; import org.apache.seatunnel.api.sink.SinkWriter; import org.apache.seatunnel.api.sink.SupportMultiTableSink; import org.apache.seatunnel.api.sink.SupportSaveMode; @@ -38,14 +37,12 @@ import org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiSinkConfig; import org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableConfig; import org.apache.seatunnel.connectors.seatunnel.hudi.exception.HudiConnectorException; -import org.apache.seatunnel.connectors.seatunnel.hudi.sink.commiter.HudiSinkAggregatedCommitter; import org.apache.seatunnel.connectors.seatunnel.hudi.sink.state.HudiAggregatedCommitInfo; import org.apache.seatunnel.connectors.seatunnel.hudi.sink.state.HudiCommitInfo; import org.apache.seatunnel.connectors.seatunnel.hudi.sink.state.HudiSinkState; import org.apache.seatunnel.connectors.seatunnel.hudi.sink.writer.HudiSinkWriter; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -82,15 +79,13 @@ public String getPluginName() { @Override public HudiSinkWriter createWriter(SinkWriter.Context context) throws IOException { - return new HudiSinkWriter( - context, seaTunnelRowType, hudiSinkConfig, hudiTableConfig, new ArrayList<>()); + return new HudiSinkWriter(context, seaTunnelRowType, hudiSinkConfig, hudiTableConfig); } @Override public SinkWriter restoreWriter( SinkWriter.Context context, List states) throws IOException { - return new HudiSinkWriter( - context, seaTunnelRowType, hudiSinkConfig, hudiTableConfig, states); + return SeaTunnelSink.super.restoreWriter(context, states); } @Override @@ -103,18 +98,6 @@ public Optional> getCommitInfoSerializer() { return Optional.of(new DefaultSerializer<>()); } - @Override - public Optional> - createAggregatedCommitter() throws IOException { - return Optional.of( - new HudiSinkAggregatedCommitter(hudiTableConfig, hudiSinkConfig, seaTunnelRowType)); - } - - @Override - public Optional> getAggregatedCommitInfoSerializer() { - return Optional.of(new DefaultSerializer<>()); - } - @Override public Optional getSaveModeHandler() { TablePath tablePath = diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSinkFactory.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSinkFactory.java index ed21b15166a..7e6d9826d95 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSinkFactory.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/HudiSinkFactory.java @@ -37,12 +37,12 @@ import java.util.List; import java.util.Optional; -import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiOptions.AUTO_COMMIT; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiOptions.CONF_FILES_PATH; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiOptions.TABLE_DFS_PATH; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiOptions.TABLE_LIST; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.BATCH_INTERVAL_MS; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.BATCH_SIZE; +import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.CDC_ENABLED; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.INDEX_CLASS_NAME; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.INDEX_TYPE; import static org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableOptions.INSERT_SHUFFLE_PARALLELISM; @@ -85,7 +85,7 @@ public OptionRule optionRule() { UPSERT_SHUFFLE_PARALLELISM, MIN_COMMITS_TO_KEEP, MAX_COMMITS_TO_KEEP, - AUTO_COMMIT, + CDC_ENABLED, SinkCommonOptions.MULTI_TABLE_SINK_REPLICA) .build(); } @@ -121,6 +121,10 @@ public TableSink createSink(TableSinkFactoryContext context) { } // table type catalogTable.getOptions().put(TABLE_TYPE.key(), hudiTableConfig.getTableType().name()); + // cdc enabled + catalogTable + .getOptions() + .put(CDC_ENABLED.key(), String.valueOf(hudiTableConfig.isCdcEnabled())); catalogTable = CatalogTable.of( newTableId, diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/commiter/HudiSinkAggregatedCommitter.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/commiter/HudiSinkAggregatedCommitter.java deleted file mode 100644 index beba719c76d..00000000000 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/commiter/HudiSinkAggregatedCommitter.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.seatunnel.connectors.seatunnel.hudi.sink.commiter; - -import org.apache.seatunnel.api.sink.SinkAggregatedCommitter; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; -import org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiSinkConfig; -import org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableConfig; -import org.apache.seatunnel.connectors.seatunnel.hudi.sink.client.HudiWriteClientProvider; -import org.apache.seatunnel.connectors.seatunnel.hudi.sink.state.HudiAggregatedCommitInfo; -import org.apache.seatunnel.connectors.seatunnel.hudi.sink.state.HudiCommitInfo; - -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.util.List; -import java.util.Stack; -import java.util.stream.Collectors; - -@Slf4j -public class HudiSinkAggregatedCommitter - implements SinkAggregatedCommitter { - - private final HudiTableConfig tableConfig; - - private final HudiWriteClientProvider writeClientProvider; - - public HudiSinkAggregatedCommitter( - HudiTableConfig tableConfig, - HudiSinkConfig sinkConfig, - SeaTunnelRowType seaTunnelRowType) { - this.tableConfig = tableConfig; - this.writeClientProvider = - new HudiWriteClientProvider( - sinkConfig, tableConfig.getTableName(), seaTunnelRowType); - } - - @Override - public List commit( - List aggregatedCommitInfo) throws IOException { - aggregatedCommitInfo = - aggregatedCommitInfo.stream() - .filter( - commit -> - commit.getHudiCommitInfoList().stream() - .anyMatch( - aggregateCommit -> - !aggregateCommit - .getWriteStatusList() - .isEmpty() - && !writeClientProvider - .getOrCreateClient() - .commit( - aggregateCommit - .getWriteInstantTime(), - aggregateCommit - .getWriteStatusList()))) - .collect(Collectors.toList()); - log.debug( - "hudi records have been committed, error commit info are {}", aggregatedCommitInfo); - return aggregatedCommitInfo; - } - - @Override - public HudiAggregatedCommitInfo combine(List commitInfos) { - return new HudiAggregatedCommitInfo(commitInfos); - } - - @Override - public void abort(List aggregatedCommitInfo) throws Exception { - writeClientProvider.getOrCreateClient().rollbackFailedWrites(); - // rollback force commit - for (HudiAggregatedCommitInfo hudiAggregatedCommitInfo : aggregatedCommitInfo) { - for (HudiCommitInfo commitInfo : hudiAggregatedCommitInfo.getHudiCommitInfoList()) { - Stack forceCommitTime = commitInfo.getForceCommitTime(); - while (!forceCommitTime.isEmpty()) { - writeClientProvider.getOrCreateClient().rollback(forceCommitTime.pop()); - } - } - } - } - - @Override - public void close() { - writeClientProvider.close(); - } -} diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/AvroSchemaConverter.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/AvroSchemaConverter.java index addbf8491f9..acb10212757 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/AvroSchemaConverter.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/AvroSchemaConverter.java @@ -50,13 +50,13 @@ private AvroSchemaConverter() { * @return Avro's {@link Schema} matching this logical type. */ public static Schema convertToSchema(SeaTunnelDataType schema) { - return convertToSchema(schema, "org.apache.seatunnel.avro.generated.record"); + return convertToSchema(schema, "record"); } /** * Converts Seatunnel {@link SeaTunnelDataType} (can be nested) into an Avro schema. * - *

The "{rowName}_" is used as the nested row type name prefix in order to generate the right + *

The "{rowName}." is used as the nested row type name prefix in order to generate the right * schema. Nested record type that only differs with type name is still compatible. * * @param dataType logical type @@ -105,10 +105,15 @@ public static Schema convertToSchema(SeaTunnelDataType dataType, String rowNa return nullableSchema(time); case DECIMAL: DecimalType decimalType = (DecimalType) dataType; - // store BigDecimal as byte[] + // store BigDecimal as Fixed + // for spark compatibility. Schema decimal = LogicalTypes.decimal(decimalType.getPrecision(), decimalType.getScale()) - .addToSchema(SchemaBuilder.builder().bytesType()); + .addToSchema( + SchemaBuilder.fixed(String.format("%s.fixed", rowName)) + .size( + computeMinBytesForDecimalPrecision( + decimalType.getPrecision()))); return nullableSchema(decimal); case ROW: SeaTunnelRowType rowType = (SeaTunnelRowType) dataType; @@ -121,7 +126,7 @@ public static Schema convertToSchema(SeaTunnelDataType dataType, String rowNa SeaTunnelDataType fieldType = rowType.getFieldType(i); SchemaBuilder.GenericDefault fieldBuilder = builder.name(fieldName) - .type(convertToSchema(fieldType, rowName + "_" + fieldName)); + .type(convertToSchema(fieldType, rowName + "." + fieldName)); builder = fieldBuilder.withDefault(null); } @@ -166,4 +171,12 @@ public static SeaTunnelDataType extractValueTypeToAvroMap(SeaTunnelDataType convertRow( seaTunnelRowType.getFieldNames()[i], createConverter(seaTunnelRowType.getFieldType(i)) .convert( - convertToSchema(seaTunnelRowType.getFieldType(i)), + convertToSchema( + seaTunnelRowType.getFieldType(i), + AvroSchemaUtils.getAvroRecordQualifiedName( + hudiTableConfig.getTableName()) + + "." + + seaTunnelRowType.getFieldNames()[i]), element.getField(i))); } return new HoodieAvroRecord<>( diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/RowDataToAvroConverters.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/RowDataToAvroConverters.java index a48179fdb7a..5c063626693 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/RowDataToAvroConverters.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/convert/RowDataToAvroConverters.java @@ -22,6 +22,7 @@ import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; +import org.apache.avro.Conversions; import org.apache.avro.Schema; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericRecord; @@ -45,6 +46,8 @@ /** Tool class used to convert from {@link SeaTunnelRow} to Avro {@link GenericRecord}. */ public class RowDataToAvroConverters implements Serializable { + private static final Conversions.DecimalConversion DECIMAL_CONVERSION = + new Conversions.DecimalConversion(); // -------------------------------------------------------------------------------- // Runtime Converters // -------------------------------------------------------------------------------- @@ -166,8 +169,9 @@ public Object convert(Schema schema, Object object) { @Override public Object convert(Schema schema, Object object) { - return ByteBuffer.wrap( - ((BigDecimal) object).unscaledValue().toByteArray()); + BigDecimal javaDecimal = (BigDecimal) object; + return DECIMAL_CONVERSION.toFixed( + javaDecimal, schema, schema.getLogicalType()); } }; break; diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiAggregatedCommitInfo.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiAggregatedCommitInfo.java index 348a040be65..065fed72ad7 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiAggregatedCommitInfo.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiAggregatedCommitInfo.java @@ -17,15 +17,6 @@ package org.apache.seatunnel.connectors.seatunnel.hudi.sink.state; -import lombok.AllArgsConstructor; -import lombok.Data; - import java.io.Serializable; -import java.util.List; - -@Data -@AllArgsConstructor -public class HudiAggregatedCommitInfo implements Serializable { - private final List hudiCommitInfoList; -} +public class HudiAggregatedCommitInfo implements Serializable {} diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiCommitInfo.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiCommitInfo.java index 0357931bb08..808cc4d942a 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiCommitInfo.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/state/HudiCommitInfo.java @@ -17,22 +17,6 @@ package org.apache.seatunnel.connectors.seatunnel.hudi.sink.state; -import org.apache.hudi.client.WriteStatus; - -import lombok.AllArgsConstructor; -import lombok.Data; - import java.io.Serializable; -import java.util.List; -import java.util.Stack; - -@Data -@AllArgsConstructor -public class HudiCommitInfo implements Serializable { - - private final String writeInstantTime; - - private final List writeStatusList; - private final Stack forceCommitTime; -} +public class HudiCommitInfo implements Serializable {} diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiRecordWriter.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiRecordWriter.java index 7eb3ab546b7..b98e2228707 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiRecordWriter.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiRecordWriter.java @@ -17,23 +17,23 @@ package org.apache.seatunnel.connectors.seatunnel.hudi.sink.writer; +import org.apache.seatunnel.api.table.type.RowKind; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; -import org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiSinkConfig; import org.apache.seatunnel.connectors.seatunnel.hudi.config.HudiTableConfig; import org.apache.seatunnel.connectors.seatunnel.hudi.exception.HudiConnectorException; import org.apache.seatunnel.connectors.seatunnel.hudi.exception.HudiErrorCode; import org.apache.seatunnel.connectors.seatunnel.hudi.sink.client.WriteClientProvider; import org.apache.seatunnel.connectors.seatunnel.hudi.sink.convert.HudiRecordConverter; -import org.apache.seatunnel.connectors.seatunnel.hudi.sink.state.HudiCommitInfo; import org.apache.avro.Schema; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hudi.avro.AvroSchemaUtils; import org.apache.hudi.client.HoodieJavaWriteClient; -import org.apache.hudi.client.WriteStatus; import org.apache.hudi.common.model.HoodieAvroPayload; +import org.apache.hudi.common.model.HoodieKey; import org.apache.hudi.common.model.HoodieRecord; -import org.apache.hudi.common.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,10 +42,10 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Stack; +import java.util.Map; +import java.util.Set; import static org.apache.seatunnel.connectors.seatunnel.hudi.sink.convert.AvroSchemaConverter.convertToSchema; @@ -64,60 +64,44 @@ public class HudiRecordWriter implements Serializable { private final SeaTunnelRowType seaTunnelRowType; - private final boolean autoCommit; - private Schema schema; private transient int batchCount = 0; private final List> writeRecords; - private Stack forceCommitTime; - - private String writeInstantTime; + private final List deleteRecordKeys; - private List writeStatusList; + private final LinkedHashMap>> buffer = + new LinkedHashMap<>(); private transient volatile boolean closed = false; private transient volatile Exception flushException; public HudiRecordWriter( - HudiSinkConfig hudiSinkConfig, HudiTableConfig hudiTableConfig, WriteClientProvider clientProvider, SeaTunnelRowType seaTunnelRowType) { this.hudiTableConfig = hudiTableConfig; - this.autoCommit = hudiSinkConfig.isAutoCommit(); this.clientProvider = clientProvider; this.seaTunnelRowType = seaTunnelRowType; this.writeRecords = new ArrayList<>(); - this.writeStatusList = new ArrayList<>(); - this.forceCommitTime = new Stack<>(); + this.deleteRecordKeys = new ArrayList<>(); this.recordConverter = new HudiRecordConverter(); } - public HudiRecordWriter( - HudiSinkConfig sinkConfig, - HudiTableConfig tableConfig, - WriteClientProvider writeClientProvider, - SeaTunnelRowType seaTunnelRowType, - HudiCommitInfo hudiCommitInfo) { - this(sinkConfig, tableConfig, writeClientProvider, seaTunnelRowType); - this.writeInstantTime = hudiCommitInfo.getWriteInstantTime(); - this.writeStatusList = hudiCommitInfo.getWriteStatusList(); - } - public void open() { - this.schema = new Schema.Parser().parse(convertToSchema(seaTunnelRowType).toString()); + this.schema = + new Schema.Parser() + .parse( + convertToSchema( + seaTunnelRowType, + AvroSchemaUtils.getAvroRecordQualifiedName( + hudiTableConfig.getTableName())) + .toString()); try { - HoodieJavaWriteClient writeClient = - clientProvider.getOrCreateClient(); - if (StringUtils.nonEmpty(writeInstantTime) && Objects.nonNull(writeStatusList)) { - if (!writeClient.commit(writeInstantTime, writeStatusList)) { - LOG.warn("Failed to commit history data."); - } - } + clientProvider.getOrCreateClient(); } catch (Exception e) { throw new HudiConnectorException( CommonErrorCodeDeprecated.WRITER_OPERATION_FAILED, @@ -133,7 +117,7 @@ public void writeRecord(SeaTunnelRow record) { batchCount++; if (hudiTableConfig.getBatchSize() > 0 && batchCount >= hudiTableConfig.getBatchSize()) { - flush(true); + flush(); } } catch (Exception e) { throw new HudiConnectorException( @@ -143,92 +127,89 @@ public void writeRecord(SeaTunnelRow record) { } } - public synchronized void flush(boolean isNeedForceCommit) { + public synchronized void flush() { if (batchCount == 0) { log.debug("No data needs to be refreshed, waiting for incoming data."); return; } checkFlushException(); - HoodieJavaWriteClient writeClient = clientProvider.getOrCreateClient(); - if (autoCommit || writeInstantTime == null) { - writeInstantTime = writeClient.startCommit(); + Boolean preChangeFlag = null; + Set>>> entries = + buffer.entrySet(); + for (Map.Entry>> entry : entries) { + boolean currentChangeFlag = entry.getValue().getKey(); + if (currentChangeFlag) { + if (preChangeFlag != null && !preChangeFlag) { + executeDelete(); + } + writeRecords.add(entry.getValue().getValue()); + } else { + if (preChangeFlag != null && preChangeFlag) { + executeWrite(); + } + deleteRecordKeys.add(entry.getKey()); + } + preChangeFlag = currentChangeFlag; } - List currentWriteStatusList; + + if (preChangeFlag != null) { + if (preChangeFlag) { + executeWrite(); + } else { + executeDelete(); + } + } + batchCount = 0; + buffer.clear(); + } + + private void executeWrite() { + HoodieJavaWriteClient writeClient = clientProvider.getOrCreateClient(); + String writeInstantTime = writeClient.startCommit(); // write records switch (hudiTableConfig.getOpType()) { case INSERT: - currentWriteStatusList = writeClient.insert(writeRecords, writeInstantTime); + writeClient.insert(writeRecords, writeInstantTime); break; case UPSERT: - currentWriteStatusList = writeClient.upsert(writeRecords, writeInstantTime); + writeClient.upsert(writeRecords, writeInstantTime); break; case BULK_INSERT: - currentWriteStatusList = writeClient.bulkInsert(writeRecords, writeInstantTime); + writeClient.bulkInsert(writeRecords, writeInstantTime); break; default: throw new HudiConnectorException( HudiErrorCode.UNSUPPORTED_OPERATION, "Unsupported operation type: " + hudiTableConfig.getOpType()); } - if (!autoCommit) { - this.writeStatusList.addAll(currentWriteStatusList); - } - /** - * when the batch size of temporary records is reached, commit is forced here, even if - * configured not to be auto commit. because a timeline supports only one commit. - */ - forceCommit(isNeedForceCommit, autoCommit); writeRecords.clear(); - batchCount = 0; - } - - public Optional prepareCommit() { - flush(false); - if (!autoCommit) { - return Optional.of( - new HudiCommitInfo(writeInstantTime, writeStatusList, forceCommitTime)); - } - return Optional.empty(); - } - - private void commit() { - if (StringUtils.nonEmpty(writeInstantTime) && !writeStatusList.isEmpty()) { - log.debug( - "Commit hudi records, the instant time is {} and write status are {}", - writeInstantTime, - writeStatusList); - clientProvider.getOrCreateClient().commit(writeInstantTime, writeStatusList); - resetUpsertCommitInfo(); - } - } - - private void forceCommit(boolean isNeedForceCommit, boolean isAutoCommit) { - if (isNeedForceCommit && !isAutoCommit) { - clientProvider.getOrCreateClient().commit(writeInstantTime, writeStatusList); - forceCommitTime.add(writeInstantTime); - resetUpsertCommitInfo(); - } } - public HudiCommitInfo snapshotState() { - HudiCommitInfo hudiCommitInfo = - new HudiCommitInfo(writeInstantTime, writeStatusList, forceCommitTime); - // reset commit info in here, because the commit info will be committed in committer. - resetUpsertCommitInfo(); - // reset the force commit stack. - forceCommitTime = new Stack<>(); - return hudiCommitInfo; - } - - protected void resetUpsertCommitInfo() { - writeInstantTime = null; - writeStatusList = new ArrayList<>(); + private void executeDelete() { + HoodieJavaWriteClient writeClient = clientProvider.getOrCreateClient(); + writeClient.delete(deleteRecordKeys, writeClient.startCommit()); + deleteRecordKeys.clear(); } protected void prepareRecords(SeaTunnelRow element) { HoodieRecord hoodieAvroPayloadHoodieRecord = recordConverter.convertRow(schema, seaTunnelRowType, element, hudiTableConfig); - writeRecords.add(hoodieAvroPayloadHoodieRecord); + HoodieKey recordKey = hoodieAvroPayloadHoodieRecord.getKey(); + boolean changeFlag = changeFlag(element.getRowKind()); + buffer.put(recordKey, Pair.of(changeFlag, hoodieAvroPayloadHoodieRecord)); + } + + private boolean changeFlag(RowKind rowKind) { + switch (rowKind) { + case DELETE: + case UPDATE_BEFORE: + return false; + case INSERT: + case UPDATE_AFTER: + return true; + default: + throw new UnsupportedOperationException("Unknown row kind: " + rowKind); + } } protected void checkFlushException() { @@ -245,10 +226,7 @@ public synchronized void close() { if (!closed) { closed = true; try { - flush(false); - if (!autoCommit) { - commit(); - } + flush(); } catch (Exception e) { LOG.warn("Flush records to Hudi failed.", e); flushException = diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiSinkWriter.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiSinkWriter.java index 317215861a2..130a79adab5 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiSinkWriter.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/sink/writer/HudiSinkWriter.java @@ -35,8 +35,6 @@ import lombok.extern.slf4j.Slf4j; import java.io.IOException; -import java.util.Collections; -import java.util.List; import java.util.Optional; @Slf4j @@ -60,27 +58,15 @@ public HudiSinkWriter( Context context, SeaTunnelRowType seaTunnelRowType, HudiSinkConfig sinkConfig, - HudiTableConfig tableConfig, - List hudiSinkState) { + HudiTableConfig tableConfig) { this.sinkConfig = sinkConfig; this.tableConfig = tableConfig; this.seaTunnelRowType = seaTunnelRowType; this.writeClientProvider = new HudiWriteClientProvider( sinkConfig, tableConfig.getTableName(), seaTunnelRowType); - if (!hudiSinkState.isEmpty()) { - this.hudiRecordWriter = - new HudiRecordWriter( - sinkConfig, - tableConfig, - writeClientProvider, - seaTunnelRowType, - hudiSinkState.get(0).getHudiCommitInfo()); - } else { - this.hudiRecordWriter = - new HudiRecordWriter( - sinkConfig, tableConfig, writeClientProvider, seaTunnelRowType); - } + this.hudiRecordWriter = + new HudiRecordWriter(tableConfig, writeClientProvider, seaTunnelRowType); } @Override @@ -89,16 +75,11 @@ public void write(SeaTunnelRow element) throws IOException { hudiRecordWriter.writeRecord(element); } - @Override - public List snapshotState(long checkpointId) throws IOException { - return Collections.singletonList( - new HudiSinkState(checkpointId, hudiRecordWriter.snapshotState())); - } - @Override public Optional prepareCommit() throws IOException { tryOpen(); - return hudiRecordWriter.prepareCommit(); + hudiRecordWriter.flush(); + return Optional.empty(); } @Override @@ -128,8 +109,7 @@ public void setMultiTableResourceManager( queueIndex, tableConfig.getTableName()); this.hudiRecordWriter = - new HudiRecordWriter( - sinkConfig, tableConfig, writeClientProvider, seaTunnelRowType); + new HudiRecordWriter(tableConfig, writeClientProvider, seaTunnelRowType); } private void tryOpen() { diff --git a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/util/HudiUtil.java b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/util/HudiUtil.java index fe6cbe3e206..ef49c28a213 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/util/HudiUtil.java +++ b/seatunnel-connectors-v2/connector-hudi/src/main/java/org/apache/seatunnel/connectors/seatunnel/hudi/util/HudiUtil.java @@ -31,6 +31,7 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.mapred.JobConf; import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hudi.avro.AvroSchemaUtils; import org.apache.hudi.client.HoodieJavaWriteClient; import org.apache.hudi.client.common.HoodieJavaEngineContext; import org.apache.hudi.common.config.HoodieStorageConfig; @@ -173,7 +174,12 @@ public static HoodieJavaWriteClient createHoodieJavaWriteClie hudiSinkConfig.getTableDfsPath(), hudiTable.getDatabase(), hudiTable.getTableName())) - .withSchema(convertToSchema(seaTunnelRowType).toString()) + .withSchema( + convertToSchema( + seaTunnelRowType, + AvroSchemaUtils.getAvroRecordQualifiedName( + tableName)) + .toString()) .withParallelism( hudiTable.getInsertShuffleParallelism(), hudiTable.getUpsertShuffleParallelism()) @@ -184,7 +190,6 @@ public static HoodieJavaWriteClient createHoodieJavaWriteClie hudiTable.getMinCommitsToKeep(), hudiTable.getMaxCommitsToKeep()) .build()) - .withAutoCommit(hudiSinkConfig.isAutoCommit()) .withCleanConfig( HoodieCleanConfig.newBuilder() .withAutoClean(true) diff --git a/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/HudiTest.java b/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/HudiTest.java index 82e85fcf4e2..7dbfc402b6f 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/HudiTest.java +++ b/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/HudiTest.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.connectors.seatunnel.hudi; +import org.apache.seatunnel.api.table.type.DecimalType; import org.apache.seatunnel.api.table.type.LocalTimeType; import org.apache.seatunnel.api.table.type.MapType; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; @@ -27,6 +28,7 @@ import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericRecord; import org.apache.hadoop.conf.Configuration; +import org.apache.hudi.avro.AvroSchemaUtils; import org.apache.hudi.client.HoodieJavaWriteClient; import org.apache.hudi.client.WriteStatus; import org.apache.hudi.client.common.HoodieJavaEngineContext; @@ -52,6 +54,7 @@ import org.junit.jupiter.api.io.TempDir; import java.io.IOException; +import java.math.BigDecimal; import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalTime; @@ -95,7 +98,8 @@ public class HudiTest { "date", "time", "timestamp3", - "map" + "map", + "decimal" }, new SeaTunnelDataType[] { BOOLEAN_TYPE, @@ -107,16 +111,19 @@ public class HudiTest { LocalTimeType.LOCAL_TIME_TYPE, LocalTimeType.LOCAL_DATE_TIME_TYPE, new MapType(STRING_TYPE, LONG_TYPE), + new DecimalType(10, 5), }); private String getSchema() { - return convertToSchema(seaTunnelRowType).toString(); + return convertToSchema( + seaTunnelRowType, AvroSchemaUtils.getAvroRecordQualifiedName(tableName)) + .toString(); } @Test void testSchema() { Assertions.assertEquals( - "{\"type\":\"record\",\"name\":\"record\",\"namespace\":\"org.apache.seatunnel.avro.generated\",\"fields\":[{\"name\":\"bool\",\"type\":[\"null\",\"boolean\"],\"default\":null},{\"name\":\"int\",\"type\":[\"null\",\"int\"],\"default\":null},{\"name\":\"longValue\",\"type\":[\"null\",\"long\"],\"default\":null},{\"name\":\"float\",\"type\":[\"null\",\"float\"],\"default\":null},{\"name\":\"name\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"date\",\"type\":[\"null\",{\"type\":\"int\",\"logicalType\":\"date\"}],\"default\":null},{\"name\":\"time\",\"type\":[\"null\",{\"type\":\"int\",\"logicalType\":\"time-millis\"}],\"default\":null},{\"name\":\"timestamp3\",\"type\":[\"null\",{\"type\":\"long\",\"logicalType\":\"timestamp-millis\"}],\"default\":null},{\"name\":\"map\",\"type\":[\"null\",{\"type\":\"map\",\"values\":[\"null\",\"long\"]}],\"default\":null}]}", + "{\"type\":\"record\",\"name\":\"hudi_record\",\"namespace\":\"hoodie.hudi\",\"fields\":[{\"name\":\"bool\",\"type\":[\"null\",\"boolean\"],\"default\":null},{\"name\":\"int\",\"type\":[\"null\",\"int\"],\"default\":null},{\"name\":\"longValue\",\"type\":[\"null\",\"long\"],\"default\":null},{\"name\":\"float\",\"type\":[\"null\",\"float\"],\"default\":null},{\"name\":\"name\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"date\",\"type\":[\"null\",{\"type\":\"int\",\"logicalType\":\"date\"}],\"default\":null},{\"name\":\"time\",\"type\":[\"null\",{\"type\":\"int\",\"logicalType\":\"time-millis\"}],\"default\":null},{\"name\":\"timestamp3\",\"type\":[\"null\",{\"type\":\"long\",\"logicalType\":\"timestamp-millis\"}],\"default\":null},{\"name\":\"map\",\"type\":[\"null\",{\"type\":\"map\",\"values\":[\"null\",\"long\"]}],\"default\":null},{\"name\":\"decimal\",\"type\":[\"null\",{\"type\":\"fixed\",\"name\":\"fixed\",\"namespace\":\"hoodie.hudi.hudi_record.decimal\",\"size\":5,\"logicalType\":\"decimal\",\"precision\":10,\"scale\":5}],\"default\":null}]}", getSchema()); } @@ -165,7 +172,8 @@ void testWriteData() throws IOException { expected.setField(7, timestamp3.toLocalDateTime()); Map map = new HashMap<>(); map.put("element", 123L); - expected.setField(9, map); + expected.setField(8, map); + expected.setField(9, BigDecimal.valueOf(10.121)); String instantTime = javaWriteClient.startCommit(); List> hoodieRecords = new ArrayList<>(); hoodieRecords.add(convertRow(expected)); @@ -178,13 +186,23 @@ void testWriteData() throws IOException { private HoodieRecord convertRow(SeaTunnelRow element) { GenericRecord rec = new GenericData.Record( - new Schema.Parser().parse(convertToSchema(seaTunnelRowType).toString())); + new Schema.Parser() + .parse( + convertToSchema( + seaTunnelRowType, + AvroSchemaUtils.getAvroRecordQualifiedName( + tableName)) + .toString())); for (int i = 0; i < seaTunnelRowType.getTotalFields(); i++) { rec.put( seaTunnelRowType.getFieldNames()[i], createConverter(seaTunnelRowType.getFieldType(i)) .convert( - convertToSchema(seaTunnelRowType.getFieldType(i)), + convertToSchema( + seaTunnelRowType.getFieldType(i), + AvroSchemaUtils.getAvroRecordQualifiedName(tableName) + + "." + + seaTunnelRowType.getFieldNames()[i]), element.getField(i))); } diff --git a/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalogTest.java b/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalogTest.java index 7be81e89ba8..d3524c85c41 100644 --- a/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalogTest.java +++ b/seatunnel-connectors-v2/connector-hudi/src/test/java/org/apache/seatunnel/connectors/seatunnel/hudi/catalog/HudiCatalogTest.java @@ -168,6 +168,7 @@ CatalogTable buildAllTypesTable(TableIdentifier tableIdentifier) { TableSchema schema = builder.build(); HashMap options = new HashMap<>(); options.put("record_key_fields", "id,boolean_col"); + options.put("cdc_enabled", "false"); options.put("table_type", "MERGE_ON_READ"); return CatalogTable.of( tableIdentifier, schema, options, Collections.singletonList("dt_col"), "null"); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/pom.xml b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/pom.xml index 583b16a162d..7fe8cc8523e 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/pom.xml +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/pom.xml @@ -30,6 +30,18 @@ 8.5.6 + + + + org.apache.seatunnel + connector-jdbc + ${project.version} + pom + import + + + + @@ -60,5 +72,25 @@ ${project.version} test + + + org.apache.seatunnel + connector-cdc-mysql + ${project.version} + test-jar + test + + + + org.testcontainers + mysql + ${testcontainer.version} + + + + mysql + mysql-connector-java + test + diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiIT.java index 28f2eb3f530..642b94471d0 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiIT.java @@ -108,7 +108,7 @@ private void extractFiles() { disabledReason = "FLINK do not support local file catalog in hudi.") public void testWriteHudi(TestContainer container) throws IOException, InterruptedException, URISyntaxException { - Container.ExecResult textWriteResult = container.executeJob("/fake_to_hudi.conf"); + Container.ExecResult textWriteResult = container.executeJob("/hudi/fake_to_hudi.conf"); Assertions.assertEquals(0, textWriteResult.getExitCode()); Configuration configuration = new Configuration(); configuration.set("fs.defaultFS", LocalFileSystem.DEFAULT_FS); @@ -146,7 +146,7 @@ public void testWriteHudi(TestContainer container) public void testWriteHudiWithOmitConfigItem(TestContainer container) throws IOException, InterruptedException, URISyntaxException { Container.ExecResult textWriteResult = - container.executeJob("/fake_to_hudi_with_omit_config_item.conf"); + container.executeJob("/hudi/fake_to_hudi_with_omit_config_item.conf"); Assertions.assertEquals(0, textWriteResult.getExitCode()); Configuration configuration = new Configuration(); configuration.set("fs.defaultFS", LocalFileSystem.DEFAULT_FS); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiMultiTableIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiMultiTableIT.java index 0a9c4555ad2..c240b85da7a 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiMultiTableIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiMultiTableIT.java @@ -105,7 +105,8 @@ private void extractFiles() { type = {EngineType.FLINK}, disabledReason = "FLINK do not support local file catalog in hudi.") public void testMultiWrite(TestContainer container) throws IOException, InterruptedException { - Container.ExecResult textWriteResult = container.executeJob("/multi_fake_to_hudi.conf"); + Container.ExecResult textWriteResult = + container.executeJob("/hudi/multi_fake_to_hudi.conf"); Assertions.assertEquals(0, textWriteResult.getExitCode()); Configuration configuration = new Configuration(); configuration.set("fs.defaultFS", LocalFileSystem.DEFAULT_FS); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSeatunnelS3MultiTableIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSeatunnelS3MultiTableIT.java index 67f3e9e884e..237fd100d26 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSeatunnelS3MultiTableIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSeatunnelS3MultiTableIT.java @@ -134,8 +134,8 @@ public void tearDown() throws Exception { @Test public void testS3MultiWrite() throws IOException, InterruptedException { - copyFileToContainer("/core-site.xml", "/tmp/seatunnel/config/core-site.xml"); - Container.ExecResult textWriteResult = executeSeaTunnelJob("/s3_fake_to_hudi.conf"); + copyFileToContainer("/hudi/core-site.xml", "/tmp/seatunnel/config/core-site.xml"); + Container.ExecResult textWriteResult = executeSeaTunnelJob("/hudi/s3_fake_to_hudi.conf"); Assertions.assertEquals(0, textWriteResult.getExitCode()); Configuration configuration = new Configuration(); configuration.set("fs.defaultFS", LocalFileSystem.DEFAULT_FS); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSinkCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSinkCDCIT.java new file mode 100644 index 00000000000..1610618fef7 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSinkCDCIT.java @@ -0,0 +1,453 @@ +/* + * 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.seatunnel.e2e.connector.hudi; + +import org.apache.seatunnel.common.utils.FileUtils; +import org.apache.seatunnel.common.utils.JsonUtils; +import org.apache.seatunnel.connectors.seatunnel.cdc.mysql.testutils.MySqlContainer; +import org.apache.seatunnel.connectors.seatunnel.cdc.mysql.testutils.MySqlVersion; +import org.apache.seatunnel.connectors.seatunnel.cdc.mysql.testutils.UniqueDatabase; +import org.apache.seatunnel.e2e.common.TestResource; +import org.apache.seatunnel.e2e.common.TestSuiteBase; +import org.apache.seatunnel.e2e.common.container.ContainerExtendedFactory; +import org.apache.seatunnel.e2e.common.container.EngineType; +import org.apache.seatunnel.e2e.common.container.TestContainer; +import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; +import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileUtil; +import org.apache.hadoop.fs.LocalFileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.hadoop.ParquetReader; +import org.apache.parquet.hadoop.example.GroupReadSupport; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestTemplate; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerLoggerFactory; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static java.lang.Thread.sleep; +import static org.awaitility.Awaitility.given; + +@DisabledOnContainer( + value = {}, + type = {EngineType.FLINK, EngineType.SPARK}, + disabledReason = + "FLINK do not support local file catalog in hudi and Currently SPARK do not support cdc") +@Slf4j +public class HudiSinkCDCIT extends TestSuiteBase implements TestResource { + + // mysql + private static final String MYSQL_HOST = "mysql_cdc_e2e"; + private static final String MYSQL_USER_NAME = "st_user"; + private static final String MYSQL_USER_PASSWORD = "seatunnel"; + private static final String MYSQL_DATABASE = "mysql_cdc"; + private static final MySqlContainer MYSQL_CONTAINER = createMySqlContainer(MySqlVersion.V8_0); + private static final String SOURCE_TABLE = "mysql_cdc_e2e_source_table"; + + private static final String MYSQL_DRIVER = + "https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/8.0.32/mysql-connector-j-8.0.32.jar"; + + private static final String DATABASE = "st"; + private static final String TABLE_NAME = "st_test"; + private static final String TABLE_PATH = "/tmp/hudi/"; + private static final String NAMESPACE = "hudi"; + private static final String NAMESPACE_TAR = "hudi.tar.gz"; + + private final UniqueDatabase inventoryDatabase = + new UniqueDatabase( + MYSQL_CONTAINER, MYSQL_DATABASE, "mysqluser", "mysqlpw", MYSQL_DATABASE); + + private final Map records = new HashMap<>(); + + private static MySqlContainer createMySqlContainer(MySqlVersion version) { + return new MySqlContainer(version) + .withConfigurationOverride("mysql/server-gtids/my.cnf") + .withSetupSQL("mysql/setup.sql") + .withNetwork(NETWORK) + .withNetworkAliases(MYSQL_HOST) + .withDatabaseName(MYSQL_DATABASE) + .withUsername(MYSQL_USER_NAME) + .withPassword(MYSQL_USER_PASSWORD) + .withLogConsumer( + new Slf4jLogConsumer(DockerLoggerFactory.getLogger("mysql-mysql-image"))); + } + + protected final ContainerExtendedFactory containerExtendedFactory = + new ContainerExtendedFactory() { + @Override + public void extend(GenericContainer container) + throws IOException, InterruptedException { + container.execInContainer( + "sh", + "-c", + "cd /tmp" + " && tar -czvf " + NAMESPACE_TAR + " " + NAMESPACE); + container.copyFileFromContainer( + "/tmp/" + NAMESPACE_TAR, "/tmp/" + NAMESPACE_TAR); + + extractFiles(); + } + + private void extractFiles() { + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command( + "sh", "-c", "cd /tmp" + " && tar -zxvf " + NAMESPACE_TAR); + try { + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + if (exitCode == 0) { + log.info("Extract files successful."); + } else { + log.error("Extract files failed with exit code " + exitCode); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + }; + + @TestContainerExtension + protected final ContainerExtendedFactory extendedFactory = + container -> { + container.execInContainer("sh", "-c", "mkdir -p " + TABLE_PATH); + container.execInContainer("sh", "-c", "chmod -R 777 " + TABLE_PATH); + Container.ExecResult extraCommands = + container.execInContainer( + "sh", + "-c", + "mkdir -p /tmp/seatunnel/plugins/MySQL-CDC/lib && cd /tmp/seatunnel/plugins/MySQL-CDC/lib && wget " + + MYSQL_DRIVER); + Assertions.assertEquals(0, extraCommands.getExitCode(), extraCommands.getStderr()); + }; + + @BeforeAll + @Override + public void startUp() throws Exception { + log.info("The second stage: Starting Mysql containers..."); + Startables.deepStart(Stream.of(MYSQL_CONTAINER)).join(); + log.info("Mysql Containers are started"); + inventoryDatabase.createAndInitialize(); + log.info("Mysql ddl execution is complete"); + } + + private void insertRecord(Record record) { + Integer id = record.getId(); + records.put(id, record); + } + + private void deleteRecord(int id) { + records.remove(id); + } + + @AfterAll + @Override + public void tearDown() throws Exception { + // close Container + if (MYSQL_CONTAINER != null) { + MYSQL_CONTAINER.close(); + } + } + + @TestTemplate + public void testMysqlCdc2Hudi(TestContainer container) + throws IOException, InterruptedException { + // Clear related content to ensure that multiple operations are not affected + clearTable(MYSQL_DATABASE, SOURCE_TABLE); + CompletableFuture.supplyAsync( + () -> { + try { + container.executeJob("/hudi/mysql_cdc_to_hudi.conf"); + } catch (Exception e) { + log.error("Commit task exception :" + e.getMessage()); + throw new RuntimeException(e); + } + return null; + }); + // insert data and check + insertAndCheckData(container); + // upsert/delete data and check + upsertAndCheckData(container); + } + + private void insertAndCheckData(TestContainer container) throws InterruptedException { + // Init table data + initSourceTableData(MYSQL_DATABASE, SOURCE_TABLE); + // Waiting 30s for source capture data + sleep(30000); + Configuration configuration = new Configuration(); + configuration.set("fs.defaultFS", LocalFileSystem.DEFAULT_FS); + + given().ignoreExceptions() + .await() + .atMost(60000, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> { + // copy hudi to local + container.executeExtraCommands(containerExtendedFactory); + Path newestCommitFilePath = + getNewestCommitFilePath( + new File( + TABLE_PATH + + File.separator + + DATABASE + + File.separator + + TABLE_NAME)); + ParquetReader reader = + ParquetReader.builder( + new GroupReadSupport(), newestCommitFilePath) + .withConf(configuration) + .build(); + + // Read data and count rows + long rowCount = 0; + Group read = reader.read(); + while (read != null) { + checkData(read); + read = reader.read(); + rowCount++; + } + Assertions.assertEquals(3, rowCount); + }); + FileUtils.deleteFile(TABLE_PATH); + } + + private void upsertAndCheckData(TestContainer container) + throws InterruptedException, IOException { + upsertDeleteSourceTable(MYSQL_DATABASE, SOURCE_TABLE); + // Waiting 30s for source capture data + sleep(30000); + Configuration configuration = new Configuration(); + configuration.set("fs.defaultFS", LocalFileSystem.DEFAULT_FS); + + given().ignoreExceptions() + .await() + .atMost(60000, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> { + // copy hudi to local + container.executeExtraCommands(containerExtendedFactory); + Path newestCommitFilePath = + getNewestCommitFilePath( + new File( + TABLE_PATH + + File.separator + + DATABASE + + File.separator + + TABLE_NAME)); + ParquetReader reader = + ParquetReader.builder( + new GroupReadSupport(), newestCommitFilePath) + .withConf(configuration) + .build(); + // Read data and count rows + long rowCount = 0; + Group read = reader.read(); + while (read != null) { + checkData(read); + read = reader.read(); + rowCount++; + } + Assertions.assertEquals(4, rowCount); + }); + FileUtils.deleteFile(TABLE_PATH); + } + + public static Path getNewestCommitFilePath(File tablePathDir) throws IOException { + File[] files = FileUtil.listFiles(tablePathDir); + Long newestCommitTime = + Arrays.stream(files) + .filter(file -> file.getName().endsWith(".parquet")) + .map( + file -> + Long.parseLong( + file.getName() + .substring( + file.getName().lastIndexOf("_") + 1, + file.getName() + .lastIndexOf(".parquet")))) + .max(Long::compareTo) + .orElseThrow( + () -> + new IllegalArgumentException( + "Not found parquet file in " + tablePathDir)); + for (File file : files) { + if (file.getName().endsWith(newestCommitTime + ".parquet")) { + return new Path(file.toURI()); + } + } + throw new IllegalArgumentException("Not found parquet file in " + tablePathDir); + } + + private void checkData(Group readRecord) { + Integer id = readRecord.getInteger("id", 0); + Record record = records.get(id); + Assertions.assertNotNull(record); + String f_json = readRecord.getString("f_json", 0); + Long f_bigint = readRecord.getLong("f_bigint", 0); + Assertions.assertEquals( + JsonUtils.parseObject(record.getJson()), (JsonUtils.parseObject(f_json))); + Assertions.assertEquals(record.getBigInt(), f_bigint); + } + + private void clearTable(String database, String tableName) { + executeSql("truncate table " + database + "." + tableName); + } + + // Execute SQL + private void executeSql(String sql) { + try (Connection connection = getJdbcConnection()) { + connection.createStatement().execute(sql); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private Connection getJdbcConnection() throws SQLException { + return DriverManager.getConnection( + MYSQL_CONTAINER.getJdbcUrl(), + MYSQL_CONTAINER.getUsername(), + MYSQL_CONTAINER.getPassword()); + } + + private void initSourceTableData(String database, String tableName) { + executeSql( + "INSERT INTO " + + database + + "." + + tableName + + " ( id, f_binary, f_blob, f_long_varbinary, f_longblob, f_tinyblob, f_varbinary, f_smallint,\n" + + " f_smallint_unsigned, f_mediumint, f_mediumint_unsigned, f_int, f_int_unsigned, f_integer,\n" + + " f_integer_unsigned, f_bigint, f_bigint_unsigned, f_numeric, f_decimal, f_float, f_double,\n" + + " f_double_precision, f_longtext, f_mediumtext, f_text, f_tinytext, f_varchar, f_date, f_datetime,\n" + + " f_timestamp, f_bit1, f_bit64, f_char, f_enum, f_mediumblob, f_long_varchar, f_real, f_time,\n" + + " f_tinyint, f_tinyint_unsigned, f_json, f_year )\n" + + "VALUES ( 1, 0x61626374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,\n" + + " 0x68656C6C6F, 0x18000000789C0BC9C82C5600A244859CFCBC7485B2C4A2A4CCBCC4A24A00697308D4, NULL,\n" + + " 0x74696E79626C6F62, 0x48656C6C6F20776F726C64, 12345, 54321, 123456, 654321, 1234567, 7654321, 1234567, 7654321,\n" + + " 123456789, 987654321, 123, 789, 12.34, 56.78, 90.12, 'This is a long text field', 'This is a medium text field',\n" + + " 'This is a text field', 'This is a tiny text field', 'This is a varchar field', '2022-04-27', '2022-04-27 14:30:00',\n" + + " '2023-04-27 11:08:40', 1, b'0101010101010101010101010101010101010101010101010101010101010101', 'C', 'enum2',\n" + + " 0x1B000000789C0BC9C82C5600A24485DCD494CCD25C85A49CFC2485B4CCD49C140083FF099A, 'This is a long varchar field',\n" + + " 12.345, '14:30:00', -128, 255, '{ \"key\": \"value\" }', 2022 ),\n" + + " ( 2, 0x61626374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,\n" + + " 0x68656C6C6F, 0x18000000789C0BC9C82C5600A244859CFCBC7485B2C4A2A4CCBCC4A24A00697308D4, NULL, 0x74696E79626C6F62,\n" + + " 0x48656C6C6F20776F726C64, 12345, 54321, 123456, 654321, 1234567, 7654321, 1234567, 7654321, 123456789, 987654321,\n" + + " 123, 789, 12.34, 56.78, 90.12, 'This is a long text field', 'This is a medium text field', 'This is a text field',\n" + + " 'This is a tiny text field', 'This is a varchar field', '2022-04-27', '2022-04-27 14:30:00', '2023-04-27 11:08:40',\n" + + " 1, b'0101010101010101010101010101010101010101010101010101010101010101', 'C', 'enum2',\n" + + " 0x1B000000789C0BC9C82C5600A24485DCD494CCD25C85A49CFC2485B4CCD49C140083FF099A, 'This is a long varchar field',\n" + + " 112.345, '14:30:00', -128, 22, '{ \"key\": \"value\" }', 2013 ),\n" + + " ( 3, 0x61626374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,\n" + + " 0x68656C6C6F, 0x18000000789C0BC9C82C5600A244859CFCBC7485B2C4A2A4CCBCC4A24A00697308D4, NULL, 0x74696E79626C6F62,\n" + + " 0x48656C6C6F20776F726C64, 12345, 54321, 123456, 654321, 1234567, 7654321, 1234567, 7654321, 123456789, 987654321, 123,\n" + + " 789, 12.34, 56.78, 90.12, 'This is a long text field', 'This is a medium text field', 'This is a text field',\n" + + " 'This is a tiny text field', 'This is a varchar field', '2022-04-27', '2022-04-27 14:30:00', '2023-04-27 11:08:40',\n" + + " 1, b'0101010101010101010101010101010101010101010101010101010101010101', 'C', 'enum2',\n" + + " 0x1B000000789C0BC9C82C5600A24485DCD494CCD25C85A49CFC2485B4CCD49C140083FF099A, 'This is a long varchar field', 112.345,\n" + + " '14:30:00', -128, 22, '{ \"key\": \"value\" }', 2021 )"); + insertRecord(new Record(1, 123456789L, "{ \"key\": \"value\" }")); + insertRecord(new Record(2, 123456789L, "{ \"key\": \"value\" }")); + insertRecord(new Record(3, 123456789L, "{ \"key\": \"value\" }")); + } + + private void upsertDeleteSourceTable(String database, String tableName) { + executeSql( + "INSERT INTO " + + database + + "." + + tableName + + " ( id, f_binary, f_blob, f_long_varbinary, f_longblob, f_tinyblob, f_varbinary, f_smallint,\n" + + " f_smallint_unsigned, f_mediumint, f_mediumint_unsigned, f_int, f_int_unsigned, f_integer,\n" + + " f_integer_unsigned, f_bigint, f_bigint_unsigned, f_numeric, f_decimal, f_float, f_double,\n" + + " f_double_precision, f_longtext, f_mediumtext, f_text, f_tinytext, f_varchar, f_date, f_datetime,\n" + + " f_timestamp, f_bit1, f_bit64, f_char, f_enum, f_mediumblob, f_long_varchar, f_real, f_time,\n" + + " f_tinyint, f_tinyint_unsigned, f_json, f_year )\n" + + "VALUES ( 4, 0x61626374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,\n" + + " 0x68656C6C6F, 0x18000000789C0BC9C82C5600A244859CFCBC7485B2C4A2A4CCBCC4A24A00697308D4, NULL,\n" + + " 0x74696E79626C6F62, 0x48656C6C6F20776F726C64, 12345, 54321, 123456, 654321, 1234567, 7654321, 1234567, 7654321,\n" + + " 1234567890, 987654321, 123, 789, 12.34, 56.78, 90.12, 'This is a long text field', 'This is a medium text field',\n" + + " 'This is a text field', 'This is a tiny text field', 'This is a varchar field', '2022-04-27', '2022-04-27 14:30:00',\n" + + " '2023-04-27 11:08:40', 1, b'0101010101010101010101010101010101010101010101010101010101010101', 'C', 'enum2',\n" + + " 0x1B000000789C0BC9C82C5600A24485DCD494CCD25C85A49CFC2485B4CCD49C140083FF099A, 'This is a long varchar field',\n" + + " 12.345, '14:30:00', -128, 255, '{ \"key\": \"value\" }', 1992 )"); + insertRecord(new Record(4, 1234567890L, "{ \"key\": \"value\" }")); + + executeSql( + "INSERT INTO " + + database + + "." + + tableName + + " ( id, f_binary, f_blob, f_long_varbinary, f_longblob, f_tinyblob, f_varbinary, f_smallint,\n" + + " f_smallint_unsigned, f_mediumint, f_mediumint_unsigned, f_int, f_int_unsigned, f_integer,\n" + + " f_integer_unsigned, f_bigint, f_bigint_unsigned, f_numeric, f_decimal, f_float, f_double,\n" + + " f_double_precision, f_longtext, f_mediumtext, f_text, f_tinytext, f_varchar, f_date, f_datetime,\n" + + " f_timestamp, f_bit1, f_bit64, f_char, f_enum, f_mediumblob, f_long_varchar, f_real, f_time,\n" + + " f_tinyint, f_tinyint_unsigned, f_json, f_year )\n" + + "VALUES ( 5, 0x61626374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,\n" + + " 0x68656C6C6F, 0x18000000789C0BC9C82C5600A244859CFCBC7485B2C4A2A4CCBCC4A24A00697308D4, NULL,\n" + + " 0x74696E79626C6F62, 0x48656C6C6F20776F726C64, 12345, 54321, 123456, 654321, 1234567, 7654321, 1234567, 7654321,\n" + + " 123456789, 987654321, 123, 789, 12.34, 56.78, 90.12, 'This is a long text field', 'This is a medium text field',\n" + + " 'This is a text field', 'This is a tiny text field', 'This is a varchar field', '2022-04-27', '2022-04-27 14:30:00',\n" + + " '2023-04-27 11:08:40', 1, b'0101010101010101010101010101010101010101010101010101010101010101', 'C', 'enum2',\n" + + " 0x1B000000789C0BC9C82C5600A24485DCD494CCD25C85A49CFC2485B4CCD49C140083FF099A, 'This is a long varchar field',\n" + + " 12.345, '14:30:00', -128, 255, '{ \"key\": \"value\" }', 1999 )"); + insertRecord(new Record(5, 123456789L, "{ \"key\": \"value\" }")); + + executeSql("DELETE FROM " + database + "." + tableName + " where id = 2"); + deleteRecord(2); + + executeSql( + "UPDATE " + + database + + "." + + tableName + + " SET f_bigint = 10000, f_json = '{ \"key\": \"value1\" }' where id = 3"); + insertRecord(new Record(3, 10000L, "{ \"key\": \"value1\" }")); + } + + @Data + @AllArgsConstructor + static class Record { + private Integer id; + private Long bigInt; + private String json; + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSparkS3MultiTableIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSparkS3MultiTableIT.java index db43348aefd..f91f340f3c3 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSparkS3MultiTableIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/java/org/apache/seatunnel/e2e/connector/hudi/HudiSparkS3MultiTableIT.java @@ -115,8 +115,8 @@ public void tearDown() throws Exception { disabledReason = "The hadoop version in current flink image is not compatible with the aws version and default container of seatunnel not support s3.") public void testS3MultiWrite(TestContainer container) throws IOException, InterruptedException { - container.copyFileToContainer("/core-site.xml", "/tmp/seatunnel/config/core-site.xml"); - Container.ExecResult textWriteResult = container.executeJob("/s3_fake_to_hudi.conf"); + container.copyFileToContainer("/hudi/core-site.xml", "/tmp/seatunnel/config/core-site.xml"); + Container.ExecResult textWriteResult = container.executeJob("/hudi/s3_fake_to_hudi.conf"); Assertions.assertEquals(0, textWriteResult.getExitCode()); Configuration configuration = new Configuration(); configuration.set("fs.defaultFS", LocalFileSystem.DEFAULT_FS); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/ddl/mysql_cdc.sql b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/ddl/mysql_cdc.sql new file mode 100644 index 00000000000..5f423752634 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/ddl/mysql_cdc.sql @@ -0,0 +1,75 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +-- ---------------------------------------------------------------------------------------------------------------- +-- DATABASE: inventory +-- ---------------------------------------------------------------------------------------------------------------- +CREATE DATABASE IF NOT EXISTS `mysql_cdc`; + +use mysql_cdc; +-- Create a mysql data source table +CREATE TABLE mysql_cdc_e2e_source_table +( + `id` int NOT NULL AUTO_INCREMENT, + `f_binary` binary(64) DEFAULT NULL, + `f_blob` blob, + `f_long_varbinary` mediumblob, + `f_longblob` longblob, + `f_tinyblob` tinyblob, + `f_varbinary` varbinary(100) DEFAULT NULL, + `f_smallint` smallint DEFAULT NULL, + `f_smallint_unsigned` smallint unsigned DEFAULT NULL, + `f_mediumint` mediumint DEFAULT NULL, + `f_mediumint_unsigned` mediumint unsigned DEFAULT NULL, + `f_int` int DEFAULT NULL, + `f_int_unsigned` int unsigned DEFAULT NULL, + `f_integer` int DEFAULT NULL, + `f_integer_unsigned` int unsigned DEFAULT NULL, + `f_bigint` bigint DEFAULT NULL, + `f_bigint_unsigned` bigint unsigned DEFAULT NULL, + `f_numeric` decimal(10, 0) DEFAULT NULL, + `f_decimal` decimal(10, 0) DEFAULT NULL, + `f_float` float DEFAULT NULL, + `f_double` double DEFAULT NULL, + `f_double_precision` double DEFAULT NULL, + `f_longtext` longtext, + `f_mediumtext` mediumtext, + `f_text` text, + `f_tinytext` tinytext, + `f_varchar` varchar(100) DEFAULT NULL, + `f_date` date DEFAULT NULL, + `f_datetime` datetime DEFAULT NULL, + `f_timestamp` timestamp NULL DEFAULT NULL, + `f_bit1` bit(1) DEFAULT NULL, + `f_bit64` bit(64) DEFAULT NULL, + `f_char` char(1) DEFAULT NULL, + `f_enum` enum ('enum1','enum2','enum3') DEFAULT NULL, + `f_mediumblob` mediumblob, + `f_long_varchar` mediumtext, + `f_real` double DEFAULT NULL, + `f_time` time DEFAULT NULL, + `f_tinyint` tinyint DEFAULT NULL, + `f_tinyint_unsigned` tinyint unsigned DEFAULT NULL, + `f_json` json DEFAULT NULL, + `f_year` year DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE = InnoDB + AUTO_INCREMENT = 2 + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; + +truncate table mysql_cdc_e2e_source_table; \ No newline at end of file diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/core-site.xml b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/core-site.xml similarity index 100% rename from seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/core-site.xml rename to seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/core-site.xml diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/fake_to_hudi.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/fake_to_hudi.conf similarity index 100% rename from seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/fake_to_hudi.conf rename to seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/fake_to_hudi.conf diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/fake_to_hudi_with_omit_config_item.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/fake_to_hudi_with_omit_config_item.conf similarity index 100% rename from seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/fake_to_hudi_with_omit_config_item.conf rename to seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/fake_to_hudi_with_omit_config_item.conf diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/multi_fake_to_hudi.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/multi_fake_to_hudi.conf similarity index 100% rename from seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/multi_fake_to_hudi.conf rename to seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/multi_fake_to_hudi.conf diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/mysql_cdc_to_hudi.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/mysql_cdc_to_hudi.conf new file mode 100644 index 00000000000..310ee72ba8c --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/mysql_cdc_to_hudi.conf @@ -0,0 +1,56 @@ +# +# 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 config file is a demonstration of streaming processing in seatunnel config +###### + +env { + parallelism = 1 + job.mode = "STREAMING" + checkpoint.interval = 5000 +} + +source { + MySQL-CDC { + result_table_name="customer_result_table" + catalog { + factory = Mysql + } + database-names=["mysql_cdc"] + table-names = ["mysql_cdc.mysql_cdc_e2e_source_table"] + format=DEFAULT + username = "st_user" + password = "seatunnel" + base-url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc" + } +} + +transform { +} + +sink { + Hudi { + op_type="UPSERT" + table_dfs_path = "/tmp/hudi" + database = "st" + table_name = "st_test" + table_type="COPY_ON_WRITE" + record_key_fields="id" + cdc_enabled = true + } +} + diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/s3_fake_to_hudi.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/s3_fake_to_hudi.conf similarity index 100% rename from seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/s3_fake_to_hudi.conf rename to seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/hudi/s3_fake_to_hudi.conf diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/server-gtids/my.cnf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/server-gtids/my.cnf new file mode 100644 index 00000000000..a390897885d --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/server-gtids/my.cnf @@ -0,0 +1,65 @@ +# +# 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. +# + +# For advice on how to change settings please see +# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html + +[mysqld] +# +# Remove leading # and set to the amount of RAM for the most important data +# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. +# innodb_buffer_pool_size = 128M +# +# Remove leading # to turn on a very important data integrity option: logging +# changes to the binary log between backups. +# log_bin +# +# Remove leading # to set options mainly useful for reporting servers. +# The server defaults are faster for transactions and fast SELECTs. +# Adjust sizes as needed, experiment to find the optimal values. +# join_buffer_size = 128M +# sort_buffer_size = 2M +# read_rnd_buffer_size = 2M +skip-host-cache +skip-name-resolve +#datadir=/var/lib/mysql +#socket=/var/lib/mysql/mysql.sock +secure-file-priv=/var/lib/mysql +user=mysql + +# Disabling symbolic-links is recommended to prevent assorted security risks +symbolic-links=0 + +#log-error=/var/log/mysqld.log +#pid-file=/var/run/mysqld/mysqld.pid + +# ---------------------------------------------- +# Enable the binlog for replication & CDC +# ---------------------------------------------- + +# Enable binary replication log and set the prefix, expiration, and log format. +# The prefix is arbitrary, expiration can be short for integration tests but would +# be longer on a production system. Row-level info is required for ingest to work. +# Server ID is required, but this will vary on production systems +server-id = 223344 +log_bin = mysql-bin +expire_logs_days = 1 +binlog_format = row + +# enable gtid mode +gtid_mode = on +enforce_gtid_consistency = on \ No newline at end of file diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/setup.sql b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/setup.sql new file mode 100644 index 00000000000..aa4534e0ad5 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-hudi-e2e/src/test/resources/mysql/setup.sql @@ -0,0 +1,27 @@ +-- +-- 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. +-- + +-- In production you would almost certainly limit the replication user must be on the follower (slave) machine, +-- to prevent other clients accessing the log from other machines. For example, 'replicator'@'follower.acme.com'. +-- However, in this database we'll grant 2 users different privileges: +-- +-- 1) 'st_user' - all privileges required by the snapshot reader AND binlog reader (used for testing) +-- 2) 'mysqluser' - all privileges +-- +GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT, DROP, LOCK TABLES ON *.* TO 'st_user'@'%'; +CREATE USER 'mysqluser' IDENTIFIED BY 'mysqlpw'; +GRANT ALL PRIVILEGES ON *.* TO 'mysqluser'@'%'; From a3b49e6354d376f5d9591e9bc31a98e05d14c89a Mon Sep 17 00:00:00 2001 From: luckyLJY <79346217+luckyLJY@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:42:32 +0800 Subject: [PATCH 15/72] [Fix][connector-v2]Fix Paimon table connector Error log information. (#7873) Co-authored-by: lucky_ljy --- .../connectors/seatunnel/paimon/sink/PaimonSinkWriter.java | 2 +- .../seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java index 8cc6d0d485f..ac0b1027d03 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java @@ -193,7 +193,7 @@ public Optional prepareCommit(long checkpointId) throws IOExce } catch (Exception e) { throw new PaimonConnectorException( PaimonConnectorErrorCode.TABLE_PRE_COMMIT_FAILED, - "Flink table store failed to prepare commit", + "Paimon pre-commit failed.", e); } } diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java index a3e457907e0..8009135346c 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/commit/PaimonAggregatedCommitter.java @@ -103,7 +103,7 @@ public List commit( } catch (Exception e) { throw new PaimonConnectorException( PaimonConnectorErrorCode.TABLE_WRITE_COMMIT_FAILED, - "Flink table store commit operation failed", + "Paimon table storage write-commit Failed.", e); } return Collections.emptyList(); From ed90a7c5f30b6388ff550fa421186ea53326ff21 Mon Sep 17 00:00:00 2001 From: hailin0 Date: Fri, 18 Oct 2024 16:58:41 +0800 Subject: [PATCH 16/72] [Improve][API] Move AlterTableNameEvent parent (#7869) --- .../apache/seatunnel/api/table/event/AlterTableNameEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/event/AlterTableNameEvent.java b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/event/AlterTableNameEvent.java index 9454f6a5469..04590cae3af 100644 --- a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/event/AlterTableNameEvent.java +++ b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/event/AlterTableNameEvent.java @@ -26,7 +26,7 @@ @Getter @ToString(callSuper = true) -public class AlterTableNameEvent extends AlterTableColumnEvent { +public class AlterTableNameEvent extends AlterTableEvent { private final TableIdentifier newTableIdentifier; public AlterTableNameEvent( From ebd1609fe5f4ff2a07990c0ab2593a375da50bf6 Mon Sep 17 00:00:00 2001 From: Jast Date: Fri, 18 Oct 2024 19:56:52 +0800 Subject: [PATCH 17/72] [Feature][Zeta] Submit job scheduling support pending (#7693) Co-authored-by: Jia Fan --- .../hybrid-cluster-deployment.md | 18 ++ .../separated-cluster-deployment.md | 17 ++ .../hybrid-cluster-deployment.md | 17 ++ .../separated-cluster-deployment.md | 17 ++ .../command/ClientExecuteCommand.java | 10 +- .../engine/client/job/JobStatusRunner.java | 82 +++++++ .../engine/client/SeaTunnelClientTest.java | 7 +- .../SeaTunnelEngineClusterRoleTest.java | 133 +++++++++++ .../engine/common/config/EngineConfig.java | 8 + .../YamlSeaTunnelDomConfigProcessor.java | 10 + .../config/server/ScheduleStrategy.java | 23 ++ .../config/server/ServerConfigOptions.java | 7 + .../seatunnel/engine/core/job/JobStatus.java | 7 + .../engine/server/CoordinatorService.java | 221 ++++++++++++++---- .../server/dag/physical/PhysicalPlan.java | 21 ++ .../server/dag/physical/ResourceUtils.java | 68 ++++-- .../engine/server/dag/physical/SubPlan.java | 3 +- .../server/execution/PendingSourceState.java | 30 +++ .../server/master/JobHistoryService.java | 20 ++ .../engine/server/master/JobMaster.java | 99 ++++++++ .../server/utils/PeekBlockingQueue.java | 96 ++++++++ .../seatunnel/engine/server/TestUtils.java | 5 +- .../server/checkpoint/SavePointTest.java | 2 +- .../seatunnel/engine/server/dag/TaskTest.java | 9 +- .../server/utils/PeekBlockingQueueTest.java | 115 +++++++++ 25 files changed, 974 insertions(+), 71 deletions(-) create mode 100644 seatunnel-engine/seatunnel-engine-client/src/main/java/org/apache/seatunnel/engine/client/job/JobStatusRunner.java create mode 100644 seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ScheduleStrategy.java create mode 100644 seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/execution/PendingSourceState.java create mode 100644 seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueue.java create mode 100644 seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueueTest.java diff --git a/docs/en/seatunnel-engine/hybrid-cluster-deployment.md b/docs/en/seatunnel-engine/hybrid-cluster-deployment.md index 88b7b6a44a2..85a169d221e 100644 --- a/docs/en/seatunnel-engine/hybrid-cluster-deployment.md +++ b/docs/en/seatunnel-engine/hybrid-cluster-deployment.md @@ -136,6 +136,24 @@ seatunnel: classloader-cache-mode: true ``` +### 4.6 Job Scheduling Strategy + +When resources are insufficient, the job scheduling strategy can be configured in the following two modes: + +1. `WAIT`: Wait for resources to be available. + +2. `REJECT`: Reject the job, default value. + +Example + +```yaml +seatunnel: + engine: + job-schedule-strategy: WAIT +``` + +When `dynamic-slot: true` is used, the `job-schedule-strategy: WAIT` configuration will become invalid and will be forcibly changed to `job-schedule-strategy: REJECT`, because this parameter is meaningless in dynamic slots. + ## 5. Configure The SeaTunnel Engine Network Service All SeaTunnel Engine network-related configurations are in the `hazelcast.yaml` file. diff --git a/docs/en/seatunnel-engine/separated-cluster-deployment.md b/docs/en/seatunnel-engine/separated-cluster-deployment.md index fd379d8dbce..68f3d011679 100644 --- a/docs/en/seatunnel-engine/separated-cluster-deployment.md +++ b/docs/en/seatunnel-engine/separated-cluster-deployment.md @@ -280,6 +280,23 @@ netty-common-4.1.89.Final.jar seatunnel-hadoop3-3.1.4-uber.jar ``` +### 4.7 Job Scheduling Strategy + +When resources are insufficient, the job scheduling strategy can be configured in the following two modes: + +1. `WAIT`: Wait for resources to be available. + +2. `REJECT`: Reject the job, default value. + +Example + +```yaml +seatunnel: + engine: + job-schedule-strategy: WAIT +``` +When `dynamic-slot: true` is used, the `job-schedule-strategy: WAIT` configuration will become invalid and will be forcibly changed to `job-schedule-strategy: REJECT`, because this parameter is meaningless in dynamic slots. + ## 5. Configuring SeaTunnel Engine Network Services All network-related configurations of the SeaTunnel Engine are in the `hazelcast-master.yaml` and `hazelcast-worker.yaml` files. diff --git a/docs/zh/seatunnel-engine/hybrid-cluster-deployment.md b/docs/zh/seatunnel-engine/hybrid-cluster-deployment.md index ad783b82328..709259d72d0 100644 --- a/docs/zh/seatunnel-engine/hybrid-cluster-deployment.md +++ b/docs/zh/seatunnel-engine/hybrid-cluster-deployment.md @@ -136,6 +136,23 @@ seatunnel: classloader-cache-mode: true ``` +### 4.6 作业调度策略 + +当资源不足时,作业调度策略可以配置为以下两种模式: + +1. `WAIT`:等待资源可用。 +2. `REJECT`:拒绝作业,默认值。 + +示例 + +```yaml +seatunnel: + engine: + job-schedule-strategy: WAIT +``` + +当`dynamic-slot: ture`时,`job-schedule-strategy: WAIT` 配置会失效,将被强制修改为`job-schedule-strategy: REJECT`,因为动态Slot时该参数没有意义,可以直接提交。 + ## 5. 配置 SeaTunnel Engine 网络服务 所有 SeaTunnel Engine 网络相关的配置都在 `hazelcast.yaml` 文件中. diff --git a/docs/zh/seatunnel-engine/separated-cluster-deployment.md b/docs/zh/seatunnel-engine/separated-cluster-deployment.md index d28ec3601c3..dbe4865272e 100644 --- a/docs/zh/seatunnel-engine/separated-cluster-deployment.md +++ b/docs/zh/seatunnel-engine/separated-cluster-deployment.md @@ -284,6 +284,23 @@ netty-common-4.1.89.Final.jar seatunnel-hadoop3-3.1.4-uber.jar ``` +### 4.7 作业调度策略 + +当资源不足时,作业调度策略可以配置为以下两种模式: + +1. `WAIT`:等待资源可用。 +2. `REJECT`:拒绝作业,默认值。 + +示例 + +```yaml +seatunnel: + engine: + job-schedule-strategy: WAIT +``` + +当`dynamic-slot: ture`时,`job-schedule-strategy: WAIT` 配置会失效,将被强制修改为`job-schedule-strategy: REJECT`,因为动态Slot时该参数没有意义,可以直接提交。 + ## 5. 配置 SeaTunnel Engine 网络服务 所有 SeaTunnel Engine 网络相关的配置都在 `hazelcast-master.yaml`和`hazelcast-worker.yaml` 文件中. diff --git a/seatunnel-core/seatunnel-starter/src/main/java/org/apache/seatunnel/core/starter/seatunnel/command/ClientExecuteCommand.java b/seatunnel-core/seatunnel-starter/src/main/java/org/apache/seatunnel/core/starter/seatunnel/command/ClientExecuteCommand.java index 251dc6a1a7e..b67288fb13a 100644 --- a/seatunnel-core/seatunnel-starter/src/main/java/org/apache/seatunnel/core/starter/seatunnel/command/ClientExecuteCommand.java +++ b/seatunnel-core/seatunnel-starter/src/main/java/org/apache/seatunnel/core/starter/seatunnel/command/ClientExecuteCommand.java @@ -28,6 +28,7 @@ import org.apache.seatunnel.engine.client.job.ClientJobExecutionEnvironment; import org.apache.seatunnel.engine.client.job.ClientJobProxy; import org.apache.seatunnel.engine.client.job.JobMetricsRunner; +import org.apache.seatunnel.engine.client.job.JobStatusRunner; import org.apache.seatunnel.engine.common.Constant; import org.apache.seatunnel.engine.common.config.ConfigProvider; import org.apache.seatunnel.engine.common.config.EngineConfig; @@ -187,7 +188,8 @@ public void execute() throws CommandExecuteException { long jobId = clientJobProxy.getJobId(); JobMetricsRunner jobMetricsRunner = new JobMetricsRunner(engineClient, jobId); executorService = - Executors.newSingleThreadScheduledExecutor( + Executors.newScheduledThreadPool( + 2, new ThreadFactoryBuilder() .setNameFormat("job-metrics-runner-%d") .setDaemon(true) @@ -197,6 +199,12 @@ public void execute() throws CommandExecuteException { 0, seaTunnelConfig.getEngineConfig().getPrintJobMetricsInfoInterval(), TimeUnit.SECONDS); + + executorService.schedule( + new JobStatusRunner(engineClient.getJobClient(), jobId), + 0, + TimeUnit.SECONDS); + // wait for job complete JobResult jobResult = clientJobProxy.waitForJobCompleteV2(); jobStatus = jobResult.getStatus(); diff --git a/seatunnel-engine/seatunnel-engine-client/src/main/java/org/apache/seatunnel/engine/client/job/JobStatusRunner.java b/seatunnel-engine/seatunnel-engine-client/src/main/java/org/apache/seatunnel/engine/client/job/JobStatusRunner.java new file mode 100644 index 00000000000..6c3ba6fcf45 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-client/src/main/java/org/apache/seatunnel/engine/client/job/JobStatusRunner.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.seatunnel.engine.client.job; + +import org.apache.seatunnel.common.utils.ExceptionUtils; +import org.apache.seatunnel.engine.core.job.JobStatus; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class JobStatusRunner implements Runnable { + + private final JobClient jobClient; + private final Long jobId; + private boolean isEnterPending = false; + + public JobStatusRunner(JobClient jobClient, Long jobId) { + this.jobClient = jobClient; + this.jobId = jobId; + } + + @Override + public void run() { + Thread.currentThread().setName("job-status-runner-" + jobId); + try { + while (isPrint(jobClient.getJobStatus(jobId))) { + Thread.sleep(5000); + } + } catch (Exception e) { + log.error("Failed to get job runner status. {}", ExceptionUtils.getMessage(e)); + } + } + + private boolean isPrint(String jobStatus) { + boolean isPrint = true; + switch (JobStatus.fromString(jobStatus)) { + case PENDING: + isEnterPending = true; + log.info( + "Job Id : {} enter pending queue, current status:{} ,please wait task schedule", + jobId, + jobStatus); + break; + case RUNNING: + case SCHEDULED: + case FAILING: + case FAILED: + case DOING_SAVEPOINT: + case SAVEPOINT_DONE: + case CANCELING: + case CANCELED: + case FINISHED: + case UNKNOWABLE: + if (isEnterPending) { + // Log only if it transitioned from the PENDING state + log.info( + "Job ID: {} has been scheduled and entered the next state. Current status: {}", + jobId, + jobStatus); + } + isPrint = false; + default: + break; + } + return isPrint; + } +} diff --git a/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelClientTest.java b/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelClientTest.java index b125e2dadbb..194da829877 100644 --- a/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelClientTest.java +++ b/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelClientTest.java @@ -435,8 +435,11 @@ public void testGetJobInfo() { CompletableFuture.supplyAsync(clientJobProxy::waitForJobComplete); long jobId = clientJobProxy.getJobId(); - // Running - Assertions.assertNotNull(jobClient.getJobInfo(jobId)); + await().atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + Assertions.assertNotNull(jobClient.getJobInfo(jobId)); + }); await().atMost(180000, TimeUnit.MILLISECONDS) .untilAsserted( diff --git a/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java b/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java index 8d03b8db10d..89134ce4671 100644 --- a/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java +++ b/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java @@ -22,10 +22,13 @@ import org.apache.seatunnel.engine.client.job.ClientJobExecutionEnvironment; import org.apache.seatunnel.engine.client.job.ClientJobProxy; import org.apache.seatunnel.engine.common.config.ConfigProvider; +import org.apache.seatunnel.engine.common.config.EngineConfig; import org.apache.seatunnel.engine.common.config.JobConfig; import org.apache.seatunnel.engine.common.config.SeaTunnelConfig; +import org.apache.seatunnel.engine.common.config.server.ScheduleStrategy; import org.apache.seatunnel.engine.common.utils.PassiveCompletableFuture; import org.apache.seatunnel.engine.core.job.JobResult; +import org.apache.seatunnel.engine.core.job.JobStatus; import org.apache.seatunnel.engine.server.SeaTunnelServerStarter; import org.awaitility.Awaitility; @@ -173,6 +176,136 @@ public void canNotSubmitJobWhenHaveNoWorkerNode() { } } + @SneakyThrows + @Test + public void enterPendingWhenResourcesNotEnough() { + HazelcastInstanceImpl masterNode = null; + String testClusterName = "Test_enterPendingWhenResourcesNotEnough"; + SeaTunnelClient seaTunnelClient = null; + + SeaTunnelConfig seaTunnelConfig = ConfigProvider.locateAndGetSeaTunnelConfig(); + // set job pending + EngineConfig engineConfig = seaTunnelConfig.getEngineConfig(); + engineConfig.setScheduleStrategy(ScheduleStrategy.WAIT); + engineConfig.getSlotServiceConfig().setDynamicSlot(false); + engineConfig.getSlotServiceConfig().setSlotNum(3); + seaTunnelConfig + .getHazelcastConfig() + .setClusterName(TestUtils.getClusterName(testClusterName)); + + // submit job + Common.setDeployMode(DeployMode.CLIENT); + String filePath = TestUtils.getResource("/client_test.conf"); + JobConfig jobConfig = new JobConfig(); + jobConfig.setName("Test_enterPendingWhenResourcesNotEnough"); + + try { + // master node must start first in ci + masterNode = SeaTunnelServerStarter.createMasterHazelcastInstance(seaTunnelConfig); + + HazelcastInstanceImpl finalMasterNode = masterNode; + Awaitility.await() + .atMost(10000, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> + Assertions.assertEquals( + 1, finalMasterNode.getCluster().getMembers().size())); + + // new seatunnel client and submit job + seaTunnelClient = createSeaTunnelClient(testClusterName); + ClientJobExecutionEnvironment jobExecutionEnv = + seaTunnelClient.createExecutionContext(filePath, jobConfig, seaTunnelConfig); + final ClientJobProxy clientJobProxy = jobExecutionEnv.execute(); + Awaitility.await() + .atMost(10000, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> + Assertions.assertEquals( + clientJobProxy.getJobStatus(), JobStatus.PENDING)); + // start two worker nodes + SeaTunnelServerStarter.createWorkerHazelcastInstance(seaTunnelConfig); + SeaTunnelServerStarter.createWorkerHazelcastInstance(seaTunnelConfig); + + // There are already resources available, wait for job enter running or complete + Awaitility.await() + .atMost(60000, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> + Assertions.assertEquals( + JobStatus.FINISHED, clientJobProxy.getJobStatus())); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (seaTunnelClient != null) { + seaTunnelClient.close(); + } + if (masterNode != null) { + masterNode.shutdown(); + } + } + } + + @SneakyThrows + @Test + public void pendingJobCancel() { + HazelcastInstanceImpl masterNode = null; + String clusterAndJobName = "Test_pendingJobCancel"; + SeaTunnelClient seaTunnelClient = null; + + SeaTunnelConfig seaTunnelConfig = ConfigProvider.locateAndGetSeaTunnelConfig(); + // set job pending + EngineConfig engineConfig = seaTunnelConfig.getEngineConfig(); + engineConfig.setScheduleStrategy(ScheduleStrategy.WAIT); + engineConfig.getSlotServiceConfig().setDynamicSlot(false); + engineConfig.getSlotServiceConfig().setSlotNum(1); + + seaTunnelConfig + .getHazelcastConfig() + .setClusterName(TestUtils.getClusterName(clusterAndJobName)); + + // submit job + Common.setDeployMode(DeployMode.CLIENT); + String filePath = TestUtils.getResource("/client_test.conf"); + JobConfig jobConfig = new JobConfig(); + jobConfig.setName(clusterAndJobName); + + try { + // master node must start first in ci + masterNode = SeaTunnelServerStarter.createMasterHazelcastInstance(seaTunnelConfig); + + // new seatunnel client and submit job + seaTunnelClient = createSeaTunnelClient(clusterAndJobName); + ClientJobExecutionEnvironment jobExecutionEnv = + seaTunnelClient.createExecutionContext(filePath, jobConfig, seaTunnelConfig); + final ClientJobProxy clientJobProxy = jobExecutionEnv.execute(); + Awaitility.await() + .atMost(10000, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> + Assertions.assertEquals( + clientJobProxy.getJobStatus(), JobStatus.PENDING)); + + // Cancel the job in the pending state + seaTunnelClient.getJobClient().cancelJob(clientJobProxy.getJobId()); + Awaitility.await() + .atMost(60000, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> + Assertions.assertNotEquals( + clientJobProxy.getJobStatus(), JobStatus.CANCELED)); + + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (seaTunnelClient != null) { + seaTunnelClient.close(); + } + if (masterNode != null) { + masterNode.shutdown(); + } + } + } + private SeaTunnelClient createSeaTunnelClient(String clusterName) { ClientConfig clientConfig = ConfigProvider.locateAndGetClientConfig(); clientConfig.setClusterName(TestUtils.getClusterName(clusterName)); diff --git a/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/EngineConfig.java b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/EngineConfig.java index 0d2d1bd671d..99a721109bd 100644 --- a/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/EngineConfig.java +++ b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/EngineConfig.java @@ -21,6 +21,7 @@ import org.apache.seatunnel.engine.common.config.server.ConnectorJarStorageConfig; import org.apache.seatunnel.engine.common.config.server.HttpConfig; import org.apache.seatunnel.engine.common.config.server.QueueType; +import org.apache.seatunnel.engine.common.config.server.ScheduleStrategy; import org.apache.seatunnel.engine.common.config.server.ServerConfigOptions; import org.apache.seatunnel.engine.common.config.server.SlotServiceConfig; import org.apache.seatunnel.engine.common.config.server.TelemetryConfig; @@ -75,6 +76,9 @@ public class EngineConfig { private TelemetryConfig telemetryConfig = ServerConfigOptions.TELEMETRY.defaultValue(); + private ScheduleStrategy scheduleStrategy = + ServerConfigOptions.JOB_SCHEDULE_STRATEGY.defaultValue(); + private HttpConfig httpConfig = ServerConfigOptions.HTTP.defaultValue(); public void setBackupCount(int newBackupCount) { @@ -82,6 +86,10 @@ public void setBackupCount(int newBackupCount) { this.backupCount = newBackupCount; } + public void setScheduleStrategy(ScheduleStrategy scheduleStrategy) { + this.scheduleStrategy = scheduleStrategy; + } + public void setPrintExecutionInfoInterval(int printExecutionInfoInterval) { checkPositive( printExecutionInfoInterval, diff --git a/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/YamlSeaTunnelDomConfigProcessor.java b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/YamlSeaTunnelDomConfigProcessor.java index 8f9339760fe..eefde7f8b83 100644 --- a/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/YamlSeaTunnelDomConfigProcessor.java +++ b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/YamlSeaTunnelDomConfigProcessor.java @@ -24,6 +24,7 @@ import org.apache.seatunnel.engine.common.config.server.ConnectorJarStorageMode; import org.apache.seatunnel.engine.common.config.server.HttpConfig; import org.apache.seatunnel.engine.common.config.server.QueueType; +import org.apache.seatunnel.engine.common.config.server.ScheduleStrategy; import org.apache.seatunnel.engine.common.config.server.ServerConfigOptions; import org.apache.seatunnel.engine.common.config.server.SlotServiceConfig; import org.apache.seatunnel.engine.common.config.server.TelemetryConfig; @@ -170,12 +171,21 @@ private void parseEngineConfig(Node engineNode, SeaTunnelConfig config) { } } else if (ServerConfigOptions.TELEMETRY.key().equals(name)) { engineConfig.setTelemetryConfig(parseTelemetryConfig(node)); + } else if (ServerConfigOptions.JOB_SCHEDULE_STRATEGY.key().equals(name)) { + engineConfig.setScheduleStrategy( + ScheduleStrategy.valueOf(getTextContent(node).toUpperCase(Locale.ROOT))); } else if (ServerConfigOptions.HTTP.key().equals(name)) { engineConfig.setHttpConfig(parseHttpConfig(node)); } else { LOGGER.warning("Unrecognized element: " + name); } } + + if (engineConfig.getSlotServiceConfig().isDynamicSlot()) { + // If dynamic slot is enabled, the schedule strategy must be REJECT + LOGGER.info("Dynamic slot is enabled, the schedule strategy is set to REJECT"); + engineConfig.setScheduleStrategy(ScheduleStrategy.REJECT); + } } private CheckpointConfig parseCheckpointConfig(Node checkpointNode) { diff --git a/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ScheduleStrategy.java b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ScheduleStrategy.java new file mode 100644 index 00000000000..2a938e792b6 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ScheduleStrategy.java @@ -0,0 +1,23 @@ +/* + * 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.seatunnel.engine.common.config.server; + +public enum ScheduleStrategy { + WAIT, + REJECT +} diff --git a/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ServerConfigOptions.java b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ServerConfigOptions.java index 6723f17e25d..e74ff59978f 100644 --- a/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ServerConfigOptions.java +++ b/seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/server/ServerConfigOptions.java @@ -138,6 +138,13 @@ public class ServerConfigOptions { .defaultValue(1440) .withDescription("The expire time of history jobs.time unit minute"); + public static final Option JOB_SCHEDULE_STRATEGY = + Options.key("job-schedule-strategy") + .enumType(ScheduleStrategy.class) + .defaultValue(ScheduleStrategy.REJECT) + .withDescription( + "When the policy is REJECT, when the task queue is full, the task will be rejected; when the policy is WAIT, when the task queue is full, the task will wait"); + public static final Option ENABLE_CONNECTOR_JAR_STORAGE = Options.key("enable") .booleanType() diff --git a/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobStatus.java b/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobStatus.java index ed3cd50bb37..53d1eb6aeee 100644 --- a/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobStatus.java +++ b/seatunnel-engine/seatunnel-engine-core/src/main/java/org/apache/seatunnel/engine/core/job/JobStatus.java @@ -29,6 +29,9 @@ public enum JobStatus { /** Job is newly created, no task has started to run. */ CREATED(EndState.NOT_END), + /** The job is waiting for resources. */ + PENDING(EndState.NOT_END), + /** * Job will scheduler every pipeline, each PhysicalVertex in the pipeline will be scheduler and * deploying @@ -79,4 +82,8 @@ private enum EndState { public boolean isEndState() { return endState != EndState.NOT_END; } + + public static JobStatus fromString(String status) { + return JobStatus.valueOf(status.toUpperCase()); + } } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/CoordinatorService.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/CoordinatorService.java index 09a2a89b7ae..02fdc98b1ee 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/CoordinatorService.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/CoordinatorService.java @@ -29,6 +29,7 @@ import org.apache.seatunnel.engine.common.Constant; import org.apache.seatunnel.engine.common.config.EngineConfig; import org.apache.seatunnel.engine.common.config.server.ConnectorJarStorageConfig; +import org.apache.seatunnel.engine.common.config.server.ScheduleStrategy; import org.apache.seatunnel.engine.common.exception.JobException; import org.apache.seatunnel.engine.common.exception.JobNotFoundException; import org.apache.seatunnel.engine.common.exception.SavePointFailedException; @@ -45,6 +46,7 @@ import org.apache.seatunnel.engine.server.event.JobEventHttpReportHandler; import org.apache.seatunnel.engine.server.event.JobEventProcessor; import org.apache.seatunnel.engine.server.execution.ExecutionState; +import org.apache.seatunnel.engine.server.execution.PendingSourceState; import org.apache.seatunnel.engine.server.execution.TaskExecutionState; import org.apache.seatunnel.engine.server.execution.TaskGroupLocation; import org.apache.seatunnel.engine.server.execution.TaskLocation; @@ -52,6 +54,7 @@ import org.apache.seatunnel.engine.server.master.JobMaster; import org.apache.seatunnel.engine.server.metrics.JobMetricsUtil; import org.apache.seatunnel.engine.server.metrics.SeaTunnelMetricsContext; +import org.apache.seatunnel.engine.server.resourcemanager.NoEnoughResourceException; import org.apache.seatunnel.engine.server.resourcemanager.ResourceManager; import org.apache.seatunnel.engine.server.resourcemanager.ResourceManagerFactory; import org.apache.seatunnel.engine.server.resourcemanager.resource.SlotProfile; @@ -60,6 +63,7 @@ import org.apache.seatunnel.engine.server.telemetry.metrics.entity.JobCounter; import org.apache.seatunnel.engine.server.telemetry.metrics.entity.ThreadPoolStatus; import org.apache.seatunnel.engine.server.utils.NodeEngineUtil; +import org.apache.seatunnel.engine.server.utils.PeekBlockingQueue; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.hazelcast.cluster.Address; @@ -72,6 +76,7 @@ import com.hazelcast.ringbuffer.Ringbuffer; import com.hazelcast.spi.impl.NodeEngineImpl; import lombok.NonNull; +import scala.Tuple2; import java.util.ArrayList; import java.util.Collections; @@ -79,6 +84,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -144,6 +151,13 @@ public class CoordinatorService { */ private final Map runningJobMasterMap = new ConcurrentHashMap<>(); + /** + * key: job id;
+ * value: job master; + */ + private final Map> pendingJobMasterMap = + new ConcurrentHashMap<>(); + /** * IMap key is {@link PipelineLocation} * @@ -173,6 +187,12 @@ public class CoordinatorService { private PassiveCompletableFuture restoreAllJobFromMasterNodeSwitchFuture; + private PeekBlockingQueue pendingJob = new PeekBlockingQueue<>(); + + private final boolean isWaitStrategy; + + private final ScheduleStrategy scheduleStrategy; + public CoordinatorService( @NonNull NodeEngineImpl nodeEngine, @NonNull SeaTunnelServer seaTunnelServer, @@ -195,6 +215,130 @@ public CoordinatorService( masterActiveListener = Executors.newSingleThreadScheduledExecutor(); masterActiveListener.scheduleAtFixedRate( this::checkNewActiveMaster, 0, 100, TimeUnit.MILLISECONDS); + scheduleStrategy = engineConfig.getScheduleStrategy(); + isWaitStrategy = scheduleStrategy.equals(ScheduleStrategy.WAIT); + logger.info("Start pending job schedule thread"); + // start pending job schedule thread + startPendingJobScheduleThread(); + } + + private void startPendingJobScheduleThread() { + Runnable pendingJobScheduleTask = + () -> { + Thread.currentThread().setName("pending-job-schedule-runner"); + while (true) { + try { + pendingJobSchedule(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + pendingJob.release(); + } + } + }; + executorService.submit(pendingJobScheduleTask); + } + + private void pendingJobSchedule() throws InterruptedException { + JobMaster jobMaster = pendingJob.peekBlocking(); + if (Objects.isNull(jobMaster)) { + // This situation almost never happens because pendingJobSchedule is single-threaded + logger.warning("The peek job master is null"); + Thread.sleep(3000); + return; + } + logger.fine( + String.format( + "Start pending job schedule, pendingJob Size : %s", pendingJob.size())); + + Long jobId = jobMaster.getJobId(); + + logger.fine( + String.format( + "Start calculating whether pending task resources are enough: %s", jobId)); + + boolean preApplyResources = jobMaster.preApplyResources(); + if (!preApplyResources) { + logger.info( + String.format( + "Current strategy is %s, and resources is not enough, skipping this schedule, JobID: %s", + scheduleStrategy, jobId)); + if (isWaitStrategy) { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + logger.severe(ExceptionUtils.getMessage(e)); + } + return; + } else { + queueRemove(jobMaster); + completeFailJob(jobMaster); + return; + } + } + + logger.info(String.format("Resources enough, start running: %s", jobId)); + + queueRemove(jobMaster); + + PendingSourceState pendingSourceState = pendingJobMasterMap.get(jobId)._1; + + MDCExecutorService mdcExecutorService = MDCTracer.tracing(jobId, executorService); + mdcExecutorService.submit( + () -> { + try { + String jobFullName = jobMaster.getPhysicalPlan().getJobFullName(); + JobStatus jobStatus = (JobStatus) runningJobStateIMap.get(jobId); + if (pendingSourceState == PendingSourceState.RESTORE) { + jobMaster + .getPhysicalPlan() + .getPipelineList() + .forEach(SubPlan::restorePipelineState); + } + logger.info( + String.format( + "The %s %s is in %s state, restore pipeline and take over this job running", + pendingSourceState, jobFullName, jobStatus)); + + pendingJobMasterMap.remove(jobId); + runningJobMasterMap.put(jobId, jobMaster); + jobMaster.run(); + } finally { + if (jobMasterCompletedSuccessfully(jobMaster, pendingSourceState)) { + runningJobMasterMap.remove(jobId); + } + } + }); + } + + private void queueRemove(JobMaster jobMaster) throws InterruptedException { + JobMaster take = pendingJob.take(); + if (take != jobMaster) { + logger.severe("The job master is not equal to the peek job master"); + } + } + + private void completeFailJob(JobMaster jobMaster) { + // If the pending queue is not enabled and resources are insufficient, stop the task from + // running + JobResult jobResult = + new JobResult( + JobStatus.FAILED, + ExceptionUtils.getMessage(new NoEnoughResourceException())); + jobMaster.getPhysicalPlan().updateJobState(JobStatus.FAILED); + jobMaster.getPhysicalPlan().completeJobEndFuture(jobResult); + + logger.info( + String.format( + "The job %s is not running because the resources is not enough insufficient", + jobMaster.getJobId())); + } + + private boolean jobMasterCompletedSuccessfully(JobMaster jobMaster, PendingSourceState state) { + return (!jobMaster.getJobMasterCompleteFuture().isCompletedExceptionally() + && state == PendingSourceState.RESTORE) + || (!jobMaster.getJobMasterCompleteFuture().isCancelled() + && state == PendingSourceState.SUBMIT); } private JobEventProcessor createJobEventProcessor( @@ -232,7 +376,9 @@ public JobHistoryService getJobHistoryService() { } public JobMaster getJobMaster(Long jobId) { - return runningJobMasterMap.get(jobId); + return Optional.ofNullable(pendingJobMasterMap.get(jobId)) + .map(t -> t._2) + .orElse(runningJobMasterMap.get(jobId)); } public EventProcessor getEventProcessor() { @@ -254,6 +400,7 @@ private void initCoordinatorService() { new JobHistoryService( runningJobStateIMap, logger, + pendingJobMasterMap, runningJobMasterMap, nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_FINISHED_JOB_STATE), nodeEngine @@ -346,9 +493,9 @@ private void restoreJobFromMasterActiveSwitch(@NonNull Long jobId, @NonNull JobI return; } - JobStatus jobStatus = (JobStatus) runningJobStateIMap.get(jobId); JobMaster jobMaster = new JobMaster( + jobId, jobInfo.getJobImmutableInformation(), nodeEngine, executorService, @@ -368,31 +515,10 @@ private void restoreJobFromMasterActiveSwitch(@NonNull Long jobId, @NonNull JobI throw new SeaTunnelEngineException(String.format("Job id %s init failed", jobId), e); } - String jobFullName = jobMaster.getPhysicalPlan().getJobFullName(); - runningJobMasterMap.put(jobId, jobMaster); - - logger.info( - String.format( - "The restore %s is in %s state, restore pipeline and take over this job running", - jobFullName, jobStatus)); - CompletableFuture.runAsync( - () -> { - try { - jobMaster - .getPhysicalPlan() - .getPipelineList() - .forEach(SubPlan::restorePipelineState); - jobMaster.run(); - } finally { - // voidCompletableFuture will be cancelled when zeta master node - // shutdown to simulate master failure, - // don't update runningJobMasterMap is this case. - if (!jobMaster.getJobMasterCompleteFuture().isCompletedExceptionally()) { - runningJobMasterMap.remove(jobId); - } - } - }, - executorService); + pendingJobMasterMap.put(jobId, new Tuple2<>(PendingSourceState.RESTORE, jobMaster)); + pendingJob.put(jobMaster); + jobMaster.getPhysicalPlan().updateJobState(JobStatus.PENDING); + logger.info(String.format("The restore job enter pending queue, JobId: %s", jobId)); } private void checkNewActiveMaster() { @@ -425,6 +551,13 @@ private void checkNewActiveMaster() { public synchronized void clearCoordinatorService() { // interrupt all JobMaster runningJobMasterMap.values().forEach(JobMaster::interrupt); + if (isWaitStrategy) { + pendingJobMasterMap.values().stream() + .filter(Objects::nonNull) + .map(Tuple2::_2) + .forEach(JobMaster::interrupt); + pendingJobMasterMap.clear(); + } executorService.shutdownNow(); runningJobMasterMap.clear(); @@ -482,6 +615,7 @@ public PassiveCompletableFuture submitJob( MDCExecutorService mdcExecutorService = MDCTracer.tracing(jobId, executorService); JobMaster jobMaster = new JobMaster( + jobId, jobImmutableInformation, this.nodeEngine, mdcExecutorService, @@ -504,10 +638,11 @@ && getJobHistoryService().getJobMetrics(jobId) != null) { "The job id %s has already been submitted and is not starting with a savepoint.", jobId)); } + pendingJobMasterMap.put( + jobId, new Tuple2<>(PendingSourceState.SUBMIT, jobMaster)); runningJobInfoIMap.put( jobId, new JobInfo(System.currentTimeMillis(), jobImmutableInformation)); - runningJobMasterMap.put(jobId, jobMaster); jobMaster.init( runningJobInfoIMap.get(jobId).getInitializationTimestamp(), false); // We specify that when init is complete, the submitJob is complete @@ -518,16 +653,13 @@ && getJobHistoryService().getJobMetrics(jobId) != null) { jobSubmitFuture.completeExceptionally(new JobException(errorMsg)); } if (!jobSubmitFuture.isCompletedExceptionally()) { - try { - jobMaster.run(); - } finally { - // voidCompletableFuture will be cancelled when zeta master node - // shutdown to simulate master failure, - // don't update runningJobMasterMap is this case. - if (!jobMaster.getJobMasterCompleteFuture().isCancelled()) { - runningJobMasterMap.remove(jobId); - } - } + pendingJob.put(jobMaster); + jobMaster.getPhysicalPlan().updateJobState(JobStatus.PENDING); + logger.info( + String.format( + "The submit job enter the pending queue , jobId: %s , jobName: %s", + jobId, + jobMaster.getJobImmutableInformation().getJobName())); } else { runningJobInfoIMap.remove(jobId); runningJobMasterMap.remove(jobId); @@ -566,7 +698,7 @@ public PassiveCompletableFuture savePoint(long jobId) { public PassiveCompletableFuture waitForJobComplete(long jobId) { // must wait for all job restore complete restoreAllJobFromMasterNodeSwitchFuture.join(); - JobMaster runningJobMaster = runningJobMasterMap.get(jobId); + JobMaster runningJobMaster = getJobMaster(jobId); if (runningJobMaster == null) { // Because operations on Imap cannot be performed within Operation. CompletableFuture jobStateFuture = @@ -594,8 +726,8 @@ public PassiveCompletableFuture waitForJobComplete(long jobId) { } } - public PassiveCompletableFuture cancelJob(long jodId) { - JobMaster runningJobMaster = runningJobMasterMap.get(jodId); + public PassiveCompletableFuture cancelJob(long jobId) { + JobMaster runningJobMaster = getJobMaster(jobId); if (runningJobMaster == null) { CompletableFuture future = new CompletableFuture<>(); future.complete(null); @@ -612,6 +744,9 @@ public PassiveCompletableFuture cancelJob(long jodId) { } public JobStatus getJobStatus(long jobId) { + if (pendingJobMasterMap.containsKey(jobId)) { + return JobStatus.PENDING; + } JobMaster runningJobMaster = runningJobMasterMap.get(jobId); if (runningJobMaster == null) { JobHistoryService.JobState jobDetailState = jobHistoryService.getJobDetailState(jobId); @@ -625,6 +760,10 @@ public JobStatus getJobStatus(long jobId) { } public JobMetrics getJobMetrics(long jobId) { + if (pendingJobMasterMap.containsKey(jobId)) { + // Tasks in pending, metric data is empty + return JobMetrics.empty(); + } JobMaster runningJobMaster = runningJobMasterMap.get(jobId); if (runningJobMaster == null) { return jobHistoryService.getJobMetrics(jobId); diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/PhysicalPlan.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/PhysicalPlan.java index ba169d6c2e3..f8039cf6495 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/PhysicalPlan.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/PhysicalPlan.java @@ -28,13 +28,17 @@ import org.apache.seatunnel.engine.core.job.JobStatus; import org.apache.seatunnel.engine.core.job.PipelineExecutionState; import org.apache.seatunnel.engine.core.job.PipelineStatus; +import org.apache.seatunnel.engine.server.execution.TaskGroupLocation; import org.apache.seatunnel.engine.server.master.JobMaster; +import org.apache.seatunnel.engine.server.resourcemanager.resource.SlotProfile; import com.hazelcast.map.IMap; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; @@ -75,6 +79,9 @@ public class PhysicalPlan { private JobMaster jobMaster; + private Map> preApplyResourceFutures = + new HashMap<>(); + /** Whether we make the job end when pipeline turn to end state. */ private boolean makeJobEndWhenPipelineEnded = true; @@ -303,6 +310,7 @@ private synchronized void stateProcess() { case CREATED: updateJobState(JobStatus.SCHEDULED); break; + case PENDING: case SCHEDULED: getPipelineList() .forEach( @@ -333,4 +341,17 @@ private synchronized void stateProcess() { throw new IllegalArgumentException("Unknown Job State: " + getJobStatus()); } } + + public void completeJobEndFuture(JobResult jobResult) { + jobEndFuture.complete(jobResult); + } + + public Map> getPreApplyResourceFutures() { + return preApplyResourceFutures; + } + + public void setPreApplyResourceFutures( + Map> preApplyResourceFutures) { + this.preApplyResourceFutures = preApplyResourceFutures; + } } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/ResourceUtils.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/ResourceUtils.java index cab3e8fa992..1a9bc279849 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/ResourceUtils.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/ResourceUtils.java @@ -17,12 +17,16 @@ package org.apache.seatunnel.engine.server.dag.physical; +import org.apache.seatunnel.common.utils.ExceptionUtils; import org.apache.seatunnel.engine.server.execution.TaskGroupLocation; +import org.apache.seatunnel.engine.server.master.JobMaster; import org.apache.seatunnel.engine.server.resourcemanager.NoEnoughResourceException; import org.apache.seatunnel.engine.server.resourcemanager.ResourceManager; import org.apache.seatunnel.engine.server.resourcemanager.resource.ResourceProfile; import org.apache.seatunnel.engine.server.resourcemanager.resource.SlotProfile; +import com.hazelcast.logging.ILogger; +import com.hazelcast.logging.Logger; import lombok.NonNull; import java.util.HashMap; @@ -32,47 +36,73 @@ public class ResourceUtils { + private static final ILogger LOGGER = Logger.getLogger(ResourceUtils.class); + public static void applyResourceForPipeline( - @NonNull ResourceManager resourceManager, @NonNull SubPlan subPlan) { + @NonNull JobMaster jobMaster, @NonNull SubPlan subPlan) { + Map> futures = new HashMap<>(); Map slotProfiles = new HashMap<>(); - // TODO If there is no enough resources for tasks, we need add some wait profile - subPlan.getCoordinatorVertexList() - .forEach( - coordinator -> - futures.put( - coordinator.getTaskGroupLocation(), - applyResourceForTask( - resourceManager, coordinator, subPlan.getTags()))); + Map> preApplyResourceFutures = + jobMaster.getPhysicalPlan().getPreApplyResourceFutures(); - subPlan.getPhysicalVertexList() - .forEach( - task -> - futures.put( - task.getTaskGroupLocation(), - applyResourceForTask( - resourceManager, task, subPlan.getTags()))); + // TODO If there is no enough resources for tasks, we need add some wait profile + allocateResources(subPlan, futures, preApplyResourceFutures); futures.forEach( (key, value) -> { try { slotProfiles.put(key, value == null ? null : value.join()); } catch (CompletionException e) { - // do nothing + LOGGER.warning("Failed to join future for task group location: " + key, e); } }); + // set it first, avoid can't get it when get resource not enough exception and need release // applied resource subPlan.getJobMaster().setOwnedSlotProfiles(subPlan.getPipelineLocation(), slotProfiles); + if (futures.size() != slotProfiles.size()) { throw new NoEnoughResourceException(); } } + private static void allocateResources( + SubPlan subPlan, + Map> futures, + Map> preApplyResourceFutures) { + subPlan.getCoordinatorVertexList() + .forEach( + coordinator -> { + TaskGroupLocation taskGroupLocation = + coordinator.getTaskGroupLocation(); + futures.put( + taskGroupLocation, + preApplyResourceFutures.get(taskGroupLocation)); + }); + + subPlan.getPhysicalVertexList() + .forEach( + task -> { + TaskGroupLocation taskGroupLocation = task.getTaskGroupLocation(); + futures.put( + taskGroupLocation, + preApplyResourceFutures.get(taskGroupLocation)); + }); + } + public static CompletableFuture applyResourceForTask( ResourceManager resourceManager, PhysicalVertex task, Map tags) { // TODO custom resource size - return resourceManager.applyResource( - task.getTaskGroupLocation().getJobId(), new ResourceProfile(), tags); + try { + return resourceManager.applyResource( + task.getTaskGroupLocation().getJobId(), new ResourceProfile(), tags); + } catch (NoEnoughResourceException e) { + LOGGER.severe( + String.format( + "Job Resource not enough, jobId: %s, message: %s", + task.getTaskGroupLocation().getJobId(), ExceptionUtils.getMessage(e))); + return null; + } } } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/SubPlan.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/SubPlan.java index 6e6667dddaf..6ee28d5a5c8 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/SubPlan.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/dag/physical/SubPlan.java @@ -625,7 +625,7 @@ private synchronized void stateProcess() { break; case SCHEDULED: try { - ResourceUtils.applyResourceForPipeline(jobMaster.getResourceManager(), this); + ResourceUtils.applyResourceForPipeline(jobMaster, this); log.debug( "slotProfiles: {}, PipelineLocation: {}", slotProfiles, @@ -673,6 +673,7 @@ private synchronized void stateProcess() { case CANCELED: if (checkNeedRestore(state) && prepareRestorePipeline()) { jobMaster.releasePipelineResource(this); + jobMaster.preApplyResources(); restorePipeline(); return; } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/execution/PendingSourceState.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/execution/PendingSourceState.java new file mode 100644 index 00000000000..c52f9fee38e --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/execution/PendingSourceState.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.seatunnel.engine.server.execution; + +/** + * This state is used in the pending scheduling queue to determine different processing logic for + * different tasks. + */ +public enum PendingSourceState { + // Task submitted through CoordinatorService.submitJob, set to SUBMIT + SUBMIT, + // Task restored through restoreAllRunningJobFromMasterNodeSwitch, set to RESTORE + RESTORE; +} diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobHistoryService.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobHistoryService.java index 0a1e6ebfccc..a73708a5485 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobHistoryService.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobHistoryService.java @@ -25,11 +25,13 @@ import org.apache.seatunnel.api.common.metrics.JobMetrics; import org.apache.seatunnel.engine.common.exception.SeaTunnelEngineException; import org.apache.seatunnel.engine.core.job.JobDAGInfo; +import org.apache.seatunnel.engine.core.job.JobImmutableInformation; import org.apache.seatunnel.engine.core.job.JobStatus; import org.apache.seatunnel.engine.core.job.JobStatusData; import org.apache.seatunnel.engine.core.job.PipelineStatus; import org.apache.seatunnel.engine.server.dag.physical.PipelineLocation; import org.apache.seatunnel.engine.server.execution.ExecutionState; +import org.apache.seatunnel.engine.server.execution.PendingSourceState; import org.apache.seatunnel.engine.server.execution.TaskGroupLocation; import com.hazelcast.logging.ILogger; @@ -37,6 +39,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; +import scala.Tuple2; import java.io.Serializable; import java.util.ArrayList; @@ -70,6 +73,8 @@ public class JobHistoryService { */ private final Map runningJobMasterMap; + private final Map> pendingJobMasterMap; + /** finishedJobVertexInfoImap key is jobId and value is JobDAGInfo */ private final IMap finishedJobDAGInfoImap; @@ -88,6 +93,7 @@ public class JobHistoryService { public JobHistoryService( IMap runningJobStateIMap, ILogger logger, + Map> pendingJobMasterMap, Map runningJobMasterMap, IMap finishedJobStateImap, IMap finishedJobMetricsImap, @@ -95,6 +101,7 @@ public JobHistoryService( int finishedJobExpireTime) { this.runningJobStateIMap = runningJobStateIMap; this.logger = logger; + this.pendingJobMasterMap = pendingJobMasterMap; this.runningJobMasterMap = runningJobMasterMap; this.finishedJobStateImap = finishedJobStateImap; this.finishedJobMetricsImap = finishedJobMetricsImap; @@ -143,6 +150,19 @@ public List getJobStatusData() { // Get detailed status of a single job public JobState getJobDetailState(Long jobId) { + if (pendingJobMasterMap.containsKey(jobId)) { + // return pending job state + JobImmutableInformation jobImmutableInformation = + pendingJobMasterMap.get(jobId)._2.getJobImmutableInformation(); + return new JobState( + jobId, + jobImmutableInformation.getJobName(), + JobStatus.PENDING, + jobImmutableInformation.getCreateTime(), + null, + null, + null); + } return runningJobMasterMap.containsKey(jobId) ? toJobStateMapper(runningJobMasterMap.get(jobId), false) : finishedJobStateImap.getOrDefault(jobId, null); diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobMaster.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobMaster.java index d85cf607dd5..2f69e43d80e 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobMaster.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/master/JobMaster.java @@ -58,6 +58,7 @@ import org.apache.seatunnel.engine.server.dag.physical.PhysicalPlan; import org.apache.seatunnel.engine.server.dag.physical.PipelineLocation; import org.apache.seatunnel.engine.server.dag.physical.PlanUtils; +import org.apache.seatunnel.engine.server.dag.physical.ResourceUtils; import org.apache.seatunnel.engine.server.dag.physical.SubPlan; import org.apache.seatunnel.engine.server.execution.TaskExecutionState; import org.apache.seatunnel.engine.server.execution.TaskGroupLocation; @@ -92,6 +93,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; @@ -106,6 +108,7 @@ public class JobMaster { private static final ILogger LOGGER = Logger.getLogger(JobMaster.class); private PhysicalPlan physicalPlan; + private final Data jobImmutableInformationData; private final NodeEngine nodeEngine; @@ -160,6 +163,8 @@ public class JobMaster { private CheckpointConfig jobCheckpointConfig; + @Getter private Long jobId; + public String getErrorMessage() { return errorMessage; } @@ -167,6 +172,7 @@ public String getErrorMessage() { private String errorMessage; public JobMaster( + @NonNull Long jobId, @NonNull Data jobImmutableInformationData, @NonNull NodeEngine nodeEngine, @NonNull ExecutorService executorService, @@ -179,6 +185,7 @@ public JobMaster( @NonNull IMap> metricsImap, EngineConfig engineConfig, SeaTunnelServer seaTunnelServer) { + this.jobId = jobId; this.jobImmutableInformationData = jobImmutableInformationData; this.nodeEngine = nodeEngine; this.executorService = executorService; @@ -343,6 +350,98 @@ public void initStateFuture() { })); } + /** + * Apply for resources + * + * @return true if apply resources successfully, otherwise false + */ + public boolean preApplyResources() { + Map> preApplyResourceFutures = + new HashMap<>(); + for (SubPlan subPlan : physicalPlan.getPipelineList()) { + Map> coordinatorFutures = + new HashMap<>(); + subPlan.getCoordinatorVertexList() + .forEach( + coordinator -> + coordinatorFutures.put( + coordinator.getTaskGroupLocation(), + ResourceUtils.applyResourceForTask( + resourceManager, + coordinator, + subPlan.getTags()))); + + Map> taskFutures = new HashMap<>(); + subPlan.getPhysicalVertexList() + .forEach( + task -> + taskFutures.put( + task.getTaskGroupLocation(), + ResourceUtils.applyResourceForTask( + resourceManager, task, subPlan.getTags()))); + + preApplyResourceFutures.putAll(coordinatorFutures); + preApplyResourceFutures.putAll(taskFutures); + } + + boolean enoughResource = + preApplyResourceFutures.values().stream() + .filter( + value -> { + try { + return value != null && value.join() != null; + } catch (CompletionException e) { + LOGGER.warning( + "Pre resource application failed, resources may be not enough"); + return false; + } + }) + .count() + == preApplyResourceFutures.size(); + + if (enoughResource) { + // Adequate resources, pass on resources to the plan + physicalPlan.setPreApplyResourceFutures(preApplyResourceFutures); + } else { + // Release the resource that has been applied + try { + RetryUtils.retryWithException( + () -> { + resourceManager + .releaseResources( + jobImmutableInformation.getJobId(), + preApplyResourceFutures.values().stream() + .filter( + value -> { + try { + return value != null + && value.join() != null; + } catch (CompletionException e) { + LOGGER.warning( + "Pre resource application failed, resources may be not enough"); + return false; + } + }) + .map(CompletableFuture::join) + .collect(Collectors.toList())) + .join(); + return null; + }, + new RetryUtils.RetryMaterial( + Constant.OPERATION_RETRY_TIME, + true, + ExceptionUtil::isOperationNeedRetryException, + Constant.OPERATION_RETRY_SLEEP)); + } catch (Exception e) { + LOGGER.warning( + String.format( + "Pre resource application failed %s", + ExceptionUtils.getMessage(e))); + } + } + return enoughResource; + } + public void run() { try { physicalPlan.startJob(); diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueue.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueue.java new file mode 100644 index 00000000000..d89e8a4565c --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueue.java @@ -0,0 +1,96 @@ +/* + * 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.seatunnel.engine.server.utils; + +import org.apache.seatunnel.common.utils.ExceptionUtils; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * PeekBlockingQueue implements blocking when peeking. Queues like BlockingQueue only support + * blocking when take() is called. The original solution used sleep(2000) to check whether there was + * data in the pending queue. This solution still had performance drawbacks, so it was changed to + * use peek blocking, which allows tasks to be scheduled more efficiently. + * + *

Application scenario: In CoordinatorService, the following process needs to be executed:
+ * 1. Peek data from the queue.
+ * 2. Check if resources are sufficient.
+ * 3. If resources are sufficient, take() the data; otherwise, do not take data from the queue. + */ +@Slf4j +public class PeekBlockingQueue { + + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + private final Lock lock = new ReentrantLock(); + private final Condition notEmpty = lock.newCondition(); + + public void put(E element) { + lock.lock(); + try { + queue.put(element); + notEmpty.signalAll(); + } catch (InterruptedException e) { + log.error("Put element into queue failed. {}", ExceptionUtils.getMessage(e)); + } finally { + lock.unlock(); + } + } + + public E take() throws InterruptedException { + return queue.take(); + } + + public void release() { + lock.lock(); + try { + if (queue.isEmpty()) { + return; + } + notEmpty.signalAll(); + } finally { + lock.unlock(); + } + } + + public E peekBlocking() throws InterruptedException { + lock.lock(); + try { + while (queue.peek() == null) { + notEmpty.await(); + } + return queue.peek(); + } finally { + lock.unlock(); + } + } + + public Integer size() { + lock.lock(); + try { + return queue.size(); + } finally { + lock.unlock(); + } + } +} diff --git a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/TestUtils.java b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/TestUtils.java index 2b63ac80187..0e367a2a482 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/TestUtils.java +++ b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/TestUtils.java @@ -62,7 +62,8 @@ public static String getResource(String confFile) { return System.getProperty("user.dir") + "/src/test/resources/" + confFile; } - public static LogicalDag getTestLogicalDag(JobContext jobContext) throws MalformedURLException { + public static LogicalDag getTestLogicalDag(JobContext jobContext, JobConfig config) + throws MalformedURLException { IdGenerator idGenerator = new IdGenerator(); Config fakeSourceConfig = ConfigFactory.parseMap( @@ -109,7 +110,7 @@ public static LogicalDag getTestLogicalDag(JobContext jobContext) throws Malform LogicalEdge edge = new LogicalEdge(fakeVertex, consoleVertex); - LogicalDag logicalDag = new LogicalDag(); + LogicalDag logicalDag = new LogicalDag(config, idGenerator); logicalDag.addLogicalVertex(fakeVertex); logicalDag.addLogicalVertex(consoleVertex); logicalDag.addEdge(edge); diff --git a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/checkpoint/SavePointTest.java b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/checkpoint/SavePointTest.java index ab3c447f837..038127dbcbb 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/checkpoint/SavePointTest.java +++ b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/checkpoint/SavePointTest.java @@ -95,7 +95,7 @@ public void testSavePointButJobGoingToFail() throws InterruptedException { public void testSavePointWithMultiTimeRequest() throws InterruptedException { long jobId = System.currentTimeMillis(); startJob(jobId, STREAM_CONF_WITH_SLEEP_PATH, false); - Thread.sleep(2000L); + Thread.sleep(5000L); PassiveCompletableFuture savepoint1 = server.getCoordinatorService().savePoint(jobId); Thread.sleep(1000L); PendingCheckpoint pendingCheckpoint1 = diff --git a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/dag/TaskTest.java b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/dag/TaskTest.java index 25391c5c180..55901df3c92 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/dag/TaskTest.java +++ b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/dag/TaskTest.java @@ -69,16 +69,17 @@ public class TaskTest extends AbstractSeaTunnelServerTest { @Test public void testTask() throws MalformedURLException { - JobContext jobContext = new JobContext(); + Long jobId = 1L; + JobContext jobContext = new JobContext(jobId); jobContext.setJobMode(JobMode.BATCH); - LogicalDag testLogicalDag = TestUtils.getTestLogicalDag(jobContext); - JobConfig config = new JobConfig(); config.setName("test"); + config.setJobContext(jobContext); + LogicalDag testLogicalDag = TestUtils.getTestLogicalDag(jobContext, config); JobImmutableInformation jobImmutableInformation = new JobImmutableInformation( - 1, + jobId, "Test", nodeEngine.getSerializationService().toData(testLogicalDag), config, diff --git a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueueTest.java b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueueTest.java new file mode 100644 index 00000000000..6716f5a2156 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/utils/PeekBlockingQueueTest.java @@ -0,0 +1,115 @@ +/* + * 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.seatunnel.engine.server.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; + +public class PeekBlockingQueueTest { + + private PeekBlockingQueue queue; + + @BeforeEach + void setUp() { + queue = new PeekBlockingQueue<>(); + } + + @Test + public void testBasic() throws InterruptedException { + queue.put("1"); + queue.put("2"); + queue.put("3"); + Assertions.assertEquals(3, queue.size()); + Assertions.assertEquals("1", queue.peekBlocking()); + Assertions.assertEquals("1", queue.take()); + Assertions.assertEquals(2, queue.size()); + Assertions.assertEquals("2", queue.peekBlocking()); + Assertions.assertEquals("2", queue.take()); + Assertions.assertEquals(1, queue.size()); + Assertions.assertEquals("3", queue.peekBlocking()); + Assertions.assertEquals("3", queue.take()); + Assertions.assertEquals(0, queue.size()); + } + + @Test + public void testPeekBlocking() throws InterruptedException { + // Test if peekBlocking successfully peek the element + CompletableFuture peekFuture = + CompletableFuture.runAsync( + () -> { + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> + Assertions.assertEquals( + "1", queue.peekBlocking())); + try { + Assertions.assertEquals("1", queue.take()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + Thread.sleep(1000); + queue.put("1"); + peekFuture.join(); + } + + @Test + public void testMultiPeekBlocking() throws InterruptedException, ExecutionException { + // Test if peekBlocking successfully peek the element + CompletableFuture peekFuture = + CompletableFuture.runAsync( + () -> { + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> + Assertions.assertEquals( + "1", queue.peekBlocking())); + try { + Assertions.assertEquals("1", queue.take()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + CompletableFuture secondPeekFuture = + CompletableFuture.runAsync( + () -> { + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> + Assertions.assertEquals( + "2", queue.peekBlocking())); + try { + Assertions.assertEquals("2", queue.take()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + Thread.sleep(1000); + queue.put("1"); + queue.put("2"); + + CompletableFuture.allOf(peekFuture, secondPeekFuture).join(); + } +} From 25b68b3623de201b689a5767a52d64158fcd589f Mon Sep 17 00:00:00 2001 From: CosmosNi <40288034+CosmosNi@users.noreply.github.com> Date: Fri, 18 Oct 2024 19:57:32 +0800 Subject: [PATCH 18/72] [Improve][Jdbc] Support postgresql inet type. (#7820) Co-authored-by: njh_cmss --- .../psql/PostgresJdbcRowConverter.java | 122 ++++++++++++++++++ .../dialect/psql/PostgresTypeConverter.java | 3 + .../seatunnel/cdc/postgres/PostgresCDCIT.java | 4 +- .../src/test/resources/ddl/inventory.sql | 13 +- .../jdbc/JdbcPostgresIdentifierIT.java | 18 ++- .../jdbc_postgres_ide_source_and_sink.conf | 4 +- 6 files changed, 150 insertions(+), 14 deletions(-) diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresJdbcRowConverter.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresJdbcRowConverter.java index f1cd4f8ec98..071e8ec6e1d 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresJdbcRowConverter.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresJdbcRowConverter.java @@ -17,26 +17,38 @@ package org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.psql; +import org.apache.seatunnel.api.table.catalog.Column; import org.apache.seatunnel.api.table.catalog.TableSchema; import org.apache.seatunnel.api.table.type.ArrayType; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; +import org.apache.seatunnel.api.table.type.SqlType; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; +import org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorErrorCode; import org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorException; import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.converter.AbstractJdbcRowConverter; import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseIdentifier; import org.apache.seatunnel.connectors.seatunnel.jdbc.utils.JdbcFieldTypeUtils; +import org.postgresql.util.PGobject; + +import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Locale; import java.util.Optional; +import static org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.psql.PostgresTypeConverter.PG_INET; + public class PostgresJdbcRowConverter extends AbstractJdbcRowConverter { private static final String PG_GEOMETRY = "GEOMETRY"; @@ -143,4 +155,114 @@ public SeaTunnelRow toInternal(ResultSet rs, TableSchema tableSchema) throws SQL } return new SeaTunnelRow(fields); } + + @Override + public PreparedStatement toExternal( + TableSchema tableSchema, SeaTunnelRow row, PreparedStatement statement) + throws SQLException { + SeaTunnelRowType rowType = tableSchema.toPhysicalRowDataType(); + String[] sourceTypes = + tableSchema.getColumns().stream() + .filter(Column::isPhysical) + .map(Column::getSourceType) + .toArray(String[]::new); + for (int fieldIndex = 0; fieldIndex < rowType.getTotalFields(); fieldIndex++) { + try { + SeaTunnelDataType seaTunnelDataType = rowType.getFieldType(fieldIndex); + int statementIndex = fieldIndex + 1; + Object fieldValue = row.getField(fieldIndex); + if (fieldValue == null) { + statement.setObject(statementIndex, null); + continue; + } + + switch (seaTunnelDataType.getSqlType()) { + case STRING: + String sourceType = sourceTypes[fieldIndex]; + if (PG_INET.equalsIgnoreCase(sourceType)) { + PGobject inetObject = new PGobject(); + inetObject.setType(PG_INET); + inetObject.setValue(String.valueOf(row.getField(fieldIndex))); + statement.setObject(statementIndex, inetObject); + } else { + statement.setString(statementIndex, (String) row.getField(fieldIndex)); + } + break; + case BOOLEAN: + statement.setBoolean(statementIndex, (Boolean) row.getField(fieldIndex)); + break; + case TINYINT: + statement.setByte(statementIndex, (Byte) row.getField(fieldIndex)); + break; + case SMALLINT: + statement.setShort(statementIndex, (Short) row.getField(fieldIndex)); + break; + case INT: + statement.setInt(statementIndex, (Integer) row.getField(fieldIndex)); + break; + case BIGINT: + statement.setLong(statementIndex, (Long) row.getField(fieldIndex)); + break; + case FLOAT: + statement.setFloat(statementIndex, (Float) row.getField(fieldIndex)); + break; + case DOUBLE: + statement.setDouble(statementIndex, (Double) row.getField(fieldIndex)); + break; + case DECIMAL: + statement.setBigDecimal( + statementIndex, (BigDecimal) row.getField(fieldIndex)); + break; + case DATE: + LocalDate localDate = (LocalDate) row.getField(fieldIndex); + statement.setDate(statementIndex, java.sql.Date.valueOf(localDate)); + break; + case TIME: + writeTime(statement, statementIndex, (LocalTime) row.getField(fieldIndex)); + break; + case TIMESTAMP: + LocalDateTime localDateTime = (LocalDateTime) row.getField(fieldIndex); + statement.setTimestamp( + statementIndex, java.sql.Timestamp.valueOf(localDateTime)); + break; + case BYTES: + statement.setBytes(statementIndex, (byte[]) row.getField(fieldIndex)); + break; + case NULL: + statement.setNull(statementIndex, java.sql.Types.NULL); + break; + case ARRAY: + SeaTunnelDataType elementType = + ((ArrayType) seaTunnelDataType).getElementType(); + Object[] array = (Object[]) row.getField(fieldIndex); + if (array == null) { + statement.setNull(statementIndex, java.sql.Types.ARRAY); + break; + } + if (SqlType.TINYINT.equals(elementType.getSqlType())) { + Short[] shortArray = new Short[array.length]; + for (int i = 0; i < array.length; i++) { + shortArray[i] = Short.valueOf(array[i].toString()); + } + statement.setObject(statementIndex, shortArray); + } else { + statement.setObject(statementIndex, array); + } + break; + case MAP: + case ROW: + default: + throw new JdbcConnectorException( + CommonErrorCodeDeprecated.UNSUPPORTED_DATA_TYPE, + "Unexpected value: " + seaTunnelDataType); + } + } catch (Exception e) { + throw new JdbcConnectorException( + JdbcConnectorErrorCode.DATA_TYPE_CAST_FAILED, + "error field:" + rowType.getFieldNames()[fieldIndex], + e); + } + } + return statement; + } } diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresTypeConverter.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresTypeConverter.java index 322bdc2a99e..980dd760e9f 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresTypeConverter.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/psql/PostgresTypeConverter.java @@ -81,6 +81,7 @@ public class PostgresTypeConverter implements TypeConverter { public static final String PG_CHAR_ARRAY = "_bpchar"; // character varying <=> varchar public static final String PG_VARCHAR = "varchar"; + public static final String PG_INET = "inet"; public static final String PG_CHARACTER_VARYING = "character varying"; // character varying[] <=> varchar[] <=> _varchar public static final String PG_VARCHAR_ARRAY = "_varchar"; @@ -221,7 +222,9 @@ public Column convert(BasicTypeDefine typeDefine) { case PG_XML: case PG_GEOMETRY: case PG_GEOGRAPHY: + case PG_INET: builder.dataType(BasicType.STRING_TYPE); + builder.sourceType(pgDataType); break; case PG_CHAR_ARRAY: case PG_VARCHAR_ARRAY: diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java index 6aec6034d36..5b6d810de7f 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java @@ -662,7 +662,7 @@ private void upsertDeleteSourceTable(String database, String tableName) { + tableName + " VALUES (2, '2', 32767, 65535, 2147483647, 5.5, 6.6, 123.12345, 404.4443, true,\n" + " 'Hello World', 'a', 'abc', 'abcd..xyz', '2020-07-17 18:00:22.123', '2020-07-17 18:00:22.123456',\n" - + " '2020-07-17', '18:00:22', 500);"); + + " '2020-07-17', '18:00:22', 500,'192.168.1.1');"); executeSql( "INSERT INTO " @@ -671,7 +671,7 @@ private void upsertDeleteSourceTable(String database, String tableName) { + tableName + " VALUES (3, '2', 32767, 65535, 2147483647, 5.5, 6.6, 123.12345, 404.4443, true,\n" + " 'Hello World', 'a', 'abc', 'abcd..xyz', '2020-07-17 18:00:22.123', '2020-07-17 18:00:22.123456',\n" - + " '2020-07-17', '18:00:22', 500);"); + + " '2020-07-17', '18:00:22', 500,'192.168.1.1');"); executeSql("DELETE FROM " + database + "." + tableName + " where id = 2;"); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/resources/ddl/inventory.sql b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/resources/ddl/inventory.sql index cff1a3980fd..1372f98a444 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/resources/ddl/inventory.sql +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/resources/ddl/inventory.sql @@ -47,6 +47,7 @@ CREATE TABLE postgres_cdc_table_1 f_date DATE, f_time TIME(0), f_default_numeric NUMERIC, + f_inet INET, PRIMARY KEY (id) ); @@ -71,6 +72,7 @@ CREATE TABLE postgres_cdc_table_2 f_date DATE, f_time TIME(0), f_default_numeric NUMERIC, + f_inet INET, PRIMARY KEY (id) ); @@ -95,6 +97,7 @@ CREATE TABLE sink_postgres_cdc_table_1 f_date DATE, f_time TIME(0), f_default_numeric NUMERIC, + f_inet INET, PRIMARY KEY (id) ); @@ -119,6 +122,7 @@ CREATE TABLE sink_postgres_cdc_table_2 f_date DATE, f_time TIME(0), f_default_numeric NUMERIC, + f_inet INET, PRIMARY KEY (id) ); @@ -142,7 +146,8 @@ CREATE TABLE full_types_no_primary_key f_timestamp6 TIMESTAMP(6), f_date DATE, f_time TIME(0), - f_default_numeric NUMERIC + f_default_numeric NUMERIC, + f_inet INET ); CREATE TABLE postgres_cdc_table_3 @@ -184,12 +189,12 @@ ALTER TABLE full_types_no_primary_key INSERT INTO postgres_cdc_table_1 VALUES (1, '2', 32767, 65535, 2147483647, 5.5, 6.6, 123.12345, 404.4443, true, 'Hello World', 'a', 'abc', 'abcd..xyz', '2020-07-17 18:00:22.123', '2020-07-17 18:00:22.123456', - '2020-07-17', '18:00:22', 500); + '2020-07-17', '18:00:22', 500,'192.168.1.1'); INSERT INTO postgres_cdc_table_2 VALUES (1, '2', 32767, 65535, 2147483647, 5.5, 6.6, 123.12345, 404.4443, true, 'Hello World', 'a', 'abc', 'abcd..xyz', '2020-07-17 18:00:22.123', '2020-07-17 18:00:22.123456', - '2020-07-17', '18:00:22', 500); + '2020-07-17', '18:00:22', 500,'192.168.1.1'); INSERT INTO postgres_cdc_table_3 VALUES (1, '2', 32767, 65535); @@ -197,4 +202,4 @@ VALUES (1, '2', 32767, 65535); INSERT INTO full_types_no_primary_key VALUES (1, '2', 32767, 65535, 2147483647, 5.5, 6.6, 123.12345, 404.4443, true, 'Hello World', 'a', 'abc', 'abcd..xyz', '2020-07-17 18:00:22.123', '2020-07-17 18:00:22.123456', - '2020-07-17', '18:00:22', 500); + '2020-07-17', '18:00:22', 500,'192.168.1.1'); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIdentifierIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIdentifierIT.java index c4037ecce6f..c7d48e9de33 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIdentifierIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIdentifierIT.java @@ -91,7 +91,8 @@ public class JdbcPostgresIdentifierIT extends TestSuiteBase implements TestResou + " multilinestring geometry(MULTILINESTRING, 4326),\n" + " multipolygon geometry(MULTIPOLYGON, 4326),\n" + " geometrycollection geometry(GEOMETRYCOLLECTION, 4326),\n" - + " geog geography(POINT, 4326)\n" + + " geog geography(POINT, 4326),\n" + + " inet_col INET\n" + ")"; private static final String PG_SINK_DDL = "CREATE TABLE IF NOT EXISTS test.public.\"PG_IDE_SINK_TABLE\" (\n" @@ -122,7 +123,8 @@ public class JdbcPostgresIdentifierIT extends TestSuiteBase implements TestResou + " \"MULTILINESTRING\" varchar(2000) NULL,\n" + " \"MULTIPOLYGON\" varchar(2000) NULL,\n" + " \"GEOMETRYCOLLECTION\" varchar(2000) NULL,\n" - + " \"GEOG\" varchar(2000) NULL\n" + + " \"GEOG\" varchar(2000) NULL,\n" + + " \"INET_COL\" INET NULL\n" + " )"; private static final String SOURCE_SQL = @@ -154,7 +156,8 @@ public class JdbcPostgresIdentifierIT extends TestSuiteBase implements TestResou + "multilinestring,\n" + "multipolygon,\n" + "geometrycollection,\n" - + "geog\n" + + "geog,\n" + + "inet_col\n" + " from pg_ide_source_table"; private static final String SINK_SQL = "SELECT\n" @@ -185,7 +188,8 @@ public class JdbcPostgresIdentifierIT extends TestSuiteBase implements TestResou + " CAST(\"MULTILINESTRING\" AS GEOMETRY) AS MULTILINESTRING,\n" + " CAST(\"MULTIPOLYGON\" AS GEOMETRY) AS MULTILINESTRING,\n" + " CAST(\"GEOMETRYCOLLECTION\" AS GEOMETRY) AS GEOMETRYCOLLECTION,\n" - + " CAST(\"GEOG\" AS GEOGRAPHY) AS GEOG\n" + + " CAST(\"GEOG\" AS GEOGRAPHY) AS GEOG,\n" + + " \"INET_COL\"\n" + "FROM\n" + " \"PG_IDE_SINK_TABLE\";"; @@ -276,7 +280,8 @@ private void initializeJdbcTable() { + " multilinestring,\n" + " multipolygon,\n" + " geometrycollection,\n" - + " geog\n" + + " geog,\n" + + " inet_col\n" + " )\n" + "VALUES\n" + " (\n" @@ -327,7 +332,8 @@ private void initializeJdbcTable() { + " 'GEOMETRYCOLLECTION(POINT(-122.3462 47.5921), LINESTRING(-122.3460 47.5924, -122.3457 47.5924))',\n" + " 4326\n" + " ),\n" - + " ST_GeographyFromText('POINT(-122.3452 47.5925)')\n" + + " ST_GeographyFromText('POINT(-122.3452 47.5925)'),\n" + + " '192.168.1.1'\n" + " )"); } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_postgres_ide_source_and_sink.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_postgres_ide_source_and_sink.conf index dcb966985e0..d56cf5d9816 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_postgres_ide_source_and_sink.conf +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_postgres_ide_source_and_sink.conf @@ -28,7 +28,7 @@ source{ password = "test" query ="""select gid, text_col, varchar_col, char_col, boolean_col, smallint_col, integer_col, bigint_col, decimal_col, numeric_col, real_col, double_precision_col, smallserial_col, serial_col, bigserial_col, date_col, timestamp_col, bpchar_col, age, name, point, linestring, polygon_colums, multipoint, - multilinestring, multipolygon, geometrycollection, geog from pg_ide_source_table""" + multilinestring, multipolygon, geometrycollection, geog,inet_col from pg_ide_source_table""" } } @@ -45,4 +45,4 @@ sink { table = "public.PG_IDE_SINK_TABLE" primary_keys = ["gid"] } -} \ No newline at end of file +} From cd9836bced8cf9ccddc72f68fbee051f201b9678 Mon Sep 17 00:00:00 2001 From: Jeremy <739772893@qq.com> Date: Fri, 18 Oct 2024 20:08:55 +0800 Subject: [PATCH 19/72] [Fix][Connector-V2][connector-file-base-hadoop] Fixed HdfsFile source load the krb5_path configuration (#7870) Co-authored-by: leibaoxin --- .../seatunnel/file/hdfs/source/BaseHdfsFileSource.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java b/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java index 2b71980935b..9cf7cace0ba 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/BaseHdfsFileSource.java @@ -74,6 +74,10 @@ public void prepare(Config pluginConfig) throws PrepareFailException { pluginConfig.getString(HdfsSourceConfigOptions.REMOTE_USER.key())); } + if (pluginConfig.hasPath(HdfsSourceConfigOptions.KRB5_PATH.key())) { + hadoopConf.setKrb5Path(pluginConfig.getString(HdfsSourceConfigOptions.KRB5_PATH.key())); + } + if (pluginConfig.hasPath(HdfsSourceConfigOptions.KERBEROS_PRINCIPAL.key())) { hadoopConf.setKerberosPrincipal( pluginConfig.getString(HdfsSourceConfigOptions.KERBEROS_PRINCIPAL.key())); From b47fca812f8b3b79bfa74e73487e063201924cb0 Mon Sep 17 00:00:00 2001 From: Jia Fan Date: Fri, 18 Oct 2024 22:05:32 +0800 Subject: [PATCH 20/72] [Improve][Doc] Correcting typo errors in logging (#7874) --- docs/en/seatunnel-engine/logging.md | 2 +- docs/zh/seatunnel-engine/logging.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/seatunnel-engine/logging.md b/docs/en/seatunnel-engine/logging.md index 7c827887b82..eefe6f42e9a 100644 --- a/docs/en/seatunnel-engine/logging.md +++ b/docs/en/seatunnel-engine/logging.md @@ -30,7 +30,7 @@ The MDC is propagated by slf4j to the logging backend which usually adds it to t Log4j 2 is controlled using property files. -The SeaTunnel Engine distribution ships with the following log4j properties files in the `confing` directory, which are used automatically if Log4j 2 is enabled: +The SeaTunnel Engine distribution ships with the following log4j properties files in the `config` directory, which are used automatically if Log4j 2 is enabled: - `log4j2_client.properties`: used by the command line client (e.g., `seatunnel.sh`) - `log4j2.properties`: used for SeaTunnel Engine server processes (e.g., `seatunnel-cluster.sh`) diff --git a/docs/zh/seatunnel-engine/logging.md b/docs/zh/seatunnel-engine/logging.md index 8f04eaa9117..bbcdb0b2922 100644 --- a/docs/zh/seatunnel-engine/logging.md +++ b/docs/zh/seatunnel-engine/logging.md @@ -30,10 +30,10 @@ MDC 由 slf4j 传播到日志后端,后者通常会自动将其添加到日志 Log4j2 使用属性文件进行控制。 -SeaTunnel Engine 发行版在 `confing` 目录中附带以下 log4j 属性文件,如果启用了 Log4j2,则会自动使用这些文件: +SeaTunnel Engine 发行版在 `config` 目录中附带以下 log4j 属性文件,如果启用了 Log4j2,则会自动使用这些文件: -- `log4j2_client.properties`: 由命令行客户端使用 (e.g., `seatunnel.sh`) -- `log4j2.properties`: 由 SeaTunnel 引擎服务使用 (e.g., `seatunnel-cluster.sh`) +- `log4j2_client.properties`: 由命令行客户端使用 (例如, `seatunnel.sh`) +- `log4j2.properties`: 由 SeaTunnel 引擎服务使用 (例如, `seatunnel-cluster.sh`) 默认情况下,日志文件输出到 `logs` 目录。 From 266d1773e56dca0484e633060ab3242d8fd2445d Mon Sep 17 00:00:00 2001 From: hailin0 Date: Sun, 20 Oct 2024 21:52:17 +0800 Subject: [PATCH 21/72] [Improve][Example] Add log config for job mdc (#7876) --- seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.cmd | 1 + seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.sh | 1 + .../seatunnel/example/engine/SeaTunnelEngineExample.java | 5 +++++ .../example/engine/SeaTunnelEngineServerExample.java | 6 ++++++ 4 files changed, 13 insertions(+) diff --git a/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.cmd b/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.cmd index cecd3797cda..ef5ead87e3f 100644 --- a/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.cmd +++ b/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.cmd @@ -66,6 +66,7 @@ REM if you want to debug, please REM set "JAVA_OPTS=%JAVA_OPTS% -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5000,suspend=n" REM Log4j2 Config +set "JAVA_OPTS=%JAVA_OPTS% -Dlog4j2.isThreadContextMapInheritable=true" if exist "%CONF_DIR%\log4j2_client.properties" ( set "JAVA_OPTS=%JAVA_OPTS% -Dhazelcast.logging.type=log4j2 -Dlog4j2.configurationFile=%CONF_DIR%\log4j2_client.properties" set "JAVA_OPTS=%JAVA_OPTS% -Dseatunnel.logs.path=%APP_DIR%\logs" diff --git a/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.sh b/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.sh index dc4c3f91e56..754f2337c62 100755 --- a/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.sh +++ b/seatunnel-core/seatunnel-starter/src/main/bin/seatunnel.sh @@ -80,6 +80,7 @@ JAVA_OPTS="${JAVA_OPTS} -Dhazelcast.config=${HAZELCAST_CONFIG}" # JAVA_OPTS="${JAVA_OPTS} -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5000,suspend=n" # Log4j2 Config +JAVA_OPTS="${JAVA_OPTS} -Dlog4j2.isThreadContextMapInheritable=true" if [ -e "${CONF_DIR}/log4j2_client.properties" ]; then JAVA_OPTS="${JAVA_OPTS} -Dhazelcast.logging.type=log4j2 -Dlog4j2.configurationFile=${CONF_DIR}/log4j2_client.properties" JAVA_OPTS="${JAVA_OPTS} -Dseatunnel.logs.path=${APP_DIR}/logs" diff --git a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java index 2a7c25e0830..26a965fefb5 100644 --- a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java +++ b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java @@ -29,6 +29,11 @@ public class SeaTunnelEngineExample { + static { + // https://logging.apache.org/log4j/2.x/manual/simple-logger.html#isThreadContextMapInheritable + System.setProperty("log4j2.isThreadContextMapInheritable", "true"); + } + public static void main(String[] args) throws FileNotFoundException, URISyntaxException, CommandException { String configurePath = args.length > 0 ? args[0] : "/examples/fake_to_console.conf"; diff --git a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineServerExample.java b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineServerExample.java index df539ea6e05..07b363c41df 100644 --- a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineServerExample.java +++ b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineServerExample.java @@ -22,6 +22,12 @@ import org.apache.seatunnel.core.starter.seatunnel.args.ServerCommandArgs; public class SeaTunnelEngineServerExample { + + static { + // https://logging.apache.org/log4j/2.x/manual/simple-logger.html#isThreadContextMapInheritable + System.setProperty("log4j2.isThreadContextMapInheritable", "true"); + } + public static void main(String[] args) throws CommandException { ServerCommandArgs serverCommandArgs = new ServerCommandArgs(); SeaTunnel.run(serverCommandArgs.buildCommand()); From 312ee866fb1612e7a98e68c592da321e56175db4 Mon Sep 17 00:00:00 2001 From: hailin0 Date: Mon, 21 Oct 2024 09:54:35 +0800 Subject: [PATCH 22/72] [Improve][Jdbc] Optimize index name conflicts when create table for postgresql (#7875) --- .../psql/PostgresCreateTableSqlBuilder.java | 22 +++++++------------ .../PostgresCreateTableSqlBuilderTest.java | 16 ++++++++------ 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilder.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilder.java index f7b98c1bb17..1fbfd7c095e 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilder.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilder.java @@ -30,11 +30,14 @@ import org.apache.commons.lang3.StringUtils; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; +@Slf4j public class PostgresCreateTableSqlBuilder { private List columns; private PrimaryKey primaryKey; @@ -161,10 +164,7 @@ private String buildColumnCommentSql(Column column, String tableName) { } private String buildUniqueKeySql(ConstraintKey constraintKey) { - String constraintName = constraintKey.getConstraintName(); - if (constraintName.length() > 25) { - constraintName = constraintName.substring(0, 25); - } + String constraintName = UUID.randomUUID().toString().replace("-", ""); String indexColumns = constraintKey.getColumnNames().stream() .map( @@ -175,16 +175,12 @@ private String buildUniqueKeySql(ConstraintKey constraintKey) { constraintKeyColumn.getColumnName(), fieldIde))) .collect(Collectors.joining(", ")); - return "CONSTRAINT " + constraintName + " UNIQUE (" + indexColumns + ")"; + return "CONSTRAINT \"" + constraintName + "\" UNIQUE (" + indexColumns + ")"; } private String buildIndexKeySql(TablePath tablePath, ConstraintKey constraintKey) { - // We add table name to index name to avoid name conflict in PG - // Since index name in PG should unique in the schema - String constraintName = tablePath.getTableName() + "_" + constraintKey.getConstraintName(); - if (constraintName.length() > 25) { - constraintName = constraintName.substring(0, 25); - } + // If the index name is omitted, PostgreSQL will choose an appropriate name based on table + // name and indexed columns. String indexColumns = constraintKey.getColumnNames().stream() .map( @@ -196,9 +192,7 @@ private String buildIndexKeySql(TablePath tablePath, ConstraintKey constraintKey fieldIde))) .collect(Collectors.joining(", ")); - return "CREATE INDEX " - + constraintName - + " ON " + return "CREATE INDEX ON " + tablePath.getSchemaAndTableName("\"") + "(" + indexColumns diff --git a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilderTest.java b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilderTest.java index 37049eced38..bc204a913a4 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilderTest.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCreateTableSqlBuilderTest.java @@ -35,6 +35,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; class PostgresCreateTableSqlBuilderTest { @@ -49,17 +50,18 @@ void build() { String createTableSql = postgresCreateTableSqlBuilder.build( catalogTable.getTableId().toTablePath()); - Assertions.assertEquals( - "CREATE TABLE \"test\" (\n" + String pattern = + "CREATE TABLE \"test\" \\(\n" + "\"id\" int4 NOT NULL PRIMARY KEY,\n" + "\"name\" text NOT NULL,\n" + "\"age\" int4 NOT NULL,\n" - + "\tCONSTRAINT unique_name UNIQUE (\"name\")\n" - + ");", - createTableSql); + + "\tCONSTRAINT \"([a-zA-Z0-9]+)\" UNIQUE \\(\"name\"\\)\n" + + "\\);"; + Assertions.assertTrue( + Pattern.compile(pattern).matcher(createTableSql).find()); + Assertions.assertEquals( - Lists.newArrayList( - "CREATE INDEX test_index_age ON \"test\"(\"age\");"), + Lists.newArrayList("CREATE INDEX ON \"test\"(\"age\");"), postgresCreateTableSqlBuilder.getCreateIndexSqls()); // skip index From f640eda911e5a8e381f0059b934271759cde8f03 Mon Sep 17 00:00:00 2001 From: Jast Date: Mon, 21 Oct 2024 10:36:48 +0800 Subject: [PATCH 23/72] [Feature][Zeta][Core] Support rest api get logs (#7818) --- docs/en/seatunnel-engine/logging.md | 12 + docs/en/seatunnel-engine/rest-api-v1.md | 67 ++++ docs/en/seatunnel-engine/rest-api-v2.md | 66 ++++ docs/zh/seatunnel-engine/logging.md | 12 + docs/zh/seatunnel-engine/rest-api-v1.md | 72 +++- docs/zh/seatunnel-engine/rest-api-v2.md | 73 ++++- .../seatunnel/common/utils/FileUtils.java | 28 ++ .../seatunnel/engine/e2e/RestApiIT.java | 102 ++++++ .../seatunnel/engine/e2e/joblog/JobLogIT.java | 18 +- .../seatunnel/engine/server/JettyService.java | 16 + .../engine/server/log/FormatType.java | 34 ++ .../engine/server/rest/RestConstant.java | 6 + .../rest/RestHttpGetCommandProcessor.java | 307 +++++++++++++++++- .../rest/servlet/AllLogNameServlet.java | 57 ++++ .../rest/servlet/AllNodeLogServlet.java | 130 ++++++++ .../server/rest/servlet/BaseServlet.java | 53 +++ .../rest/servlet/CurrentNodeLogServlet.java | 69 ++++ .../server/rest/servlet/LogBaseServlet.java | 191 +++++++++++ .../rest/servlet/SystemMonitoringServlet.java | 42 +-- 19 files changed, 1302 insertions(+), 53 deletions(-) create mode 100644 seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/log/FormatType.java create mode 100644 seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllLogNameServlet.java create mode 100644 seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllNodeLogServlet.java create mode 100644 seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/CurrentNodeLogServlet.java create mode 100644 seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java diff --git a/docs/en/seatunnel-engine/logging.md b/docs/en/seatunnel-engine/logging.md index eefe6f42e9a..094cb15febd 100644 --- a/docs/en/seatunnel-engine/logging.md +++ b/docs/en/seatunnel-engine/logging.md @@ -80,6 +80,18 @@ appender.file.layout.pattern = [%X{ST-JID}] %d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%- SeaTunnel Engine automatically integrates Log framework bridge, allowing existing applications that work against Log4j1/Logback classes to continue working. +### Query Logs via REST API + +SeaTunnel provides an API for querying logs. + +**Usage examples:** +- Retrieve logs for all nodes with `jobId` of `733584788375666689`: `http://localhost:8080/logs/733584788375666689` +- Retrieve the log list for all nodes: `http://localhost:8080/logs` +- Retrieve the log list for all nodes in JSON format: `http://localhost:8080/logs?format=json` +- Retrieve log file content: `http://localhost:8080/logs/job-898380162133917698.log` + +For more details, please refer to the [REST-API](rest-api-v2.md). + ## Best practices for developers You can create an SLF4J logger by calling `org.slf4j.LoggerFactory#LoggerFactory.getLogger` with the Class of your class as an argument. diff --git a/docs/en/seatunnel-engine/rest-api-v1.md b/docs/en/seatunnel-engine/rest-api-v1.md index aaefcbc5faf..f9d4fbc3e6e 100644 --- a/docs/en/seatunnel-engine/rest-api-v1.md +++ b/docs/en/seatunnel-engine/rest-api-v1.md @@ -776,3 +776,70 @@ If the parameter is an empty `Map` object, it means that the tags of the current ``` +------------------------------------------------------------------------------------------ + +### Get All Node Log Content + +

+ GET /hazelcast/rest/maps/logs/:jobId (Returns a list of logs.) + +#### Request Parameters + +#### Parameters (Add in the `params` field of the request body) + +> | Parameter Name | Required | Type | Description | +> |----------------------|------------|---------|---------------------------------| +> | jobId | optional | string | job id | + +When `jobId` is empty, it returns log information for all nodes; otherwise, it returns the log list of the specified `jobId` across all nodes. + +#### Response + +Returns a list of logs and content from the requested nodes. + +#### Get All Log Files List + +If you'd like to view the log list first, you can use a `GET` request to retrieve the log list: +`http://localhost:5801/hazelcast/rest/maps/logs?format=json` + +```json +[ + { + "node": "localhost:5801", + "logLink": "http://localhost:5801/hazelcast/rest/maps/logs/job-899485770241277953.log", + "logName": "job-899485770241277953.log" + }, + { + "node": "localhost:5801", + "logLink": "http://localhost:5801/hazelcast/rest/maps/logs/job-899470314109468673.log", + "logName": "job-899470314109468673.log" + } +] +``` + +The supported formats are `json` and `html`, with `html` as the default. + +#### Examples + +Retrieve logs for all nodes with the `jobId` of `733584788375666689`: `http://localhost:5801/hazelcast/rest/maps/logs/733584788375666689` +Retrieve the log list for all nodes: `http://localhost:5801/hazelcast/rest/maps/logs` +Retrieve the log list for all nodes in JSON format: `http://localhost:5801/hazelcast/rest/maps/logs?format=json` +Retrieve log file content: `http://localhost:5801/hazelcast/rest/maps/logs/job-898380162133917698.log` + +
+ +### Get Log Content from a Single Node + +
+ GET /hazelcast/rest/maps/log (Returns a list of logs.) + +#### Response + +Returns a list of logs from the requested node. + +#### Examples + +To get a list of logs from the current node: `http://localhost:5801/hazelcast/rest/maps/log` +To get the content of a log file: `http://localhost:5801/hazelcast/rest/maps/log/job-898380162133917698.log` + +
\ No newline at end of file diff --git a/docs/en/seatunnel-engine/rest-api-v2.md b/docs/en/seatunnel-engine/rest-api-v2.md index 1e7cf10d4e6..c21d0531221 100644 --- a/docs/en/seatunnel-engine/rest-api-v2.md +++ b/docs/en/seatunnel-engine/rest-api-v2.md @@ -740,3 +740,69 @@ If the parameter is an empty `Map` object, it means that the tags of the current ``` +------------------------------------------------------------------------------------------ + +### Get Logs from All Nodes + +
+ GET /logs/:jobId (Returns a list of logs.) + +#### Request Parameters + +#### Parameters (to be added in the `params` field of the request body) + +> | Parameter Name | Required | Type | Description | +> |-----------------------|--------------|---------|------------------------------------| +> | jobId | optional | string | job id | + +If `jobId` is empty, the request will return logs from all nodes. Otherwise, it will return the list of logs for the specified `jobId` from all nodes. + +#### Response + +Returns a list of logs from the requested nodes along with their content. + +#### Return List of All Log Files + +If you want to view the log list first, you can retrieve it via a `GET` request: `http://localhost:8080/logs?format=json` + +```json +[ + { + "node": "localhost:8080", + "logLink": "http://localhost:8080/logs/job-899485770241277953.log", + "logName": "job-899485770241277953.log" + }, + { + "node": "localhost:8080", + "logLink": "http://localhost:8080/logs/job-899470314109468673.log", + "logName": "job-899470314109468673.log" + } +] +``` + +Supported formats are `json` and `html`, with `html` as the default. + +#### Examples + +Retrieve logs for `jobId` `733584788375666689` across all nodes: `http://localhost:8080/logs/733584788375666689` +Retrieve the list of logs from all nodes: `http://localhost:8080/logs` +Retrieve the list of logs in JSON format: `http://localhost:8080/logs?format=json` +Retrieve the content of a specific log file: `http://localhost:8080/logs/job-898380162133917698.log` + +
+ +### Get Log Content from a Single Node + +
+ GET /log (Returns a list of logs.) + +#### Response + +Returns a list of logs from the requested node. + +#### Examples + +To get a list of logs from the current node: `http://localhost:5801/log` +To get the content of a log file: `http://localhost:5801/log/job-898380162133917698.log` + +
\ No newline at end of file diff --git a/docs/zh/seatunnel-engine/logging.md b/docs/zh/seatunnel-engine/logging.md index bbcdb0b2922..7d4f4f1d62b 100644 --- a/docs/zh/seatunnel-engine/logging.md +++ b/docs/zh/seatunnel-engine/logging.md @@ -80,6 +80,18 @@ appender.file.layout.pattern = [%X{ST-JID}] %d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%- SeaTunnel Engine 自动集成了大多数 Log 桥接器,允许针对 Log4j1/Logback 类工作的现有应用程序继续工作。 +### REST-API方式查询日志 + +SeaTunnel 提供了一个 API,用于查询日志。 + +**使用样例:** +- 获取所有节点jobId为`733584788375666689`的日志信息:`http://localhost:8080/logs/733584788375666689` +- 获取所有节点日志列表:`http://localhost:8080/logs` +- 获取所有节点日志列表以JSON格式返回:`http://localhost:8080/logs?format=json` +- 获取日志文件内容:`http://localhost:8080/logs/job-898380162133917698.log` + +有关详细信息,请参阅 [REST-API](rest-api-v2.md)。 + ## 开发人员最佳实践 您可以通过调用 `org.slf4j.LoggerFactory#LoggerFactory.getLogger` 并以您的类的类作为参数来创建 SLF4J 记录器。 diff --git a/docs/zh/seatunnel-engine/rest-api-v1.md b/docs/zh/seatunnel-engine/rest-api-v1.md index a59c6bbde5f..d9ae96714ac 100644 --- a/docs/zh/seatunnel-engine/rest-api-v1.md +++ b/docs/zh/seatunnel-engine/rest-api-v1.md @@ -778,4 +778,74 @@ network: "message": "Invalid JSON format in request body." } ``` - \ No newline at end of file + + + +------------------------------------------------------------------------------------------ + +### 获取所有节点日志内容 + +
+ GET /hazelcast/rest/maps/logs/:jobId (返回日志列表。) + +#### 请求参数 + +#### 参数(在请求体中params字段中添加) + +> | 参数名称 | 是否必传 | 参数类型 | 参数描述 | +> |----------------------|----------|--------|-----------------------------------| +> | jobId | optional | string | job id | + +当`jobId`为空时,返回所有节点的日志信息,否则返回指定`jobId`在所有节点的的日志列表。 + +#### 响应 + +返回请求节点的日志列表、内容 + +#### 返回所有日志文件列表 + +如果你想先查看日志列表,可以通过`GET`请求获取日志列表,`http://localhost:5801/hazelcast/rest/maps/logs?format=json` + +```json +[ + { + "node": "localhost:5801", + "logLink": "http://localhost:5801/hazelcast/rest/maps/logs/job-899485770241277953.log", + "logName": "job-899485770241277953.log" + }, + { + "node": "localhost:5801", + "logLink": "http://localhost:5801/hazelcast/rest/maps/logs/job-899470314109468673.log", + "logName": "job-899470314109468673.log" + } +] +``` + +当前支持的格式有`json`和`html`,默认为`html`。 + +#### 例子 + +获取所有节点jobId为`733584788375666689`的日志信息:`http://localhost:5801/hazelcast/rest/maps/logs/733584788375666689` +获取所有节点日志列表:`http://localhost:5801/hazelcast/rest/maps/logs` +获取所有节点日志列表以JSON格式返回:`http://localhost:5801/hazelcast/rest/maps/logs?format=json` +获取日志文件内容:`http://localhost:5801/hazelcast/rest/maps/logs/job-898380162133917698.log`` + + +
+ + +### 获取单节点日志内容 + +
+ GET /hazelcast/rest/maps/log (返回日志列表。) + +#### 响应 + +返回请求节点的日志列表 + +#### 例子 + +获取当前节点的日志列表:`http://localhost:5801/hazelcast/rest/maps/log` +获取日志文件内容:`http://localhost:5801/hazelcast/rest/maps/log/job-898380162133917698.log` + +
diff --git a/docs/zh/seatunnel-engine/rest-api-v2.md b/docs/zh/seatunnel-engine/rest-api-v2.md index 75a03f93fdb..50f7eef3b10 100644 --- a/docs/zh/seatunnel-engine/rest-api-v2.md +++ b/docs/zh/seatunnel-engine/rest-api-v2.md @@ -738,4 +738,75 @@ seatunnel: "message": "Invalid JSON format in request body." } ``` - \ No newline at end of file + + + +------------------------------------------------------------------------------------------ + +### 获取所有节点日志内容 + +
+ GET /logs/:jobId (返回日志列表。) + +#### 请求参数 + +#### 参数(在请求体中params字段中添加) + +> | 参数名称 | 是否必传 | 参数类型 | 参数描述 | +> |----------------------|----------|--------|-----------------------------------| +> | jobId | optional | string | job id | + +当`jobId`为空时,返回所有节点的日志信息,否则返回指定`jobId`在所有节点的的日志列表。 + +#### 响应 + +返回请求节点的日志列表、内容 + +#### 返回所有日志文件列表 + +如果你想先查看日志列表,可以通过`GET`请求获取日志列表,`http://localhost:8080/logs?format=json` + +```json +[ + { + "node": "localhost:8080", + "logLink": "http://localhost:8080/logs/job-899485770241277953.log", + "logName": "job-899485770241277953.log" + }, + { + "node": "localhost:8080", + "logLink": "http://localhost:8080/logs/job-899470314109468673.log", + "logName": "job-899470314109468673.log" + } +] +``` + +当前支持的格式有`json`和`html`,默认为`html`。 + + +#### 例子 + +获取所有节点jobId为`733584788375666689`的日志信息:`http://localhost:8080/logs/733584788375666689` +获取所有节点日志列表:`http://localhost:8080/logs` +获取所有节点日志列表以JSON格式返回:`http://localhost:8080/logs?format=json` +获取日志文件内容:`http://localhost:8080/logs/job-898380162133917698.log` + + +
+ + +### 获取单节点日志内容 + +
+ GET /log (返回日志列表。) + +#### 响应 + +返回请求节点的日志列表 + +#### 例子 + +获取当前节点的日志列表:`http://localhost:5801/log` +获取日志文件内容:`http://localhost:5801/log/job-898380162133917698.log`` + +
diff --git a/seatunnel-common/src/main/java/org/apache/seatunnel/common/utils/FileUtils.java b/seatunnel-common/src/main/java/org/apache/seatunnel/common/utils/FileUtils.java index bfdda942755..279c4bf4cad 100644 --- a/seatunnel-common/src/main/java/org/apache/seatunnel/common/utils/FileUtils.java +++ b/seatunnel-common/src/main/java/org/apache/seatunnel/common/utils/FileUtils.java @@ -37,6 +37,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -194,4 +195,31 @@ private static void deleteFiles(@NonNull File file) { throw CommonError.fileOperationFailed("SeaTunnel", "delete", file.toString(), e); } } + + public static List listFile(String dirPath) { + try { + File file = new File(dirPath); + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files == null) { + return null; + } + return Arrays.stream(files) + .map( + currFile -> { + if (currFile.isDirectory()) { + return null; + } else { + return Arrays.asList(currFile); + } + }) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + return Arrays.asList(file); + } catch (Exception e) { + throw CommonError.fileOperationFailed("SeaTunnel", "list", dirPath, e); + } + } } diff --git a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java index 0dd90edddad..e0070ba2b15 100644 --- a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java +++ b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/RestApiIT.java @@ -27,6 +27,9 @@ import org.apache.seatunnel.engine.server.SeaTunnelServerStarter; import org.apache.seatunnel.engine.server.rest.RestConstant; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; + import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -39,13 +42,18 @@ import com.hazelcast.instance.impl.HazelcastInstanceImpl; import lombok.extern.slf4j.Slf4j; +import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static io.restassured.RestAssured.given; +import static org.apache.seatunnel.e2e.common.util.ContainerUtil.PROJECT_ROOT_PATH; import static org.apache.seatunnel.engine.server.rest.RestConstant.CONTEXT_PATH; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; @@ -75,6 +83,12 @@ public class RestApiIT { @BeforeEach void beforeClass() throws Exception { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + context.setConfigLocation( + Paths.get( + PROJECT_ROOT_PATH + + "/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/resources/job-log-file/log4j2.properties") + .toUri()); String testClusterName = TestUtils.getClusterName("RestApiIT"); node1Config = ConfigProvider.locateAndGetSeaTunnelConfig(); node1Config.getEngineConfig().getHttpConfig().setPort(8080); @@ -138,6 +152,94 @@ void beforeClass() throws Exception { node2Config.getEngineConfig().getHttpConfig().getPort()); } + @Test + public void testGetLog() { + Arrays.asList(node2, node1) + .forEach( + instance -> + ports.forEach( + (key, value) -> { + // Verify log list interface logs/ + given().get( + HOST + + key + + CONTEXT_PATH + + RestConstant.GET_LOGS) + .then() + .statusCode(200) + .body( + containsString( + clientJobProxy.getJobId() + + ".log")); + + given().get( + HOST + + value + + node1Config + .getEngineConfig() + .getHttpConfig() + .getContextPath() + + RestConstant.GET_LOGS) + .then() + .statusCode(200) + .body( + containsString( + clientJobProxy.getJobId() + + ".log")); + + // Verify log list interface logs/:jobId + String logListV1 = + given().get( + HOST + + key + + CONTEXT_PATH + + RestConstant.GET_LOGS + + "/" + + clientJobProxy + .getJobId()) + .body() + .prettyPrint(); + Assertions.assertTrue( + logListV1.contains( + clientJobProxy.getJobId() + ".log")); + + String logListV2 = + given().get( + HOST + + value + + node1Config + .getEngineConfig() + .getHttpConfig() + .getContextPath() + + RestConstant.GET_LOGS + + "/" + + clientJobProxy + .getJobId()) + .body() + .prettyPrint(); + Assertions.assertTrue( + logListV2.contains( + clientJobProxy.getJobId() + ".log")); + + // verify access log link + verifyLogLink(logListV1); + verifyLogLink(logListV2); + })); + } + + private static void verifyLogLink(String logListV1) { + Pattern pattern = Pattern.compile("href\\s*=\\s*\"([^\"]+)\""); + Matcher matcher = pattern.matcher(logListV1); + while (matcher.find()) { + String link = matcher.group(1); + Assertions.assertTrue( + given().get(link) + .body() + .prettyPrint() + .contains("Init JobMaster for Job fake_to_file")); + } + } + @Test public void testGetRunningJobById() { diff --git a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java index 8df9479ff6a..1aa52488f66 100644 --- a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java +++ b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java @@ -134,15 +134,17 @@ private void assertConsoleLog() { } private void assertFileLog() throws IOException, InterruptedException { - Container.ExecResult execResult = - server.execInContainer( - "sh", "-c", "cat /tmp/seatunnel/logs/job-862969647010611201.log"); + String catLog = "cat /tmp/seatunnel/logs/job-862969647010611201.log"; + String apiGetLog = "curl http://localhost:8080/log/job-862969647010611201.log"; + Container.ExecResult execResult = server.execInContainer("sh", "-c", catLog); String serverLogs = execResult.getStdout(); - execResult = - secondServer.execInContainer( - "sh", "-c", "cat /tmp/seatunnel/logs/job-862969647010611201.log"); + Container.ExecResult apiExecResult = server.execInContainer("sh", "-c", apiGetLog); + + execResult = secondServer.execInContainer("sh", "-c", catLog); String secondServerLogs = execResult.getStdout(); + Container.ExecResult apiSecondExecResult = + secondServer.execInContainer("sh", "-c", apiGetLog); Stream.of( // 2024-09-21 16:37:44,503 INFO [.f.s.FakeSourceSplitEnumerator] @@ -159,6 +161,10 @@ private void assertFileLog() throws IOException, InterruptedException { Assertions.assertTrue( pattern.matcher(serverLogs).find() || pattern.matcher(secondServerLogs).find()); + Assertions.assertTrue( + pattern.matcher(apiExecResult.getStdout()).find() + || pattern.matcher(apiSecondExecResult.getStdout()) + .find()); }); } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java index 0668308460a..f9fff817f77 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java @@ -25,6 +25,9 @@ import org.apache.seatunnel.engine.common.config.SeaTunnelConfig; import org.apache.seatunnel.engine.server.rest.filter.ExceptionHandlingFilter; +import org.apache.seatunnel.engine.server.rest.servlet.AllLogNameServlet; +import org.apache.seatunnel.engine.server.rest.servlet.AllNodeLogServlet; +import org.apache.seatunnel.engine.server.rest.servlet.CurrentNodeLogServlet; import org.apache.seatunnel.engine.server.rest.servlet.EncryptConfigServlet; import org.apache.seatunnel.engine.server.rest.servlet.FinishedJobsServlet; import org.apache.seatunnel.engine.server.rest.servlet.JobInfoServlet; @@ -48,6 +51,9 @@ import static org.apache.seatunnel.engine.server.rest.RestConstant.ENCRYPT_CONFIG; import static org.apache.seatunnel.engine.server.rest.RestConstant.FINISHED_JOBS_INFO; +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_ALL_LOG_NAME; +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_LOG; +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_LOGS; import static org.apache.seatunnel.engine.server.rest.RestConstant.JOB_INFO_URL; import static org.apache.seatunnel.engine.server.rest.RestConstant.OVERVIEW; import static org.apache.seatunnel.engine.server.rest.RestConstant.RUNNING_JOBS_URL; @@ -102,6 +108,12 @@ public void createJettyServer() { ServletHolder runningThreadsHolder = new ServletHolder(new RunningThreadsServlet(nodeEngine)); + ServletHolder allNodeLogServletHolder = + new ServletHolder(new AllNodeLogServlet(nodeEngine)); + ServletHolder currentNodeLogServlet = + new ServletHolder(new CurrentNodeLogServlet(nodeEngine)); + ServletHolder allLogNameServlet = new ServletHolder(new AllLogNameServlet(nodeEngine)); + context.addServlet(overviewHolder, convertUrlToPath(OVERVIEW)); context.addServlet(runningJobsHolder, convertUrlToPath(RUNNING_JOBS_URL)); context.addServlet(finishedJobsHolder, convertUrlToPath(FINISHED_JOBS_INFO)); @@ -119,6 +131,10 @@ public void createJettyServer() { context.addServlet(runningThreadsHolder, convertUrlToPath(RUNNING_THREADS)); + context.addServlet(allNodeLogServletHolder, convertUrlToPath(GET_LOGS)); + context.addServlet(currentNodeLogServlet, convertUrlToPath(GET_LOG)); + context.addServlet(allLogNameServlet, convertUrlToPath(GET_ALL_LOG_NAME)); + server.setHandler(context); try { diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/log/FormatType.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/log/FormatType.java new file mode 100644 index 00000000000..2dfa63ce63c --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/log/FormatType.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.seatunnel.engine.server.log; + +/** Log interface return format */ +public enum FormatType { + JSON, + // html is default format + HTML; + + public static FormatType fromString(String formatType) { + try { + return Enum.valueOf(FormatType.class, formatType.toUpperCase()); + } catch (Exception e) { + // if formatType is not valid, return default format + return HTML; + } + } +} diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestConstant.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestConstant.java index 97d858ee815..e8c39da7f28 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestConstant.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestConstant.java @@ -76,6 +76,12 @@ public class RestConstant { public static final String STOP_JOB_URL = "/stop-job"; public static final String STOP_JOBS_URL = "/stop-jobs"; public static final String UPDATE_TAGS_URL = "/update-tags"; + // Get All Nodes Log + public static final String GET_LOGS = "/logs"; + // Get Current Node Log + public static final String GET_LOG = "/log"; + // Code internal Use , Get Node Log Name + public static final String GET_ALL_LOG_NAME = "/get-all-log-name"; // metrics public static final String TELEMETRY_METRICS_URL = "/hazelcast/rest/instance/metrics"; diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java index b860dbc1c74..1b9cb65a6a1 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java @@ -20,10 +20,13 @@ import org.apache.seatunnel.shade.com.fasterxml.jackson.core.JsonProcessingException; import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.JsonNode; import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.node.ArrayNode; import org.apache.seatunnel.api.common.metrics.JobMetrics; import org.apache.seatunnel.api.table.catalog.TablePath; +import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException; import org.apache.seatunnel.common.utils.DateTimeUtils; +import org.apache.seatunnel.common.utils.FileUtils; import org.apache.seatunnel.common.utils.JsonUtils; import org.apache.seatunnel.engine.common.Constant; import org.apache.seatunnel.engine.common.env.EnvironmentUtil; @@ -34,8 +37,10 @@ import org.apache.seatunnel.engine.core.job.JobImmutableInformation; import org.apache.seatunnel.engine.core.job.JobInfo; import org.apache.seatunnel.engine.core.job.JobStatus; +import org.apache.seatunnel.engine.server.NodeExtension; import org.apache.seatunnel.engine.server.SeaTunnelServer; import org.apache.seatunnel.engine.server.dag.DAGUtils; +import org.apache.seatunnel.engine.server.log.FormatType; import org.apache.seatunnel.engine.server.log.Log4j2HttpGetCommandProcessor; import org.apache.seatunnel.engine.server.master.JobHistoryService.JobState; import org.apache.seatunnel.engine.server.operation.GetClusterHealthMetricsOperation; @@ -46,6 +51,13 @@ import org.apache.seatunnel.engine.server.utils.NodeEngineUtil; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.builder.api.Component; +import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration; +import org.apache.logging.log4j.core.config.properties.PropertiesConfiguration; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; import com.hazelcast.cluster.Address; import com.hazelcast.cluster.Cluster; @@ -53,22 +65,33 @@ import com.hazelcast.internal.ascii.TextCommandService; import com.hazelcast.internal.ascii.rest.HttpCommandProcessor; import com.hazelcast.internal.ascii.rest.HttpGetCommand; +import com.hazelcast.internal.ascii.rest.RestValue; import com.hazelcast.internal.json.JsonArray; import com.hazelcast.internal.json.JsonObject; import com.hazelcast.internal.json.JsonValue; import com.hazelcast.internal.util.JsonUtil; import com.hazelcast.internal.util.StringUtil; +import com.hazelcast.jet.datamodel.Tuple3; import com.hazelcast.jet.impl.execution.init.CustomClassLoadedObject; import com.hazelcast.map.IMap; import com.hazelcast.spi.impl.NodeEngine; import com.hazelcast.spi.impl.NodeEngineImpl; import io.prometheus.client.exporter.common.TextFormat; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.StringWriter; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -88,6 +111,9 @@ import static org.apache.seatunnel.api.common.metrics.MetricNames.SOURCE_RECEIVED_QPS; import static org.apache.seatunnel.engine.server.rest.RestConstant.CONTEXT_PATH; import static org.apache.seatunnel.engine.server.rest.RestConstant.FINISHED_JOBS_INFO; +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_ALL_LOG_NAME; +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_LOG; +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_LOGS; import static org.apache.seatunnel.engine.server.rest.RestConstant.JOB_INFO_URL; import static org.apache.seatunnel.engine.server.rest.RestConstant.OVERVIEW; import static org.apache.seatunnel.engine.server.rest.RestConstant.RUNNING_JOBS_URL; @@ -128,6 +154,7 @@ public RestHttpGetCommandProcessor( @Override public void handle(HttpGetCommand httpGetCommand) { String uri = httpGetCommand.getURI(); + try { if (uri.startsWith(CONTEXT_PATH + RUNNING_JOBS_URL)) { handleRunningJobsInfo(httpGetCommand); @@ -148,6 +175,12 @@ public void handle(HttpGetCommand httpGetCommand) { handleMetrics(httpGetCommand, TextFormat.CONTENT_TYPE_OPENMETRICS_100); } else if (uri.startsWith(CONTEXT_PATH + THREAD_DUMP)) { getThreadDump(httpGetCommand); + } else if (uri.startsWith(CONTEXT_PATH + GET_ALL_LOG_NAME)) { + getAllLogName(httpGetCommand); + } else if (uri.startsWith(CONTEXT_PATH + GET_LOGS)) { + getAllNodeLog(httpGetCommand, uri); + } else if (uri.startsWith(CONTEXT_PATH + GET_LOG)) { + getCurrentNodeLog(httpGetCommand, uri); } else { original.handle(httpGetCommand); } @@ -230,6 +263,11 @@ public void getThreadDump(HttpGetCommand command) { } private void getSystemMonitoringInformation(HttpGetCommand command) { + JsonArray jsonValues = getSystemMonitoringInformationJsonValues(); + this.prepareResponse(command, jsonValues); + } + + private JsonArray getSystemMonitoringInformationJsonValues() { Cluster cluster = textCommandService.getNode().hazelcastInstance.getCluster(); nodeEngine = textCommandService.getNode().hazelcastInstance.node.nodeEngine; @@ -262,7 +300,7 @@ private void getSystemMonitoringInformation(HttpGetCommand command) { return jobInfo; }) .collect(JsonArray::new, JsonArray::add, JsonArray::add); - this.prepareResponse(command, jsonValues); + return jsonValues; } private void handleRunningJobsInfo(HttpGetCommand command) { @@ -610,9 +648,8 @@ private Map aggregateMap(Map inputMap, boolean private void handleMetrics(HttpGetCommand httpGetCommand, String contentType) { StringWriter stringWriter = new StringWriter(); - org.apache.seatunnel.engine.server.NodeExtension nodeExtension = - (org.apache.seatunnel.engine.server.NodeExtension) - textCommandService.getNode().getNodeExtension(); + NodeExtension nodeExtension = + (NodeExtension) textCommandService.getNode().getNodeExtension(); try { TextFormat.writeFormat( contentType, @@ -763,4 +800,266 @@ private JsonObject getJobInfoJson(JobState jobState, String jobMetrics, JobDAGIn .add(RestConstant.PLUGIN_JARS_URLS, new JsonArray()) .add(RestConstant.METRICS, toJsonObject(getJobMetrics(jobMetrics))); } + + private PropertiesConfiguration getLogConfiguration() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + return (PropertiesConfiguration) context.getConfiguration(); + } + + private void getAllNodeLog(HttpGetCommand httpGetCommand, String uri) + throws NoSuchFieldException, IllegalAccessException { + + // Analysis uri, get logName and jobId param + String param = getParam(uri); + boolean isLogFile = param.contains(".log"); + String logName = isLogFile ? param : StringUtils.EMPTY; + String jobId = !isLogFile ? param : StringUtils.EMPTY; + + String logPath = getLogPath(); + if (StringUtils.isBlank(logPath)) { + logger.warning( + "Log file path is empty, no log file path configured in the current configuration file"); + httpGetCommand.send404(); + return; + } + JsonArray systemMonitoringInformationJsonValues = + getSystemMonitoringInformationJsonValues(); + + if (StringUtils.isBlank(logName)) { + StringBuffer logLink = new StringBuffer(); + ArrayList> allLogNameList = new ArrayList<>(); + + systemMonitoringInformationJsonValues.forEach( + systemMonitoringInformation -> { + String host = systemMonitoringInformation.asObject().get("host").asString(); + int port = + Integer.valueOf( + systemMonitoringInformation + .asObject() + .get("port") + .asString()); + String url = "http://" + host + ":" + port + CONTEXT_PATH; + String allName = sendGet(url + GET_ALL_LOG_NAME); + logger.fine(String.format("Request: %s , Result: %s", url, allName)); + ArrayNode jsonNodes = JsonUtils.parseArray(allName); + + jsonNodes.forEach( + jsonNode -> { + String fileName = jsonNode.asText(); + if (StringUtils.isNotBlank(jobId) + && !fileName.contains(jobId)) { + return; + } + allLogNameList.add( + Tuple3.tuple3( + host + ":" + port, + url + GET_LOGS + "/" + fileName, + fileName)); + }); + }); + FormatType formatType = getFormatType(uri); + switch (formatType) { + case JSON: + JsonArray jsonArray = + allLogNameList.stream() + .map( + tuple -> { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("node", tuple.f0()); + jsonObject.add("logLink", tuple.f1()); + jsonObject.add("logName", tuple.f2()); + return jsonObject; + }) + .collect(JsonArray::new, JsonArray::add, JsonArray::add); + this.prepareResponse(httpGetCommand, jsonArray); + return; + case HTML: + default: + allLogNameList.forEach( + tuple -> + logLink.append( + buildLogLink( + tuple.f1(), tuple.f0() + "-" + tuple.f2()))); + String logContent = buildWebSiteContent(logLink); + this.prepareResponse(httpGetCommand, getRestValue(logContent)); + } + } else { + prepareLogResponse(httpGetCommand, logPath, logName); + } + } + + private FormatType getFormatType(String uri) { + Map uriParam = getUriParam(uri); + return FormatType.fromString(uriParam.get("format")); + } + + private Map getUriParam(String uri) { + String queryString = uri.contains("?") ? uri.substring(uri.indexOf("?") + 1) : ""; + return Arrays.stream(queryString.split("&")) + .map(param -> param.split("=", 2)) + .filter(pair -> pair.length == 2) + .collect(Collectors.toMap(pair -> pair[0], pair -> pair[1])); + } + + private String getParam(String uri) { + uri = StringUtil.stripTrailingSlash(uri); + int indexEnd = uri.indexOf('/', URI_MAPS.length()); + if (indexEnd != -1) { + String param = uri.substring(indexEnd + 1); + logger.fine(String.format("Request: %s , Param: %s", uri, param)); + return param; + } + return StringUtils.EMPTY; + } + + private static RestValue getRestValue(String logContent) { + RestValue restValue = new RestValue(); + restValue.setContentType("text/html; charset=UTF-8".getBytes(StandardCharsets.UTF_8)); + restValue.setValue(logContent.getBytes(StandardCharsets.UTF_8)); + return restValue; + } + + private static String buildWebSiteContent(StringBuffer logLink) { + return "Seatunnel log\n" + + "\n" + + "

Seatunnel log

\n" + + "
    \n" + + logLink.toString() + + "
\n" + + ""; + } + + private String getFileLogPath(PropertiesConfiguration config) + throws NoSuchFieldException, IllegalAccessException { + Field propertiesField = BuiltConfiguration.class.getDeclaredField("appendersComponent"); + propertiesField.setAccessible(true); + Component propertiesComponent = (Component) propertiesField.get(config); + StrSubstitutor substitutor = config.getStrSubstitutor(); + return propertiesComponent.getComponents().stream() + .filter(component -> "fileAppender".equals(component.getAttributes().get("name"))) + .map(component -> substitutor.replace(component.getAttributes().get("fileName"))) + .findFirst() + .orElse(null); + } + + /** Get configuration log path */ + private String getLogPath() throws NoSuchFieldException, IllegalAccessException { + String routingAppender = "routingAppender"; + String fileAppender = "fileAppender"; + PropertiesConfiguration config = getLogConfiguration(); + // Get routingAppender log file path + String routingLogFilePath = getRoutingLogFilePath(config); + + // Get fileAppender log file path + String fileLogPath = getFileLogPath(config); + String logRef = + config.getLoggerConfig(StringUtils.EMPTY).getAppenderRefs().stream() + .map(Object::toString) + .filter(ref -> ref.contains(routingAppender) || ref.contains(fileAppender)) + .findFirst() + .orElse(StringUtils.EMPTY); + if (logRef.equals(routingAppender)) { + return routingLogFilePath.substring(0, routingLogFilePath.lastIndexOf("/")); + } else if (logRef.equals(fileAppender)) { + return fileLogPath.substring(0, routingLogFilePath.lastIndexOf("/")); + } else { + logger.warning(String.format("Log file path is empty, get logRef : %s", logRef)); + return null; + } + } + + /** Get Current Node Log By /log request */ + private void getCurrentNodeLog(HttpGetCommand httpGetCommand, String uri) + throws NoSuchFieldException, IllegalAccessException { + String logName = getParam(uri); + String logPath = getLogPath(); + + if (StringUtils.isBlank(logName)) { + // Get Current Node Log List + List logFileList = FileUtils.listFile(logPath); + StringBuffer logLink = new StringBuffer(); + for (File file : logFileList) { + logLink.append(buildLogLink("log/" + file.getName(), file.getName())); + } + this.prepareResponse(httpGetCommand, getRestValue(buildWebSiteContent(logLink))); + } else { + // Get Current Node Log Content + prepareLogResponse(httpGetCommand, logPath, logName); + } + } + + /** Prepare Log Response */ + private void prepareLogResponse(HttpGetCommand httpGetCommand, String logPath, String logName) { + String logFilePath = logPath + "/" + logName; + try { + String logContent = FileUtils.readFileToStr(new File(logFilePath).toPath()); + this.prepareResponse(httpGetCommand, logContent); + } catch (SeaTunnelRuntimeException e) { + // If the log file does not exist, return 400 + httpGetCommand.send400(); + logger.warning( + String.format("Log file content is empty, get log path : %s", logFilePath)); + } + } + + public String buildLogLink(String href, String name) { + return "
  • " + name + "
  • \n"; + } + + private static String sendGet(String urlString) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(urlString).openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.connect(); + + if (connection.getResponseCode() == 200) { + try (InputStream is = connection.getInputStream(); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int len; + while ((len = is.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + return baos.toString(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private void getAllLogName(HttpGetCommand httpGetCommand) + throws NoSuchFieldException, IllegalAccessException { + String logPath = getLogPath(); + List logFileList = FileUtils.listFile(logPath); + List fileNameList = + logFileList.stream().map(File::getName).collect(Collectors.toList()); + try { + this.prepareResponse(httpGetCommand, JsonUtils.toJsonString(fileNameList)); + } catch (SeaTunnelRuntimeException e) { + httpGetCommand.send400(); + logger.warning(String.format("Log file name get failed, get log path: %s", logPath)); + } + } + + private static String getRoutingLogFilePath(PropertiesConfiguration config) + throws NoSuchFieldException, IllegalAccessException { + Field propertiesField = BuiltConfiguration.class.getDeclaredField("appendersComponent"); + propertiesField.setAccessible(true); + Component propertiesComponent = (Component) propertiesField.get(config); + StrSubstitutor substitutor = config.getStrSubstitutor(); + return propertiesComponent.getComponents().stream() + .filter( + component -> + "routingAppender".equals(component.getAttributes().get("name"))) + .flatMap(component -> component.getComponents().stream()) + .flatMap(component -> component.getComponents().stream()) + .flatMap(component -> component.getComponents().stream()) + .map(component -> substitutor.replace(component.getAttributes().get("fileName"))) + .findFirst() + .orElse(null); + } } diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllLogNameServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllLogNameServlet.java new file mode 100644 index 00000000000..20220958866 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllLogNameServlet.java @@ -0,0 +1,57 @@ +/* + * 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.seatunnel.engine.server.rest.servlet; + +import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException; +import org.apache.seatunnel.common.utils.FileUtils; + +import com.hazelcast.spi.impl.NodeEngineImpl; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +public class AllLogNameServlet extends LogBaseServlet { + + public AllLogNameServlet(NodeEngineImpl nodeEngine) { + super(nodeEngine); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String logPath = getLogPath(); + List logFileList = FileUtils.listFile(logPath); + List fileNameList = + logFileList.stream().map(File::getName).collect(Collectors.toList()); + try { + writeJson(resp, fileNameList); + } catch (SeaTunnelRuntimeException e) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + log.warn(String.format("Log file name get failed, get log path: %s", logPath)); + } + } +} diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllNodeLogServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllNodeLogServlet.java new file mode 100644 index 00000000000..a9932d85b31 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/AllNodeLogServlet.java @@ -0,0 +1,130 @@ +/* + * 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.seatunnel.engine.server.rest.servlet; + +import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.node.ArrayNode; + +import org.apache.seatunnel.common.utils.JsonUtils; +import org.apache.seatunnel.engine.common.config.server.HttpConfig; +import org.apache.seatunnel.engine.server.SeaTunnelServer; +import org.apache.seatunnel.engine.server.log.FormatType; + +import org.apache.commons.lang3.StringUtils; + +import com.hazelcast.internal.json.JsonArray; +import com.hazelcast.internal.json.JsonObject; +import com.hazelcast.jet.datamodel.Tuple3; +import com.hazelcast.spi.impl.NodeEngineImpl; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.ArrayList; + +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_ALL_LOG_NAME; +import static org.apache.seatunnel.engine.server.rest.RestConstant.GET_LOGS; + +@Slf4j +public class AllNodeLogServlet extends LogBaseServlet { + + public AllNodeLogServlet(NodeEngineImpl nodeEngine) { + super(nodeEngine); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + SeaTunnelServer seaTunnelServer = getSeaTunnelServer(false); + HttpConfig httpConfig = + seaTunnelServer.getSeaTunnelConfig().getEngineConfig().getHttpConfig(); + String contextPath = httpConfig.getContextPath(); + int port = httpConfig.getPort(); + String uri = req.getRequestURI(); + + // Analysis uri, get logName and jobId param + String param = getLogParam(uri, contextPath); + boolean isLogFile = param.contains(".log"); + String logName = isLogFile ? param : StringUtils.EMPTY; + String jobId = !isLogFile ? param : StringUtils.EMPTY; + + String logPath = getLogPath(); + JsonArray systemMonitoringInformationJsonValues = + getSystemMonitoringInformationJsonValues(); + + if (StringUtils.isBlank(logName)) { + StringBuffer logLink = new StringBuffer(); + ArrayList> allLogNameList = new ArrayList<>(); + + systemMonitoringInformationJsonValues.forEach( + systemMonitoringInformation -> { + String host = systemMonitoringInformation.asObject().get("host").asString(); + String url = "http://" + host + ":" + port + contextPath; + String allName = sendGet(url + GET_ALL_LOG_NAME); + log.debug(String.format("Request: %s , Result: %s", url, allName)); + ArrayNode jsonNodes = JsonUtils.parseArray(allName); + + jsonNodes.forEach( + jsonNode -> { + String fileName = jsonNode.asText(); + if (StringUtils.isNotBlank(jobId) + && !fileName.contains(jobId)) { + return; + } + allLogNameList.add( + Tuple3.tuple3( + host + ":" + port, + url + GET_LOGS + "/" + fileName, + fileName)); + }); + }); + + FormatType formatType = FormatType.fromString(req.getParameter("format")); + switch (formatType) { + case JSON: + JsonArray jsonArray = + allLogNameList.stream() + .map( + tuple -> { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("node", tuple.f0()); + jsonObject.add("logLink", tuple.f1()); + jsonObject.add("logName", tuple.f2()); + return jsonObject; + }) + .collect(JsonArray::new, JsonArray::add, JsonArray::add); + writeJson(resp, jsonArray); + return; + case HTML: + default: + allLogNameList.forEach( + tuple -> + logLink.append( + buildLogLink( + tuple.f1(), tuple.f0() + "-" + tuple.f2()))); + String logContent = buildWebSiteContent(logLink); + writeHtml(resp, logContent); + } + } else { + prepareLogResponse(resp, logPath, logName); + } + } +} diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java index 5553e2c85ec..df597411463 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/BaseServlet.java @@ -40,6 +40,7 @@ import org.apache.seatunnel.engine.server.dag.DAGUtils; import org.apache.seatunnel.engine.server.master.JobHistoryService; import org.apache.seatunnel.engine.server.operation.CancelJobOperation; +import org.apache.seatunnel.engine.server.operation.GetClusterHealthMetricsOperation; import org.apache.seatunnel.engine.server.operation.GetJobMetricsOperation; import org.apache.seatunnel.engine.server.operation.GetJobStatusOperation; import org.apache.seatunnel.engine.server.operation.SavePointJobOperation; @@ -53,6 +54,9 @@ import org.apache.commons.lang3.StringUtils; import com.google.gson.Gson; +import com.hazelcast.cluster.Address; +import com.hazelcast.cluster.Cluster; +import com.hazelcast.cluster.Member; import com.hazelcast.instance.impl.Node; import com.hazelcast.internal.json.JsonArray; import com.hazelcast.internal.json.JsonObject; @@ -61,6 +65,7 @@ import com.hazelcast.internal.util.JsonUtil; import com.hazelcast.jet.impl.execution.init.CustomClassLoadedObject; import com.hazelcast.spi.impl.NodeEngineImpl; +import lombok.extern.slf4j.Slf4j; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -72,6 +77,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -93,6 +100,7 @@ import static org.apache.seatunnel.engine.server.rest.RestConstant.TABLE_SOURCE_RECEIVED_COUNT; import static org.apache.seatunnel.engine.server.rest.RestConstant.TABLE_SOURCE_RECEIVED_QPS; +@Slf4j public class BaseServlet extends HttpServlet { protected final NodeEngineImpl nodeEngine; @@ -137,6 +145,16 @@ protected void writeJson(HttpServletResponse resp, Object obj, int statusCode) resp.getWriter().write(new Gson().toJson(obj)); } + protected void write(HttpServletResponse resp, Object obj) throws IOException { + resp.setContentType("text/plain"); + resp.getWriter().write(obj.toString()); + } + + protected void writeHtml(HttpServletResponse resp, Object obj) throws IOException { + resp.setContentType("text/html; charset=UTF-8"); + resp.getWriter().write(obj.toString()); + } + protected JsonObject convertToJson(JobInfo jobInfo, long jobId) { JsonObject jobInfoJson = new JsonObject(); @@ -584,6 +602,41 @@ private void submitJob( voidPassiveCompletableFuture.join(); } + protected JsonArray getSystemMonitoringInformationJsonValues() { + Cluster cluster = nodeEngine.getHazelcastInstance().getCluster(); + + Set members = cluster.getMembers(); + JsonArray jsonValues = + members.stream() + .map( + member -> { + Address address = member.getAddress(); + String input = null; + try { + input = + (String) + NodeEngineUtil.sendOperationToMemberNode( + nodeEngine, + new GetClusterHealthMetricsOperation(), + address) + .get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to get cluster health metrics", e); + } + String[] parts = input.split(", "); + JsonObject jobInfo = new JsonObject(); + Arrays.stream(parts) + .forEach( + part -> { + String[] keyValue = part.split("="); + jobInfo.add(keyValue[0], keyValue[1]); + }); + return jobInfo; + }) + .collect(JsonArray::new, JsonArray::add, JsonArray::add); + return jsonValues; + } + private JsonObject metricsToJsonObject(Map jobMetrics) { JsonObject members = new JsonObject(); jobMetrics.forEach( diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/CurrentNodeLogServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/CurrentNodeLogServlet.java new file mode 100644 index 00000000000..d73c3d785d6 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/CurrentNodeLogServlet.java @@ -0,0 +1,69 @@ +/* + * 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.seatunnel.engine.server.rest.servlet; + +import org.apache.seatunnel.common.utils.FileUtils; +import org.apache.seatunnel.engine.common.config.server.HttpConfig; +import org.apache.seatunnel.engine.server.SeaTunnelServer; + +import org.apache.commons.lang3.StringUtils; + +import com.hazelcast.spi.impl.NodeEngineImpl; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +@Slf4j +public class CurrentNodeLogServlet extends LogBaseServlet { + + public CurrentNodeLogServlet(NodeEngineImpl nodeEngine) { + super(nodeEngine); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + SeaTunnelServer seaTunnelServer = getSeaTunnelServer(false); + HttpConfig httpConfig = + seaTunnelServer.getSeaTunnelConfig().getEngineConfig().getHttpConfig(); + String contextPath = httpConfig.getContextPath(); + String uri = req.getRequestURI(); + String logName = getLogParam(uri, contextPath); + String logPath = getLogPath(); + + if (StringUtils.isBlank(logName)) { + // Get Current Node Log List + List logFileList = FileUtils.listFile(logPath); + StringBuffer logLink = new StringBuffer(); + for (File file : logFileList) { + logLink.append(buildLogLink("log/" + file.getName(), file.getName())); + } + writeHtml(resp, buildWebSiteContent(logLink)); + } else { + // Get Current Node Log Content + prepareLogResponse(resp, logPath, logName); + } + } +} diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java new file mode 100644 index 00000000000..42eb815980f --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java @@ -0,0 +1,191 @@ +/* + * 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.seatunnel.engine.server.rest.servlet; + +import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException; +import org.apache.seatunnel.common.utils.ExceptionUtils; +import org.apache.seatunnel.common.utils.FileUtils; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.builder.api.Component; +import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration; +import org.apache.logging.log4j.core.config.properties.PropertiesConfiguration; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; + +import com.hazelcast.internal.util.StringUtil; +import com.hazelcast.spi.impl.NodeEngineImpl; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletResponse; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.URL; + +@Slf4j +public class LogBaseServlet extends BaseServlet { + + public LogBaseServlet(NodeEngineImpl nodeEngine) { + super(nodeEngine); + } + + protected String getLogParam(String uri, String contextPath) { + uri = uri.substring(uri.indexOf(contextPath) + contextPath.length()); + uri = StringUtil.stripTrailingSlash(uri).substring(1); + int indexEnd = uri.indexOf('/'); + if (indexEnd != -1) { + String param = uri.substring(indexEnd + 1); + return param; + } + return ""; + } + + /** Get configuration log path */ + protected String getLogPath() { + try { + String routingAppender = "routingAppender"; + String fileAppender = "fileAppender"; + PropertiesConfiguration config = getLogConfiguration(); + // Get routingAppender log file path + String routingLogFilePath = getRoutingLogFilePath(config); + + // Get fileAppender log file path + String fileLogPath = getFileLogPath(config); + String logRef = + config.getLoggerConfig(StringUtils.EMPTY).getAppenderRefs().stream() + .map(Object::toString) + .filter( + ref -> + ref.contains(routingAppender) + || ref.contains(fileAppender)) + .findFirst() + .orElse(StringUtils.EMPTY); + if (logRef.equals(routingAppender)) { + return routingLogFilePath.substring(0, routingLogFilePath.lastIndexOf("/")); + } else if (logRef.equals(fileAppender)) { + return fileLogPath.substring(0, routingLogFilePath.lastIndexOf("/")); + } else { + log.warn(String.format("Log file path is empty, get logRef : %s", logRef)); + return null; + } + } catch (NoSuchFieldException | IllegalAccessException e) { + log.error("Get log path error", ExceptionUtils.getMessage(e)); + return null; + } + } + + private String getFileLogPath(PropertiesConfiguration config) + throws NoSuchFieldException, IllegalAccessException { + Field propertiesField = BuiltConfiguration.class.getDeclaredField("appendersComponent"); + propertiesField.setAccessible(true); + Component propertiesComponent = (Component) propertiesField.get(config); + StrSubstitutor substitutor = config.getStrSubstitutor(); + return propertiesComponent.getComponents().stream() + .filter(component -> "fileAppender".equals(component.getAttributes().get("name"))) + .map(component -> substitutor.replace(component.getAttributes().get("fileName"))) + .findFirst() + .orElse(null); + } + + private String getRoutingLogFilePath(PropertiesConfiguration config) + throws NoSuchFieldException, IllegalAccessException { + Field propertiesField = BuiltConfiguration.class.getDeclaredField("appendersComponent"); + propertiesField.setAccessible(true); + Component propertiesComponent = (Component) propertiesField.get(config); + StrSubstitutor substitutor = config.getStrSubstitutor(); + return propertiesComponent.getComponents().stream() + .filter( + component -> + "routingAppender".equals(component.getAttributes().get("name"))) + .flatMap(component -> component.getComponents().stream()) + .flatMap(component -> component.getComponents().stream()) + .flatMap(component -> component.getComponents().stream()) + .map(component -> substitutor.replace(component.getAttributes().get("fileName"))) + .findFirst() + .orElse(null); + } + + private PropertiesConfiguration getLogConfiguration() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + return (PropertiesConfiguration) context.getConfiguration(); + } + + protected String sendGet(String urlString) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(urlString).openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.connect(); + + if (connection.getResponseCode() == 200) { + try (InputStream is = connection.getInputStream(); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int len; + while ((len = is.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + return baos.toString(); + } + } + } catch (IOException e) { + log.error("Send get Fail.", ExceptionUtils.getMessage(e)); + } + return null; + } + + protected String buildLogLink(String href, String name) { + return "
  • " + name + "
  • \n"; + } + + protected String buildWebSiteContent(StringBuffer logLink) { + return "Seatunnel log\n" + + "\n" + + "

    Seatunnel log

    \n" + + "
      \n" + + logLink.toString() + + "
    \n" + + ""; + } + + /** Prepare Log Response */ + protected void prepareLogResponse(HttpServletResponse resp, String logPath, String logName) { + if (StringUtils.isBlank(logPath)) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + log.warn( + "Log file path is empty, no log file path configured in the current configuration file"); + return; + } + String logFilePath = logPath + "/" + logName; + try { + String logContent = FileUtils.readFileToStr(new File(logFilePath).toPath()); + write(resp, logContent); + } catch (SeaTunnelRuntimeException | IOException e) { + // If the log file does not exist, return 400 + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + log.warn(String.format("Log file content is empty, get log path : %s", logFilePath)); + } + } +} diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/SystemMonitoringServlet.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/SystemMonitoringServlet.java index 0643695e48e..9439c9e58e6 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/SystemMonitoringServlet.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/SystemMonitoringServlet.java @@ -17,14 +17,7 @@ package org.apache.seatunnel.engine.server.rest.servlet; -import org.apache.seatunnel.engine.server.operation.GetClusterHealthMetricsOperation; -import org.apache.seatunnel.engine.server.utils.NodeEngineUtil; - -import com.hazelcast.cluster.Address; -import com.hazelcast.cluster.Cluster; -import com.hazelcast.cluster.Member; import com.hazelcast.internal.json.JsonArray; -import com.hazelcast.internal.json.JsonObject; import com.hazelcast.spi.impl.NodeEngineImpl; import lombok.extern.slf4j.Slf4j; @@ -33,9 +26,6 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Arrays; -import java.util.Set; -import java.util.concurrent.ExecutionException; @Slf4j public class SystemMonitoringServlet extends BaseServlet { @@ -47,37 +37,7 @@ public SystemMonitoringServlet(NodeEngineImpl nodeEngine) { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Cluster cluster = nodeEngine.getHazelcastInstance().getCluster(); - - Set members = cluster.getMembers(); - JsonArray jsonValues = - members.stream() - .map( - member -> { - Address address = member.getAddress(); - String input = null; - try { - input = - (String) - NodeEngineUtil.sendOperationToMemberNode( - nodeEngine, - new GetClusterHealthMetricsOperation(), - address) - .get(); - } catch (InterruptedException | ExecutionException e) { - log.error("Failed to get cluster health metrics", e); - } - String[] parts = input.split(", "); - JsonObject jobInfo = new JsonObject(); - Arrays.stream(parts) - .forEach( - part -> { - String[] keyValue = part.split("="); - jobInfo.add(keyValue[0], keyValue[1]); - }); - return jobInfo; - }) - .collect(JsonArray::new, JsonArray::add, JsonArray::add); + JsonArray jsonValues = getSystemMonitoringInformationJsonValues(); writeJson(resp, jsonValues); } } From c0f27c2f76c6562b18a7b4c76d2c7ef4452f4b4a Mon Sep 17 00:00:00 2001 From: zhangdonghao <39961809+hawk9821@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:41:38 +0800 Subject: [PATCH 24/72] [Feature][Connector-V2] Piamon Sink supports changelog-procuder is lookup and full-compaction mode (#7834) --- docs/en/connector-v2/sink/Paimon.md | 45 +++++- docs/zh/connector-v2/sink/Paimon.md | 70 ++++++-- .../paimon/config/PaimonSinkConfig.java | 33 +++- .../seatunnel/paimon/sink/PaimonSink.java | 15 +- .../paimon/sink/PaimonSinkWriter.java | 48 +++++- .../seatunnel/paimon/utils/SchemaUtil.java | 5 + .../e2e/connector/paimon/PaimonRecord.java | 30 ++++ .../e2e/connector/paimon/PaimonSinkCDCIT.java | 152 +++++++++++++++++- ...ngelog_fake_cdc_sink_paimon_case1_ddl.conf | 53 ++++++ ...ake_cdc_sink_paimon_case1_insert_data.conf | 67 ++++++++ ...ake_cdc_sink_paimon_case1_update_data.conf | 71 ++++++++ .../changelog_fake_cdc_sink_paimon_case2.conf | 83 ++++++++++ .../resources/changelog_paimon_to_paimon.conf | 48 ++++++ .../container/AbstractTestContainer.java | 24 ++- .../e2e/common/container/TestContainer.java | 13 +- .../flink/AbstractTestFlinkContainer.java | 9 +- .../ConnectorPackageServiceContainer.java | 9 +- .../seatunnel/SeaTunnelContainer.java | 27 +++- .../spark/AbstractTestSparkContainer.java | 10 +- .../e2e/common/util/JobIdGenerator.java | 27 ++++ 20 files changed, 786 insertions(+), 53 deletions(-) create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_ddl.conf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_insert_data.conf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_update_data.conf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case2.conf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_paimon_to_paimon.conf create mode 100644 seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java diff --git a/docs/en/connector-v2/sink/Paimon.md b/docs/en/connector-v2/sink/Paimon.md index 8133b6e8360..c9e4b3a9b61 100644 --- a/docs/en/connector-v2/sink/Paimon.md +++ b/docs/en/connector-v2/sink/Paimon.md @@ -31,7 +31,7 @@ libfb303-xxx.jar ## Options -| name | type | required | default value | Description | +| name | type | required | default value | Description | |-----------------------------|--------|----------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| | warehouse | String | Yes | - | Paimon warehouse path | | catalog_type | String | No | filesystem | Catalog type of Paimon, support filesystem and hive | @@ -43,7 +43,7 @@ libfb303-xxx.jar | data_save_mode | Enum | No | APPEND_DATA | The data save mode | | paimon.table.primary-keys | String | No | - | Default comma-separated list of columns (primary key) that identify a row in tables.(Notice: The partition field needs to be included in the primary key fields) | | paimon.table.partition-keys | String | No | - | Default comma-separated list of partition fields to use when creating tables. | -| paimon.table.write-props | Map | No | - | Properties passed through to paimon table initialization, [reference](https://paimon.apache.org/docs/master/maintenance/configurations/#coreoptions). | +| paimon.table.write-props | Map | No | - | Properties passed through to paimon table initialization, [reference](https://paimon.apache.org/docs/master/maintenance/configurations/#coreoptions). | | paimon.hadoop.conf | Map | No | - | Properties in hadoop conf | | paimon.hadoop.conf-path | String | No | - | The specified loading path for the 'core-site.xml', 'hdfs-site.xml', 'hive-site.xml' files | @@ -52,9 +52,14 @@ You must configure the `changelog-producer=input` option to enable the changelog The changelog producer mode of the paimon table has [four mode](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/) which is `none`、`input`、`lookup` and `full-compaction`. -Currently, we only support the `none` and `input` mode. The default is `none` which will not output the changelog file. The `input` mode will output the changelog file in paimon table. +All `changelog-producer` modes are currently supported. The default is `none`. -When you use a streaming mode to read paimon table, these two mode will produce [different results](https://github.com/apache/seatunnel/blob/dev/docs/en/connector-v2/source/Paimon.md#changelog). +* [`none`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#none) +* [`input`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#input) +* [`lookup`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#lookup) +* [`full-compaction`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#full-compaction) +> note: +> When you use a streaming mode to read paimon table,different mode will produce [different results](https://github.com/apache/seatunnel/blob/dev/docs/en/connector-v2/source/Paimon.md#changelog)。 ## Examples @@ -250,6 +255,38 @@ sink { } ``` +#### Write with the `changelog-producer` attribute + +```hocon +env { + parallelism = 1 + job.mode = "STREAMING" + checkpoint.interval = 5000 +} + +source { + Mysql-CDC { + base-url = "jdbc:mysql://127.0.0.1:3306/seatunnel" + username = "root" + password = "******" + table-names = ["seatunnel.role"] + } +} + +sink { + Paimon { + catalog_name = "seatunnel_test" + warehouse = "file:///tmp/seatunnel/paimon/hadoop-sink/" + database = "seatunnel" + table = "role" + paimon.table.write-props = { + changelog-producer = full-compaction + changelog-tmp-path = /tmp/paimon/changelog + } + } +} +``` + ### Write to dynamic bucket table Single dynamic bucket table with write props of paimon,operates on the primary key table and bucket is -1. diff --git a/docs/zh/connector-v2/sink/Paimon.md b/docs/zh/connector-v2/sink/Paimon.md index 32d35a5e958..375c8c90caf 100644 --- a/docs/zh/connector-v2/sink/Paimon.md +++ b/docs/zh/connector-v2/sink/Paimon.md @@ -30,30 +30,35 @@ libfb303-xxx.jar ## 连接器选项 -| 名称 | 类型 | 是否必须 | 默认值 | 描述 | -|-----------------------------|-------|----------|------------------------------|---------------------------------------------------------------------------------------------------| -| warehouse | 字符串 | 是 | - | Paimon warehouse路径 | -| catalog_type | 字符串 | 否 | filesystem | Paimon的catalog类型,目前支持filesystem和hive | -| catalog_uri | 字符串 | 否 | - | Paimon catalog的uri,仅当catalog_type为hive时需要配置 | -| database | 字符串 | 是 | - | 数据库名称 | -| table | 字符串 | 是 | - | 表名 | -| hdfs_site_path | 字符串 | 否 | - | hdfs-site.xml文件路径 | -| schema_save_mode | 枚举 | 否 | CREATE_SCHEMA_WHEN_NOT_EXIST | Schema保存模式 | -| data_save_mode | 枚举 | 否 | APPEND_DATA | 数据保存模式 | -| paimon.table.primary-keys | 字符串 | 否 | - | 主键字段列表,联合主键使用逗号分隔(注意:分区字段需要包含在主键字段中) | -| paimon.table.partition-keys | 字符串 | 否 | - | 分区字段列表,多字段使用逗号分隔 | -| paimon.table.write-props | Map | 否 | - | Paimon表初始化指定的属性, [参考](https://paimon.apache.org/docs/master/maintenance/configurations/#coreoptions) | -| paimon.hadoop.conf | Map | 否 | - | Hadoop配置文件属性信息 | -| paimon.hadoop.conf-path | 字符串 | 否 | - | Hadoop配置文件目录,用于加载'core-site.xml', 'hdfs-site.xml', 'hive-site.xml'文件配置 | +| 名称 | 类型 | 是否必须 | 默认值 | 描述 | +|-----------------------------|------|------|------------------------------|-------------------------------------------------------------------------------------------------------| +| warehouse | 字符串 | 是 | - | Paimon warehouse路径 | +| catalog_type | 字符串 | 否 | filesystem | Paimon的catalog类型,目前支持filesystem和hive | +| catalog_uri | 字符串 | 否 | - | Paimon catalog的uri,仅当catalog_type为hive时需要配置 | +| database | 字符串 | 是 | - | 数据库名称 | +| table | 字符串 | 是 | - | 表名 | +| hdfs_site_path | 字符串 | 否 | - | hdfs-site.xml文件路径 | +| schema_save_mode | 枚举 | 否 | CREATE_SCHEMA_WHEN_NOT_EXIST | Schema保存模式 | +| data_save_mode | 枚举 | 否 | APPEND_DATA | 数据保存模式 | +| paimon.table.primary-keys | 字符串 | 否 | - | 主键字段列表,联合主键使用逗号分隔(注意:分区字段需要包含在主键字段中) | +| paimon.table.partition-keys | 字符串 | 否 | - | 分区字段列表,多字段使用逗号分隔 | +| paimon.table.write-props | Map | 否 | - | Paimon表初始化指定的属性, [参考](https://paimon.apache.org/docs/master/maintenance/configurations/#coreoptions) | +| paimon.hadoop.conf | Map | 否 | - | Hadoop配置文件属性信息 | +| paimon.hadoop.conf-path | 字符串 | 否 | - | Hadoop配置文件目录,用于加载'core-site.xml', 'hdfs-site.xml', 'hive-site.xml'文件配置 | ## 更新日志 你必须配置`changelog-producer=input`来启用paimon表的changelog产生模式。如果你使用了paimon sink的自动建表功能,你可以在`paimon.table.write-props`中指定这个属性。 Paimon表的changelog产生模式有[四种](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/),分别是`none`、`input`、`lookup` 和 `full-compaction`。 -目前,我们只支持`none`和`input`模式。默认是`none`,这种模式将不会产生changelog文件。`input`模式将会在Paimon表下产生changelog文件。 +目前支持全部`changelog-producer`模式。默认是`none`模式。 -当你使用流模式去读paimon表的数据时,这两种模式将会产生[不同的结果](https://github.com/apache/seatunnel/blob/dev/docs/en/connector-v2/source/Paimon.md#changelog)。 +* [`none`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#none) +* [`input`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#input) +* [`lookup`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#lookup) +* [`full-compaction`](https://paimon.apache.org/docs/master/primary-key-table/changelog-producer/#full-compaction) +> 注意: + > 当你使用流模式去读paimon表的数据时,不同模式将会产生[不同的结果](https://github.com/apache/seatunnel/blob/dev/docs/en/connector-v2/source/Paimon.md#changelog)。 ## 示例 @@ -248,6 +253,37 @@ sink { } } ``` +#### 使用`changelog-producer`属性写入 + +```hocon +env { + parallelism = 1 + job.mode = "STREAMING" + checkpoint.interval = 5000 +} + +source { + Mysql-CDC { + base-url = "jdbc:mysql://127.0.0.1:3306/seatunnel" + username = "root" + password = "******" + table-names = ["seatunnel.role"] + } +} + +sink { + Paimon { + catalog_name = "seatunnel_test" + warehouse = "file:///tmp/seatunnel/paimon/hadoop-sink/" + database = "seatunnel" + table = "role" + paimon.table.write-props = { + changelog-producer = full-compaction + changelog-tmp-path = /tmp/paimon/changelog + } + } +} +``` ### 动态分桶paimon单表 diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/config/PaimonSinkConfig.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/config/PaimonSinkConfig.java index 9b358a2e8c4..87766ff96b0 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/config/PaimonSinkConfig.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/config/PaimonSinkConfig.java @@ -23,16 +23,22 @@ import org.apache.seatunnel.api.sink.DataSaveMode; import org.apache.seatunnel.api.sink.SchemaSaveMode; +import org.apache.paimon.CoreOptions; + import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; @Getter @Slf4j public class PaimonSinkConfig extends PaimonConfig { + + public static final String CHANGELOG_TMP_PATH = "changelog-tmp-path"; + public static final Option SCHEMA_SAVE_MODE = Options.key("schema_save_mode") .enumType(SchemaSaveMode.class) @@ -44,7 +50,6 @@ public class PaimonSinkConfig extends PaimonConfig { .enumType(DataSaveMode.class) .defaultValue(DataSaveMode.APPEND_DATA) .withDescription("data_save_mode"); - public static final Option PRIMARY_KEYS = Options.key("paimon.table.primary-keys") .stringType() @@ -66,11 +71,13 @@ public class PaimonSinkConfig extends PaimonConfig { .withDescription( "Properties passed through to paimon table initialization, such as 'file.format', 'bucket'(org.apache.paimon.CoreOptions)"); - private SchemaSaveMode schemaSaveMode; - private DataSaveMode dataSaveMode; - private List primaryKeys; - private List partitionKeys; - private Map writeProps; + private final SchemaSaveMode schemaSaveMode; + private final DataSaveMode dataSaveMode; + private final CoreOptions.ChangelogProducer changelogProducer; + private final String changelogTmpPath; + private final List primaryKeys; + private final List partitionKeys; + private final Map writeProps; public PaimonSinkConfig(ReadonlyConfig readonlyConfig) { super(readonlyConfig); @@ -79,6 +86,20 @@ public PaimonSinkConfig(ReadonlyConfig readonlyConfig) { this.primaryKeys = stringToList(readonlyConfig.get(PRIMARY_KEYS), ","); this.partitionKeys = stringToList(readonlyConfig.get(PARTITION_KEYS), ","); this.writeProps = readonlyConfig.get(WRITE_PROPS); + this.changelogProducer = + Stream.of(CoreOptions.ChangelogProducer.values()) + .filter( + cp -> + cp.toString() + .equalsIgnoreCase( + writeProps.getOrDefault( + CoreOptions.CHANGELOG_PRODUCER + .key(), + ""))) + .findFirst() + .orElse(null); + this.changelogTmpPath = + writeProps.getOrDefault(CHANGELOG_TMP_PATH, System.getProperty("java.io.tmpdir")); checkConfig(); } diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSink.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSink.java index 73d2151b896..86828c9a587 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSink.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSink.java @@ -94,7 +94,12 @@ public String getPluginName() { @Override public PaimonSinkWriter createWriter(SinkWriter.Context context) throws IOException { return new PaimonSinkWriter( - context, table, seaTunnelRowType, jobContext, paimonHadoopConfiguration); + context, + table, + seaTunnelRowType, + jobContext, + paimonSinkConfig, + paimonHadoopConfiguration); } @Override @@ -108,7 +113,13 @@ public PaimonSinkWriter createWriter(SinkWriter.Context context) throws IOExcept public SinkWriter restoreWriter( SinkWriter.Context context, List states) throws IOException { return new PaimonSinkWriter( - context, table, seaTunnelRowType, states, jobContext, paimonHadoopConfiguration); + context, + table, + seaTunnelRowType, + states, + jobContext, + paimonSinkConfig, + paimonHadoopConfiguration); } @Override diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java index ac0b1027d03..e57e62c9814 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/sink/PaimonSinkWriter.java @@ -24,6 +24,7 @@ import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.common.utils.SeaTunnelException; import org.apache.seatunnel.connectors.seatunnel.paimon.config.PaimonHadoopConfiguration; +import org.apache.seatunnel.connectors.seatunnel.paimon.config.PaimonSinkConfig; import org.apache.seatunnel.connectors.seatunnel.paimon.exception.PaimonConnectorErrorCode; import org.apache.seatunnel.connectors.seatunnel.paimon.exception.PaimonConnectorException; import org.apache.seatunnel.connectors.seatunnel.paimon.security.PaimonSecurityContext; @@ -33,7 +34,9 @@ import org.apache.seatunnel.connectors.seatunnel.paimon.utils.JobContextUtil; import org.apache.seatunnel.connectors.seatunnel.paimon.utils.RowConverter; +import org.apache.paimon.CoreOptions; import org.apache.paimon.data.InternalRow; +import org.apache.paimon.disk.IOManager; import org.apache.paimon.schema.TableSchema; import org.apache.paimon.table.BucketMode; import org.apache.paimon.table.FileStoreTable; @@ -58,6 +61,8 @@ import java.util.UUID; import java.util.stream.Collectors; +import static org.apache.paimon.disk.IOManagerImpl.splitPaths; + @Slf4j public class PaimonSinkWriter implements SinkWriter, @@ -65,14 +70,14 @@ public class PaimonSinkWriter private String commitUser = UUID.randomUUID().toString(); + private final FileStoreTable table; + private final WriteBuilder tableWriteBuilder; private final TableWrite tableWrite; private List committables = new ArrayList<>(); - private final Table table; - private final SeaTunnelRowType seaTunnelRowType; private final SinkWriter.Context context; @@ -90,18 +95,30 @@ public PaimonSinkWriter( Table table, SeaTunnelRowType seaTunnelRowType, JobContext jobContext, + PaimonSinkConfig paimonSinkConfig, PaimonHadoopConfiguration paimonHadoopConfiguration) { - this.table = table; + this.table = (FileStoreTable) table; + CoreOptions.ChangelogProducer changelogProducer = + this.table.coreOptions().changelogProducer(); + if (Objects.nonNull(paimonSinkConfig.getChangelogProducer()) + && changelogProducer != paimonSinkConfig.getChangelogProducer()) { + log.warn( + "configured the props named 'changelog-producer' which is not compatible with the options in table , so it will use the table's 'changelog-producer'"); + } + String changelogTmpPath = paimonSinkConfig.getChangelogTmpPath(); this.tableWriteBuilder = JobContextUtil.isBatchJob(jobContext) ? this.table.newBatchWriteBuilder() : this.table.newStreamWriteBuilder(); - this.tableWrite = tableWriteBuilder.newWrite(); + this.tableWrite = + tableWriteBuilder + .newWrite() + .withIOManager(IOManager.create(splitPaths(changelogTmpPath))); this.seaTunnelRowType = seaTunnelRowType; this.context = context; this.jobContext = jobContext; - this.tableSchema = ((FileStoreTable) table).schema(); - BucketMode bucketMode = ((FileStoreTable) table).bucketMode(); + this.tableSchema = this.table.schema(); + BucketMode bucketMode = this.table.bucketMode(); this.dynamicBucket = BucketMode.DYNAMIC == bucketMode || BucketMode.GLOBAL_DYNAMIC == bucketMode; int bucket = ((FileStoreTable) table).coreOptions().bucket(); @@ -124,8 +141,15 @@ public PaimonSinkWriter( SeaTunnelRowType seaTunnelRowType, List states, JobContext jobContext, + PaimonSinkConfig paimonSinkConfig, PaimonHadoopConfiguration paimonHadoopConfiguration) { - this(context, table, seaTunnelRowType, jobContext, paimonHadoopConfiguration); + this( + context, + table, + seaTunnelRowType, + jobContext, + paimonSinkConfig, + paimonHadoopConfiguration); if (Objects.isNull(states) || states.isEmpty()) { return; } @@ -186,7 +210,8 @@ public Optional prepareCommit(long checkpointId) throws IOExce fileCommittables = ((BatchTableWrite) tableWrite).prepareCommit(); } else { fileCommittables = - ((StreamTableWrite) tableWrite).prepareCommit(false, checkpointId); + ((StreamTableWrite) tableWrite) + .prepareCommit(waitCompaction(), checkpointId); } committables.addAll(fileCommittables); return Optional.of(new PaimonCommitInfo(fileCommittables, checkpointId)); @@ -224,4 +249,11 @@ public void close() throws IOException { committables.clear(); } } + + private boolean waitCompaction() { + CoreOptions.ChangelogProducer changelogProducer = + this.table.coreOptions().changelogProducer(); + return changelogProducer == CoreOptions.ChangelogProducer.LOOKUP + || changelogProducer == CoreOptions.ChangelogProducer.FULL_COMPACTION; + } } diff --git a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/SchemaUtil.java b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/SchemaUtil.java index fa8ed338208..ca825a269f9 100644 --- a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/SchemaUtil.java +++ b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/SchemaUtil.java @@ -25,6 +25,7 @@ import org.apache.seatunnel.connectors.seatunnel.paimon.exception.PaimonConnectorErrorCode; import org.apache.seatunnel.connectors.seatunnel.paimon.exception.PaimonConnectorException; +import org.apache.paimon.CoreOptions; import org.apache.paimon.schema.Schema; import org.apache.paimon.types.DataField; import org.apache.paimon.types.DataType; @@ -61,6 +62,10 @@ public static Schema toPaimonSchema( paiSchemaBuilder.partitionKeys(partitionKeys); } Map writeProps = paimonSinkConfig.getWriteProps(); + CoreOptions.ChangelogProducer changelogProducer = paimonSinkConfig.getChangelogProducer(); + if (changelogProducer != null) { + writeProps.remove(PaimonSinkConfig.CHANGELOG_TMP_PATH); + } if (!writeProps.isEmpty()) { paiSchemaBuilder.options(writeProps); } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonRecord.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonRecord.java index 700bf25f510..c17d8dbc141 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonRecord.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonRecord.java @@ -21,18 +21,23 @@ package org.apache.seatunnel.e2e.connector.paimon; import org.apache.paimon.data.Timestamp; +import org.apache.paimon.types.RowKind; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.Arrays; + @Data @NoArgsConstructor @AllArgsConstructor public class PaimonRecord { + public RowKind rowKind; public Long pkId; public String name; public Integer score; + public String op; public String dt; public Timestamp oneTime; public Timestamp twoTime; @@ -45,6 +50,12 @@ public PaimonRecord(Long pkId, String name) { this.name = name; } + public PaimonRecord(RowKind rowKind, Long pkId, String name) { + this(pkId, name); + this.rowKind = rowKind; + this.name = name; + } + public PaimonRecord(Long pkId, String name, String dt) { this(pkId, name); this.dt = dt; @@ -68,4 +79,23 @@ public PaimonRecord( this.threeTime = threeTime; this.fourTime = fourTime; } + + public String toChangeLogFull() { + Object[] objects = new Object[4]; + objects[0] = rowKind.shortString(); + objects[1] = pkId; + objects[2] = name; + objects[3] = score; + return Arrays.toString(objects); + } + + public String toChangeLogLookUp() { + Object[] objects = new Object[5]; + objects[0] = rowKind.shortString(); + objects[1] = pkId; + objects[2] = name; + objects[3] = score; + objects[4] = op; + return Arrays.toString(objects); + } } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java index dc6bfc9eba3..293cf6c76e5 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java @@ -18,6 +18,7 @@ package org.apache.seatunnel.e2e.connector.paimon; import org.apache.seatunnel.common.utils.FileUtils; +import org.apache.seatunnel.common.utils.SeaTunnelException; import org.apache.seatunnel.core.starter.utils.CompressionUtils; import org.apache.seatunnel.e2e.common.TestResource; import org.apache.seatunnel.e2e.common.TestSuiteBase; @@ -25,6 +26,7 @@ import org.apache.seatunnel.e2e.common.container.EngineType; import org.apache.seatunnel.e2e.common.container.TestContainer; import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; +import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.lang3.StringUtils; @@ -58,7 +60,9 @@ import java.io.IOException; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -84,6 +88,7 @@ public class PaimonSinkCDCIT extends TestSuiteBase implements TestResource { private String CATALOG_ROOT_DIR_WIN = "C:/Users/"; private String CATALOG_DIR_WIN = CATALOG_ROOT_DIR_WIN + NAMESPACE + "/"; private boolean isWindows; + private boolean changeLogEnabled = false; @BeforeAll @Override @@ -545,6 +550,129 @@ public void testSinkPaimonTruncateTable(TestContainer container) throws Exceptio }); } + @TestTemplate + public void testChangelogLookup(TestContainer container) throws Exception { + // create Piamon table (changelog-producer=lookup) + Container.ExecResult writeResult = + container.executeJob("/changelog_fake_cdc_sink_paimon_case1_ddl.conf"); + Assertions.assertEquals(0, writeResult.getExitCode()); + TimeUnit.SECONDS.sleep(20); + String[] jobIds = + new String[] { + JobIdGenerator.newJobId(), JobIdGenerator.newJobId(), JobIdGenerator.newJobId() + }; + log.info("jobIds: {}", Arrays.toString(jobIds)); + List> futures = new ArrayList<>(); + // read changelog and write to append only paimon table + futures.add( + CompletableFuture.runAsync( + () -> { + try { + container.executeJob("/changelog_paimon_to_paimon.conf", jobIds[0]); + } catch (Exception e) { + throw new SeaTunnelException(e); + } + })); + TimeUnit.SECONDS.sleep(10); + // dml: insert data + futures.add( + CompletableFuture.runAsync( + () -> { + try { + container.executeJob( + "/changelog_fake_cdc_sink_paimon_case1_insert_data.conf", + jobIds[1]); + } catch (Exception e) { + throw new SeaTunnelException(e); + } + })); + // dml: update and delete data + TimeUnit.SECONDS.sleep(10); + futures.add( + CompletableFuture.runAsync( + () -> { + try { + container.executeJob( + "/changelog_fake_cdc_sink_paimon_case1_update_data.conf", + jobIds[2]); + } catch (Exception e) { + throw new SeaTunnelException(e); + } + })); + // stream job running 30 seconds + TimeUnit.SECONDS.sleep(30); + // cancel stream job + container.cancelJob(jobIds[1]); + container.cancelJob(jobIds[2]); + container.cancelJob(jobIds[0]); + changeLogEnabled = true; + TimeUnit.SECONDS.sleep(10); + // copy paimon to local + container.executeExtraCommands(containerExtendedFactory); + List paimonRecords1 = loadPaimonData("seatunnel_namespace", "st_test_sink"); + List actual1 = + paimonRecords1.stream() + .map(PaimonRecord::toChangeLogLookUp) + .collect(Collectors.toList()); + log.info("paimon records: {}", actual1); + Assertions.assertEquals(8, actual1.size()); + Assertions.assertEquals( + Arrays.asList( + "[+I, 1, A, 100, +I]", + "[+I, 2, B, 100, +I]", + "[+I, 3, C, 100, +I]", + "[+I, 1, A, 100, -U]", + "[+I, 1, Aa, 200, +U]", + "[+I, 2, B, 100, -U]", + "[+I, 2, Bb, 90, +U]", + "[+I, 3, C, 100, -D]"), + actual1); + List paimonRecords2 = loadPaimonData("seatunnel_namespace", "st_test_lookup"); + List actual2 = + paimonRecords2.stream() + .map(PaimonRecord::toChangeLogFull) + .collect(Collectors.toList()); + log.info("paimon records: {}", actual2); + Assertions.assertEquals(2, actual2.size()); + Assertions.assertEquals(Arrays.asList("[+U, 1, Aa, 200]", "[+I, 2, Bb, 90]"), actual2); + changeLogEnabled = false; + futures.forEach(future -> future.cancel(true)); + } + + @TestTemplate + public void testChangelogFullCompaction(TestContainer container) throws Exception { + String jobId = JobIdGenerator.newJobId(); + log.info("jobId: {}", jobId); + CompletableFuture voidCompletableFuture = + CompletableFuture.runAsync( + () -> { + try { + container.executeJob( + "/changelog_fake_cdc_sink_paimon_case2.conf", jobId); + } catch (Exception e) { + throw new SeaTunnelException(e); + } + }); + // stream job running 20 seconds + TimeUnit.SECONDS.sleep(20); + changeLogEnabled = true; + // cancel stream job + container.cancelJob(jobId); + TimeUnit.SECONDS.sleep(5); + // copy paimon to local + container.executeExtraCommands(containerExtendedFactory); + List paimonRecords = loadPaimonData("seatunnel_namespace", "st_test_full"); + List actual = + paimonRecords.stream() + .map(PaimonRecord::toChangeLogFull) + .collect(Collectors.toList()); + log.info("paimon records: {}", actual); + Assertions.assertEquals(2, actual.size()); + Assertions.assertEquals(Arrays.asList("[+U, 1, Aa, 200]", "[+I, 2, Bb, 90]"), actual); + changeLogEnabled = false; + voidCompletableFuture.cancel(true); + } + protected final ContainerExtendedFactory containerExtendedFactory = container -> { if (isWindows) { @@ -619,13 +747,31 @@ private List loadPaimonData(String dbName, String tbName) throws E try (RecordReader reader = tableRead.createReader(plan)) { reader.forEachRemaining( row -> { - PaimonRecord paimonRecord = - new PaimonRecord(row.getLong(0), row.getString(1).toString()); + PaimonRecord paimonRecord; + if (changeLogEnabled) { + paimonRecord = + new PaimonRecord( + row.getRowKind(), + row.getLong(0), + row.getString(1).toString()); + } else { + paimonRecord = + new PaimonRecord(row.getLong(0), row.getString(1).toString()); + } if (table.schema().fieldNames().contains("score")) { paimonRecord.setScore(row.getInt(2)); } + if (table.schema().fieldNames().contains("op")) { + paimonRecord.setOp(row.getString(3).toString()); + } result.add(paimonRecord); - log.info("key_id:" + row.getLong(0) + ", name:" + row.getString(1)); + log.info( + "rowKind:" + + row.getRowKind().shortString() + + ", key_id:" + + row.getLong(0) + + ", name:" + + row.getString(1)); }); } log.info( diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_ddl.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_ddl.conf new file mode 100644 index 00000000000..6a327275055 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_ddl.conf @@ -0,0 +1,53 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +###### +###### This config file is a demonstration of streaming processing in seatunnel config +###### + +env { + parallelism = 1 + job.mode = "batch" +} + +source { + FakeSource { + schema = { + fields { + pk_id = bigint + name = string + score = int + } + primaryKey { + name = "pk_id" + columnNames = [pk_id] + } + } + rows = [] + } +} + +sink { + Paimon { + warehouse = "file:///tmp/paimon" + database = "seatunnel_namespace" + table = "st_test_lookup" + paimon.table.write-props = { + changelog-producer = lookup + changelog-tmp-path = "/tmp/paimon/changelog" + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_insert_data.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_insert_data.conf new file mode 100644 index 00000000000..9b7310177cd --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_insert_data.conf @@ -0,0 +1,67 @@ +# +# 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 config file is a demonstration of streaming processing in seatunnel config +###### + +env { + parallelism = 1 + job.mode = "Streaming" + checkpoint.interval = 2000 +} + +source { + FakeSource { + schema = { + fields { + pk_id = bigint + name = string + score = int + } + primaryKey { + name = "pk_id" + columnNames = [pk_id] + } + } + rows = [ + { + kind = INSERT + fields = [1, "A", 100] + }, + { + kind = INSERT + fields = [2, "B", 100] + }, + { + kind = INSERT + fields = [3, "C", 100] + } + ] + } +} + +sink { + Paimon { + warehouse = "file:///tmp/paimon" + database = "seatunnel_namespace" + table = "st_test_lookup" + paimon.table.write-props = { + changelog-producer = lookup + changelog-tmp-path = "/tmp/paimon/changelog" + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_update_data.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_update_data.conf new file mode 100644 index 00000000000..271ad20bff8 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case1_update_data.conf @@ -0,0 +1,71 @@ +# +# 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 config file is a demonstration of streaming processing in seatunnel config +###### + +env { + parallelism = 1 + job.mode = "Streaming" + checkpoint.interval = 2000 +} + +source { + FakeSource { + schema = { + fields { + pk_id = bigint + name = string + score = int + } + primaryKey { + name = "pk_id" + columnNames = [pk_id] + } + } + rows = [ + { + kind = UPDATE_BEFORE + fields = [1, "A", 100] + }, + { + kind = UPDATE_AFTER + fields = [1, "Aa", 200] + }, + { + kind = INSERT + fields = [2, "Bb", 90] + }, + { + kind = DELETE + fields = [3, "C", 100] + } + ] + } +} + +sink { + Paimon { + warehouse = "file:///tmp/paimon" + database = "seatunnel_namespace" + table = "st_test_lookup" + paimon.table.write-props = { + changelog-producer = lookup + changelog-tmp-path = "/tmp/paimon/changelog" + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case2.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case2.conf new file mode 100644 index 00000000000..f7135e645f0 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_fake_cdc_sink_paimon_case2.conf @@ -0,0 +1,83 @@ +# +# 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 config file is a demonstration of streaming processing in seatunnel config +###### + +env { + parallelism = 1 + job.mode = "Streaming" + checkpoint.interval = 2000 +} + +source { + FakeSource { + schema = { + fields { + pk_id = bigint + name = string + score = int + } + primaryKey { + name = "pk_id" + columnNames = [pk_id] + } + } + rows = [ + { + kind = INSERT + fields = [1, "A", 100] + }, + { + kind = INSERT + fields = [2, "B", 100] + }, + { + kind = INSERT + fields = [3, "C", 100] + }, + { + kind = UPDATE_BEFORE + fields = [1, "A", 100] + }, + { + kind = UPDATE_AFTER + fields = [1, "Aa", 200] + }, + { + kind = INSERT + fields = [2, "Bb", 90] + }, + { + kind = DELETE + fields = [3, "C", 100] + }, + ] + } +} + +sink { + Paimon { + warehouse = "file:///tmp/paimon" + database = "seatunnel_namespace" + table = "st_test_full" + paimon.table.write-props = { + changelog-producer = full-compaction + changelog-tmp-path = "/tmp/paimon/changelog" + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_paimon_to_paimon.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_paimon_to_paimon.conf new file mode 100644 index 00000000000..d23d11d9e08 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/resources/changelog_paimon_to_paimon.conf @@ -0,0 +1,48 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "Streaming" + checkpoint.interval = 2000 +} + +source { + Paimon { + warehouse = "/tmp/paimon" + database = "seatunnel_namespace" + table = "st_test_lookup" + } +} + +transform { + RowKindExtractor { + custom_field_name = op + transform_type = SHORT + } +} + +sink { + Paimon { + warehouse = "/tmp/paimon" + database = "seatunnel_namespace" + table = "st_test_sink" + paimon.table.write-props = { + write-only = true + } + } +} diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/AbstractTestContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/AbstractTestContainer.java index d7bd0f4d747..10d5685c6d2 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/AbstractTestContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/AbstractTestContainer.java @@ -19,6 +19,8 @@ import org.apache.seatunnel.e2e.common.util.ContainerUtil; +import org.apache.commons.lang3.StringUtils; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.Container; @@ -68,6 +70,8 @@ public AbstractTestContainer() { protected abstract String getSavePointCommand(); + protected abstract String getCancelJobCommand(); + protected abstract String getRestoreCommand(); protected abstract String getConnectorNamePrefix(); @@ -95,11 +99,11 @@ protected void copySeaTunnelStarterLoggingToContainer(GenericContainer contai protected Container.ExecResult executeJob(GenericContainer container, String confFile) throws IOException, InterruptedException { - return executeJob(container, confFile, null); + return executeJob(container, confFile, null, null); } protected Container.ExecResult executeJob( - GenericContainer container, String confFile, List variables) + GenericContainer container, String confFile, String jobId, List variables) throws IOException, InterruptedException { final String confInContainerPath = copyConfigFileToContainer(container, confFile); // copy connectors @@ -118,6 +122,10 @@ protected Container.ExecResult executeJob( command.add(adaptPathForWin(confInContainerPath)); command.add("--name"); command.add(new File(confInContainerPath).getName()); + if (StringUtils.isNoneEmpty(jobId)) { + command.add("--set-job-id"); + command.add(jobId); + } List extraStartShellCommands = new ArrayList<>(getExtraStartShellCommands()); if (variables != null && !variables.isEmpty()) { variables.forEach( @@ -142,6 +150,18 @@ protected Container.ExecResult savepointJob(GenericContainer container, Strin return executeCommand(container, command); } + protected Container.ExecResult cancelJob(GenericContainer container, String jobId) + throws IOException, InterruptedException { + final List command = new ArrayList<>(); + String binPath = Paths.get(SEATUNNEL_HOME, "bin", getStartShellName()).toString(); + // base command + command.add(adaptPathForWin(binPath)); + command.add(getCancelJobCommand()); + command.add(jobId); + command.addAll(getExtraStartShellCommands()); + return executeCommand(container, command); + } + protected Container.ExecResult restoreJob( GenericContainer container, String confFile, String jobId) throws IOException, InterruptedException { diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/TestContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/TestContainer.java index e83a3635e80..72584158f64 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/TestContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/TestContainer.java @@ -39,15 +39,20 @@ void executeExtraCommands(ContainerExtendedFactory extendedFactory) Container.ExecResult executeJob(String confFile, List variables) throws IOException, InterruptedException; + default Container.ExecResult executeJob(String confFile, String jobId) + throws IOException, InterruptedException { + throw new UnsupportedOperationException("Not implemented"); + } + default Container.ExecResult executeConnectorCheck(String[] args) throws IOException, InterruptedException { throw new UnsupportedOperationException("Not implemented"); - }; + } default Container.ExecResult executeBaseCommand(String[] args) throws IOException, InterruptedException { throw new UnsupportedOperationException("Not implemented"); - }; + } default Container.ExecResult savepointJob(String jobId) throws IOException, InterruptedException { @@ -59,6 +64,10 @@ default Container.ExecResult restoreJob(String confFile, String jobId) throw new UnsupportedOperationException("Not implemented"); } + default Container.ExecResult cancelJob(String jobId) throws IOException, InterruptedException { + throw new UnsupportedOperationException("Not implemented"); + } + String getServerLogs(); void copyFileToContainer(String path, String targetPath); diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/flink/AbstractTestFlinkContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/flink/AbstractTestFlinkContainer.java index ff16c0c7541..47b3de5ff5d 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/flink/AbstractTestFlinkContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/flink/AbstractTestFlinkContainer.java @@ -131,6 +131,11 @@ protected String getSavePointCommand() { throw new UnsupportedOperationException("Not implemented"); } + @Override + protected String getCancelJobCommand() { + throw new UnsupportedOperationException("Not implemented"); + } + @Override protected String getRestoreCommand() { throw new UnsupportedOperationException("Not implemented"); @@ -150,14 +155,14 @@ public void executeExtraCommands(ContainerExtendedFactory extendedFactory) @Override public Container.ExecResult executeJob(String confFile) throws IOException, InterruptedException { - return executeJob(confFile, null); + return executeJob(confFile, Collections.emptyList()); } @Override public Container.ExecResult executeJob(String confFile, List variables) throws IOException, InterruptedException { log.info("test in container: {}", identifier()); - return executeJob(jobManager, confFile, variables); + return executeJob(jobManager, confFile, null, variables); } @Override diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/ConnectorPackageServiceContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/ConnectorPackageServiceContainer.java index 3a27d78d423..ea8bcd8788b 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/ConnectorPackageServiceContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/ConnectorPackageServiceContainer.java @@ -189,6 +189,11 @@ protected String getSavePointCommand() { return "-s"; } + @Override + protected String getCancelJobCommand() { + return "-can"; + } + @Override protected String getRestoreCommand() { return "-r"; @@ -220,14 +225,14 @@ public void executeExtraCommands(ContainerExtendedFactory extendedFactory) @Override public Container.ExecResult executeJob(String confFile) throws IOException, InterruptedException { - return executeJob(confFile, null); + return executeJob(confFile, Collections.emptyList()); } @Override public Container.ExecResult executeJob(String confFile, List variables) throws IOException, InterruptedException { log.info("test in container: {}", identifier()); - return executeJob(server1, confFile, variables); + return executeJob(server1, confFile, null, variables); } @Override diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java index 96e5162d7a9..14d89571b95 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java @@ -24,6 +24,7 @@ import org.apache.seatunnel.e2e.common.container.TestContainerId; import org.apache.seatunnel.e2e.common.util.ContainerUtil; +import org.apache.commons.compress.utils.Lists; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; @@ -241,6 +242,11 @@ protected String getSavePointCommand() { return "-s"; } + @Override + protected String getCancelJobCommand() { + return "-can"; + } + @Override protected String getRestoreCommand() { return "-r"; @@ -281,16 +287,27 @@ public Container.ExecResult executeBaseCommand(String[] args) @Override public Container.ExecResult executeJob(String confFile) throws IOException, InterruptedException { - return executeJob(confFile, null); + return executeJob(confFile, Lists.newArrayList()); } @Override public Container.ExecResult executeJob(String confFile, List variables) throws IOException, InterruptedException { + return executeJob(confFile, null, variables); + } + + @Override + public Container.ExecResult executeJob(String confFile, String jobId) + throws IOException, InterruptedException { + return executeJob(confFile, jobId, null); + } + + private Container.ExecResult executeJob(String confFile, String jobId, List variables) + throws IOException, InterruptedException { log.info("test in container: {}", identifier()); List beforeThreads = ContainerUtil.getJVMThreadNames(server); runningCount.incrementAndGet(); - Container.ExecResult result = executeJob(server, confFile, variables); + Container.ExecResult result = executeJob(server, confFile, jobId, variables); if (runningCount.decrementAndGet() > 0) { // only check thread when job all finished. return result; @@ -322,7 +339,6 @@ public Container.ExecResult executeJob(String confFile, List variables) .collect(Collectors.joining())); }); } - // classLoaderObjectCheck(1); return result; } @@ -467,6 +483,11 @@ public Container.ExecResult restoreJob(String confFile, String jobId) return result; } + @Override + public Container.ExecResult cancelJob(String jobId) throws IOException, InterruptedException { + return cancelJob(server, jobId); + } + @Override public String getServerLogs() { return server.getLogs(); diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/spark/AbstractTestSparkContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/spark/AbstractTestSparkContainer.java index 9970ffb3aa7..b13851582c2 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/spark/AbstractTestSparkContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/spark/AbstractTestSparkContainer.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.time.Duration; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Stream; @@ -87,6 +88,11 @@ protected String getSavePointCommand() { throw new UnsupportedOperationException("Not implemented"); } + @Override + protected String getCancelJobCommand() { + throw new UnsupportedOperationException("Not implemented"); + } + @Override protected String getRestoreCommand() { throw new UnsupportedOperationException("Not implemented"); @@ -105,14 +111,14 @@ public void executeExtraCommands(ContainerExtendedFactory extendedFactory) @Override public Container.ExecResult executeJob(String confFile) throws IOException, InterruptedException { - return executeJob(confFile, null); + return executeJob(confFile, Collections.emptyList()); } @Override public Container.ExecResult executeJob(String confFile, List variables) throws IOException, InterruptedException { log.info("test in container: {}", identifier()); - return executeJob(master, confFile, variables); + return executeJob(master, confFile, null, variables); } @Override diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java new file mode 100644 index 00000000000..6904593b242 --- /dev/null +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java @@ -0,0 +1,27 @@ +/* + * 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.seatunnel.e2e.common.util; + +import java.util.concurrent.ThreadLocalRandom; + +public class JobIdGenerator { + + public static String newJobId() { + return String.valueOf(Math.abs(ThreadLocalRandom.current().nextLong())); + } +} From 139919334df43b8c320cb1139700a05124f55358 Mon Sep 17 00:00:00 2001 From: Shashwat Tiwari Date: Mon, 21 Oct 2024 14:35:29 +0530 Subject: [PATCH 25/72] [Feature][Connector-V2] Jdbc DB2 support upsert SQL (#7879) --- .../jdbc/internal/dialect/db2/DB2Dialect.java | 54 ++++++- .../seatunnel/jdbc/AbstractJdbcIT.java | 22 +++ .../connectors/seatunnel/jdbc/JdbcCase.java | 2 + .../connectors/seatunnel/jdbc/JdbcDb2IT.java | 6 +- .../seatunnel/jdbc/JdbcDb2UpsertIT.java | 133 ++++++++++++++++++ .../jdbc_db2_source_and_sink_upsert.conf | 57 ++++++++ 6 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2UpsertIT.java create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_db2_source_and_sink_upsert.conf diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/db2/DB2Dialect.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/db2/DB2Dialect.java index 6150dd4330d..5af57bf1045 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/db2/DB2Dialect.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/db2/DB2Dialect.java @@ -22,7 +22,9 @@ import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.JdbcDialect; import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.JdbcDialectTypeMapper; +import java.util.Arrays; import java.util.Optional; +import java.util.stream.Collectors; public class DB2Dialect implements JdbcDialect { @@ -44,6 +46,56 @@ public JdbcDialectTypeMapper getJdbcDialectTypeMapper() { @Override public Optional getUpsertStatement( String database, String tableName, String[] fieldNames, String[] uniqueKeyFields) { - return Optional.empty(); + // Generate field list for USING and INSERT clauses + String fieldList = String.join(", ", fieldNames); + + // Generate placeholder list for VALUES clause + String placeholderList = + Arrays.stream(fieldNames).map(field -> "?").collect(Collectors.joining(", ")); + + // Generate ON clause + String onClause = + Arrays.stream(uniqueKeyFields) + .map(field -> "target." + field + " = source." + field) + .collect(Collectors.joining(" AND ")); + + // Generate WHEN MATCHED clause + String whenMatchedClause = + Arrays.stream(fieldNames) + .map(field -> "target." + field + " <> source." + field) + .collect(Collectors.joining(" OR ")); + + // Generate UPDATE SET clause + String updateSetClause = + Arrays.stream(fieldNames) + .map(field -> "target." + field + " = source." + field) + .collect(Collectors.joining(", ")); + + // Generate WHEN NOT MATCHED clause + String insertClause = + "INSERT (" + + fieldList + + ") VALUES (" + + Arrays.stream(fieldNames) + .map(field -> "source." + field) + .collect(Collectors.joining(", ")) + + ")"; + + // Combine all parts to form the final SQL statement + String mergeStatement = + String.format( + "MERGE INTO %s.%s AS target USING (VALUES (%s)) AS source (%s) ON %s " + + "WHEN MATCHED AND (%s) THEN UPDATE SET %s " + + "WHEN NOT MATCHED THEN %s;", + database, + tableName, + placeholderList, + fieldList, + onClause, + whenMatchedClause, + updateSetClause, + insertClause); + + return Optional.of(mergeStatement); } } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/AbstractJdbcIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/AbstractJdbcIT.java index 24b916d4049..9747396b61f 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/AbstractJdbcIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/AbstractJdbcIT.java @@ -209,6 +209,17 @@ protected void createNeededTables() { jdbcCase.getSourceTable())); statement.execute(createSource); + if (jdbcCase.getAdditionalSqlOnSource() != null) { + String additionalSql = + String.format( + jdbcCase.getAdditionalSqlOnSource(), + buildTableInfoWithSchema( + jdbcCase.getDatabase(), + jdbcCase.getSchema(), + jdbcCase.getSourceTable())); + statement.execute(additionalSql); + } + if (!jdbcCase.isUseSaveModeCreateTable()) { if (jdbcCase.getSinkCreateSql() != null) { createTemplate = jdbcCase.getSinkCreateSql(); @@ -223,6 +234,17 @@ protected void createNeededTables() { statement.execute(createSink); } + if (jdbcCase.getAdditionalSqlOnSink() != null) { + String additionalSql = + String.format( + jdbcCase.getAdditionalSqlOnSink(), + buildTableInfoWithSchema( + jdbcCase.getDatabase(), + jdbcCase.getSchema(), + jdbcCase.getSinkTable())); + statement.execute(additionalSql); + } + connection.commit(); } catch (Exception exception) { log.error(ExceptionUtils.getMessage(exception)); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcCase.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcCase.java index 3dd7b64b95d..e6bbbd19a70 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcCase.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcCase.java @@ -48,6 +48,8 @@ public class JdbcCase { private String jdbcUrl; private String createSql; private String sinkCreateSql; + private String additionalSqlOnSource; + private String additionalSqlOnSink; private String insertSql; private List configFile; private Pair> testData; diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2IT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2IT.java index 22a29b3b679..a876d9bf7a0 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2IT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2IT.java @@ -44,9 +44,9 @@ public class JdbcDb2IT extends AbstractJdbcIT { private static final String DB2_CONTAINER_HOST = "db2-e2e"; - private static final String DB2_DATABASE = "E2E"; - private static final String DB2_SOURCE = "SOURCE"; - private static final String DB2_SINK = "SINK"; + protected static final String DB2_DATABASE = "E2E"; + protected static final String DB2_SOURCE = "SOURCE"; + protected static final String DB2_SINK = "SINK"; private static final String DB2_URL = "jdbc:db2://" + HOST + ":%s/%s"; diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2UpsertIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2UpsertIT.java new file mode 100644 index 00000000000..d6e0147368e --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDb2UpsertIT.java @@ -0,0 +1,133 @@ +/* + * 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.seatunnel.connectors.seatunnel.jdbc; + +import org.apache.seatunnel.e2e.common.container.TestContainer; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestTemplate; +import org.testcontainers.containers.Container; + +import com.google.common.collect.Lists; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +public class JdbcDb2UpsertIT extends JdbcDb2IT { + + private static final String CREATE_SQL_SINK = + "create table %s\n" + + "(\n" + + " C_BOOLEAN BOOLEAN,\n" + + " C_SMALLINT SMALLINT,\n" + + " C_INT INTEGER NOT NULL PRIMARY KEY,\n" + + " C_INTEGER INTEGER,\n" + + " C_BIGINT BIGINT,\n" + + " C_DECIMAL DECIMAL(5),\n" + + " C_DEC DECIMAL(5),\n" + + " C_NUMERIC DECIMAL(5),\n" + + " C_NUM DECIMAL(5),\n" + + " C_REAL REAL,\n" + + " C_FLOAT DOUBLE,\n" + + " C_DOUBLE DOUBLE,\n" + + " C_DOUBLE_PRECISION DOUBLE,\n" + + " C_CHAR CHARACTER(1),\n" + + " C_VARCHAR VARCHAR(255),\n" + + " C_BINARY BINARY(1),\n" + + " C_VARBINARY VARBINARY(2048),\n" + + " C_DATE DATE,\n" + + " C_UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n" + + ");\n"; + + // create a trigger to update the timestamp when the row is updated. + // if no changes are made to the row, the timestamp should not be updated. + private static final String CREATE_TRIGGER_SQL = + "CREATE TRIGGER c_updated_at_trigger\n" + + " BEFORE UPDATE ON %s\n" + + " REFERENCING NEW AS new_row\n" + + " FOR EACH ROW\n" + + "BEGIN ATOMIC\n" + + "SET new_row.c_updated_at = CURRENT_TIMESTAMP;\n" + + "END;"; + + private static final List CONFIG_FILE = + Lists.newArrayList("/jdbc_db2_source_and_sink_upsert.conf"); + + @Override + JdbcCase getJdbcCase() { + jdbcCase = super.getJdbcCase(); + jdbcCase.setSinkCreateSql(CREATE_SQL_SINK); + jdbcCase.setConfigFile(CONFIG_FILE); + jdbcCase.setAdditionalSqlOnSink(CREATE_TRIGGER_SQL); + return jdbcCase; + } + + @TestTemplate + public void testDb2UpsertE2e(TestContainer container) + throws IOException, InterruptedException, SQLException { + try { + // step 1: run the job to migrate data from source to sink. + Container.ExecResult execResult = + container.executeJob("/jdbc_db2_source_and_sink_upsert.conf"); + Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); + List> updatedAtTimestampsBeforeUpdate = + query( + String.format( + "SELECT C_UPDATED_AT FROM %s", + buildTableInfoWithSchema(DB2_DATABASE, DB2_SINK))); + // step 2: run the job to update the data in the sink. + // expected: timestamps should not be updated as the data is not changed. + execResult = container.executeJob("/jdbc_db2_source_and_sink_upsert.conf"); + Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); + List> updatedAtTimestampsAfterUpdate = + query( + String.format( + "SELECT C_UPDATED_AT FROM %s", + buildTableInfoWithSchema(DB2_DATABASE, DB2_SINK))); + Assertions.assertIterableEquals( + updatedAtTimestampsBeforeUpdate, updatedAtTimestampsAfterUpdate); + } finally { + clearTable(DB2_DATABASE, DB2_SINK); + } + } + + private List> query(String sql) { + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + List> result = new ArrayList<>(); + int columnCount = resultSet.getMetaData().getColumnCount(); + while (resultSet.next()) { + ArrayList objects = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + objects.add(resultSet.getString(i)); + } + result.add(objects); + log.debug(String.format("Print query, sql: %s, data: %s", sql, objects)); + } + connection.commit(); + return result; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_db2_source_and_sink_upsert.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_db2_source_and_sink_upsert.conf new file mode 100644 index 00000000000..518a027d34d --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_db2_source_and_sink_upsert.conf @@ -0,0 +1,57 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" +} + +source { + # This is a example source plugin **only for test and demonstrate the feature source plugin** + Jdbc { + driver = com.ibm.db2.jcc.DB2Driver + url = "jdbc:db2://db2-e2e:50000/E2E" + user = "db2inst1" + password = "123456" + query = """ + select * from "E2E".SOURCE; + """ + } + + # If you would like to get more information about how to configure seatunnel and see full list of source plugins, + # please go to https://seatunnel.apache.org/docs/connector-v2/source/Jdbc +} + +sink { + Jdbc { + driver = com.ibm.db2.jcc.DB2Driver + url = "jdbc:db2://db2-e2e:50000/E2E" + user = "db2inst1" + password = "123456" + database = "E2E" + table = "SINK" + enable_upsert = true + # The primary keys of the table, which will be used to generate the upsert sql + generate_sink_sql = true + primary_keys = [ + C_INT + ] + } + + # If you would like to get more information about how to configure seatunnel and see full list of sink plugins, + # please go to https://seatunnel.apache.org/docs/connector-v2/sink/Jdbc +} From 5cf307c052d472ab5b01a1337f9b6e40949570a6 Mon Sep 17 00:00:00 2001 From: Jast Date: Tue, 22 Oct 2024 07:33:49 +0800 Subject: [PATCH 26/72] [Fix][Doc] Fix jetty doc error (#7883) --- docs/en/seatunnel-engine/rest-api-v2.md | 12 +++++++----- docs/zh/seatunnel-engine/rest-api-v2.md | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/en/seatunnel-engine/rest-api-v2.md b/docs/en/seatunnel-engine/rest-api-v2.md index c21d0531221..acd8bd636de 100644 --- a/docs/en/seatunnel-engine/rest-api-v2.md +++ b/docs/en/seatunnel-engine/rest-api-v2.md @@ -17,8 +17,9 @@ The v2 version of the api uses jetty support. It is the same as the interface sp seatunnel: engine: - enable-http: true - port: 8080 + http: + enable-http: true + port: 8080 ``` Context-path can also be configured as follows: @@ -27,9 +28,10 @@ Context-path can also be configured as follows: seatunnel: engine: - enable-http: true - port: 8080 - context-path: /seatunnel + http: + enable-http: true + port: 8080 + context-path: /seatunnel ``` ## API reference diff --git a/docs/zh/seatunnel-engine/rest-api-v2.md b/docs/zh/seatunnel-engine/rest-api-v2.md index 50f7eef3b10..26ca116845e 100644 --- a/docs/zh/seatunnel-engine/rest-api-v2.md +++ b/docs/zh/seatunnel-engine/rest-api-v2.md @@ -13,8 +13,9 @@ v2版本的api使用jetty支持,与v1版本的接口规范相同 ,可以通过 seatunnel: engine: - enable-http: true - port: 8080 + http: + enable-http: true + port: 8080 ``` 同时也可以配置context-path,配置如下: @@ -23,9 +24,10 @@ seatunnel: seatunnel: engine: - enable-http: true - port: 8080 - context-path: /seatunnel + http: + enable-http: true + port: 8080 + context-path: /seatunnel ``` ## API参考 From ff1b7d7b36e5d66f9f2a954fb81a4cf90c02d47a Mon Sep 17 00:00:00 2001 From: hailin0 Date: Tue, 22 Oct 2024 09:29:29 +0800 Subject: [PATCH 27/72] [Improve][Example] Improve zeta local/cluster example (#7877) --- docs/en/contribution/setup.md | 2 +- docs/zh/contribution/setup.md | 2 +- .../SeaTunnelEngineClusterClientExample.java | 81 +++++++++++++++++++ ... SeaTunnelEngineClusterServerExample.java} | 2 +- ....java => SeaTunnelEngineLocalExample.java} | 6 +- 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineClusterClientExample.java rename seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/{SeaTunnelEngineServerExample.java => SeaTunnelEngineClusterServerExample.java} (96%) rename seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/{SeaTunnelEngineExample.java => SeaTunnelEngineLocalExample.java} (93%) diff --git a/docs/en/contribution/setup.md b/docs/en/contribution/setup.md index b2579e1ee1e..8fd632a24b0 100644 --- a/docs/en/contribution/setup.md +++ b/docs/en/contribution/setup.md @@ -80,7 +80,7 @@ After all the above things are done, you just finish the environment setup and c of box. All examples are in module `seatunnel-examples`, you could pick one you are interested in, [Running Or Debugging It In IDEA](https://www.jetbrains.com/help/idea/run-debug-configuration.html) as you wish. -Here we use `seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java` +Here we use `seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineLocalExample.java` as an example, when you run it successfully you can see the output as below: ```log diff --git a/docs/zh/contribution/setup.md b/docs/zh/contribution/setup.md index c00c3132c22..662663a4961 100644 --- a/docs/zh/contribution/setup.md +++ b/docs/zh/contribution/setup.md @@ -75,7 +75,7 @@ Apache SeaTunnel 使用 `Spotless` 来统一代码风格和格式检查。可以 完成上面所有的工作后,环境搭建已经完成, 可以直接运行我们的示例了。 所有的示例在 `seatunnel-examples` 模块里, 你可以随意选择进行编译和调试,参考 [running or debugging it in IDEA](https://www.jetbrains.com/help/idea/run-debug-configuration.html)。 -我们使用 `seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java` +我们使用 `seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineLocalExample.java` 作为示例, 运行成功后的输出如下: ```log diff --git a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineClusterClientExample.java b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineClusterClientExample.java new file mode 100644 index 00000000000..c1d47520bb3 --- /dev/null +++ b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineClusterClientExample.java @@ -0,0 +1,81 @@ +/* + * 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.seatunnel.example.engine; + +import org.apache.seatunnel.core.starter.SeaTunnel; +import org.apache.seatunnel.core.starter.seatunnel.args.ClientCommandArgs; + +import java.io.FileNotFoundException; +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.Collections; + +public class SeaTunnelEngineClusterClientExample { + + public static void main(String[] args) throws Exception { + String id = "834720088434147329"; + String configurePath = "/examples/fake_to_console.conf"; + + submit(configurePath, id); + + // list(); + // savepoint(id); + // restore(configurePath, id); + // cancel(id); + } + + public static void list() { + ClientCommandArgs clientCommandArgs = new ClientCommandArgs(); + clientCommandArgs.setListJob(true); + SeaTunnel.run(clientCommandArgs.buildCommand()); + } + + public static void submit(String configurePath, String id) + throws FileNotFoundException, URISyntaxException { + String configFile = SeaTunnelEngineLocalExample.getTestConfigFile(configurePath); + ClientCommandArgs clientCommandArgs = new ClientCommandArgs(); + clientCommandArgs.setConfigFile(configFile); + clientCommandArgs.setCheckConfig(false); + clientCommandArgs.setJobName(Paths.get(configFile).getFileName().toString()); + clientCommandArgs.setAsync(true); + clientCommandArgs.setCustomJobId(id); + SeaTunnel.run(clientCommandArgs.buildCommand()); + } + + public static void restore(String configurePath, String id) + throws FileNotFoundException, URISyntaxException { + String configFile = SeaTunnelEngineLocalExample.getTestConfigFile(configurePath); + ClientCommandArgs clientCommandArgs = new ClientCommandArgs(); + clientCommandArgs.setConfigFile(configFile); + clientCommandArgs.setRestoreJobId(id); + clientCommandArgs.setAsync(true); + SeaTunnel.run(clientCommandArgs.buildCommand()); + } + + public static void savepoint(String id) { + ClientCommandArgs clientCommandArgs = new ClientCommandArgs(); + clientCommandArgs.setSavePointJobId(id); + SeaTunnel.run(clientCommandArgs.buildCommand()); + } + + public static void cancel(String id) { + ClientCommandArgs clientCommandArgs = new ClientCommandArgs(); + clientCommandArgs.setCancelJobId(Collections.singletonList(id)); + SeaTunnel.run(clientCommandArgs.buildCommand()); + } +} diff --git a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineServerExample.java b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineClusterServerExample.java similarity index 96% rename from seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineServerExample.java rename to seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineClusterServerExample.java index 07b363c41df..969fad5c346 100644 --- a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineServerExample.java +++ b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineClusterServerExample.java @@ -21,7 +21,7 @@ import org.apache.seatunnel.core.starter.exception.CommandException; import org.apache.seatunnel.core.starter.seatunnel.args.ServerCommandArgs; -public class SeaTunnelEngineServerExample { +public class SeaTunnelEngineClusterServerExample { static { // https://logging.apache.org/log4j/2.x/manual/simple-logger.html#isThreadContextMapInheritable diff --git a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineLocalExample.java similarity index 93% rename from seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java rename to seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineLocalExample.java index 26a965fefb5..3c23400214b 100644 --- a/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineExample.java +++ b/seatunnel-examples/seatunnel-engine-examples/src/main/java/org/apache/seatunnel/example/engine/SeaTunnelEngineLocalExample.java @@ -27,7 +27,7 @@ import java.net.URL; import java.nio.file.Paths; -public class SeaTunnelEngineExample { +public class SeaTunnelEngineLocalExample { static { // https://logging.apache.org/log4j/2.x/manual/simple-logger.html#isThreadContextMapInheritable @@ -43,14 +43,14 @@ public static void main(String[] args) clientCommandArgs.setCheckConfig(false); clientCommandArgs.setJobName(Paths.get(configFile).getFileName().toString()); // Change Execution Mode to CLUSTER to use client mode, before do this, you should start - // SeaTunnelEngineServerExample + // SeaTunnelEngineClusterServerExample clientCommandArgs.setMasterType(MasterType.LOCAL); SeaTunnel.run(clientCommandArgs.buildCommand()); } public static String getTestConfigFile(String configFile) throws FileNotFoundException, URISyntaxException { - URL resource = SeaTunnelEngineExample.class.getResource(configFile); + URL resource = SeaTunnelEngineLocalExample.class.getResource(configFile); if (resource == null) { throw new FileNotFoundException("Can't find config file: " + configFile); } From 4406fbc0a0516c8152df3370d37534348a0f8cc4 Mon Sep 17 00:00:00 2001 From: Jia Fan Date: Tue, 22 Oct 2024 10:08:39 +0800 Subject: [PATCH 28/72] [Fix][Doc] Fix zh doc build error (#7882) --- .github/workflows/backend.yml | 38 ++++++++++++++ .github/workflows/documents.yml | 66 ------------------------- docs/zh/seatunnel-engine/rest-api-v1.md | 4 +- docs/zh/seatunnel-engine/rest-api-v2.md | 4 +- tools/documents/sync.sh | 28 ++++++++--- 5 files changed, 63 insertions(+), 77 deletions(-) delete mode 100644 .github/workflows/documents.yml diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index f98ac8c80a6..34645086d42 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -125,6 +125,12 @@ jobs: echo "deleted-poms=$true_or_false" >> $GITHUB_OUTPUT echo "deleted-poms_files=$file_list" >> $GITHUB_OUTPUT + doc_files=`python tools/update_modules_check/check_file_updates.py ua $workspace apache/dev origin/$current_branch "docs/**"` + true_or_false=${doc_files%%$'\n'*} + file_list=${doc_files#*$'\n'} + echo "docs=$true_or_false" >> $GITHUB_OUTPUT + echo "docs_files=$file_list" >> $GITHUB_OUTPUT + engine_e2e_files=`python tools/update_modules_check/check_file_updates.py ua $workspace apache/dev origin/$current_branch "seatunnel-e2e/seatunnel-engine-e2e/**"` true_or_false=${engine_e2e_files%%$'\n'*} file_list=${engine_e2e_files#*$'\n'} @@ -268,6 +274,38 @@ jobs: - name: Check Dependencies Licenses run: tools/dependencies/checkLicense.sh + document: + if: needs.changes.outputs.api == 'true' || needs.changes.outputs.docs == 'true' + needs: [ changes, sanity-check ] + name: Build website + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout PR + uses: actions/checkout@v3 + with: + path: seatunnel-pr + + - name: Checkout website repo + uses: actions/checkout@v3 + with: + repository: apache/seatunnel-website + path: seatunnel-website + + - name: Sync PR changes to website + run: | + bash seatunnel-pr/tools/documents/sync.sh seatunnel-pr seatunnel-website + + - uses: actions/setup-node@v2 + with: + node-version: 16.19.0 + + - name: Run docusaurus build + run: | + cd seatunnel-website + npm set strict-ssl false + npm install + npm run build unit-test: needs: [ changes, sanity-check ] if: needs.changes.outputs.api == 'true' || (needs.changes.outputs.api == 'false' && needs.changes.outputs.ut-modules != '') diff --git a/.github/workflows/documents.yml b/.github/workflows/documents.yml deleted file mode 100644 index 61d064f0109..00000000000 --- a/.github/workflows/documents.yml +++ /dev/null @@ -1,66 +0,0 @@ -# -# 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. -# - -name: Documents - -on: - pull_request: - paths: - - 'docs/**' - -jobs: - build: - name: Build website - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - name: Checkout PR - uses: actions/checkout@v3 - with: - path: seatunnel-pr - - - name: Checkout website repo - uses: actions/checkout@v3 - with: - repository: apache/seatunnel-website - path: seatunnel-website - - - name: Sync PR changes to website - run: | - bash seatunnel-pr/tools/documents/sync.sh seatunnel-pr seatunnel-website - - - uses: actions/setup-node@v2 - with: - node-version: 16.19.0 - - - name: Run docusaurus build - run: | - cd seatunnel-website - npm set strict-ssl false - npm install - npm run build - - code-style: - name: Code style - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v3 - with: - submodules: true - - name: Check code style - run: ./mvnw --batch-mode --quiet --no-snapshot-updates clean spotless:check diff --git a/docs/zh/seatunnel-engine/rest-api-v1.md b/docs/zh/seatunnel-engine/rest-api-v1.md index d9ae96714ac..5aa9f111dfa 100644 --- a/docs/zh/seatunnel-engine/rest-api-v1.md +++ b/docs/zh/seatunnel-engine/rest-api-v1.md @@ -786,7 +786,7 @@ network: ### 获取所有节点日志内容
    - GET /hazelcast/rest/maps/logs/:jobId (返回日志列表。) + GET /hazelcast/rest/maps/logs/:jobId (返回日志列表。) #### 请求参数 @@ -837,7 +837,7 @@ network: ### 获取单节点日志内容
    - GET /hazelcast/rest/maps/log (返回日志列表。) + GET /hazelcast/rest/maps/log (返回日志列表。) #### 响应 diff --git a/docs/zh/seatunnel-engine/rest-api-v2.md b/docs/zh/seatunnel-engine/rest-api-v2.md index 26ca116845e..4964415cb2f 100644 --- a/docs/zh/seatunnel-engine/rest-api-v2.md +++ b/docs/zh/seatunnel-engine/rest-api-v2.md @@ -748,7 +748,7 @@ seatunnel: ### 获取所有节点日志内容
    - GET /logs/:jobId (返回日志列表。) + GET /logs/:jobId (返回日志列表。) #### 请求参数 @@ -800,7 +800,7 @@ seatunnel: ### 获取单节点日志内容
    - GET /log (返回日志列表。) + GET /log (返回日志列表。) #### 响应 diff --git a/tools/documents/sync.sh b/tools/documents/sync.sh index b30d93d57f7..b57eb9779ec 100644 --- a/tools/documents/sync.sh +++ b/tools/documents/sync.sh @@ -24,11 +24,14 @@ PR_IMG_DIR="${PR_DIR}/docs/images" PR_IMG_ICON_DIR="${PR_DIR}/docs/images/icons" PR_DOC_DIR="${PR_DIR}/docs/en" PR_SIDEBAR_PATH="${PR_DIR}/docs/sidebars.js" +PR_ZH_DOC_DIR="${PR_DIR}/docs/zh" WEBSITE_DIR=$2 WEBSITE_IMG_DIR="${WEBSITE_DIR}/static/image_en" +WEBSITE_ZH_IMG_DIR="${WEBSITE_DIR}/static/image_zh" WEBSITE_DOC_DIR="${WEBSITE_DIR}/docs" WEBSITE_ICON_DIR="${WEBSITE_DIR}/docs/images/icons" +WEBSITE_ZH_DOC_DIR="${WEBSITE_DIR}/i18n/zh-CN/docusaurus-plugin-content-docs/current" DOCUSAURUS_DOC_SIDEBARS_FILE="${WEBSITE_DIR}/sidebars.js" @@ -85,18 +88,19 @@ function rm_exists_files() { ############################################################## function replace_images_path(){ replace_dir=$1 + target=$2 for file_path in "${replace_dir}"/*; do if test -f "${file_path}"; then if [ "${file_path##*.}"x = "md"x ] || [ "${file_path##*.}"x = "mdx"x ]; then - echo " ---> Replace images path to /doc/image_en in ${file_path}" + echo " ---> Replace images path to /doc/${target} in ${file_path}" if [[ "$OSTYPE" == "darwin"* ]]; then - sed -E -i '' "s/(\.\.\/)*images/\/image_en/g" "${file_path}" + sed -E -i '' "s/(\.\.\/)*images/\/${target}/g" "${file_path}" else - sed -E -i "s/(\.\.\/)*images/\/image_en/g" "${file_path}" + sed -E -i "s/(\.\.\/)*images/\/${target}/g" "${file_path}" fi fi else - replace_images_path "${file_path}" + replace_images_path "${file_path}" "${target}" fi done } @@ -107,8 +111,9 @@ function replace_images_path(){ function prepare_docs() { echo "===>>>: Start documents sync." - echo "===>>>: Rebuild directory docs, static/image_en." + echo "===>>>: Rebuild directory docs, static/image_en(zh)." rebuild_dirs "${WEBSITE_DOC_DIR}" "${WEBSITE_IMG_DIR}" + rebuild_dirs "${WEBSITE_DOC_DIR}" "${WEBSITE_ZH_IMG_DIR}" echo "===>>>: Remove exists file sidebars.js." rm_exists_files "${DOCUSAURUS_DOC_SIDEBARS_FILE}" @@ -119,15 +124,24 @@ function prepare_docs() { echo "===>>>: Rsync images to ${WEBSITE_IMG_DIR}" rsync -av --exclude='/icons' "${PR_IMG_DIR}"/ "${WEBSITE_IMG_DIR}" + echo "===>>>: Rsync images to ${WEBSITE_ZH_IMG_DIR}" + rsync -av --exclude='/icons' "${PR_IMG_DIR}"/ "${WEBSITE_ZH_IMG_DIR}" + mkdir -p ${WEBSITE_ICON_DIR} echo "===>>>: Rsync icons to ${WEBSITE_ICON_DIR}" rsync -av "${PR_IMG_ICON_DIR}"/ "${WEBSITE_ICON_DIR}" - echo "===>>>: Rsync documents to ${WEBSITE_DOC_DIR}" + echo "===>>>: Rsync en documents to ${WEBSITE_DOC_DIR}" rsync -av "${PR_DOC_DIR}"/ "${WEBSITE_DOC_DIR}" + echo "===>>>: Rsync zh documents to ${WEBSITE_ZH_DOC_DIR}" + rsync -av "${PR_ZH_DOC_DIR}"/ "${WEBSITE_ZH_DOC_DIR}" + echo "===>>>: Replace images path in ${WEBSITE_DOC_DIR}" - replace_images_path "${WEBSITE_DOC_DIR}" + replace_images_path "${WEBSITE_DOC_DIR}" "image_en" + + echo "===>>>: Replace images path in ${WEBSITE_ZH_DOC_DIR}" + replace_images_path "${WEBSITE_ZH_DOC_DIR}" "image_zh" echo "===>>>: End documents sync" } From 26c528a5ed55175c3980ff6fc9725679387f4dc3 Mon Sep 17 00:00:00 2001 From: Jast Date: Wed, 23 Oct 2024 14:22:14 +0800 Subject: [PATCH 29/72] [Fix][Connector-V2][FTP] Fix FTP connector connection_mode is not effective (#7865) --- .../file/ftp/config/FtpConfigOptions.java | 4 +- .../file/ftp/system/FtpConnectionMode.java | 6 +- .../ftp/system/SeaTunnelFTPFileSystem.java | 21 +-- .../e2e/connector/file/ftp/FtpFileIT.java | 72 +++++++++- .../fake_to_ftp_file_text_for_passive.conf | 88 ++++++++++++ .../ftp_file_text_to_assert_for_passive.conf | 136 ++++++++++++++++++ 6 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/fake_to_ftp_file_text_for_passive.conf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/ftp_file_text_to_assert_for_passive.conf diff --git a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/config/FtpConfigOptions.java b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/config/FtpConfigOptions.java index 1f00a56abfd..645225b9eac 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/config/FtpConfigOptions.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/config/FtpConfigOptions.java @@ -22,7 +22,7 @@ import org.apache.seatunnel.connectors.seatunnel.file.config.BaseSourceConfigOptions; import org.apache.seatunnel.connectors.seatunnel.file.ftp.system.FtpConnectionMode; -import static org.apache.seatunnel.connectors.seatunnel.file.ftp.system.FtpConnectionMode.ACTIVE_LOCAL_DATA_CONNECTION_MODE; +import static org.apache.seatunnel.connectors.seatunnel.file.ftp.system.FtpConnectionMode.ACTIVE_LOCAL; public class FtpConfigOptions extends BaseSourceConfigOptions { public static final Option FTP_PASSWORD = @@ -42,6 +42,6 @@ public class FtpConfigOptions extends BaseSourceConfigOptions { public static final Option FTP_CONNECTION_MODE = Options.key("connection_mode") .enumType(FtpConnectionMode.class) - .defaultValue(ACTIVE_LOCAL_DATA_CONNECTION_MODE) + .defaultValue(ACTIVE_LOCAL) .withDescription("FTP server connection mode "); } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/FtpConnectionMode.java b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/FtpConnectionMode.java index 068aa5974c1..44f2264fb2c 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/FtpConnectionMode.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/FtpConnectionMode.java @@ -21,10 +21,10 @@ public enum FtpConnectionMode { /** ACTIVE_LOCAL_DATA_CONNECTION_MODE */ - ACTIVE_LOCAL_DATA_CONNECTION_MODE("active_local"), + ACTIVE_LOCAL("active_local"), /** PASSIVE_LOCAL_DATA_CONNECTION_MODE */ - PASSIVE_LOCAL_DATA_CONNECTION_MODE("passive_local"); + PASSIVE_LOCAL("passive_local"); private final String mode; @@ -38,7 +38,7 @@ public String getMode() { public static FtpConnectionMode fromMode(String mode) { for (FtpConnectionMode ftpConnectionModeEnum : FtpConnectionMode.values()) { - if (ftpConnectionModeEnum.getMode().equals(mode)) { + if (ftpConnectionModeEnum.getMode().equals(mode.toLowerCase())) { return ftpConnectionModeEnum; } } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/SeaTunnelFTPFileSystem.java b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/SeaTunnelFTPFileSystem.java index 04ba218e455..029890918da 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/SeaTunnelFTPFileSystem.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/SeaTunnelFTPFileSystem.java @@ -40,6 +40,8 @@ import org.apache.hadoop.net.NetUtils; import org.apache.hadoop.util.Progressable; +import lombok.extern.slf4j.Slf4j; + import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -52,6 +54,7 @@ */ @InterfaceAudience.Public @InterfaceStability.Stable +@Slf4j public class SeaTunnelFTPFileSystem extends FileSystem { public static final Log LOG = LogFactory.getLog(SeaTunnelFTPFileSystem.class); @@ -156,10 +159,7 @@ private FTPClient connect() throws IOException { } setFsFtpConnectionMode( - client, - conf.get( - FS_FTP_CONNECTION_MODE, - FtpConnectionMode.ACTIVE_LOCAL_DATA_CONNECTION_MODE.getMode())); + client, conf.get(FS_FTP_CONNECTION_MODE, FtpConnectionMode.ACTIVE_LOCAL.getMode())); return client; } @@ -172,13 +172,18 @@ private FTPClient connect() throws IOException { */ private void setFsFtpConnectionMode(FTPClient client, String mode) { switch (FtpConnectionMode.fromMode(mode)) { - case ACTIVE_LOCAL_DATA_CONNECTION_MODE: - client.enterLocalActiveMode(); - break; - case PASSIVE_LOCAL_DATA_CONNECTION_MODE: + case PASSIVE_LOCAL: client.enterLocalPassiveMode(); break; + case ACTIVE_LOCAL: + client.enterLocalActiveMode(); + break; default: + log.warn( + "Unsupported FTP connection mode: " + mode, + " Using default FTP connection mode: " + + FtpConnectionMode.ACTIVE_LOCAL.getMode()); + client.enterLocalActiveMode(); break; } } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java index 70b2463ea89..bc246e9007b 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java @@ -31,8 +31,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestTemplate; +import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.lifecycle.Startables; import org.testcontainers.shaded.com.github.dockerjava.core.command.ExecStartResultCallback; @@ -42,10 +44,15 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Properties; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; @DisabledOnContainer( @@ -68,14 +75,30 @@ public class FtpFileIT extends TestSuiteBase implements TestResource { private GenericContainer ftpContainer; + private String ftpPassiveAddress; + + private BiFunction generateExposedPorts = + (startPort, endPort) -> + IntStream.rangeClosed(startPort, endPort).boxed().toArray(Integer[]::new); + + private BiFunction> generatePortBindings = + (startPort, endPort) -> + IntStream.rangeClosed(startPort, endPort) + .mapToObj(i -> i + ":" + i) + .collect(Collectors.toList()); + @BeforeAll @Override public void startUp() throws Exception { + int passiveStartPort = 30000; + int passiveEndPort = 30004; ftpContainer = new GenericContainer<>(FTP_IMAGE) .withExposedPorts(FTP_PORT) .withNetwork(NETWORK) .withExposedPorts(FTP_PORT) + .withExposedPorts( + generateExposedPorts.apply(passiveStartPort, passiveEndPort)) .withNetworkAliases(ftp_CONTAINER_HOST) .withEnv("FILE_OPEN_MODE", "0666") .withEnv("WRITE_ENABLE", "YES") @@ -85,13 +108,31 @@ public void startUp() throws Exception { .withEnv("LOCAL_UMASK", "000") .withEnv("FTP_USER", USERNAME) .withEnv("FTP_PASS", PASSWORD) - .withEnv("PASV_ADDRESS", "0.0.0.0") + .withEnv("PASV_MIN_PORT", String.valueOf(passiveStartPort)) + .withEnv("PASV_MAX_PORT", String.valueOf(passiveEndPort)) .withLogConsumer(new Slf4jLogConsumer(log)) + // Modify the strategy mode because the passive mode port does not need to + // be checked here, it does not start with the FTP startup. + .waitingFor(Wait.forLogMessage(".*", 1)) .withPrivilegedMode(true); - ftpContainer.setPortBindings(Collections.singletonList("21:21")); + List portBind = new ArrayList<>(); + portBind.add("21:21"); + portBind.addAll(generatePortBindings.apply(passiveStartPort, passiveEndPort)); + + ftpContainer.setPortBindings(portBind); ftpContainer.start(); Startables.deepStart(Stream.of(ftpContainer)).join(); + + // Get the passive mode address of the FTP container + Properties properties = new Properties(); + properties.load( + new StringReader( + ftpContainer + .execInContainer("sh", "-c", "cat /etc/vsftpd/vsftpd.conf") + .getStdout())); + ftpPassiveAddress = properties.getProperty("pasv_address"); + log.info("ftp container started"); ContainerUtil.copyFileIntoContainers( @@ -126,6 +167,33 @@ public void startUp() throws Exception { ftpContainer.execInContainer("sh", "-c", "chown -R ftp:ftp /home/vsftpd/seatunnel/"); } + @TestTemplate + public void testFtpFileReadAndWriteForPassive(TestContainer container) + throws IOException, InterruptedException { + List configParams = Collections.singletonList("ftpHost=" + ftpPassiveAddress); + // Test passive mode + assertJobExecution( + container, "/text/ftp_file_text_to_assert_for_passive.conf", configParams); + assertJobExecution(container, "/text/fake_to_ftp_file_text_for_passive.conf", configParams); + + String homePath = "/home/vsftpd/seatunnel/tmp/seatunnel/passive_text"; + // test write ftp text file + Assertions.assertEquals(1, getFileListFromContainer(homePath).size()); + + // Confirm data is written correctly + Container.ExecResult execResult = + ftpContainer.execInContainer("sh", "-c", "awk 'END {print NR}' " + homePath + "/*"); + Assertions.assertEquals("15", execResult.getStdout().trim()); + + deleteFileFromContainer(homePath); + } + + private void assertJobExecution(TestContainer container, String configPath, List params) + throws IOException, InterruptedException { + Container.ExecResult execResult = container.executeJob(configPath, params); + Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); + } + @TestTemplate public void testFtpFileReadAndWrite(TestContainer container) throws IOException, InterruptedException { diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/fake_to_ftp_file_text_for_passive.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/fake_to_ftp_file_text_for_passive.conf new file mode 100644 index 00000000000..d81c1ff6a97 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/fake_to_ftp_file_text_for_passive.conf @@ -0,0 +1,88 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" + + # You can set spark configuration here + spark.app.name = "SeaTunnel" + spark.executor.instances = 1 + spark.executor.cores = 1 + spark.executor.memory = "1g" + spark.master = local +} + +source { + FakeSource { + result_table_name = "ftp" + row.num = 15 + schema = { + fields { + c_map = "map" + c_array = "array" + c_string = string + c_boolean = boolean + c_tinyint = tinyint + c_smallint = smallint + c_int = int + c_bigint = bigint + c_float = float + c_double = double + c_bytes = bytes + c_date = date + c_decimal = "decimal(38, 18)" + c_timestamp = timestamp + c_row = { + c_map = "map" + c_array = "array" + c_string = string + c_boolean = boolean + c_tinyint = tinyint + c_smallint = smallint + c_int = int + c_bigint = bigint + c_float = float + c_double = double + c_bytes = bytes + c_date = date + c_decimal = "decimal(38, 18)" + c_timestamp = timestamp + } + } + } + } +} + +sink { + FtpFile { + host = ${ftpHost} + port = 21 + user = seatunnel + password = pass + connection_mode = "passive_local" + path = "/tmp/seatunnel/passive_text" + source_table_name = "ftp" + row_delimiter = "\n" + partition_dir_expression = "${k0}=${v0}" + is_partition_field_write_in_file = true + file_name_expression = "${transactionId}_${now}" + file_format_type = "text" + filename_time_format = "yyyy.MM.dd" + is_enable_transaction = true + } +} \ No newline at end of file diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/ftp_file_text_to_assert_for_passive.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/ftp_file_text_to_assert_for_passive.conf new file mode 100644 index 00000000000..cfa64dd6007 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/text/ftp_file_text_to_assert_for_passive.conf @@ -0,0 +1,136 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" + + # You can set spark configuration here + spark.app.name = "SeaTunnel" + spark.executor.instances = 1 + spark.executor.cores = 1 + spark.executor.memory = "1g" + spark.master = local +} + +source { + FtpFile { + host = ${ftpHost} + port = 21 + user = seatunnel + password = pass + connection_mode = "passive_local" + path = "/tmp/seatunnel/read/text" + file_format_type = "text" + result_table_name = "ftp" + schema = { + fields { + c_map = "map" + c_array = "array" + c_string = string + c_boolean = boolean + c_tinyint = tinyint + c_smallint = smallint + c_int = int + c_bigint = bigint + c_float = float + c_double = double + c_bytes = bytes + c_date = date + c_decimal = "decimal(38, 18)" + c_timestamp = timestamp + c_row = { + c_map = "map" + c_array = "array" + c_string = string + c_boolean = boolean + c_tinyint = tinyint + c_smallint = smallint + c_int = int + c_bigint = bigint + c_float = float + c_double = double + c_bytes = bytes + c_date = date + c_decimal = "decimal(38, 18)" + c_timestamp = timestamp + } + } + } + } +} + +sink { + Assert { + source_table_name = "ftp" + rules { + row_rules = [ + { + rule_type = MAX_ROW + rule_value = 5 + } + ], + field_rules = [ + { + field_name = c_string + field_type = string + field_value = [ + { + rule_type = NOT_NULL + } + ] + }, + { + field_name = c_boolean + field_type = boolean + field_value = [ + { + rule_type = NOT_NULL + } + ] + }, + { + field_name = c_double + field_type = double + field_value = [ + { + rule_type = NOT_NULL + } + ] + }, + { + field_name = name + field_type = string + field_value = [ + { + rule_type = NOT_NULL + } + ] + }, + { + field_name = hobby + field_type = string + field_value = [ + { + rule_type = NOT_NULL + } + ] + } + ] + } + } +} \ No newline at end of file From 13bd46b511e1d8e9bb5fe4da5ba740ba4b41c577 Mon Sep 17 00:00:00 2001 From: zhangdonghao <39961809+hawk9821@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:23:54 +0800 Subject: [PATCH 30/72] [Improve][E2E] modify the method of obtaining JobId (#7880) --- .../seatunnel/cdc/mysql/MysqlCDCIT.java | 24 +++------- .../cdc/postgres/OpengaussCDCIT.java | 42 +++++----------- .../seatunnel/cdc/oracle/OracleCDCIT.java | 22 +++------ .../seatunnel/cdc/postgres/PostgresCDCIT.java | 43 +++++------------ .../e2e/connector/tidb/TiDBCDCIT.java | 22 ++------- .../e2e/connector/paimon/PaimonSinkCDCIT.java | 11 +++-- .../e2e/common/util/JobIdGenerator.java | 4 +- .../engine/e2e/CheckpointEnableIT.java | 48 +++++-------------- 8 files changed, 64 insertions(+), 152 deletions(-) diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCIT.java index f5057a4fd0d..bf7e8d8fe7c 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCIT.java @@ -27,6 +27,7 @@ import org.apache.seatunnel.e2e.common.container.TestContainer; import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; +import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -49,8 +50,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Stream; import static org.awaitility.Awaitility.await; @@ -329,11 +328,13 @@ public void testMultiTableWithRestore(TestContainer container) clearTable(MYSQL_DATABASE2, SOURCE_TABLE_1); clearTable(MYSQL_DATABASE2, SOURCE_TABLE_2); + Long jobId = JobIdGenerator.newJobId(); CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/mysqlcdc_to_mysql_with_multi_table_mode_one_table.conf"); + "/mysqlcdc_to_mysql_with_multi_table_mode_one_table.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -365,26 +366,15 @@ public void testMultiTableWithRestore(TestContainer container) .pollInterval(1000, TimeUnit.MILLISECONDS) .until(() -> getConnectionStatus("st_user_sink").size() == 1); - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job mysqlcdc_to_mysql_with_multi_table_mode_one_table.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - String jobId; - if (matcher.matches()) { - jobId = matcher.group(1); - } else { - throw new RuntimeException("Can not find jobId"); - } - - Assertions.assertEquals(0, container.savepointJob(jobId).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // Restore job with add a new table CompletableFuture.supplyAsync( () -> { try { container.restoreJob( - "/mysqlcdc_to_mysql_with_multi_table_mode_two_table.conf", jobId); + "/mysqlcdc_to_mysql_with_multi_table_mode_two_table.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-opengauss-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/OpengaussCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-opengauss-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/OpengaussCDCIT.java index 5529c823966..ed3fdd74b40 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-opengauss-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/OpengaussCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-opengauss-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/OpengaussCDCIT.java @@ -24,6 +24,7 @@ import org.apache.seatunnel.e2e.common.container.TestContainer; import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; +import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -288,12 +289,14 @@ public void testOpengaussCdcMultiTableE2e(TestContainer container) { disabledReason = "Currently SPARK and FLINK do not support restore") public void testMultiTableWithRestore(TestContainer container) throws IOException, InterruptedException { + Long jobId = JobIdGenerator.newJobId(); try { CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/opengausscdc_to_opengauss_with_multi_table_mode_one_table.conf"); + "/opengausscdc_to_opengauss_with_multi_table_mode_one_table.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -319,19 +322,7 @@ public void testMultiTableWithRestore(TestContainer container) OPENGAUSS_SCHEMA, SINK_TABLE_1))))); - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job opengausscdc_to_opengauss_with_multi_table_mode_one_table.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - String jobId; - if (matcher.matches()) { - jobId = matcher.group(1); - } else { - throw new RuntimeException("Can not find jobId"); - } - - Assertions.assertEquals(0, container.savepointJob(jobId).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // Restore job with add a new table CompletableFuture.supplyAsync( @@ -339,7 +330,7 @@ public void testMultiTableWithRestore(TestContainer container) try { container.restoreJob( "/opengausscdc_to_opengauss_with_multi_table_mode_two_table.conf", - jobId); + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -397,12 +388,14 @@ public void testMultiTableWithRestore(TestContainer container) disabledReason = "Currently SPARK and FLINK do not support restore") public void testAddFiledWithRestore(TestContainer container) throws IOException, InterruptedException { + Long jobId = JobIdGenerator.newJobId(); try { CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/opengausscdc_to_opengauss_test_add_Filed.conf"); + "/opengausscdc_to_opengauss_test_add_Filed.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -425,19 +418,7 @@ public void testAddFiledWithRestore(TestContainer container) OPENGAUSS_SCHEMA, SINK_TABLE_3))))); - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job opengausscdc_to_opengauss_test_add_Filed.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - String jobId; - if (matcher.matches()) { - jobId = matcher.group(1); - } else { - throw new RuntimeException("Can not find jobId"); - } - - Assertions.assertEquals(0, container.savepointJob(jobId).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // add filed add insert source table data addFieldsForTable(OPENGAUSS_SCHEMA, SOURCE_TABLE_3); @@ -449,7 +430,8 @@ public void testAddFiledWithRestore(TestContainer container) () -> { try { container.restoreJob( - "/opengausscdc_to_opengauss_test_add_Filed.conf", jobId); + "/opengausscdc_to_opengauss_test_add_Filed.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-oracle-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/oracle/OracleCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-oracle-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/oracle/OracleCDCIT.java index 03cd2039b03..86282b7ff03 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-oracle-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/oracle/OracleCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-oracle-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/oracle/OracleCDCIT.java @@ -23,6 +23,7 @@ import org.apache.seatunnel.e2e.common.container.TestContainer; import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; +import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -390,11 +391,13 @@ public void testMultiTableWithRestore(TestContainer container) insertSourceTable(DATABASE, SOURCE_TABLE1); insertSourceTable(DATABASE, SOURCE_TABLE2); + Long jobId = JobIdGenerator.newJobId(); CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/oraclecdc_to_oracle_with_multi_table_mode_one_table.conf"); + "/oraclecdc_to_oracle_with_multi_table_mode_one_table.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -432,26 +435,15 @@ public void testMultiTableWithRestore(TestContainer container) getSourceQuerySQL( DATABASE, SINK_TABLE1))))); - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job oraclecdc_to_oracle_with_multi_table_mode_one_table.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - String jobId; - if (matcher.matches()) { - jobId = matcher.group(1); - } else { - throw new RuntimeException("Can not find jobId"); - } - - Assertions.assertEquals(0, container.savepointJob(jobId).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // Restore job with add a new table CompletableFuture.supplyAsync( () -> { try { container.restoreJob( - "/oraclecdc_to_oracle_with_multi_table_mode_two_table.conf", jobId); + "/oraclecdc_to_oracle_with_multi_table_mode_two_table.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java index 5b6d810de7f..3abca057fb2 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-postgres-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/PostgresCDCIT.java @@ -24,6 +24,7 @@ import org.apache.seatunnel.e2e.common.container.TestContainer; import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; +import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -274,12 +275,14 @@ public void testPostgresCdcMultiTableE2e(TestContainer container) { disabledReason = "Currently SPARK and FLINK do not support restore") public void testMultiTableWithRestore(TestContainer container) throws IOException, InterruptedException { + Long jobId = JobIdGenerator.newJobId(); try { CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/pgcdc_to_pg_with_multi_table_mode_one_table.conf"); + "/pgcdc_to_pg_with_multi_table_mode_one_table.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -305,26 +308,15 @@ public void testMultiTableWithRestore(TestContainer container) POSTGRESQL_SCHEMA, SINK_TABLE_1))))); - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job pgcdc_to_pg_with_multi_table_mode_one_table.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - String jobId; - if (matcher.matches()) { - jobId = matcher.group(1); - } else { - throw new RuntimeException("Can not find jobId"); - } - - Assertions.assertEquals(0, container.savepointJob(jobId).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // Restore job with add a new table CompletableFuture.supplyAsync( () -> { try { container.restoreJob( - "/pgcdc_to_pg_with_multi_table_mode_two_table.conf", jobId); + "/pgcdc_to_pg_with_multi_table_mode_two_table.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -382,12 +374,14 @@ public void testMultiTableWithRestore(TestContainer container) disabledReason = "Currently SPARK and FLINK do not support restore") public void testAddFiledWithRestore(TestContainer container) throws IOException, InterruptedException { + Long jobId = JobIdGenerator.newJobId(); try { CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/postgrescdc_to_postgres_test_add_Filed.conf"); + "/postgrescdc_to_postgres_test_add_Filed.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -410,19 +404,7 @@ public void testAddFiledWithRestore(TestContainer container) POSTGRESQL_SCHEMA, SINK_TABLE_3))))); - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job postgrescdc_to_postgres_test_add_Filed.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - String jobId; - if (matcher.matches()) { - jobId = matcher.group(1); - } else { - throw new RuntimeException("Can not find jobId"); - } - - Assertions.assertEquals(0, container.savepointJob(jobId).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // add filed add insert source table data addFieldsForTable(POSTGRESQL_SCHEMA, SOURCE_TABLE_3); @@ -434,7 +416,8 @@ public void testAddFiledWithRestore(TestContainer container) () -> { try { container.restoreJob( - "/postgrescdc_to_postgres_test_add_Filed.conf", jobId); + "/postgrescdc_to_postgres_test_add_Filed.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-tidb-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tidb/TiDBCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-tidb-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tidb/TiDBCDCIT.java index f1322645326..78c7077940c 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-tidb-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tidb/TiDBCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-tidb-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tidb/TiDBCDCIT.java @@ -24,6 +24,7 @@ import org.apache.seatunnel.e2e.common.container.TestContainer; import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; +import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -41,8 +42,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import static org.awaitility.Awaitility.await; @@ -167,11 +166,11 @@ public void testMultiTableWithRestore(TestContainer container) // Clear related content to ensure that multiple operations are not affected clearTable(TIDB_DATABASE, SOURCE_TABLE); clearTable(TIDB_DATABASE, SINK_TABLE); - + Long jobId = JobIdGenerator.newJobId(); CompletableFuture.supplyAsync( () -> { try { - container.executeJob("/tidb/tidbcdc_to_tidb.conf"); + container.executeJob("/tidb/tidbcdc_to_tidb.conf", String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -192,24 +191,13 @@ public void testMultiTableWithRestore(TestContainer container) query(getSinkQuerySQL(TIDB_DATABASE, SINK_TABLE)))); }); - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job tidbcdc_to_tidb.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - String jobId; - if (matcher.matches()) { - jobId = matcher.group(1); - } else { - throw new RuntimeException("Can not find jobId"); - } - Assertions.assertEquals(0, container.savepointJob(jobId).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // Restore job CompletableFuture.supplyAsync( () -> { try { - container.restoreJob("/tidb/tidbcdc_to_tidb.conf", jobId); + container.restoreJob("/tidb/tidbcdc_to_tidb.conf", String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java index 293cf6c76e5..05d64679317 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-paimon-e2e/src/test/java/org/apache/seatunnel/e2e/connector/paimon/PaimonSinkCDCIT.java @@ -559,7 +559,9 @@ public void testChangelogLookup(TestContainer container) throws Exception { TimeUnit.SECONDS.sleep(20); String[] jobIds = new String[] { - JobIdGenerator.newJobId(), JobIdGenerator.newJobId(), JobIdGenerator.newJobId() + String.valueOf(JobIdGenerator.newJobId()), + String.valueOf(JobIdGenerator.newJobId()), + String.valueOf(JobIdGenerator.newJobId()) }; log.info("jobIds: {}", Arrays.toString(jobIds)); List> futures = new ArrayList<>(); @@ -641,14 +643,15 @@ public void testChangelogLookup(TestContainer container) throws Exception { @TestTemplate public void testChangelogFullCompaction(TestContainer container) throws Exception { - String jobId = JobIdGenerator.newJobId(); + Long jobId = JobIdGenerator.newJobId(); log.info("jobId: {}", jobId); CompletableFuture voidCompletableFuture = CompletableFuture.runAsync( () -> { try { container.executeJob( - "/changelog_fake_cdc_sink_paimon_case2.conf", jobId); + "/changelog_fake_cdc_sink_paimon_case2.conf", + String.valueOf(jobId)); } catch (Exception e) { throw new SeaTunnelException(e); } @@ -657,7 +660,7 @@ public void testChangelogFullCompaction(TestContainer container) throws Exceptio TimeUnit.SECONDS.sleep(20); changeLogEnabled = true; // cancel stream job - container.cancelJob(jobId); + container.cancelJob(String.valueOf(jobId)); TimeUnit.SECONDS.sleep(5); // copy paimon to local container.executeExtraCommands(containerExtendedFactory); diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java index 6904593b242..08fe26893ff 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/util/JobIdGenerator.java @@ -21,7 +21,7 @@ public class JobIdGenerator { - public static String newJobId() { - return String.valueOf(Math.abs(ThreadLocalRandom.current().nextLong())); + public static Long newJobId() { + return Math.abs(ThreadLocalRandom.current().nextLong()); } } diff --git a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/CheckpointEnableIT.java b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/CheckpointEnableIT.java index 5ee0a1e7bdc..f4150829414 100644 --- a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/CheckpointEnableIT.java +++ b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/CheckpointEnableIT.java @@ -24,6 +24,7 @@ import org.apache.seatunnel.e2e.common.container.TestContainerId; import org.apache.seatunnel.e2e.common.container.flink.AbstractTestFlinkContainer; import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; +import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.TestTemplate; @@ -96,12 +97,14 @@ public void testZetaBatchCheckpointEnable(TestContainer container) public void testZetaStreamingCheckpointInterval(TestContainer container) throws IOException, InterruptedException, ExecutionException { // start job + Long jobId = JobIdGenerator.newJobId(); CompletableFuture startFuture = CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/checkpoint-streaming-enable-test-resources/stream_fakesource_to_localfile_interval.conf"); + "/checkpoint-streaming-enable-test-resources/stream_fakesource_to_localfile_interval.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -109,24 +112,9 @@ public void testZetaStreamingCheckpointInterval(TestContainer container) }); // wait obtain job id - AtomicReference jobId = new AtomicReference<>(); - await().atMost(60000, TimeUnit.MILLISECONDS) - .untilAsserted( - () -> { - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job stream_fakesource_to_localfile_interval.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - if (matcher.matches()) { - jobId.set(matcher.group(1)); - } - Assertions.assertNotNull(jobId.get()); - }); - Thread.sleep(15000); Assertions.assertTrue(container.getServerLogs().contains("checkpoint is enabled")); - Assertions.assertEquals(0, container.savepointJob(jobId.get()).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); Assertions.assertEquals(0, startFuture.get().getExitCode()); // restore job CompletableFuture.supplyAsync( @@ -134,7 +122,7 @@ public void testZetaStreamingCheckpointInterval(TestContainer container) try { return container.restoreJob( "/checkpoint-streaming-enable-test-resources/stream_fakesource_to_localfile_interval.conf", - jobId.get()); + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); @@ -164,36 +152,22 @@ public void testZetaStreamingCheckpointInterval(TestContainer container) public void testZetaStreamingCheckpointNoInterval(TestContainer container) throws IOException, InterruptedException { // start job + Long jobId = JobIdGenerator.newJobId(); CompletableFuture.supplyAsync( () -> { try { return container.executeJob( - "/checkpoint-streaming-enable-test-resources/stream_fakesource_to_localfile.conf"); + "/checkpoint-streaming-enable-test-resources/stream_fakesource_to_localfile.conf", + String.valueOf(jobId)); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); throw new RuntimeException(e); } }); - // wait obtain job id - AtomicReference jobId = new AtomicReference<>(); - await().atMost(60000, TimeUnit.MILLISECONDS) - .untilAsserted( - () -> { - Pattern jobIdPattern = - Pattern.compile( - ".*Init JobMaster for Job stream_fakesource_to_localfile.conf \\(([0-9]*)\\).*", - Pattern.DOTALL); - Matcher matcher = jobIdPattern.matcher(container.getServerLogs()); - if (matcher.matches()) { - jobId.set(matcher.group(1)); - } - Assertions.assertNotNull(jobId.get()); - }); - Thread.sleep(15000); Assertions.assertTrue(container.getServerLogs().contains("checkpoint is enabled")); - Assertions.assertEquals(0, container.savepointJob(jobId.get()).getExitCode()); + Assertions.assertEquals(0, container.savepointJob(String.valueOf(jobId)).getExitCode()); // restore job CompletableFuture.supplyAsync( @@ -202,7 +176,7 @@ public void testZetaStreamingCheckpointNoInterval(TestContainer container) return container .restoreJob( "/checkpoint-streaming-enable-test-resources/stream_fakesource_to_localfile.conf", - jobId.get()) + String.valueOf(jobId)) .getExitCode(); } catch (Exception e) { log.error("Commit task exception :" + e.getMessage()); From 15431c94d9b9f34839173d67c1b638f058f58722 Mon Sep 17 00:00:00 2001 From: Zhilin Li Date: Wed, 23 Oct 2024 20:05:13 +0800 Subject: [PATCH 31/72] [Feature][Transform-SQL]Support sql transform to generate UUID (#7881) --- docs/en/transform-v2/sql-functions.md | 11 +++++++++++ docs/zh/transform-v2/sql-functions.md | 11 +++++++++++ .../src/test/resources/sql_transform/func_string.conf | 11 ++++++++++- .../seatunnel/transform/sql/zeta/ZetaSQLFunction.java | 6 ++++++ .../seatunnel/transform/sql/zeta/ZetaSQLType.java | 1 + 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/en/transform-v2/sql-functions.md b/docs/en/transform-v2/sql-functions.md index 3438a24de9c..ce01df937fc 100644 --- a/docs/en/transform-v2/sql-functions.md +++ b/docs/en/transform-v2/sql-functions.md @@ -973,3 +973,14 @@ It is used to determine whether the condition is valid and return different valu Example: case when c_string in ('c_string') then 1 else 0 end + +### UUID + +```UUID()``` + +Generate a uuid through java function. + +Example: + +select UUID() as seatunnel_uuid + diff --git a/docs/zh/transform-v2/sql-functions.md b/docs/zh/transform-v2/sql-functions.md index 57c440a39b3..13dc3a9bc54 100644 --- a/docs/zh/transform-v2/sql-functions.md +++ b/docs/zh/transform-v2/sql-functions.md @@ -964,3 +964,14 @@ from 示例: case when c_string in ('c_string') then 1 else 0 end + +### UUID + +```UUID()``` + +通过java函数生成uuid + +示例: + +select UUID() as seatunnel_uuid + diff --git a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf index ebeea3659c2..90999c6bf42 100644 --- a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf +++ b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf @@ -54,7 +54,7 @@ transform { sql { source_table_name = "fake" result_table_name = "fake1" - query = "select ascii(c1) as c1_1, ascii(c2) as c2_1, bit_length(c4) as c4_1, length(c4) as c4_2, octet_length(c4) as c4_3, char(c5) as c5_1, concat(c1,id,'!') as c1_2, hextoraw(c6) as c6_1, rawtohex(c7) as c7_1, insert(name,2,2,'**') as name1, lower(name) as name2, upper(name) as name3, left(name, 3) as name4, right(name, 4) as name5, lpad(name, 10, '*') as name6, rpad(name, 10, '*') as name7, ltrim(c8, '*') as c8_1, rtrim(c8, '*') as c8_2, trim(c8, '*') as c8_3, regexp_replace(c9, 'w+', 'W', 'i') as c9_1, regexp_like(name, '[A-Z ]*', 'i') as name8, regexp_substr(c10, '\\d{4}') as c10_1, regexp_substr(c10, '(\\d{4})-(\\d{2})-(\\d{2})', 1, 1, null, 2) as c10_2, repeat(name||' ',3) as name9, replace(name,' ','_') as name10, soundex(name) as name11, name || space(3) as name12, substring(name, 1, 3) as name13, to_char(id) as id1, to_char(c11,'yyyy-MM-dd') as c11_1, translate(name, 'ing', 'ING') as name14, des_decrypt('1234567890', des_encrypt('1234567890', name)) as name15 from fake" + query = "select ascii(c1) as c1_1, ascii(c2) as c2_1, bit_length(c4) as c4_1, length(c4) as c4_2, octet_length(c4) as c4_3, char(c5) as c5_1, concat(c1,id,'!') as c1_2, hextoraw(c6) as c6_1, rawtohex(c7) as c7_1, insert(name,2,2,'**') as name1, lower(name) as name2, upper(name) as name3, left(name, 3) as name4, right(name, 4) as name5, lpad(name, 10, '*') as name6, rpad(name, 10, '*') as name7, ltrim(c8, '*') as c8_1, rtrim(c8, '*') as c8_2, trim(c8, '*') as c8_3, regexp_replace(c9, 'w+', 'W', 'i') as c9_1, regexp_like(name, '[A-Z ]*', 'i') as name8, regexp_substr(c10, '\\d{4}') as c10_1, regexp_substr(c10, '(\\d{4})-(\\d{2})-(\\d{2})', 1, 1, null, 2) as c10_2, repeat(name||' ',3) as name9, replace(name,' ','_') as name10, soundex(name) as name11, name || space(3) as name12, substring(name, 1, 3) as name13, to_char(id) as id1, to_char(c11,'yyyy-MM-dd') as c11_1, translate(name, 'ing', 'ING') as name14, des_decrypt('1234567890', des_encrypt('1234567890', name)) as name15,UUID() as uuid from fake" } } @@ -286,6 +286,15 @@ sink { field_value = [ {equals_to = "Joy Ding"} ] + }, + { + field_name = uuid + field_type = STRING + field_value = [ + { + rule_type = NOT_NULL + } + ] } ] } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java index a6221e4a277..ce02832712c 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java @@ -61,6 +61,8 @@ import java.util.List; import java.util.Map; +import static java.util.UUID.randomUUID; + public class ZetaSQLFunction { // ============================internal functions===================== @@ -171,6 +173,8 @@ public class ZetaSQLFunction { public static final String IFNULL = "IFNULL"; public static final String NULLIF = "NULLIF"; + public static final String UUID = "UUID"; + private final SeaTunnelRowType inputRowType; private final ZetaSQLType zetaSQLType; private final ZetaSQLFilter zetaSQLFilter; @@ -515,6 +519,8 @@ public Object executeFunctionExpr(String functionName, List args) { return SystemFunction.ifnull(args); case NULLIF: return SystemFunction.nullif(args); + case UUID: + return randomUUID().toString(); default: for (ZetaUDF udf : udfList) { if (udf.functionName().equalsIgnoreCase(functionName)) { diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java index bf3211d0d4a..3d9715561c7 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java @@ -384,6 +384,7 @@ private SeaTunnelDataType getFunctionType(Function function) { case ZetaSQLFunction.MONTHNAME: case ZetaSQLFunction.FORMATDATETIME: case ZetaSQLFunction.FROM_UNIXTIME: + case ZetaSQLFunction.UUID: return BasicType.STRING_TYPE; case ZetaSQLFunction.ASCII: case ZetaSQLFunction.LOCATE: From 88be4fd236e82b5cb7c31d311c1a4741398fb5eb Mon Sep 17 00:00:00 2001 From: hailin0 Date: Fri, 25 Oct 2024 10:53:39 +0800 Subject: [PATCH 32/72] [Hotfix][Config] Fix configuration key sort disorder (#7893) --- .../seatunnel-config-base/pom.xml | 18 + .../com/typesafe/config/impl/ConfigImpl.java | 471 ++++++++++++++++++ .../config/impl/SimpleConfigObject.java | 10 +- .../apache/seatunnel/config/ConfigTest.java | 43 ++ .../core/starter/utils/ConfigBuilder.java | 6 +- .../core/starter/utils/ConfigBuilderTest.java | 45 ++ 6 files changed, 585 insertions(+), 8 deletions(-) create mode 100644 seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/ConfigImpl.java create mode 100644 seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java create mode 100644 seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java diff --git a/seatunnel-config/seatunnel-config-base/pom.xml b/seatunnel-config/seatunnel-config-base/pom.xml index 6c75e35cbd0..5610cab85e5 100644 --- a/seatunnel-config/seatunnel-config-base/pom.xml +++ b/seatunnel-config/seatunnel-config-base/pom.xml @@ -69,11 +69,29 @@ com/typesafe/config/ConfigParseOptions.class com/typesafe/config/ConfigMergeable.class com/typesafe/config/impl/ConfigParser.class + com/typesafe/config/impl/ConfigParser$1.class + com/typesafe/config/impl/ConfigParser$ParseContext.class com/typesafe/config/impl/ConfigNodePath.class com/typesafe/config/impl/PathParser.class + com/typesafe/config/impl/PathParser$Element.class com/typesafe/config/impl/Path.class com/typesafe/config/impl/SimpleConfigObject.class + com/typesafe/config/impl/SimpleConfigObject$1.class + com/typesafe/config/impl/SimpleConfigObject$RenderComparator.class + com/typesafe/config/impl/SimpleConfigObject$ResolveModifier.class com/typesafe/config/impl/PropertiesParser.class + com/typesafe/config/impl/PropertiesParser$1.class + com/typesafe/config/impl/ConfigImpl.class + com/typesafe/config/impl/ConfigImpl$1.class + com/typesafe/config/impl/ConfigImpl$ClasspathNameSource.class + com/typesafe/config/impl/ConfigImpl$ClasspathNameSourceWithClass.class + com/typesafe/config/impl/ConfigImpl$DebugHolder.class + com/typesafe/config/impl/ConfigImpl$DefaultIncluderHolder.class + com/typesafe/config/impl/ConfigImpl$EnvVariablesHolder.class + com/typesafe/config/impl/ConfigImpl$FileNameSource.class + com/typesafe/config/impl/ConfigImpl$LoaderCache.class + com/typesafe/config/impl/ConfigImpl$LoaderCacheHolder.class + com/typesafe/config/impl/ConfigImpl$SystemPropertiesHolder.class diff --git a/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/ConfigImpl.java b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/ConfigImpl.java new file mode 100644 index 00000000000..f078897ed2d --- /dev/null +++ b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/ConfigImpl.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2011-2012 Typesafe Inc. + */ + +package org.apache.seatunnel.shade.com.typesafe.config.impl; + +import org.apache.seatunnel.shade.com.typesafe.config.Config; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigException; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigIncluder; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigMemorySize; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigObject; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigOrigin; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigParseOptions; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigParseable; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigValue; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Callable; + +/** + * Internal implementation detail, not ABI stable, do not touch. For use only by the {@link + * com.typesafe.config} package. + */ +public class ConfigImpl { + + private static class LoaderCache { + private Config currentSystemProperties; + private WeakReference currentLoader; + private Map cache; + + LoaderCache() { + this.currentSystemProperties = null; + this.currentLoader = new WeakReference(null); + this.cache = new LinkedHashMap(); + } + + // for now, caching as long as the loader remains the same, + // drop entire cache if it changes. + synchronized Config getOrElseUpdate( + ClassLoader loader, String key, Callable updater) { + if (loader != currentLoader.get()) { + // reset the cache if we start using a different loader + cache.clear(); + currentLoader = new WeakReference(loader); + } + + Config systemProperties = systemPropertiesAsConfig(); + if (systemProperties != currentSystemProperties) { + cache.clear(); + currentSystemProperties = systemProperties; + } + + Config config = cache.get(key); + if (config == null) { + try { + config = updater.call(); + } catch (RuntimeException e) { + throw e; // this will include ConfigException + } catch (Exception e) { + throw new ConfigException.Generic(e.getMessage(), e); + } + if (config == null) + throw new ConfigException.BugOrBroken("null config from cache updater"); + cache.put(key, config); + } + + return config; + } + } + + private static class LoaderCacheHolder { + static final LoaderCache cache = new LoaderCache(); + } + + public static Config computeCachedConfig( + ClassLoader loader, String key, Callable updater) { + LoaderCache cache; + try { + cache = LoaderCacheHolder.cache; + } catch (ExceptionInInitializerError e) { + throw ConfigImplUtil.extractInitializerError(e); + } + return cache.getOrElseUpdate(loader, key, updater); + } + + static class FileNameSource implements SimpleIncluder.NameSource { + @Override + public ConfigParseable nameToParseable(String name, ConfigParseOptions parseOptions) { + return Parseable.newFile(new File(name), parseOptions); + } + }; + + static class ClasspathNameSource implements SimpleIncluder.NameSource { + @Override + public ConfigParseable nameToParseable(String name, ConfigParseOptions parseOptions) { + return Parseable.newResources(name, parseOptions); + } + }; + + static class ClasspathNameSourceWithClass implements SimpleIncluder.NameSource { + private final Class klass; + + public ClasspathNameSourceWithClass(Class klass) { + this.klass = klass; + } + + @Override + public ConfigParseable nameToParseable(String name, ConfigParseOptions parseOptions) { + return Parseable.newResources(klass, name, parseOptions); + } + }; + + public static ConfigObject parseResourcesAnySyntax( + Class klass, String resourceBasename, ConfigParseOptions baseOptions) { + SimpleIncluder.NameSource source = new ClasspathNameSourceWithClass(klass); + return SimpleIncluder.fromBasename(source, resourceBasename, baseOptions); + } + + public static ConfigObject parseResourcesAnySyntax( + String resourceBasename, ConfigParseOptions baseOptions) { + SimpleIncluder.NameSource source = new ClasspathNameSource(); + return SimpleIncluder.fromBasename(source, resourceBasename, baseOptions); + } + + public static ConfigObject parseFileAnySyntax(File basename, ConfigParseOptions baseOptions) { + SimpleIncluder.NameSource source = new FileNameSource(); + return SimpleIncluder.fromBasename(source, basename.getPath(), baseOptions); + } + + static AbstractConfigObject emptyObject(String originDescription) { + ConfigOrigin origin = + originDescription != null ? SimpleConfigOrigin.newSimple(originDescription) : null; + return emptyObject(origin); + } + + public static Config emptyConfig(String originDescription) { + return emptyObject(originDescription).toConfig(); + } + + static AbstractConfigObject empty(ConfigOrigin origin) { + return emptyObject(origin); + } + + // default origin for values created with fromAnyRef and no origin specified + private static final ConfigOrigin defaultValueOrigin = + SimpleConfigOrigin.newSimple("hardcoded value"); + private static final ConfigBoolean defaultTrueValue = + new ConfigBoolean(defaultValueOrigin, true); + private static final ConfigBoolean defaultFalseValue = + new ConfigBoolean(defaultValueOrigin, false); + private static final ConfigNull defaultNullValue = new ConfigNull(defaultValueOrigin); + private static final SimpleConfigList defaultEmptyList = + new SimpleConfigList(defaultValueOrigin, Collections.emptyList()); + private static final SimpleConfigObject defaultEmptyObject = + SimpleConfigObject.empty(defaultValueOrigin); + + private static SimpleConfigList emptyList(ConfigOrigin origin) { + if (origin == null || origin == defaultValueOrigin) return defaultEmptyList; + else return new SimpleConfigList(origin, Collections.emptyList()); + } + + private static AbstractConfigObject emptyObject(ConfigOrigin origin) { + // we want null origin to go to SimpleConfigObject.empty() to get the + // origin "empty config" rather than "hardcoded value" + if (origin == defaultValueOrigin) return defaultEmptyObject; + else return SimpleConfigObject.empty(origin); + } + + private static ConfigOrigin valueOrigin(String originDescription) { + if (originDescription == null) return defaultValueOrigin; + else return SimpleConfigOrigin.newSimple(originDescription); + } + + public static ConfigValue fromAnyRef(Object object, String originDescription) { + ConfigOrigin origin = valueOrigin(originDescription); + return fromAnyRef(object, origin, FromMapMode.KEYS_ARE_KEYS); + } + + public static ConfigObject fromPathMap( + Map pathMap, String originDescription) { + ConfigOrigin origin = valueOrigin(originDescription); + return (ConfigObject) fromAnyRef(pathMap, origin, FromMapMode.KEYS_ARE_PATHS); + } + + static AbstractConfigValue fromAnyRef(Object object, ConfigOrigin origin, FromMapMode mapMode) { + if (origin == null) throw new ConfigException.BugOrBroken("origin not supposed to be null"); + + if (object == null) { + if (origin != defaultValueOrigin) return new ConfigNull(origin); + else return defaultNullValue; + } else if (object instanceof AbstractConfigValue) { + return (AbstractConfigValue) object; + } else if (object instanceof Boolean) { + if (origin != defaultValueOrigin) { + return new ConfigBoolean(origin, (Boolean) object); + } else if ((Boolean) object) { + return defaultTrueValue; + } else { + return defaultFalseValue; + } + } else if (object instanceof String) { + return new ConfigString.Quoted(origin, (String) object); + } else if (object instanceof Number) { + // here we always keep the same type that was passed to us, + // rather than figuring out if a Long would fit in an Int + // or a Double has no fractional part. i.e. deliberately + // not using ConfigNumber.newNumber() when we have a + // Double, Integer, or Long. + if (object instanceof Double) { + return new ConfigDouble(origin, (Double) object, null); + } else if (object instanceof Integer) { + return new ConfigInt(origin, (Integer) object, null); + } else if (object instanceof Long) { + return new ConfigLong(origin, (Long) object, null); + } else { + return ConfigNumber.newNumber(origin, ((Number) object).doubleValue(), null); + } + } else if (object instanceof Duration) { + return new ConfigLong(origin, ((Duration) object).toMillis(), null); + } else if (object instanceof Map) { + if (((Map) object).isEmpty()) return emptyObject(origin); + + if (mapMode == FromMapMode.KEYS_ARE_KEYS) { + Map values = + new LinkedHashMap(); + for (Map.Entry entry : ((Map) object).entrySet()) { + Object key = entry.getKey(); + if (!(key instanceof String)) + throw new ConfigException.BugOrBroken( + "bug in method caller: not valid to create ConfigObject from map with non-String key: " + + key); + AbstractConfigValue value = fromAnyRef(entry.getValue(), origin, mapMode); + values.put((String) key, value); + } + + return new SimpleConfigObject(origin, values); + } else { + return PropertiesParser.fromPathMap(origin, (Map) object); + } + } else if (object instanceof Iterable) { + Iterator i = ((Iterable) object).iterator(); + if (!i.hasNext()) return emptyList(origin); + + List values = new ArrayList(); + while (i.hasNext()) { + AbstractConfigValue v = fromAnyRef(i.next(), origin, mapMode); + values.add(v); + } + + return new SimpleConfigList(origin, values); + } else if (object instanceof ConfigMemorySize) { + return new ConfigLong(origin, ((ConfigMemorySize) object).toBytes(), null); + } else { + throw new ConfigException.BugOrBroken( + "bug in method caller: not valid to create ConfigValue from: " + object); + } + } + + private static class DefaultIncluderHolder { + static final ConfigIncluder defaultIncluder = new SimpleIncluder(null); + } + + static ConfigIncluder defaultIncluder() { + try { + return DefaultIncluderHolder.defaultIncluder; + } catch (ExceptionInInitializerError e) { + throw ConfigImplUtil.extractInitializerError(e); + } + } + + private static Properties getSystemProperties() { + // Avoid ConcurrentModificationException due to parallel setting of system properties by + // copying properties + final Properties systemProperties = System.getProperties(); + final Properties systemPropertiesCopy = new Properties(); + synchronized (systemProperties) { + systemPropertiesCopy.putAll(systemProperties); + } + return systemPropertiesCopy; + } + + private static AbstractConfigObject loadSystemProperties() { + return (AbstractConfigObject) + Parseable.newProperties( + getSystemProperties(), + ConfigParseOptions.defaults() + .setOriginDescription("system properties")) + .parse(); + } + + private static class SystemPropertiesHolder { + // this isn't final due to the reloadSystemPropertiesConfig() hack below + static volatile AbstractConfigObject systemProperties = loadSystemProperties(); + } + + static AbstractConfigObject systemPropertiesAsConfigObject() { + try { + return SystemPropertiesHolder.systemProperties; + } catch (ExceptionInInitializerError e) { + throw ConfigImplUtil.extractInitializerError(e); + } + } + + public static Config systemPropertiesAsConfig() { + return systemPropertiesAsConfigObject().toConfig(); + } + + public static void reloadSystemPropertiesConfig() { + // ConfigFactory.invalidateCaches() relies on this having the side + // effect that it drops all caches + SystemPropertiesHolder.systemProperties = loadSystemProperties(); + } + + private static AbstractConfigObject loadEnvVariables() { + return PropertiesParser.fromStringMap(newSimpleOrigin("env variables"), System.getenv()); + } + + private static class EnvVariablesHolder { + static volatile AbstractConfigObject envVariables = loadEnvVariables(); + } + + static AbstractConfigObject envVariablesAsConfigObject() { + try { + return EnvVariablesHolder.envVariables; + } catch (ExceptionInInitializerError e) { + throw ConfigImplUtil.extractInitializerError(e); + } + } + + public static Config envVariablesAsConfig() { + return envVariablesAsConfigObject().toConfig(); + } + + public static void reloadEnvVariablesConfig() { + // ConfigFactory.invalidateCaches() relies on this having the side + // effect that it drops all caches + EnvVariablesHolder.envVariables = loadEnvVariables(); + } + + public static Config defaultReference(final ClassLoader loader) { + return computeCachedConfig( + loader, + "defaultReference", + new Callable() { + @Override + public Config call() { + Config unresolvedResources = + Parseable.newResources( + "reference.conf", + ConfigParseOptions.defaults() + .setClassLoader(loader)) + .parse() + .toConfig(); + return systemPropertiesAsConfig() + .withFallback(unresolvedResources) + .resolve(); + } + }); + } + + private static class DebugHolder { + private static String LOADS = "loads"; + private static String SUBSTITUTIONS = "substitutions"; + + private static Map loadDiagnostics() { + Map result = new LinkedHashMap(); + result.put(LOADS, false); + result.put(SUBSTITUTIONS, false); + + // People do -Dconfig.trace=foo,bar to enable tracing of different things + String s = System.getProperty("config.trace"); + if (s == null) { + return result; + } else { + String[] keys = s.split(","); + for (String k : keys) { + if (k.equals(LOADS)) { + result.put(LOADS, true); + } else if (k.equals(SUBSTITUTIONS)) { + result.put(SUBSTITUTIONS, true); + } else { + System.err.println( + "config.trace property contains unknown trace topic '" + k + "'"); + } + } + return result; + } + } + + private static final Map diagnostics = loadDiagnostics(); + + private static final boolean traceLoadsEnabled = diagnostics.get(LOADS); + private static final boolean traceSubstitutionsEnabled = diagnostics.get(SUBSTITUTIONS); + + static boolean traceLoadsEnabled() { + return traceLoadsEnabled; + } + + static boolean traceSubstitutionsEnabled() { + return traceSubstitutionsEnabled; + } + } + + public static boolean traceLoadsEnabled() { + try { + return DebugHolder.traceLoadsEnabled(); + } catch (ExceptionInInitializerError e) { + throw ConfigImplUtil.extractInitializerError(e); + } + } + + public static boolean traceSubstitutionsEnabled() { + try { + return DebugHolder.traceSubstitutionsEnabled(); + } catch (ExceptionInInitializerError e) { + throw ConfigImplUtil.extractInitializerError(e); + } + } + + public static void trace(String message) { + System.err.println(message); + } + + public static void trace(int indentLevel, String message) { + while (indentLevel > 0) { + System.err.print(" "); + indentLevel -= 1; + } + System.err.println(message); + } + + // the basic idea here is to add the "what" and have a canonical + // toplevel error message. the "original" exception may however have extra + // detail about what happened. call this if you have a better "what" than + // further down on the stack. + static ConfigException.NotResolved improveNotResolved( + Path what, ConfigException.NotResolved original) { + String newMessage = + what.render() + + " has not been resolved, you need to call Config#resolve()," + + " see API docs for Config#resolve()"; + if (newMessage.equals(original.getMessage())) return original; + else return new ConfigException.NotResolved(newMessage, original); + } + + public static ConfigOrigin newSimpleOrigin(String description) { + if (description == null) { + return defaultValueOrigin; + } else { + return SimpleConfigOrigin.newSimple(description); + } + } + + public static ConfigOrigin newFileOrigin(String filename) { + return SimpleConfigOrigin.newFile(filename); + } + + public static ConfigOrigin newURLOrigin(URL url) { + return SimpleConfigOrigin.newURL(url); + } +} diff --git a/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/SimpleConfigObject.java b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/SimpleConfigObject.java index 735df6829c9..b10148977b7 100644 --- a/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/SimpleConfigObject.java +++ b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/SimpleConfigObject.java @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -277,7 +278,7 @@ protected SimpleConfigObject mergedWithObject(AbstractConfigObject abstractFallb boolean changed = false; boolean allResolved = true; Map merged = new LinkedHashMap<>(); - Set allKeys = new HashSet<>(); + Set allKeys = new LinkedHashSet<>(); allKeys.addAll(this.keySet()); allKeys.addAll(fallback.keySet()); @@ -386,8 +387,7 @@ ResolveResult resolveSubstitutions( ResolveSource sourceWithParent = source.pushParent(this); try { - SimpleConfigObject.ResolveModifier modifier = - new SimpleConfigObject.ResolveModifier(context, sourceWithParent); + ResolveModifier modifier = new ResolveModifier(context, sourceWithParent); AbstractConfigValue value = this.modifyMayThrow(modifier); return ResolveResult.make(modifier.context, value).asObjectResult(); } catch (NotPossibleToResolve | RuntimeException var6) { @@ -562,7 +562,7 @@ public boolean containsValue(Object v) { } public Set> entrySet() { - HashSet> entries = new HashSet<>(); + HashSet> entries = new LinkedHashSet<>(); for (Entry stringAbstractConfigValueEntry : this.value.entrySet()) { @@ -584,7 +584,7 @@ public int size() { } public Collection values() { - return new HashSet<>(this.value.values()); + return new ArrayList<>(this.value.values()); } static SimpleConfigObject empty() { diff --git a/seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java b/seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java new file mode 100644 index 00000000000..6d8eb73ffae --- /dev/null +++ b/seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java @@ -0,0 +1,43 @@ +/* + * 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.seatunnel.config; + +import org.apache.seatunnel.shade.com.typesafe.config.Config; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory; +import org.apache.seatunnel.shade.com.typesafe.config.ConfigRenderOptions; + +import org.apache.seatunnel.config.utils.FileUtils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.net.URISyntaxException; + +public class ConfigTest { + + @Test + public void testConfigKeyOrder() throws URISyntaxException { + String expected = + "{\"env\":{\"job.mode\":\"BATCH\"},\"source\":[{\"row.num\":100,\"schema\":{\"fields\":{\"name\":\"string\",\"age\":\"int\"}},\"plugin_name\":\"FakeSource\"}],\"sink\":[{\"plugin_name\":\"Console\"}]}"; + + Config config = + ConfigFactory.parseFile( + FileUtils.getFileFromResources("/seatunnel/serialize.conf")); + Assertions.assertEquals(expected, config.root().render(ConfigRenderOptions.concise())); + } +} diff --git a/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigBuilder.java b/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigBuilder.java index 40dea79166a..47d47b0f4c5 100644 --- a/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigBuilder.java +++ b/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigBuilder.java @@ -38,7 +38,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -123,7 +123,7 @@ public static Config of( public static Map configDesensitization(Map configMap) { return configMap.entrySet().stream() .collect( - HashMap::new, + LinkedHashMap::new, (m, p) -> { String key = p.getKey(); Object value = p.getValue(); @@ -154,7 +154,7 @@ public static Map configDesensitization(Map conf } } }, - HashMap::putAll); + LinkedHashMap::putAll); } public static Config of( diff --git a/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java b/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java new file mode 100644 index 00000000000..a9196c19a3e --- /dev/null +++ b/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java @@ -0,0 +1,45 @@ +/* + * 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.seatunnel.core.starter.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ConfigBuilderTest { + + @Test + public void testConfigDesensitizationSort() { + Map config = new LinkedHashMap<>(); + config.put("a", "1"); + config.put("b", "1"); + config.put("c", "1"); + config.put("d", "1"); + config.put("e", "1"); + config.put("f", "1"); + + Map desensitizationConfig = ConfigBuilder.configDesensitization(config); + List keys = new ArrayList<>(desensitizationConfig.keySet()); + Assertions.assertIterableEquals(Arrays.asList("a", "b", "c", "d", "e", "f"), keys); + } +} From 6a31f91729f6e337ae66006b34427742cf40f182 Mon Sep 17 00:00:00 2001 From: Nian Liu Date: Fri, 25 Oct 2024 11:43:49 +0800 Subject: [PATCH 33/72] [Feature] [connector-milvus] update milvus connector to support dynamic schema, failed retry, etc. (#7885) --- docs/en/connector-v2/sink/Mivlus.md | 10 +- docs/en/connector-v2/source/Mivlus.md | 8 +- .../api/table/catalog/PhysicalColumn.java | 16 +- .../api/table/type/SeaTunnelRow.java | 11 + .../common/constants/CommonOptions.java | 21 +- .../connector-milvus/pom.xml | 25 +- .../milvus/catalog/MilvusCatalog.java | 170 +++---- .../milvus/catalog/MilvusOptions.java | 4 + .../milvus/config/MilvusSinkConfig.java | 27 ++ .../milvus/config/MilvusSourceConfig.java | 12 + .../milvus/convert/MilvusConvertUtils.java | 417 ------------------ .../exception/MilvusConnectionErrorCode.java | 7 +- .../milvus/sink/MilvusBufferBatchWriter.java | 349 +++++++++++++++ .../seatunnel/milvus/sink/MilvusSink.java | 4 +- .../milvus/sink/MilvusSinkWriter.java | 59 +-- .../sink/batch/MilvusBufferBatchWriter.java | 148 ------- .../seatunnel/milvus/source/MilvusSource.java | 9 +- .../milvus/source/MilvusSourceReader.java | 259 ++++++----- .../milvus/source/MilvusSourceSplit.java | 1 + .../source/MilvusSourceSplitEnumertor.java | 86 +++- .../milvus/utils/MilvusConnectorUtils.java | 73 +++ .../milvus/utils/MilvusConvertUtils.java | 279 ++++++++++++ .../utils/sink/MilvusSinkConverter.java | 294 ++++++++++++ .../utils/source/MilvusSourceConverter.java | 364 +++++++++++++++ .../seatunnel/SeaTunnelContainer.java | 4 +- .../fieldmapper/FieldMapperTransform.java | 7 +- 26 files changed, 1812 insertions(+), 852 deletions(-) rename seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBatchWriter.java => seatunnel-common/src/main/java/org/apache/seatunnel/common/constants/CommonOptions.java (74%) delete mode 100644 seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/convert/MilvusConvertUtils.java create mode 100644 seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusBufferBatchWriter.java delete mode 100644 seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBufferBatchWriter.java create mode 100644 seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConnectorUtils.java create mode 100644 seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConvertUtils.java create mode 100644 seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/sink/MilvusSinkConverter.java create mode 100644 seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/source/MilvusSourceConverter.java diff --git a/docs/en/connector-v2/sink/Mivlus.md b/docs/en/connector-v2/sink/Mivlus.md index 081f427a5df..6b6598fae30 100644 --- a/docs/en/connector-v2/sink/Mivlus.md +++ b/docs/en/connector-v2/sink/Mivlus.md @@ -4,8 +4,11 @@ ## Description -Write data to Milvus or Zilliz Cloud - +This Milvus sink connector write data to Milvus or Zilliz Cloud, it has the following features: +- support read and write data by partition +- support write dynamic schema data from Metadata Column +- json data will be converted to json string and sink as json as well +- retry automatically to bypass ratelimit and grpc limit ## Key Features - [x] [batch](../../concept/connector-v2-features.md) @@ -34,7 +37,7 @@ Write data to Milvus or Zilliz Cloud ## Sink Options -| Name | Type | Required | Default | Description | +| Name | Type | Required | Default | Description | |----------------------|---------|----------|------------------------------|-----------------------------------------------------------| | url | String | Yes | - | The URL to connect to Milvus or Zilliz Cloud. | | token | String | Yes | - | User:password | @@ -44,6 +47,7 @@ Write data to Milvus or Zilliz Cloud | enable_upsert | boolean | No | false | Upsert data not insert. | | enable_dynamic_field | boolean | No | true | Enable create table with dynamic field. | | batch_size | int | No | 1000 | Write batch size. | +| partition_key | String | No | | Milvus partition key field | ## Task Example diff --git a/docs/en/connector-v2/source/Mivlus.md b/docs/en/connector-v2/source/Mivlus.md index a56df4c5fe7..e9560489762 100644 --- a/docs/en/connector-v2/source/Mivlus.md +++ b/docs/en/connector-v2/source/Mivlus.md @@ -4,7 +4,11 @@ ## Description -Read data from Milvus or Zilliz Cloud +This Milvus source connector reads data from Milvus or Zilliz Cloud, it has the following features: +- support read and write data by partition +- support read dynamic schema data into Metadata Column +- json data will be converted to json string and sink as json as well +- retry automatically to bypass ratelimit and grpc limit ## Key Features @@ -53,3 +57,5 @@ source { } ``` +## Changelog + diff --git a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/catalog/PhysicalColumn.java b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/catalog/PhysicalColumn.java index db9da1b2b75..2a425000222 100644 --- a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/catalog/PhysicalColumn.java +++ b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/catalog/PhysicalColumn.java @@ -215,11 +215,25 @@ public static PhysicalColumn of( String comment, String sourceType, Map options) { + return new PhysicalColumn( + name, dataType, columnLength, nullable, defaultValue, comment, sourceType, options); + } + + public static PhysicalColumn of( + String name, + SeaTunnelDataType dataType, + Long columnLength, + Integer scale, + boolean nullable, + Object defaultValue, + String comment, + String sourceType, + Map options) { return new PhysicalColumn( name, dataType, columnLength, - null, + scale, nullable, defaultValue, comment, diff --git a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/SeaTunnelRow.java b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/SeaTunnelRow.java index 10a5b33a935..32ddb4f841e 100644 --- a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/SeaTunnelRow.java +++ b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/SeaTunnelRow.java @@ -20,6 +20,7 @@ import java.io.Serializable; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -35,6 +36,8 @@ public final class SeaTunnelRow implements Serializable { private volatile int size; + private Map options = new HashMap<>(); + public SeaTunnelRow(int arity) { this.fields = new Object[arity]; } @@ -55,6 +58,10 @@ public void setRowKind(RowKind rowKind) { this.rowKind = rowKind; } + public void setOptions(Map options) { + this.options = options; + } + public int getArity() { return fields.length; } @@ -67,6 +74,10 @@ public RowKind getRowKind() { return this.rowKind; } + public Map getOptions() { + return options; + } + public Object[] getFields() { return fields; } diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBatchWriter.java b/seatunnel-common/src/main/java/org/apache/seatunnel/common/constants/CommonOptions.java similarity index 74% rename from seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBatchWriter.java rename to seatunnel-common/src/main/java/org/apache/seatunnel/common/constants/CommonOptions.java index 91e04342dc6..eb281487090 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBatchWriter.java +++ b/seatunnel-common/src/main/java/org/apache/seatunnel/common/constants/CommonOptions.java @@ -15,17 +15,20 @@ * limitations under the License. */ -package org.apache.seatunnel.connectors.seatunnel.milvus.sink.batch; +package org.apache.seatunnel.common.constants; -import org.apache.seatunnel.api.table.type.SeaTunnelRow; +import lombok.Getter; -public interface MilvusBatchWriter { +@Getter +public enum CommonOptions { + JSON("Json"), + METADATA("Metadata"), + PARTITION("Partition"), + ; - void addToBatch(SeaTunnelRow element); + private final String name; - boolean needFlush(); - - boolean flush(); - - void close(); + CommonOptions(String name) { + this.name = name; + } } diff --git a/seatunnel-connectors-v2/connector-milvus/pom.xml b/seatunnel-connectors-v2/connector-milvus/pom.xml index fc972ce1968..9a5fed37ab2 100644 --- a/seatunnel-connectors-v2/connector-milvus/pom.xml +++ b/seatunnel-connectors-v2/connector-milvus/pom.xml @@ -28,12 +28,20 @@ connector-milvus SeaTunnel : Connectors V2 : Milvus - + + + + com.google.code.gson + gson + 2.10.1 + + + io.milvus milvus-sdk-java - 2.4.3 + 2.4.5 org.slf4j @@ -42,19 +50,6 @@ - - org.mockito - mockito-core - 4.11.0 - test - - - org.mockito - mockito-inline - 4.11.0 - test - - diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusCatalog.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusCatalog.java index c1e1ac292da..6c0b846b432 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusCatalog.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusCatalog.java @@ -24,7 +24,6 @@ import org.apache.seatunnel.api.table.catalog.ConstraintKey; import org.apache.seatunnel.api.table.catalog.InfoPreviewResult; import org.apache.seatunnel.api.table.catalog.PreviewResult; -import org.apache.seatunnel.api.table.catalog.PrimaryKey; import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.catalog.TableSchema; import org.apache.seatunnel.api.table.catalog.VectorIndex; @@ -33,20 +32,21 @@ import org.apache.seatunnel.api.table.catalog.exception.DatabaseNotExistException; import org.apache.seatunnel.api.table.catalog.exception.TableAlreadyExistException; import org.apache.seatunnel.api.table.catalog.exception.TableNotExistException; -import org.apache.seatunnel.api.table.type.ArrayType; -import org.apache.seatunnel.api.table.type.SeaTunnelDataType; +import org.apache.seatunnel.common.constants.CommonOptions; import org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig; -import org.apache.seatunnel.connectors.seatunnel.milvus.convert.MilvusConvertUtils; import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; +import org.apache.seatunnel.connectors.seatunnel.milvus.utils.sink.MilvusSinkConverter; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import com.google.protobuf.ProtocolStringList; import io.milvus.client.MilvusServiceClient; import io.milvus.common.clientenum.ConsistencyLevelEnum; -import io.milvus.grpc.DataType; import io.milvus.grpc.ListDatabasesResponse; import io.milvus.grpc.ShowCollectionsResponse; +import io.milvus.grpc.ShowPartitionsResponse; import io.milvus.grpc.ShowType; import io.milvus.param.ConnectParam; import io.milvus.param.IndexType; @@ -61,6 +61,8 @@ import io.milvus.param.collection.HasCollectionParam; import io.milvus.param.collection.ShowCollectionsParam; import io.milvus.param.index.CreateIndexParam; +import io.milvus.param.partition.CreatePartitionParam; +import io.milvus.param.partition.ShowPartitionsParam; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; @@ -70,6 +72,7 @@ import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.CREATE_INDEX; @Slf4j public class MilvusCatalog implements Catalog { @@ -196,7 +199,8 @@ public void createTable(TablePath tablePath, CatalogTable catalogTable, boolean checkNotNull(tableSchema, "tableSchema must not be null"); createTableInternal(tablePath, catalogTable); - if (CollectionUtils.isNotEmpty(tableSchema.getConstraintKeys())) { + if (CollectionUtils.isNotEmpty(tableSchema.getConstraintKeys()) + && config.get(CREATE_INDEX)) { for (ConstraintKey constraintKey : tableSchema.getConstraintKeys()) { if (constraintKey .getConstraintType() @@ -231,27 +235,61 @@ private void createIndexInternal( public void createTableInternal(TablePath tablePath, CatalogTable catalogTable) { try { + Map options = catalogTable.getOptions(); + + // partition key logic + boolean existPartitionKeyField = options.containsKey(MilvusOptions.PARTITION_KEY_FIELD); + String partitionKeyField = + existPartitionKeyField ? options.get(MilvusOptions.PARTITION_KEY_FIELD) : null; + // if options set, will overwrite aut read + if (StringUtils.isNotEmpty(config.get(MilvusSinkConfig.PARTITION_KEY))) { + existPartitionKeyField = true; + partitionKeyField = config.get(MilvusSinkConfig.PARTITION_KEY); + } + TableSchema tableSchema = catalogTable.getTableSchema(); List fieldTypes = new ArrayList<>(); for (Column column : tableSchema.getColumns()) { - fieldTypes.add(convertToFieldType(column, tableSchema.getPrimaryKey())); + if (column.getOptions() != null + && column.getOptions().containsKey(CommonOptions.METADATA.getName()) + && (Boolean) column.getOptions().get(CommonOptions.METADATA.getName())) { + // skip dynamic field + continue; + } + FieldType fieldType = + MilvusSinkConverter.convertToFieldType( + column, + tableSchema.getPrimaryKey(), + partitionKeyField, + config.get(MilvusSinkConfig.ENABLE_AUTO_ID)); + fieldTypes.add(fieldType); } - Map options = catalogTable.getOptions(); Boolean enableDynamicField = (options.containsKey(MilvusOptions.ENABLE_DYNAMIC_FIELD)) ? Boolean.valueOf(options.get(MilvusOptions.ENABLE_DYNAMIC_FIELD)) : config.get(MilvusSinkConfig.ENABLE_DYNAMIC_FIELD); - + String collectionDescription = ""; + if (config.get(MilvusSinkConfig.COLLECTION_DESCRIPTION) != null + && config.get(MilvusSinkConfig.COLLECTION_DESCRIPTION) + .containsKey(tablePath.getTableName())) { + // use description from config first + collectionDescription = + config.get(MilvusSinkConfig.COLLECTION_DESCRIPTION) + .get(tablePath.getTableName()); + } else if (null != catalogTable.getComment()) { + collectionDescription = catalogTable.getComment(); + } CreateCollectionParam.Builder builder = CreateCollectionParam.newBuilder() .withDatabaseName(tablePath.getDatabaseName()) .withCollectionName(tablePath.getTableName()) + .withDescription(collectionDescription) .withFieldTypes(fieldTypes) .withEnableDynamicField(enableDynamicField) .withConsistencyLevel(ConsistencyLevelEnum.BOUNDED); - if (null != catalogTable.getComment()) { - builder.withDescription(catalogTable.getComment()); + if (StringUtils.isNotEmpty(options.get(MilvusOptions.SHARDS_NUM))) { + builder.withShardsNum(Integer.parseInt(options.get(MilvusOptions.SHARDS_NUM))); } CreateCollectionParam createCollectionParam = builder.build(); @@ -260,89 +298,51 @@ public void createTableInternal(TablePath tablePath, CatalogTable catalogTable) throw new MilvusConnectorException( MilvusConnectionErrorCode.CREATE_COLLECTION_ERROR, response.getMessage()); } + + // not exist partition key field, will read show partitions to create + if (!existPartitionKeyField && options.containsKey(MilvusOptions.PARTITION_KEY_FIELD)) { + createPartitionInternal(options.get(MilvusOptions.PARTITION_KEY_FIELD), tablePath); + } + } catch (Exception e) { throw new MilvusConnectorException( MilvusConnectionErrorCode.CREATE_COLLECTION_ERROR, e); } } - private FieldType convertToFieldType(Column column, PrimaryKey primaryKey) { - SeaTunnelDataType seaTunnelDataType = column.getDataType(); - FieldType.Builder build = - FieldType.newBuilder() - .withName(column.getName()) - .withDataType( - MilvusConvertUtils.convertSqlTypeToDataType( - seaTunnelDataType.getSqlType())); - switch (seaTunnelDataType.getSqlType()) { - case ROW: - build.withMaxLength(65535); - break; - case DATE: - build.withMaxLength(20); - break; - case INT: - build.withDataType(DataType.Int32); - break; - case SMALLINT: - build.withDataType(DataType.Int16); - break; - case TINYINT: - build.withDataType(DataType.Int8); - break; - case FLOAT: - build.withDataType(DataType.Float); - break; - case DOUBLE: - build.withDataType(DataType.Double); - break; - case MAP: - build.withDataType(DataType.JSON); - break; - case BOOLEAN: - build.withDataType(DataType.Bool); - break; - case STRING: - if (column.getColumnLength() == 0) { - build.withMaxLength(512); - } else { - build.withMaxLength((int) (column.getColumnLength() / 4)); - } - break; - case ARRAY: - ArrayType arrayType = (ArrayType) column.getDataType(); - SeaTunnelDataType elementType = arrayType.getElementType(); - build.withElementType( - MilvusConvertUtils.convertSqlTypeToDataType(elementType.getSqlType())); - build.withMaxCapacity(4095); - switch (elementType.getSqlType()) { - case STRING: - if (column.getColumnLength() == 0) { - build.withMaxLength(512); - } else { - build.withMaxLength((int) (column.getColumnLength() / 4)); - } - break; - } - break; - case BINARY_VECTOR: - case FLOAT_VECTOR: - case FLOAT16_VECTOR: - case BFLOAT16_VECTOR: - build.withDimension(column.getScale()); - break; + private void createPartitionInternal(String partitionNames, TablePath tablePath) { + R showPartitionsResponseR = + this.client.showPartitions( + ShowPartitionsParam.newBuilder() + .withDatabaseName(tablePath.getDatabaseName()) + .withCollectionName(tablePath.getTableName()) + .build()); + if (!Objects.equals(showPartitionsResponseR.getStatus(), R.success().getStatus())) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.SHOW_PARTITION_ERROR, + showPartitionsResponseR.getMessage()); } - - if (null != primaryKey && primaryKey.getColumnNames().contains(column.getName())) { - build.withPrimaryKey(true); - if (null != primaryKey.getEnableAutoId()) { - build.withAutoID(primaryKey.getEnableAutoId()); - } else { - build.withAutoID(config.get(MilvusSinkConfig.ENABLE_AUTO_ID)); + ProtocolStringList existPartitionNames = + showPartitionsResponseR.getData().getPartitionNamesList(); + + // start to loop create partition + String[] partitionNameArray = partitionNames.split(","); + for (String partitionName : partitionNameArray) { + if (existPartitionNames.contains(partitionName)) { + continue; + } + R response = + this.client.createPartition( + CreatePartitionParam.newBuilder() + .withDatabaseName(tablePath.getDatabaseName()) + .withCollectionName(tablePath.getTableName()) + .withPartitionName(partitionName) + .build()); + if (!R.success().getStatus().equals(response.getStatus())) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.CREATE_PARTITION_ERROR, response.getMessage()); } } - - return build.build(); } @Override diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusOptions.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusOptions.java index b589b21d3da..96241546f6c 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusOptions.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/catalog/MilvusOptions.java @@ -14,9 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.seatunnel.connectors.seatunnel.milvus.catalog; public class MilvusOptions { public static final String ENABLE_DYNAMIC_FIELD = "enableDynamicField"; + public static final String SHARDS_NUM = "shardsNum"; + public static final String PARTITION_KEY_FIELD = "partitionKeyField"; + public static final String PARTITION_NAMES = "partitionNames"; } diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSinkConfig.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSinkConfig.java index cd286c987df..8d874fc0ae3 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSinkConfig.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSinkConfig.java @@ -23,6 +23,8 @@ import org.apache.seatunnel.api.sink.SchemaSaveMode; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import static org.apache.seatunnel.api.sink.DataSaveMode.APPEND_DATA; import static org.apache.seatunnel.api.sink.DataSaveMode.DROP_DATA; @@ -32,6 +34,16 @@ public class MilvusSinkConfig extends MilvusCommonConfig { public static final Option DATABASE = Options.key("database").stringType().noDefaultValue().withDescription("database"); + public static final Option> COLLECTION_DESCRIPTION = + Options.key("collection_description") + .mapType() + .defaultValue(new HashMap<>()) + .withDescription("collection description"); + public static final Option PARTITION_KEY = + Options.key("partition_key") + .stringType() + .noDefaultValue() + .withDescription("Milvus partition key field"); public static final Option SCHEMA_SAVE_MODE = Options.key("schema_save_mode") @@ -70,4 +82,19 @@ public class MilvusSinkConfig extends MilvusCommonConfig { .intType() .defaultValue(1000) .withDescription("writer batch size"); + public static final Option RATE_LIMIT = + Options.key("rate_limit") + .intType() + .defaultValue(100000) + .withDescription("writer rate limit"); + public static final Option LOAD_COLLECTION = + Options.key("load_collection") + .booleanType() + .defaultValue(false) + .withDescription("if load collection"); + public static final Option CREATE_INDEX = + Options.key("create_index") + .booleanType() + .defaultValue(false) + .withDescription("if load collection"); } diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSourceConfig.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSourceConfig.java index b3efba279dc..94b98548386 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSourceConfig.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/config/MilvusSourceConfig.java @@ -33,4 +33,16 @@ public class MilvusSourceConfig extends MilvusCommonConfig { .stringType() .noDefaultValue() .withDescription("Milvus collection to read"); + + public static final Option BATCH_SIZE = + Options.key("batch_size") + .intType() + .defaultValue(1000) + .withDescription("writer batch size"); + + public static final Option RATE_LIMIT = + Options.key("rate_limit") + .intType() + .defaultValue(1000000) + .withDescription("writer rate limit"); } diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/convert/MilvusConvertUtils.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/convert/MilvusConvertUtils.java deleted file mode 100644 index 65027077957..00000000000 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/convert/MilvusConvertUtils.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * 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.seatunnel.connectors.seatunnel.milvus.convert; - -import org.apache.seatunnel.api.configuration.ReadonlyConfig; -import org.apache.seatunnel.api.table.catalog.CatalogTable; -import org.apache.seatunnel.api.table.catalog.Column; -import org.apache.seatunnel.api.table.catalog.ConstraintKey; -import org.apache.seatunnel.api.table.catalog.PhysicalColumn; -import org.apache.seatunnel.api.table.catalog.PrimaryKey; -import org.apache.seatunnel.api.table.catalog.TableIdentifier; -import org.apache.seatunnel.api.table.catalog.TablePath; -import org.apache.seatunnel.api.table.catalog.TableSchema; -import org.apache.seatunnel.api.table.catalog.VectorIndex; -import org.apache.seatunnel.api.table.catalog.exception.CatalogException; -import org.apache.seatunnel.api.table.type.ArrayType; -import org.apache.seatunnel.api.table.type.BasicType; -import org.apache.seatunnel.api.table.type.SeaTunnelDataType; -import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SqlType; -import org.apache.seatunnel.api.table.type.VectorType; -import org.apache.seatunnel.common.utils.BufferUtils; -import org.apache.seatunnel.common.utils.JsonUtils; -import org.apache.seatunnel.connectors.seatunnel.milvus.catalog.MilvusOptions; -import org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSourceConfig; -import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; -import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.util.Lists; - -import com.google.gson.Gson; -import com.google.gson.JsonParser; -import com.google.protobuf.ProtocolStringList; -import io.milvus.client.MilvusServiceClient; -import io.milvus.common.utils.JacksonUtils; -import io.milvus.grpc.CollectionSchema; -import io.milvus.grpc.DataType; -import io.milvus.grpc.DescribeCollectionResponse; -import io.milvus.grpc.DescribeIndexResponse; -import io.milvus.grpc.FieldSchema; -import io.milvus.grpc.IndexDescription; -import io.milvus.grpc.KeyValuePair; -import io.milvus.grpc.ShowCollectionsResponse; -import io.milvus.grpc.ShowType; -import io.milvus.param.ConnectParam; -import io.milvus.param.R; -import io.milvus.param.collection.DescribeCollectionParam; -import io.milvus.param.collection.ShowCollectionsParam; -import io.milvus.param.index.DescribeIndexParam; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class MilvusConvertUtils { - - private static final String CATALOG_NAME = "Milvus"; - - private static final Gson gson = new Gson(); - - public static Map getSourceTables(ReadonlyConfig config) { - MilvusServiceClient client = null; - try { - client = - new MilvusServiceClient( - ConnectParam.newBuilder() - .withUri(config.get(MilvusSourceConfig.URL)) - .withToken(config.get(MilvusSourceConfig.TOKEN)) - .build()); - - String database = config.get(MilvusSourceConfig.DATABASE); - List collectionList = new ArrayList<>(); - if (StringUtils.isNotEmpty(config.get(MilvusSourceConfig.COLLECTION))) { - collectionList.add(config.get(MilvusSourceConfig.COLLECTION)); - } else { - R response = - client.showCollections( - ShowCollectionsParam.newBuilder() - .withDatabaseName(database) - .withShowType(ShowType.All) - .build()); - if (response.getStatus() != R.Status.Success.getCode()) { - throw new MilvusConnectorException( - MilvusConnectionErrorCode.SHOW_COLLECTIONS_ERROR); - } - - ProtocolStringList collections = response.getData().getCollectionNamesList(); - if (CollectionUtils.isEmpty(collections)) { - throw new MilvusConnectorException( - MilvusConnectionErrorCode.DATABASE_NO_COLLECTIONS, database); - } - collectionList.addAll(collections); - } - - Map map = new HashMap<>(); - for (String collection : collectionList) { - CatalogTable catalogTable = getCatalogTable(client, database, collection); - map.put(TablePath.of(database, collection), catalogTable); - } - return map; - } catch (Exception e) { - throw new CatalogException(e.getMessage(), e); - } finally { - if (client != null) { - client.close(); - } - } - } - - public static CatalogTable getCatalogTable( - MilvusServiceClient client, String database, String collection) { - R response = - client.describeCollection( - DescribeCollectionParam.newBuilder() - .withDatabaseName(database) - .withCollectionName(collection) - .build()); - - if (response.getStatus() != R.Status.Success.getCode()) { - throw new MilvusConnectorException(MilvusConnectionErrorCode.DESC_COLLECTION_ERROR); - } - - // collection column - DescribeCollectionResponse data = response.getData(); - CollectionSchema schema = data.getSchema(); - List columns = new ArrayList<>(); - for (FieldSchema fieldSchema : schema.getFieldsList()) { - columns.add(MilvusConvertUtils.convertColumn(fieldSchema)); - } - - // primary key - PrimaryKey primaryKey = buildPrimaryKey(schema.getFieldsList()); - - // index - R describeIndexResponseR = - client.describeIndex( - DescribeIndexParam.newBuilder() - .withDatabaseName(database) - .withCollectionName(collection) - .build()); - if (describeIndexResponseR.getStatus() != R.Status.Success.getCode()) { - throw new MilvusConnectorException(MilvusConnectionErrorCode.DESC_INDEX_ERROR); - } - DescribeIndexResponse indexResponse = describeIndexResponseR.getData(); - List vectorIndexes = buildVectorIndexes(indexResponse); - - // build tableSchema - TableSchema tableSchema = - TableSchema.builder() - .columns(columns) - .primaryKey(primaryKey) - .constraintKey( - ConstraintKey.of( - ConstraintKey.ConstraintType.VECTOR_INDEX_KEY, - "vector_index", - vectorIndexes)) - .build(); - - // build tableId - TableIdentifier tableId = TableIdentifier.of(CATALOG_NAME, database, collection); - - // build options info - Map options = new HashMap<>(); - options.put( - MilvusOptions.ENABLE_DYNAMIC_FIELD, String.valueOf(schema.getEnableDynamicField())); - - return CatalogTable.of( - tableId, tableSchema, options, new ArrayList<>(), schema.getDescription()); - } - - private static List buildVectorIndexes( - DescribeIndexResponse indexResponse) { - if (CollectionUtils.isEmpty(indexResponse.getIndexDescriptionsList())) { - return null; - } - - List list = new ArrayList<>(); - for (IndexDescription per : indexResponse.getIndexDescriptionsList()) { - Map paramsMap = - per.getParamsList().stream() - .collect( - Collectors.toMap(KeyValuePair::getKey, KeyValuePair::getValue)); - - VectorIndex index = - new VectorIndex( - per.getIndexName(), - per.getFieldName(), - paramsMap.get("index_type"), - paramsMap.get("metric_type")); - - list.add(index); - } - - return list; - } - - public static PrimaryKey buildPrimaryKey(List fields) { - for (FieldSchema field : fields) { - if (field.getIsPrimaryKey()) { - return PrimaryKey.of( - field.getName(), Lists.newArrayList(field.getName()), field.getAutoID()); - } - } - - return null; - } - - public static PhysicalColumn convertColumn(FieldSchema fieldSchema) { - DataType dataType = fieldSchema.getDataType(); - PhysicalColumn.PhysicalColumnBuilder builder = PhysicalColumn.builder(); - builder.name(fieldSchema.getName()); - builder.sourceType(dataType.name()); - builder.comment(fieldSchema.getDescription()); - - switch (dataType) { - case Bool: - builder.dataType(BasicType.BOOLEAN_TYPE); - break; - case Int8: - builder.dataType(BasicType.BYTE_TYPE); - break; - case Int16: - builder.dataType(BasicType.SHORT_TYPE); - break; - case Int32: - builder.dataType(BasicType.INT_TYPE); - break; - case Int64: - builder.dataType(BasicType.LONG_TYPE); - break; - case Float: - builder.dataType(BasicType.FLOAT_TYPE); - break; - case Double: - builder.dataType(BasicType.DOUBLE_TYPE); - break; - case VarChar: - builder.dataType(BasicType.STRING_TYPE); - for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { - if (keyValuePair.getKey().equals("max_length")) { - builder.columnLength(Long.parseLong(keyValuePair.getValue()) * 4); - break; - } - } - break; - case String: - case JSON: - builder.dataType(BasicType.STRING_TYPE); - break; - case Array: - builder.dataType(ArrayType.STRING_ARRAY_TYPE); - break; - case FloatVector: - builder.dataType(VectorType.VECTOR_FLOAT_TYPE); - for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { - if (keyValuePair.getKey().equals("dim")) { - builder.scale(Integer.valueOf(keyValuePair.getValue())); - break; - } - } - break; - case BinaryVector: - builder.dataType(VectorType.VECTOR_BINARY_TYPE); - for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { - if (keyValuePair.getKey().equals("dim")) { - builder.scale(Integer.valueOf(keyValuePair.getValue())); - break; - } - } - break; - case SparseFloatVector: - builder.dataType(VectorType.VECTOR_SPARSE_FLOAT_TYPE); - break; - case Float16Vector: - builder.dataType(VectorType.VECTOR_FLOAT16_TYPE); - for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { - if (keyValuePair.getKey().equals("dim")) { - builder.scale(Integer.valueOf(keyValuePair.getValue())); - break; - } - } - break; - case BFloat16Vector: - builder.dataType(VectorType.VECTOR_BFLOAT16_TYPE); - for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { - if (keyValuePair.getKey().equals("dim")) { - builder.scale(Integer.valueOf(keyValuePair.getValue())); - break; - } - } - break; - default: - throw new UnsupportedOperationException("Unsupported data type: " + dataType); - } - - return builder.build(); - } - - public static Object convertBySeaTunnelType(SeaTunnelDataType fieldType, Object value) { - SqlType sqlType = fieldType.getSqlType(); - switch (sqlType) { - case INT: - return Integer.parseInt(value.toString()); - case BIGINT: - return Long.parseLong(value.toString()); - case SMALLINT: - return Short.parseShort(value.toString()); - case STRING: - case DATE: - return value.toString(); - case FLOAT_VECTOR: - ByteBuffer floatVectorBuffer = (ByteBuffer) value; - Float[] floats = BufferUtils.toFloatArray(floatVectorBuffer); - return Arrays.stream(floats).collect(Collectors.toList()); - case BINARY_VECTOR: - case BFLOAT16_VECTOR: - case FLOAT16_VECTOR: - ByteBuffer vector = (ByteBuffer) value; - return gson.toJsonTree(vector.array()); - case SPARSE_FLOAT_VECTOR: - return JsonParser.parseString(JacksonUtils.toJsonString(value)).getAsJsonObject(); - case FLOAT: - return Float.parseFloat(value.toString()); - case BOOLEAN: - return Boolean.parseBoolean(value.toString()); - case DOUBLE: - return Double.parseDouble(value.toString()); - case ARRAY: - ArrayType arrayType = (ArrayType) fieldType; - switch (arrayType.getElementType().getSqlType()) { - case STRING: - String[] stringArray = (String[]) value; - return Arrays.asList(stringArray); - case INT: - Integer[] intArray = (Integer[]) value; - return Arrays.asList(intArray); - case BIGINT: - Long[] longArray = (Long[]) value; - return Arrays.asList(longArray); - case FLOAT: - Float[] floatArray = (Float[]) value; - return Arrays.asList(floatArray); - case DOUBLE: - Double[] doubleArray = (Double[]) value; - return Arrays.asList(doubleArray); - } - case ROW: - SeaTunnelRow row = (SeaTunnelRow) value; - return JsonUtils.toJsonString(row.getFields()); - case MAP: - return JacksonUtils.toJsonString(value); - default: - throw new MilvusConnectorException( - MilvusConnectionErrorCode.NOT_SUPPORT_TYPE, sqlType.name()); - } - } - - public static DataType convertSqlTypeToDataType(SqlType sqlType) { - switch (sqlType) { - case BOOLEAN: - return DataType.Bool; - case TINYINT: - return DataType.Int8; - case SMALLINT: - return DataType.Int16; - case INT: - return DataType.Int32; - case BIGINT: - return DataType.Int64; - case FLOAT: - return DataType.Float; - case DOUBLE: - return DataType.Double; - case STRING: - return DataType.VarChar; - case ARRAY: - return DataType.Array; - case FLOAT_VECTOR: - return DataType.FloatVector; - case BINARY_VECTOR: - return DataType.BinaryVector; - case FLOAT16_VECTOR: - return DataType.Float16Vector; - case BFLOAT16_VECTOR: - return DataType.BFloat16Vector; - case SPARSE_FLOAT_VECTOR: - return DataType.SparseFloatVector; - case DATE: - return DataType.VarChar; - case ROW: - return DataType.VarChar; - } - throw new CatalogException( - String.format("Not support convert to milvus type, sqlType is %s", sqlType)); - } -} diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/exception/MilvusConnectionErrorCode.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/exception/MilvusConnectionErrorCode.java index 3acc3de804c..5aaee447ea6 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/exception/MilvusConnectionErrorCode.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/exception/MilvusConnectionErrorCode.java @@ -35,7 +35,12 @@ public enum MilvusConnectionErrorCode implements SeaTunnelErrorCode { CREATE_DATABASE_ERROR("MILVUS-13", "Create database error"), CREATE_COLLECTION_ERROR("MILVUS-14", "Create collection error"), CREATE_INDEX_ERROR("MILVUS-15", "Create index error"), - ; + INIT_CLIENT_ERROR("MILVUS-16", "Init milvus client error"), + WRITE_DATA_FAIL("MILVUS-17", "Write milvus data fail"), + READ_DATA_FAIL("MILVUS-18", "Read milvus data fail"), + LIST_PARTITIONS_FAILED("MILVUS-19", "Failed to list milvus partition"), + SHOW_PARTITION_ERROR("MILVUS-20", "Desc partition error"), + CREATE_PARTITION_ERROR("MILVUS-21", "Create partition error"); private final String code; private final String description; diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusBufferBatchWriter.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusBufferBatchWriter.java new file mode 100644 index 00000000000..bc71d177c64 --- /dev/null +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusBufferBatchWriter.java @@ -0,0 +1,349 @@ +/* + * 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.seatunnel.connectors.seatunnel.milvus.sink; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.PrimaryKey; +import org.apache.seatunnel.api.table.type.SeaTunnelRow; +import org.apache.seatunnel.common.constants.CommonOptions; +import org.apache.seatunnel.common.utils.SeaTunnelException; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; +import org.apache.seatunnel.connectors.seatunnel.milvus.utils.MilvusConnectorUtils; +import org.apache.seatunnel.connectors.seatunnel.milvus.utils.sink.MilvusSinkConverter; + +import org.apache.commons.lang3.StringUtils; + +import com.google.gson.JsonObject; +import io.milvus.v2.client.ConnectConfig; +import io.milvus.v2.client.MilvusClientV2; +import io.milvus.v2.common.IndexParam; +import io.milvus.v2.service.collection.request.AlterCollectionReq; +import io.milvus.v2.service.collection.request.DescribeCollectionReq; +import io.milvus.v2.service.collection.request.GetLoadStateReq; +import io.milvus.v2.service.collection.request.LoadCollectionReq; +import io.milvus.v2.service.collection.response.DescribeCollectionResp; +import io.milvus.v2.service.index.request.CreateIndexReq; +import io.milvus.v2.service.partition.request.CreatePartitionReq; +import io.milvus.v2.service.partition.request.HasPartitionReq; +import io.milvus.v2.service.vector.request.InsertReq; +import io.milvus.v2.service.vector.request.UpsertReq; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.BATCH_SIZE; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.CREATE_INDEX; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.ENABLE_AUTO_ID; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.ENABLE_UPSERT; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.LOAD_COLLECTION; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.RATE_LIMIT; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.TOKEN; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.URL; + +@Slf4j +public class MilvusBufferBatchWriter { + + private final CatalogTable catalogTable; + private final ReadonlyConfig config; + private final String collectionName; + private final Boolean autoId; + private final Boolean enableUpsert; + private Boolean hasPartitionKey; + + private MilvusClientV2 milvusClient; + private final MilvusSinkConverter milvusSinkConverter; + private int batchSize; + private volatile Map> milvusDataCache; + private final AtomicLong writeCache = new AtomicLong(); + private final AtomicLong writeCount = new AtomicLong(); + + private final List jsonFieldNames; + private final String dynamicFieldName; + + public MilvusBufferBatchWriter(CatalogTable catalogTable, ReadonlyConfig config) + throws SeaTunnelException { + this.catalogTable = catalogTable; + this.config = config; + this.autoId = + getAutoId( + catalogTable.getTableSchema().getPrimaryKey(), config.get(ENABLE_AUTO_ID)); + this.enableUpsert = config.get(ENABLE_UPSERT); + this.batchSize = config.get(BATCH_SIZE); + this.collectionName = catalogTable.getTablePath().getTableName(); + this.milvusDataCache = new HashMap<>(); + this.milvusSinkConverter = new MilvusSinkConverter(); + + this.dynamicFieldName = MilvusConnectorUtils.getDynamicField(catalogTable); + this.jsonFieldNames = MilvusConnectorUtils.getJsonField(catalogTable); + + initMilvusClient(config); + } + /* + * set up the Milvus client + */ + private void initMilvusClient(ReadonlyConfig config) throws SeaTunnelException { + try { + log.info("begin to init Milvus client"); + String dbName = catalogTable.getTablePath().getDatabaseName(); + String collectionName = catalogTable.getTablePath().getTableName(); + + ConnectConfig connectConfig = + ConnectConfig.builder().uri(config.get(URL)).token(config.get(TOKEN)).build(); + this.milvusClient = new MilvusClientV2(connectConfig); + if (StringUtils.isNotEmpty(dbName)) { + milvusClient.useDatabase(dbName); + } + this.hasPartitionKey = + MilvusConnectorUtils.hasPartitionKey(milvusClient, collectionName); + // set rate limit + if (config.get(RATE_LIMIT) > 0) { + log.info("set rate limit for collection: " + collectionName); + Map properties = new HashMap<>(); + properties.put("collection.insertRate.max.mb", config.get(RATE_LIMIT).toString()); + properties.put("collection.upsertRate.max.mb", config.get(RATE_LIMIT).toString()); + AlterCollectionReq alterCollectionReq = + AlterCollectionReq.builder() + .collectionName(collectionName) + .properties(properties) + .build(); + milvusClient.alterCollection(alterCollectionReq); + } + try { + if (config.get(CREATE_INDEX)) { + // create index + log.info("create index for collection: " + collectionName); + DescribeCollectionResp describeCollectionResp = + milvusClient.describeCollection( + DescribeCollectionReq.builder() + .collectionName(collectionName) + .build()); + List indexParams = new ArrayList<>(); + for (String fieldName : describeCollectionResp.getVectorFieldNames()) { + IndexParam indexParam = + IndexParam.builder() + .fieldName(fieldName) + .metricType(IndexParam.MetricType.COSINE) + .build(); + indexParams.add(indexParam); + } + CreateIndexReq createIndexReq = + CreateIndexReq.builder() + .collectionName(collectionName) + .indexParams(indexParams) + .build(); + milvusClient.createIndex(createIndexReq); + } + } catch (Exception e) { + log.warn("create index failed, maybe index already exists"); + } + if (config.get(LOAD_COLLECTION) + && !milvusClient.getLoadState( + GetLoadStateReq.builder().collectionName(collectionName).build())) { + log.info("load collection: " + collectionName); + milvusClient.loadCollection( + LoadCollectionReq.builder().collectionName(collectionName).build()); + } + log.info("init Milvus client success"); + } catch (Exception e) { + log.error("init Milvus client failed", e); + throw new MilvusConnectorException(MilvusConnectionErrorCode.INIT_CLIENT_ERROR, e); + } + } + + private Boolean getAutoId(PrimaryKey primaryKey, Boolean enableAutoId) { + if (null != primaryKey && null != primaryKey.getEnableAutoId()) { + return primaryKey.getEnableAutoId(); + } else { + return enableAutoId; + } + } + + public void addToBatch(SeaTunnelRow element) { + // put data to cache by partition + if (element.getOptions().containsKey(CommonOptions.PARTITION.getName())) { + String partitionName = + element.getOptions().get(CommonOptions.PARTITION.getName()).toString(); + if (!milvusDataCache.containsKey(partitionName)) { + Boolean hasPartition = + milvusClient.hasPartition( + HasPartitionReq.builder() + .collectionName(collectionName) + .partitionName(partitionName) + .build()); + if (!hasPartition) { + log.info("create partition: " + partitionName); + CreatePartitionReq createPartitionReq = + CreatePartitionReq.builder() + .collectionName(collectionName) + .partitionName(partitionName) + .build(); + milvusClient.createPartition(createPartitionReq); + log.info("create partition success"); + } + } + } + JsonObject data = + milvusSinkConverter.buildMilvusData( + catalogTable, config, jsonFieldNames, dynamicFieldName, element); + String partitionName = + element.getOptions() + .getOrDefault(CommonOptions.PARTITION.getName(), "_default") + .toString(); + this.milvusDataCache.computeIfAbsent(partitionName, k -> new ArrayList<>()); + milvusDataCache.get(partitionName).add(data); + writeCache.incrementAndGet(); + } + + public boolean needFlush() { + return this.writeCache.get() >= this.batchSize; + } + + public void flush() throws Exception { + log.info("Starting to put {} records to Milvus.", this.writeCache.get()); + // Flush the batch writer + // Get the number of records completed + if (this.milvusDataCache.isEmpty()) { + return; + } + writeData2Collection(); + log.info( + "Successfully put {} records to Milvus. Total records written: {}", + this.writeCache.get(), + this.writeCount.get()); + this.milvusDataCache = new HashMap<>(); + this.writeCache.set(0L); + } + + public void close() throws Exception { + String collectionName = catalogTable.getTablePath().getTableName(); + // set rate limit + Map properties = new HashMap<>(); + properties.put("collection.insertRate.max.mb", "-1"); + properties.put("collection.upsertRate.max.mb", "-1"); + AlterCollectionReq alterCollectionReq = + AlterCollectionReq.builder() + .collectionName(collectionName) + .properties(properties) + .build(); + milvusClient.alterCollection(alterCollectionReq); + this.milvusClient.close(10); + } + + private void writeData2Collection() throws Exception { + try { + for (String partitionName : milvusDataCache.keySet()) { + // default to use upsertReq, but upsert only works when autoID is disabled + List data = milvusDataCache.get(partitionName); + if (Objects.equals(partitionName, "_default") || hasPartitionKey) { + partitionName = null; + } + if (enableUpsert && !autoId) { + upsertWrite(partitionName, data); + } else { + insertWrite(partitionName, data); + } + } + } catch (Exception e) { + log.error("write data to Milvus failed", e); + log.error("error data: " + milvusDataCache); + throw new MilvusConnectorException(MilvusConnectionErrorCode.WRITE_DATA_FAIL); + } + writeCount.addAndGet(this.writeCache.get()); + } + + private void upsertWrite(String partitionName, List data) + throws InterruptedException { + UpsertReq upsertReq = + UpsertReq.builder().collectionName(this.collectionName).data(data).build(); + if (StringUtils.isNotEmpty(partitionName)) { + upsertReq.setPartitionName(partitionName); + } + try { + milvusClient.upsert(upsertReq); + } catch (Exception e) { + if (e.getMessage().contains("rate limit exceeded") + || e.getMessage().contains("received message larger than max")) { + if (data.size() > 10) { + log.warn("upsert data failed, retry in smaller chunks: {} ", data.size() / 2); + this.batchSize = this.batchSize / 2; + log.info("sleep 1 minute to avoid rate limit"); + // sleep 1 minute to avoid rate limit + Thread.sleep(60000); + log.info("sleep 1 minute success"); + // Split the data and retry in smaller chunks + List firstHalf = data.subList(0, data.size() / 2); + List secondHalf = data.subList(data.size() / 2, data.size()); + upsertWrite(partitionName, firstHalf); + upsertWrite(partitionName, secondHalf); + } else { + // If the data size is 10, throw the exception to avoid infinite recursion + throw new MilvusConnectorException( + MilvusConnectionErrorCode.WRITE_DATA_FAIL, + "upsert data failed," + " size down to 10, break", + e); + } + } else { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.WRITE_DATA_FAIL, + "upsert data failed with unknown exception", + e); + } + } + log.info("upsert data success"); + } + + private void insertWrite(String partitionName, List data) { + InsertReq insertReq = + InsertReq.builder().collectionName(this.collectionName).data(data).build(); + if (StringUtils.isNotEmpty(partitionName)) { + insertReq.setPartitionName(partitionName); + } + try { + milvusClient.insert(insertReq); + } catch (Exception e) { + if (e.getMessage().contains("rate limit exceeded") + || e.getMessage().contains("received message larger than max")) { + if (data.size() > 10) { + log.warn("insert data failed, retry in smaller chunks: {} ", data.size() / 2); + // Split the data and retry in smaller chunks + List firstHalf = data.subList(0, data.size() / 2); + List secondHalf = data.subList(data.size() / 2, data.size()); + this.batchSize = this.batchSize / 2; + insertWrite(partitionName, firstHalf); + insertWrite(partitionName, secondHalf); + } else { + // If the data size is 10, throw the exception to avoid infinite recursion + throw new MilvusConnectorException( + MilvusConnectionErrorCode.WRITE_DATA_FAIL, "insert data failed", e); + } + } else { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.WRITE_DATA_FAIL, + "insert data failed with unknown exception", + e); + } + } + } +} diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSink.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSink.java index 10f4b6ca69d..9167d806df1 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSink.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSink.java @@ -38,10 +38,13 @@ import org.apache.seatunnel.connectors.seatunnel.milvus.state.MilvusCommitInfo; import org.apache.seatunnel.connectors.seatunnel.milvus.state.MilvusSinkState; +import lombok.extern.slf4j.Slf4j; + import java.util.Collections; import java.util.List; import java.util.Optional; +@Slf4j public class MilvusSink implements SeaTunnelSink< SeaTunnelRow, @@ -61,7 +64,6 @@ public MilvusSink(ReadonlyConfig config, CatalogTable catalogTable) { @Override public SinkWriter createWriter( SinkWriter.Context context) { - return new MilvusSinkWriter(context, catalogTable, config, Collections.emptyList()); } diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSinkWriter.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSinkWriter.java index 8fee6ebc68f..98b2b46c3b4 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSinkWriter.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/MilvusSinkWriter.java @@ -21,74 +21,53 @@ import org.apache.seatunnel.api.sink.SinkCommitter; import org.apache.seatunnel.api.sink.SinkWriter; import org.apache.seatunnel.api.table.catalog.CatalogTable; -import org.apache.seatunnel.api.table.catalog.PrimaryKey; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig; -import org.apache.seatunnel.connectors.seatunnel.milvus.sink.batch.MilvusBatchWriter; -import org.apache.seatunnel.connectors.seatunnel.milvus.sink.batch.MilvusBufferBatchWriter; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; import org.apache.seatunnel.connectors.seatunnel.milvus.state.MilvusCommitInfo; import org.apache.seatunnel.connectors.seatunnel.milvus.state.MilvusSinkState; -import io.milvus.v2.client.ConnectConfig; -import io.milvus.v2.client.MilvusClientV2; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.util.List; import java.util.Optional; -import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.BATCH_SIZE; - -@Slf4j /** MilvusSinkWriter is a sink writer that will write {@link SeaTunnelRow} to Milvus. */ +@Slf4j public class MilvusSinkWriter implements SinkWriter { - private final Context context; - private final ReadonlyConfig config; - private MilvusBatchWriter batchWriter; + private final MilvusBufferBatchWriter batchWriter; + private ReadonlyConfig config; public MilvusSinkWriter( Context context, CatalogTable catalogTable, ReadonlyConfig config, List milvusSinkStates) { - this.context = context; + this.batchWriter = new MilvusBufferBatchWriter(catalogTable, config); this.config = config; - ConnectConfig connectConfig = - ConnectConfig.builder() - .uri(config.get(MilvusSinkConfig.URL)) - .token(config.get(MilvusSinkConfig.TOKEN)) - .dbName(config.get(MilvusSinkConfig.DATABASE)) - .build(); - this.batchWriter = - new MilvusBufferBatchWriter( - catalogTable, - config.get(BATCH_SIZE), - getAutoId(catalogTable.getTableSchema().getPrimaryKey()), - config.get(MilvusSinkConfig.ENABLE_UPSERT), - new MilvusClientV2(connectConfig)); + log.info("create Milvus sink writer success"); + log.info("MilvusSinkWriter config: " + config); } /** * write data to third party data receiver. * * @param element the data need be written. - * @throws IOException throw IOException when write data failed. */ @Override public void write(SeaTunnelRow element) { batchWriter.addToBatch(element); if (batchWriter.needFlush()) { - batchWriter.flush(); - } - } - - private Boolean getAutoId(PrimaryKey primaryKey) { - if (null != primaryKey && null != primaryKey.getEnableAutoId()) { - return primaryKey.getEnableAutoId(); - } else { - return config.get(MilvusSinkConfig.ENABLE_AUTO_ID); + try { + // Flush the batch writer + batchWriter.flush(); + } catch (Exception e) { + log.error("flush Milvus sink writer failed", e); + throw new MilvusConnectorException(MilvusConnectionErrorCode.WRITE_DATA_FAIL, e); + } } } @@ -102,7 +81,6 @@ private Boolean getAutoId(PrimaryKey primaryKey) { */ @Override public Optional prepareCommit() throws IOException { - batchWriter.flush(); return Optional.empty(); } @@ -122,9 +100,14 @@ public void abortPrepare() {} */ @Override public void close() throws IOException { - if (batchWriter != null) { + try { + log.info("Stopping Milvus Client"); batchWriter.flush(); batchWriter.close(); + log.info("Stop Milvus Client success"); + } catch (Exception e) { + log.error("Stop Milvus Client failed", e); + throw new MilvusConnectorException(MilvusConnectionErrorCode.CLOSE_CLIENT_ERROR, e); } } } diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBufferBatchWriter.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBufferBatchWriter.java deleted file mode 100644 index 46f4e7ce7c7..00000000000 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/sink/batch/MilvusBufferBatchWriter.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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.seatunnel.connectors.seatunnel.milvus.sink.batch; - -import org.apache.seatunnel.api.table.catalog.CatalogTable; -import org.apache.seatunnel.api.table.catalog.PrimaryKey; -import org.apache.seatunnel.api.table.type.SeaTunnelDataType; -import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; -import org.apache.seatunnel.common.utils.SeaTunnelException; -import org.apache.seatunnel.connectors.seatunnel.milvus.convert.MilvusConvertUtils; -import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; -import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; - -import org.apache.commons.collections4.CollectionUtils; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.milvus.v2.client.MilvusClientV2; -import io.milvus.v2.service.vector.request.InsertReq; -import io.milvus.v2.service.vector.request.UpsertReq; - -import java.util.ArrayList; -import java.util.List; - -import static org.apache.seatunnel.api.table.catalog.PrimaryKey.isPrimaryKeyField; - -public class MilvusBufferBatchWriter implements MilvusBatchWriter { - - private final int batchSize; - private final CatalogTable catalogTable; - private final Boolean autoId; - private final Boolean enableUpsert; - private final String collectionName; - private MilvusClientV2 milvusClient; - - private volatile List milvusDataCache; - private volatile int writeCount = 0; - private static final Gson GSON = new Gson(); - - public MilvusBufferBatchWriter( - CatalogTable catalogTable, - Integer batchSize, - Boolean autoId, - Boolean enableUpsert, - MilvusClientV2 milvusClient) { - this.catalogTable = catalogTable; - this.autoId = autoId; - this.enableUpsert = enableUpsert; - this.milvusClient = milvusClient; - this.collectionName = catalogTable.getTablePath().getTableName(); - this.batchSize = batchSize; - this.milvusDataCache = new ArrayList<>(batchSize); - } - - @Override - public void addToBatch(SeaTunnelRow element) { - JsonObject data = buildMilvusData(element); - milvusDataCache.add(data); - writeCount++; - } - - @Override - public boolean needFlush() { - return this.writeCount >= this.batchSize; - } - - @Override - public synchronized boolean flush() { - if (CollectionUtils.isEmpty(this.milvusDataCache)) { - return true; - } - writeData2Collection(); - this.milvusDataCache = new ArrayList<>(this.batchSize); - this.writeCount = 0; - return true; - } - - @Override - public void close() { - try { - this.milvusClient.close(10); - } catch (InterruptedException e) { - throw new SeaTunnelException(e); - } - } - - private JsonObject buildMilvusData(SeaTunnelRow element) { - SeaTunnelRowType seaTunnelRowType = catalogTable.getSeaTunnelRowType(); - PrimaryKey primaryKey = catalogTable.getTableSchema().getPrimaryKey(); - - JsonObject data = new JsonObject(); - for (int i = 0; i < seaTunnelRowType.getFieldNames().length; i++) { - String fieldName = seaTunnelRowType.getFieldNames()[i]; - - if (autoId && isPrimaryKeyField(primaryKey, fieldName)) { - continue; // if create table open AutoId, then don't need insert data with - // primaryKey field. - } - - SeaTunnelDataType fieldType = seaTunnelRowType.getFieldType(i); - Object value = element.getField(i); - if (null == value) { - throw new MilvusConnectorException( - MilvusConnectionErrorCode.FIELD_IS_NULL, fieldName); - } - - data.add( - fieldName, - GSON.toJsonTree(MilvusConvertUtils.convertBySeaTunnelType(fieldType, value))); - } - return data; - } - - private void writeData2Collection() { - // default to use upsertReq, but upsert only works when autoID is disabled - if (enableUpsert && !autoId) { - UpsertReq upsertReq = - UpsertReq.builder() - .collectionName(this.collectionName) - .data(this.milvusDataCache) - .build(); - milvusClient.upsert(upsertReq); - } else { - InsertReq insertReq = - InsertReq.builder() - .collectionName(this.collectionName) - .data(this.milvusDataCache) - .build(); - milvusClient.insert(insertReq); - } - } -} diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSource.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSource.java index 76ccfb743e5..abb7e9c898d 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSource.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSource.java @@ -28,7 +28,7 @@ import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSourceConfig; -import org.apache.seatunnel.connectors.seatunnel.milvus.convert.MilvusConvertUtils; +import org.apache.seatunnel.connectors.seatunnel.milvus.utils.MilvusConvertUtils; import java.util.ArrayList; import java.util.List; @@ -42,9 +42,10 @@ public class MilvusSource private final ReadonlyConfig config; private final Map sourceTables; - public MilvusSource(ReadonlyConfig sourceConfig) { - this.config = sourceConfig; - this.sourceTables = MilvusConvertUtils.getSourceTables(config); + public MilvusSource(ReadonlyConfig sourceConfing) { + this.config = sourceConfing; + MilvusConvertUtils milvusConvertUtils = new MilvusConvertUtils(sourceConfing); + this.sourceTables = milvusConvertUtils.getSourceTables(); } @Override diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceReader.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceReader.java index 7464c652b31..32f4e3e61b9 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceReader.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceReader.java @@ -24,44 +24,51 @@ import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.catalog.TableSchema; -import org.apache.seatunnel.api.table.type.RowKind; -import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; -import org.apache.seatunnel.common.exception.CommonErrorCode; -import org.apache.seatunnel.common.utils.BufferUtils; +import org.apache.seatunnel.common.constants.CommonOptions; import org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSourceConfig; import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; +import org.apache.seatunnel.connectors.seatunnel.milvus.utils.source.MilvusSourceConverter; import org.apache.curator.shaded.com.google.common.collect.Lists; +import org.codehaus.plexus.util.StringUtils; + import io.milvus.client.MilvusServiceClient; import io.milvus.grpc.GetLoadStateResponse; import io.milvus.grpc.LoadState; +import io.milvus.grpc.QueryResults; import io.milvus.orm.iterator.QueryIterator; import io.milvus.param.ConnectParam; import io.milvus.param.R; +import io.milvus.param.RpcStatus; +import io.milvus.param.collection.AlterCollectionParam; import io.milvus.param.collection.GetLoadStateParam; import io.milvus.param.dml.QueryIteratorParam; +import io.milvus.param.dml.QueryParam; import io.milvus.response.QueryResultsWrapper; import lombok.extern.slf4j.Slf4j; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Collections; import java.util.Deque; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentLinkedDeque; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSourceConfig.BATCH_SIZE; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSourceConfig.RATE_LIMIT; + @Slf4j public class MilvusSourceReader implements SourceReader { private final Deque pendingSplits = new ConcurrentLinkedDeque<>(); private final ReadonlyConfig config; private final Context context; - private Map sourceTables; + private final Map sourceTables; private MilvusServiceClient client; @@ -84,11 +91,36 @@ public void open() throws Exception { .withUri(config.get(MilvusSourceConfig.URL)) .withToken(config.get(MilvusSourceConfig.TOKEN)) .build()); + setRateLimit(config.get(RATE_LIMIT).toString()); + } + + private void setRateLimit(String rateLimit) { + log.info("Set rate limit: " + rateLimit); + for (Map.Entry entry : sourceTables.entrySet()) { + TablePath tablePath = entry.getKey(); + String collectionName = tablePath.getTableName(); + + AlterCollectionParam alterCollectionParam = + AlterCollectionParam.newBuilder() + .withDatabaseName(tablePath.getDatabaseName()) + .withCollectionName(collectionName) + .withProperty("collection.queryRate.max.qps", rateLimit) + .build(); + R response = client.alterCollection(alterCollectionParam); + if (response.getStatus() != R.Status.Success.getCode()) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.SERVER_RESPONSE_FAILED, response.getException()); + } + } + log.info("Set rate limit success"); } @Override public void close() throws IOException { + log.info("Close milvus source reader"); + setRateLimit("-1"); client.close(); + log.info("Close milvus source reader success"); } @Override @@ -96,7 +128,13 @@ public void pollNext(Collector output) throws Exception { synchronized (output.getCheckpointLock()) { MilvusSourceSplit split = pendingSplits.poll(); if (null != split) { - handleEveryRowInternal(split, output); + try { + log.info("Begin to read data from split: " + split); + pollNextData(split, output); + } catch (Exception e) { + log.error("Read data from split: " + split + " failed", e); + throw new MilvusConnectorException(MilvusConnectionErrorCode.READ_DATA_FAIL, e); + } } else { if (!noMoreSplit) { log.info("Milvus source wait split!"); @@ -113,9 +151,12 @@ public void pollNext(Collector output) throws Exception { Thread.sleep(1000L); } - private void handleEveryRowInternal(MilvusSourceSplit split, Collector output) { + private void pollNextData(MilvusSourceSplit split, Collector output) + throws InterruptedException { TablePath tablePath = split.getTablePath(); + String partitionName = split.getPartitionName(); TableSchema tableSchema = sourceTables.get(tablePath).getTableSchema(); + log.info("begin to read data from milvus, table schema: " + tableSchema); if (null == tableSchema) { throw new MilvusConnectorException( MilvusConnectionErrorCode.SOURCE_TABLE_SCHEMA_IS_NULL); @@ -136,129 +177,117 @@ private void handleEveryRowInternal(MilvusSourceSplit split, Collector response = client.queryIterator(param); - if (response.getStatus() != R.Status.Success.getCode()) { + R queryResultsR = client.query(queryParam.build()); + + if (queryResultsR.getStatus() != R.Status.Success.getCode()) { throw new MilvusConnectorException( MilvusConnectionErrorCode.SERVER_RESPONSE_FAILED, loadStateResponse.getException()); } + QueryResultsWrapper wrapper = new QueryResultsWrapper(queryResultsR.getData()); + List records = wrapper.getRowRecords(); + log.info("Total records num: " + records.get(0).getFieldValues().get("count(*)")); - QueryIterator iterator = response.getData(); - while (true) { - List next = iterator.next(); - if (next == null || next.isEmpty()) { - break; - } else { - for (QueryResultsWrapper.RowRecord record : next) { - SeaTunnelRow seaTunnelRow = - convertToSeaTunnelRow(record, tableSchema, tablePath); - output.collect(seaTunnelRow); - } - } - } + long batchSize = (long) config.get(BATCH_SIZE); + queryIteratorData(tablePath, partitionName, tableSchema, output, batchSize); } - public SeaTunnelRow convertToSeaTunnelRow( - QueryResultsWrapper.RowRecord record, TableSchema tableSchema, TablePath tablePath) { - SeaTunnelRowType typeInfo = tableSchema.toPhysicalRowDataType(); - Object[] fields = new Object[record.getFieldValues().size()]; - Map fieldValuesMap = record.getFieldValues(); - String[] fieldNames = typeInfo.getFieldNames(); - for (int fieldIndex = 0; fieldIndex < typeInfo.getTotalFields(); fieldIndex++) { - SeaTunnelDataType seaTunnelDataType = typeInfo.getFieldType(fieldIndex); - Object filedValues = fieldValuesMap.get(fieldNames[fieldIndex]); - switch (seaTunnelDataType.getSqlType()) { - case STRING: - fields[fieldIndex] = filedValues.toString(); - break; - case BOOLEAN: - if (filedValues instanceof Boolean) { - fields[fieldIndex] = filedValues; - } else { - fields[fieldIndex] = Boolean.valueOf(filedValues.toString()); - } - break; - case INT: - if (filedValues instanceof Integer) { - fields[fieldIndex] = filedValues; - } else { - fields[fieldIndex] = Integer.valueOf(filedValues.toString()); - } - break; - case BIGINT: - if (filedValues instanceof Long) { - fields[fieldIndex] = filedValues; - } else { - fields[fieldIndex] = Long.parseLong(filedValues.toString()); - } - break; - case FLOAT: - if (filedValues instanceof Float) { - fields[fieldIndex] = filedValues; - } else { - fields[fieldIndex] = Float.parseFloat(filedValues.toString()); - } - break; - case DOUBLE: - if (filedValues instanceof Double) { - fields[fieldIndex] = filedValues; - } else { - fields[fieldIndex] = Double.parseDouble(filedValues.toString()); - } - break; - case FLOAT_VECTOR: - if (filedValues instanceof List) { - List list = (List) filedValues; - Float[] arrays = new Float[list.size()]; - for (int i = 0; i < list.size(); i++) { - arrays[i] = Float.parseFloat(list.get(i).toString()); - } - fields[fieldIndex] = BufferUtils.toByteBuffer(arrays); - break; - } else { - throw new MilvusConnectorException( - CommonErrorCode.UNSUPPORTED_DATA_TYPE, - "Unexpected vector value: " + filedValues); - } - case BINARY_VECTOR: - case FLOAT16_VECTOR: - case BFLOAT16_VECTOR: - if (filedValues instanceof ByteBuffer) { - fields[fieldIndex] = filedValues; + private void queryIteratorData( + TablePath tablePath, + String partitionName, + TableSchema tableSchema, + Collector output, + long batchSize) + throws InterruptedException { + try { + MilvusSourceConverter sourceConverter = new MilvusSourceConverter(tableSchema); + + QueryIteratorParam.Builder param = + QueryIteratorParam.newBuilder() + .withDatabaseName(tablePath.getDatabaseName()) + .withCollectionName(tablePath.getTableName()) + .withOutFields(Lists.newArrayList("*")) + .withBatchSize(batchSize); + + if (StringUtils.isNotEmpty(partitionName)) { + param.withPartitionNames(Collections.singletonList(partitionName)); + } + + R response = client.queryIterator(param.build()); + if (response.getStatus() != R.Status.Success.getCode()) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.SERVER_RESPONSE_FAILED, response.getException()); + } + int maxFailRetry = 3; + QueryIterator iterator = response.getData(); + while (maxFailRetry > 0) { + try { + List next = iterator.next(); + if (next == null || next.isEmpty()) { break; } else { - throw new MilvusConnectorException( - CommonErrorCode.UNSUPPORTED_DATA_TYPE, - "Unexpected vector value: " + filedValues); + for (QueryResultsWrapper.RowRecord record : next) { + SeaTunnelRow seaTunnelRow = + sourceConverter.convertToSeaTunnelRow( + record, tableSchema, tablePath); + if (StringUtils.isNotEmpty(partitionName)) { + Map options = new HashMap<>(); + options.put(CommonOptions.PARTITION.getName(), partitionName); + seaTunnelRow.setOptions(options); + } + output.collect(seaTunnelRow); + } } - case SPARSE_FLOAT_VECTOR: - if (filedValues instanceof Map) { - fields[fieldIndex] = filedValues; - break; + } catch (Exception e) { + if (e.getMessage().contains("rate limit exceeded")) { + // for rateLimit, we can try iterator again after 30s, no need to update + // batch size directly + maxFailRetry--; + if (maxFailRetry == 0) { + log.error( + "Iterate next data from milvus failed, batchSize = {}, throw exception", + batchSize, + e); + throw new MilvusConnectorException( + MilvusConnectionErrorCode.READ_DATA_FAIL, e); + } + log.error( + "Iterate next data from milvus failed, batchSize = {}, will retry after 30 s, maxRetry: {}", + batchSize, + maxFailRetry, + e); + Thread.sleep(30000); } else { + // if this error, we need to reduce batch size and try again, so throw + // exception here throw new MilvusConnectorException( - CommonErrorCode.UNSUPPORTED_DATA_TYPE, - "Unexpected vector value: " + filedValues); + MilvusConnectionErrorCode.READ_DATA_FAIL, e); } - default: - throw new MilvusConnectorException( - CommonErrorCode.UNSUPPORTED_DATA_TYPE, - "Unexpected value: " + seaTunnelDataType.getSqlType().name()); + } + } + } catch (Exception e) { + if (e.getMessage().contains("rate limit exceeded") && batchSize > 10) { + log.error( + "Query Iterate data from milvus failed, retry from beginning with smaller batch size: {} after 30 s", + batchSize / 2, + e); + Thread.sleep(30000); + queryIteratorData(tablePath, partitionName, tableSchema, output, batchSize / 2); + } else { + throw new MilvusConnectorException(MilvusConnectionErrorCode.READ_DATA_FAIL, e); } } - - SeaTunnelRow seaTunnelRow = new SeaTunnelRow(fields); - seaTunnelRow.setTableId(tablePath.getFullName()); - seaTunnelRow.setRowKind(RowKind.INSERT); - return seaTunnelRow; } @Override @@ -268,7 +297,7 @@ public List snapshotState(long checkpointId) throws Exception @Override public void addSplits(List splits) { - log.info("Adding milvus splits to reader: {}", splits); + log.info("Adding milvus splits to reader: " + splits); pendingSplits.addAll(splits); } diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplit.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplit.java index e79d74b6dc0..d448242d9aa 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplit.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplit.java @@ -29,6 +29,7 @@ public class MilvusSourceSplit implements SourceSplit { private TablePath tablePath; private String splitId; + private String partitionName; @Override public String splitId() { diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplitEnumertor.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplitEnumertor.java index e01e9c8ad5d..1c181baffc1 100644 --- a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplitEnumertor.java +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/source/MilvusSourceSplitEnumertor.java @@ -22,8 +22,19 @@ import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; +import org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSourceConfig; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; +import io.milvus.client.MilvusClient; +import io.milvus.client.MilvusServiceClient; +import io.milvus.grpc.DescribeCollectionResponse; +import io.milvus.grpc.FieldSchema; +import io.milvus.grpc.ShowPartitionsResponse; +import io.milvus.param.ConnectParam; +import io.milvus.param.R; +import io.milvus.param.collection.DescribeCollectionParam; +import io.milvus.param.partition.ShowPartitionsParam; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @@ -45,8 +56,9 @@ public class MilvusSourceSplitEnumertor private final ConcurrentLinkedQueue pendingTables; private final Map> pendingSplits; private final Object stateLock = new Object(); + private MilvusClient client = null; - private ReadonlyConfig config; + private final ReadonlyConfig config; public MilvusSourceSplitEnumertor( Context context, @@ -66,7 +78,14 @@ public MilvusSourceSplitEnumertor( } @Override - public void open() {} + public void open() { + ConnectParam connectParam = + ConnectParam.newBuilder() + .withUri(config.get(MilvusSourceConfig.URL)) + .withToken(config.get(MilvusSourceConfig.TOKEN)) + .build(); + this.client = new MilvusServiceClient(connectParam); + } @Override public void run() throws Exception { @@ -92,17 +111,56 @@ public void run() throws Exception { } private Collection generateSplits(CatalogTable table) { - log.info("Start splitting table {} into chunks...", table.getTablePath()); - MilvusSourceSplit milvusSourceSplit = - MilvusSourceSplit.builder() - .splitId(createSplitId(table.getTablePath(), 0)) - .tablePath(table.getTablePath()) - .build(); - - return Collections.singletonList(milvusSourceSplit); + log.info("Start splitting table {} into chunks by partition...", table.getTablePath()); + String database = table.getTablePath().getDatabaseName(); + String collection = table.getTablePath().getTableName(); + R describeCollectionResponseR = + client.describeCollection( + DescribeCollectionParam.newBuilder() + .withDatabaseName(database) + .withCollectionName(collection) + .build()); + boolean hasPartitionKey = + describeCollectionResponseR.getData().getSchema().getFieldsList().stream() + .anyMatch(FieldSchema::getIsPartitionKey); + List milvusSourceSplits = new ArrayList<>(); + if (!hasPartitionKey) { + ShowPartitionsParam showPartitionsParam = + ShowPartitionsParam.newBuilder() + .withDatabaseName(database) + .withCollectionName(collection) + .build(); + R showPartitionsResponseR = + client.showPartitions(showPartitionsParam); + if (showPartitionsResponseR.getStatus() != R.Status.Success.getCode()) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.LIST_PARTITIONS_FAILED, + "Failed to show partitions: " + showPartitionsResponseR.getMessage()); + } + List partitionList = showPartitionsResponseR.getData().getPartitionNamesList(); + for (String partitionName : partitionList) { + MilvusSourceSplit milvusSourceSplit = + MilvusSourceSplit.builder() + .tablePath(table.getTablePath()) + .splitId(createSplitId(table.getTablePath(), partitionName)) + .partitionName(partitionName) + .build(); + log.info("Generated split: {}", milvusSourceSplit); + milvusSourceSplits.add(milvusSourceSplit); + } + } else { + MilvusSourceSplit milvusSourceSplit = + MilvusSourceSplit.builder() + .tablePath(table.getTablePath()) + .splitId(createSplitId(table.getTablePath(), "0")) + .build(); + log.info("Generated split: {}", milvusSourceSplit); + milvusSourceSplits.add(milvusSourceSplit); + } + return milvusSourceSplits; } - protected String createSplitId(TablePath tablePath, int index) { + protected String createSplitId(TablePath tablePath, String index) { return String.format("%s-%s", tablePath, index); } @@ -133,7 +191,11 @@ private void assignSplit(Collection readers) { } @Override - public void close() throws IOException {} + public void close() throws IOException { + if (client != null) { + client.close(); + } + } @Override public void addSplitsBack(List splits, int subtaskId) { diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConnectorUtils.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConnectorUtils.java new file mode 100644 index 00000000000..f816d259556 --- /dev/null +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConnectorUtils.java @@ -0,0 +1,73 @@ +/* + * 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.seatunnel.connectors.seatunnel.milvus.utils; + +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.Column; +import org.apache.seatunnel.common.constants.CommonOptions; + +import io.milvus.v2.client.MilvusClientV2; +import io.milvus.v2.service.collection.request.CreateCollectionReq; +import io.milvus.v2.service.collection.request.DescribeCollectionReq; +import io.milvus.v2.service.collection.response.DescribeCollectionResp; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class MilvusConnectorUtils { + + public static Boolean hasPartitionKey(MilvusClientV2 milvusClient, String collectionName) { + + DescribeCollectionResp describeCollectionResp = + milvusClient.describeCollection( + DescribeCollectionReq.builder().collectionName(collectionName).build()); + return describeCollectionResp.getCollectionSchema().getFieldSchemaList().stream() + .anyMatch(CreateCollectionReq.FieldSchema::getIsPartitionKey); + } + + public static String getDynamicField(CatalogTable catalogTable) { + List columns = catalogTable.getTableSchema().getColumns(); + Column dynamicField = null; + for (Column column : columns) { + if (column.getOptions() != null + && (Boolean) + column.getOptions() + .getOrDefault(CommonOptions.METADATA.getName(), false)) { + // skip dynamic field + dynamicField = column; + } + } + return dynamicField == null ? null : dynamicField.getName(); + } + + public static List getJsonField(CatalogTable catalogTable) { + List columns = catalogTable.getTableSchema().getColumns(); + List jsonColumn = new ArrayList<>(); + for (Column column : columns) { + if (column.getOptions() != null + && column.getOptions().containsKey(CommonOptions.JSON.getName()) + && (Boolean) column.getOptions().get(CommonOptions.JSON.getName())) { + // skip dynamic field + jsonColumn.add(column.getName()); + } + } + return jsonColumn; + } +} diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConvertUtils.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConvertUtils.java new file mode 100644 index 00000000000..1a1692fd6c5 --- /dev/null +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/MilvusConvertUtils.java @@ -0,0 +1,279 @@ +/* + * 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.seatunnel.connectors.seatunnel.milvus.utils; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.Column; +import org.apache.seatunnel.api.table.catalog.ConstraintKey; +import org.apache.seatunnel.api.table.catalog.PhysicalColumn; +import org.apache.seatunnel.api.table.catalog.PrimaryKey; +import org.apache.seatunnel.api.table.catalog.TableIdentifier; +import org.apache.seatunnel.api.table.catalog.TablePath; +import org.apache.seatunnel.api.table.catalog.TableSchema; +import org.apache.seatunnel.api.table.catalog.VectorIndex; +import org.apache.seatunnel.common.constants.CommonOptions; +import org.apache.seatunnel.connectors.seatunnel.milvus.catalog.MilvusOptions; +import org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSourceConfig; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; +import org.apache.seatunnel.connectors.seatunnel.milvus.utils.source.MilvusSourceConverter; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.util.Lists; + +import com.google.protobuf.ProtocolStringList; +import io.milvus.client.MilvusServiceClient; +import io.milvus.grpc.CollectionSchema; +import io.milvus.grpc.DescribeCollectionResponse; +import io.milvus.grpc.DescribeIndexResponse; +import io.milvus.grpc.FieldSchema; +import io.milvus.grpc.IndexDescription; +import io.milvus.grpc.KeyValuePair; +import io.milvus.grpc.ShowCollectionsResponse; +import io.milvus.grpc.ShowPartitionsResponse; +import io.milvus.grpc.ShowType; +import io.milvus.param.ConnectParam; +import io.milvus.param.R; +import io.milvus.param.collection.DescribeCollectionParam; +import io.milvus.param.collection.ShowCollectionsParam; +import io.milvus.param.index.DescribeIndexParam; +import io.milvus.param.partition.ShowPartitionsParam; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.seatunnel.api.table.type.BasicType.STRING_TYPE; + +@Slf4j +public class MilvusConvertUtils { + private final ReadonlyConfig config; + + public MilvusConvertUtils(ReadonlyConfig config) { + this.config = config; + } + + public Map getSourceTables() { + MilvusServiceClient client = + new MilvusServiceClient( + ConnectParam.newBuilder() + .withUri(config.get(MilvusSourceConfig.URL)) + .withToken(config.get(MilvusSourceConfig.TOKEN)) + .build()); + + String database = config.get(MilvusSourceConfig.DATABASE); + List collectionList = new ArrayList<>(); + if (StringUtils.isNotEmpty(config.get(MilvusSourceConfig.COLLECTION))) { + collectionList.add(config.get(MilvusSourceConfig.COLLECTION)); + } else { + R response = + client.showCollections( + ShowCollectionsParam.newBuilder() + .withDatabaseName(database) + .withShowType(ShowType.All) + .build()); + if (response.getStatus() != R.Status.Success.getCode()) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.SHOW_COLLECTIONS_ERROR); + } + + ProtocolStringList collections = response.getData().getCollectionNamesList(); + if (CollectionUtils.isEmpty(collections)) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.DATABASE_NO_COLLECTIONS, database); + } + collectionList.addAll(collections); + } + + Map map = new HashMap<>(); + for (String collection : collectionList) { + CatalogTable catalogTable = getCatalogTable(client, database, collection); + TablePath tablePath = TablePath.of(database, null, collection); + map.put(tablePath, catalogTable); + } + client.close(); + return map; + } + + public CatalogTable getCatalogTable( + MilvusServiceClient client, String database, String collection) { + R response = + client.describeCollection( + DescribeCollectionParam.newBuilder() + .withDatabaseName(database) + .withCollectionName(collection) + .build()); + + if (response.getStatus() != R.Status.Success.getCode()) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.DESC_COLLECTION_ERROR, response.getMessage()); + } + log.info( + "describe collection database: {}, collection: {}, response: {}", + database, + collection, + response); + // collection column + DescribeCollectionResponse collectionResponse = response.getData(); + CollectionSchema schema = collectionResponse.getSchema(); + List columns = new ArrayList<>(); + boolean existPartitionKeyField = false; + String partitionKeyField = null; + for (FieldSchema fieldSchema : schema.getFieldsList()) { + PhysicalColumn physicalColumn = MilvusSourceConverter.convertColumn(fieldSchema); + columns.add(physicalColumn); + if (fieldSchema.getIsPartitionKey()) { + existPartitionKeyField = true; + partitionKeyField = fieldSchema.getName(); + } + } + if (collectionResponse.getSchema().getEnableDynamicField()) { + Map options = new HashMap<>(); + + options.put(CommonOptions.METADATA.getName(), true); + PhysicalColumn dynamicColumn = + PhysicalColumn.builder() + .name(CommonOptions.METADATA.getName()) + .dataType(STRING_TYPE) + .options(options) + .build(); + columns.add(dynamicColumn); + } + + // primary key + PrimaryKey primaryKey = buildPrimaryKey(schema.getFieldsList()); + + // index + R describeIndexResponseR = + client.describeIndex( + DescribeIndexParam.newBuilder() + .withDatabaseName(database) + .withCollectionName(collection) + .build()); + if (describeIndexResponseR.getStatus() != R.Status.Success.getCode()) { + throw new MilvusConnectorException(MilvusConnectionErrorCode.DESC_INDEX_ERROR); + } + DescribeIndexResponse indexResponse = describeIndexResponseR.getData(); + List vectorIndexes = buildVectorIndexes(indexResponse); + + // build tableSchema + TableSchema tableSchema = + TableSchema.builder() + .columns(columns) + .primaryKey(primaryKey) + .constraintKey( + ConstraintKey.of( + ConstraintKey.ConstraintType.VECTOR_INDEX_KEY, + "vector_index", + vectorIndexes)) + .build(); + + // build tableId + String CATALOG_NAME = "Milvus"; + TableIdentifier tableId = TableIdentifier.of(CATALOG_NAME, database, null, collection); + // build options info + Map options = new HashMap<>(); + options.put( + MilvusOptions.ENABLE_DYNAMIC_FIELD, String.valueOf(schema.getEnableDynamicField())); + options.put(MilvusOptions.SHARDS_NUM, String.valueOf(collectionResponse.getShardsNum())); + if (existPartitionKeyField) { + options.put(MilvusOptions.PARTITION_KEY_FIELD, partitionKeyField); + } else { + fillPartitionNames(options, client, database, collection); + } + + return CatalogTable.of( + tableId, tableSchema, options, new ArrayList<>(), schema.getDescription()); + } + + private static void fillPartitionNames( + Map options, + MilvusServiceClient client, + String database, + String collection) { + // not exist partition key, will read partition + R partitionsResponseR = + client.showPartitions( + ShowPartitionsParam.newBuilder() + .withDatabaseName(database) + .withCollectionName(collection) + .build()); + if (partitionsResponseR.getStatus() != R.Status.Success.getCode()) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.SHOW_PARTITION_ERROR, + partitionsResponseR.getMessage()); + } + + ProtocolStringList partitionNamesList = + partitionsResponseR.getData().getPartitionNamesList(); + List list = new ArrayList<>(); + for (String partition : partitionNamesList) { + if (partition.equals("_default")) { + continue; + } + list.add(partition); + } + if (CollectionUtils.isEmpty(partitionNamesList)) { + return; + } + + options.put(MilvusOptions.PARTITION_NAMES, String.join(",", list)); + } + + private static List buildVectorIndexes( + DescribeIndexResponse indexResponse) { + if (CollectionUtils.isEmpty(indexResponse.getIndexDescriptionsList())) { + return null; + } + + List list = new ArrayList<>(); + for (IndexDescription per : indexResponse.getIndexDescriptionsList()) { + Map paramsMap = + per.getParamsList().stream() + .collect( + Collectors.toMap(KeyValuePair::getKey, KeyValuePair::getValue)); + + VectorIndex index = + new VectorIndex( + per.getIndexName(), + per.getFieldName(), + paramsMap.get("index_type"), + paramsMap.get("metric_type")); + + list.add(index); + } + + return list; + } + + public static PrimaryKey buildPrimaryKey(List fields) { + for (FieldSchema field : fields) { + if (field.getIsPrimaryKey()) { + return PrimaryKey.of( + field.getName(), Lists.newArrayList(field.getName()), field.getAutoID()); + } + } + + return null; + } +} diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/sink/MilvusSinkConverter.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/sink/MilvusSinkConverter.java new file mode 100644 index 00000000000..18aa3dbccfd --- /dev/null +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/sink/MilvusSinkConverter.java @@ -0,0 +1,294 @@ +/* + * 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.seatunnel.connectors.seatunnel.milvus.utils.sink; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.Column; +import org.apache.seatunnel.api.table.catalog.PrimaryKey; +import org.apache.seatunnel.api.table.catalog.exception.CatalogException; +import org.apache.seatunnel.api.table.type.ArrayType; +import org.apache.seatunnel.api.table.type.SeaTunnelDataType; +import org.apache.seatunnel.api.table.type.SeaTunnelRow; +import org.apache.seatunnel.api.table.type.SeaTunnelRowType; +import org.apache.seatunnel.api.table.type.SqlType; +import org.apache.seatunnel.common.constants.CommonOptions; +import org.apache.seatunnel.common.utils.BufferUtils; +import org.apache.seatunnel.common.utils.JsonUtils; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectionErrorCode; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; + +import org.apache.commons.lang3.StringUtils; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.milvus.common.utils.JacksonUtils; +import io.milvus.grpc.DataType; +import io.milvus.param.collection.FieldType; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.apache.seatunnel.api.table.catalog.PrimaryKey.isPrimaryKeyField; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.ENABLE_AUTO_ID; +import static org.apache.seatunnel.connectors.seatunnel.milvus.config.MilvusSinkConfig.ENABLE_DYNAMIC_FIELD; + +public class MilvusSinkConverter { + private static final Gson gson = new Gson(); + + public Object convertBySeaTunnelType( + SeaTunnelDataType fieldType, Boolean isJson, Object value) { + SqlType sqlType = fieldType.getSqlType(); + switch (sqlType) { + case INT: + return Integer.parseInt(value.toString()); + case TINYINT: + return Byte.parseByte(value.toString()); + case BIGINT: + return Long.parseLong(value.toString()); + case SMALLINT: + return Short.parseShort(value.toString()); + case STRING: + case DATE: + if (isJson) { + return gson.fromJson(value.toString(), JsonObject.class); + } + return value.toString(); + case FLOAT_VECTOR: + ByteBuffer floatVectorBuffer = (ByteBuffer) value; + Float[] floats = BufferUtils.toFloatArray(floatVectorBuffer); + return Arrays.stream(floats).collect(Collectors.toList()); + case BINARY_VECTOR: + case BFLOAT16_VECTOR: + case FLOAT16_VECTOR: + ByteBuffer binaryVector = (ByteBuffer) value; + return gson.toJsonTree(binaryVector.array()); + case SPARSE_FLOAT_VECTOR: + return JsonParser.parseString(JacksonUtils.toJsonString(value)).getAsJsonObject(); + case FLOAT: + return Float.parseFloat(value.toString()); + case BOOLEAN: + return Boolean.parseBoolean(value.toString()); + case DOUBLE: + return Double.parseDouble(value.toString()); + case ARRAY: + ArrayType arrayType = (ArrayType) fieldType; + switch (arrayType.getElementType().getSqlType()) { + case STRING: + String[] stringArray = (String[]) value; + return Arrays.asList(stringArray); + case SMALLINT: + Short[] shortArray = (Short[]) value; + return Arrays.asList(shortArray); + case TINYINT: + Byte[] byteArray = (Byte[]) value; + return Arrays.asList(byteArray); + case INT: + Integer[] intArray = (Integer[]) value; + return Arrays.asList(intArray); + case BIGINT: + Long[] longArray = (Long[]) value; + return Arrays.asList(longArray); + case FLOAT: + Float[] floatArray = (Float[]) value; + return Arrays.asList(floatArray); + case DOUBLE: + Double[] doubleArray = (Double[]) value; + return Arrays.asList(doubleArray); + } + case ROW: + SeaTunnelRow row = (SeaTunnelRow) value; + return JsonUtils.toJsonString(row.getFields()); + case MAP: + return JacksonUtils.toJsonString(value); + default: + throw new MilvusConnectorException( + MilvusConnectionErrorCode.NOT_SUPPORT_TYPE, sqlType.name()); + } + } + + public static FieldType convertToFieldType( + Column column, PrimaryKey primaryKey, String partitionKeyField, Boolean autoId) { + SeaTunnelDataType seaTunnelDataType = column.getDataType(); + DataType milvusDataType = convertSqlTypeToDataType(seaTunnelDataType.getSqlType()); + FieldType.Builder build = + FieldType.newBuilder().withName(column.getName()).withDataType(milvusDataType); + if (StringUtils.isNotEmpty(column.getComment())) { + build.withDescription(column.getComment()); + } + switch (seaTunnelDataType.getSqlType()) { + case ROW: + build.withMaxLength(65535); + break; + case DATE: + build.withMaxLength(20); + break; + case STRING: + if (column.getOptions() != null + && column.getOptions().get(CommonOptions.JSON.getName()) != null + && (Boolean) column.getOptions().get(CommonOptions.JSON.getName())) { + // check if is json + build.withDataType(DataType.JSON); + } else if (column.getColumnLength() == null || column.getColumnLength() == 0) { + build.withMaxLength(65535); + } else { + build.withMaxLength((int) (column.getColumnLength() / 4)); + } + break; + case ARRAY: + ArrayType arrayType = (ArrayType) column.getDataType(); + SeaTunnelDataType elementType = arrayType.getElementType(); + build.withElementType(convertSqlTypeToDataType(elementType.getSqlType())); + build.withMaxCapacity(4095); + switch (elementType.getSqlType()) { + case STRING: + if (column.getColumnLength() == null || column.getColumnLength() == 0) { + build.withMaxLength(65535); + } else { + build.withMaxLength((int) (column.getColumnLength() / 4)); + } + break; + } + break; + case BINARY_VECTOR: + case FLOAT_VECTOR: + case FLOAT16_VECTOR: + case BFLOAT16_VECTOR: + build.withDimension(column.getScale()); + break; + } + + // check is primaryKey + if (null != primaryKey && primaryKey.getColumnNames().contains(column.getName())) { + build.withPrimaryKey(true); + List integerTypes = new ArrayList<>(); + integerTypes.add(SqlType.INT); + integerTypes.add(SqlType.SMALLINT); + integerTypes.add(SqlType.TINYINT); + integerTypes.add(SqlType.BIGINT); + if (integerTypes.contains(seaTunnelDataType.getSqlType())) { + build.withDataType(DataType.Int64); + } else { + build.withDataType(DataType.VarChar); + build.withMaxLength(65535); + } + if (null != primaryKey.getEnableAutoId()) { + build.withAutoID(primaryKey.getEnableAutoId()); + } else { + build.withAutoID(autoId); + } + } + + // check is partitionKey + if (column.getName().equals(partitionKeyField)) { + build.withPartitionKey(true); + } + + return build.build(); + } + + public static DataType convertSqlTypeToDataType(SqlType sqlType) { + switch (sqlType) { + case BOOLEAN: + return DataType.Bool; + case TINYINT: + return DataType.Int8; + case SMALLINT: + return DataType.Int16; + case INT: + return DataType.Int32; + case BIGINT: + return DataType.Int64; + case FLOAT: + return DataType.Float; + case DOUBLE: + return DataType.Double; + case STRING: + return DataType.VarChar; + case ARRAY: + return DataType.Array; + case MAP: + return DataType.JSON; + case FLOAT_VECTOR: + return DataType.FloatVector; + case BINARY_VECTOR: + return DataType.BinaryVector; + case FLOAT16_VECTOR: + return DataType.Float16Vector; + case BFLOAT16_VECTOR: + return DataType.BFloat16Vector; + case SPARSE_FLOAT_VECTOR: + return DataType.SparseFloatVector; + case DATE: + return DataType.VarChar; + case ROW: + return DataType.VarChar; + } + throw new CatalogException( + String.format("Not support convert to milvus type, sqlType is %s", sqlType)); + } + + public JsonObject buildMilvusData( + CatalogTable catalogTable, + ReadonlyConfig config, + List jsonFields, + String dynamicField, + SeaTunnelRow element) { + SeaTunnelRowType seaTunnelRowType = catalogTable.getSeaTunnelRowType(); + PrimaryKey primaryKey = catalogTable.getTableSchema().getPrimaryKey(); + Boolean autoId = config.get(ENABLE_AUTO_ID); + + JsonObject data = new JsonObject(); + Gson gson = new Gson(); + for (int i = 0; i < seaTunnelRowType.getFieldNames().length; i++) { + String fieldName = seaTunnelRowType.getFieldNames()[i]; + Boolean isJson = jsonFields.contains(fieldName); + if (autoId && isPrimaryKeyField(primaryKey, fieldName)) { + continue; // if create table open AutoId, then don't need insert data with + // primaryKey field. + } + + SeaTunnelDataType fieldType = seaTunnelRowType.getFieldType(i); + Object value = element.getField(i); + if (null == value) { + throw new MilvusConnectorException( + MilvusConnectionErrorCode.FIELD_IS_NULL, fieldName); + } + // if the field is dynamic field, then parse the dynamic field + if (dynamicField != null + && dynamicField.equals(fieldName) + && config.get(ENABLE_DYNAMIC_FIELD)) { + JsonObject dynamicData = gson.fromJson(value.toString(), JsonObject.class); + dynamicData + .entrySet() + .forEach( + entry -> { + data.add(entry.getKey(), entry.getValue()); + }); + continue; + } + Object object = convertBySeaTunnelType(fieldType, isJson, value); + data.add(fieldName, gson.toJsonTree(object)); + } + return data; + } +} diff --git a/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/source/MilvusSourceConverter.java b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/source/MilvusSourceConverter.java new file mode 100644 index 00000000000..bda3f96a420 --- /dev/null +++ b/seatunnel-connectors-v2/connector-milvus/src/main/java/org/apache/seatunnel/connectors/seatunnel/milvus/utils/source/MilvusSourceConverter.java @@ -0,0 +1,364 @@ +/* + * 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.seatunnel.connectors.seatunnel.milvus.utils.source; + +import org.apache.seatunnel.api.table.catalog.Column; +import org.apache.seatunnel.api.table.catalog.PhysicalColumn; +import org.apache.seatunnel.api.table.catalog.TablePath; +import org.apache.seatunnel.api.table.catalog.TableSchema; +import org.apache.seatunnel.api.table.type.ArrayType; +import org.apache.seatunnel.api.table.type.BasicType; +import org.apache.seatunnel.api.table.type.RowKind; +import org.apache.seatunnel.api.table.type.SeaTunnelDataType; +import org.apache.seatunnel.api.table.type.SeaTunnelRow; +import org.apache.seatunnel.api.table.type.SeaTunnelRowType; +import org.apache.seatunnel.api.table.type.SqlType; +import org.apache.seatunnel.api.table.type.VectorType; +import org.apache.seatunnel.common.constants.CommonOptions; +import org.apache.seatunnel.common.exception.CommonErrorCode; +import org.apache.seatunnel.common.utils.BufferUtils; +import org.apache.seatunnel.connectors.seatunnel.milvus.exception.MilvusConnectorException; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.milvus.grpc.DataType; +import io.milvus.grpc.FieldSchema; +import io.milvus.grpc.KeyValuePair; +import io.milvus.response.QueryResultsWrapper; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.seatunnel.api.table.type.BasicType.STRING_TYPE; + +public class MilvusSourceConverter { + private final List existField; + private Gson gson = new Gson(); + + public MilvusSourceConverter(TableSchema tableSchema) { + this.existField = + tableSchema.getColumns().stream() + .filter( + column -> + column.getOptions() == null + || !column.getOptions() + .containsValue(CommonOptions.METADATA)) + .map(Column::getName) + .collect(Collectors.toList()); + } + + public SeaTunnelRow convertToSeaTunnelRow( + QueryResultsWrapper.RowRecord record, TableSchema tableSchema, TablePath tablePath) { + // get field names and types + SeaTunnelRowType typeInfo = tableSchema.toPhysicalRowDataType(); + String[] fieldNames = typeInfo.getFieldNames(); + + Object[] seatunnelField = new Object[typeInfo.getTotalFields()]; + // get field values from source milvus + Map fieldValuesMap = record.getFieldValues(); + // filter dynamic field + JsonObject dynamicField = convertDynamicField(fieldValuesMap); + + for (int fieldIndex = 0; fieldIndex < typeInfo.getTotalFields(); fieldIndex++) { + if (fieldNames[fieldIndex].equals(CommonOptions.METADATA.getName())) { + seatunnelField[fieldIndex] = dynamicField.toString(); + continue; + } + SeaTunnelDataType seaTunnelDataType = typeInfo.getFieldType(fieldIndex); + Object filedValues = fieldValuesMap.get(fieldNames[fieldIndex]); + switch (seaTunnelDataType.getSqlType()) { + case STRING: + seatunnelField[fieldIndex] = filedValues.toString(); + break; + case BOOLEAN: + if (filedValues instanceof Boolean) { + seatunnelField[fieldIndex] = filedValues; + } else { + seatunnelField[fieldIndex] = Boolean.valueOf(filedValues.toString()); + } + break; + case TINYINT: + if (filedValues instanceof Byte) { + seatunnelField[fieldIndex] = filedValues; + } else { + seatunnelField[fieldIndex] = Byte.parseByte(filedValues.toString()); + } + break; + case SMALLINT: + if (filedValues instanceof Short) { + seatunnelField[fieldIndex] = filedValues; + } else { + seatunnelField[fieldIndex] = Short.parseShort(filedValues.toString()); + } + case INT: + if (filedValues instanceof Integer) { + seatunnelField[fieldIndex] = filedValues; + } else { + seatunnelField[fieldIndex] = Integer.valueOf(filedValues.toString()); + } + break; + case BIGINT: + if (filedValues instanceof Long) { + seatunnelField[fieldIndex] = filedValues; + } else { + seatunnelField[fieldIndex] = Long.parseLong(filedValues.toString()); + } + break; + case FLOAT: + if (filedValues instanceof Float) { + seatunnelField[fieldIndex] = filedValues; + } else { + seatunnelField[fieldIndex] = Float.parseFloat(filedValues.toString()); + } + break; + case DOUBLE: + if (filedValues instanceof Double) { + seatunnelField[fieldIndex] = filedValues; + } else { + seatunnelField[fieldIndex] = Double.parseDouble(filedValues.toString()); + } + break; + case ARRAY: + if (filedValues instanceof List) { + List list = (List) filedValues; + ArrayType arrayType = (ArrayType) seaTunnelDataType; + SqlType elementType = arrayType.getElementType().getSqlType(); + switch (elementType) { + case STRING: + String[] arrays = new String[list.size()]; + for (int i = 0; i < list.size(); i++) { + arrays[i] = list.get(i).toString(); + } + seatunnelField[fieldIndex] = arrays; + break; + case BOOLEAN: + Boolean[] booleanArrays = new Boolean[list.size()]; + for (int i = 0; i < list.size(); i++) { + booleanArrays[i] = Boolean.valueOf(list.get(i).toString()); + } + seatunnelField[fieldIndex] = booleanArrays; + break; + case TINYINT: + Byte[] byteArrays = new Byte[list.size()]; + for (int i = 0; i < list.size(); i++) { + byteArrays[i] = Byte.parseByte(list.get(i).toString()); + } + seatunnelField[fieldIndex] = byteArrays; + break; + case SMALLINT: + Short[] shortArrays = new Short[list.size()]; + for (int i = 0; i < list.size(); i++) { + shortArrays[i] = Short.parseShort(list.get(i).toString()); + } + seatunnelField[fieldIndex] = shortArrays; + break; + case INT: + Integer[] intArrays = new Integer[list.size()]; + for (int i = 0; i < list.size(); i++) { + intArrays[i] = Integer.valueOf(list.get(i).toString()); + } + seatunnelField[fieldIndex] = intArrays; + break; + case BIGINT: + Long[] longArrays = new Long[list.size()]; + for (int i = 0; i < list.size(); i++) { + longArrays[i] = Long.parseLong(list.get(i).toString()); + } + seatunnelField[fieldIndex] = longArrays; + break; + case FLOAT: + Float[] floatArrays = new Float[list.size()]; + for (int i = 0; i < list.size(); i++) { + floatArrays[i] = Float.parseFloat(list.get(i).toString()); + } + seatunnelField[fieldIndex] = floatArrays; + break; + case DOUBLE: + Double[] doubleArrays = new Double[list.size()]; + for (int i = 0; i < list.size(); i++) { + doubleArrays[i] = Double.parseDouble(list.get(i).toString()); + } + seatunnelField[fieldIndex] = doubleArrays; + break; + default: + throw new MilvusConnectorException( + CommonErrorCode.UNSUPPORTED_DATA_TYPE, + "Unexpected array value: " + filedValues); + } + } else { + throw new MilvusConnectorException( + CommonErrorCode.UNSUPPORTED_DATA_TYPE, + "Unexpected array value: " + filedValues); + } + break; + case FLOAT_VECTOR: + if (filedValues instanceof List) { + List list = (List) filedValues; + Float[] arrays = new Float[list.size()]; + for (int i = 0; i < list.size(); i++) { + arrays[i] = Float.parseFloat(list.get(i).toString()); + } + seatunnelField[fieldIndex] = BufferUtils.toByteBuffer(arrays); + break; + } else { + throw new MilvusConnectorException( + CommonErrorCode.UNSUPPORTED_DATA_TYPE, + "Unexpected vector value: " + filedValues); + } + case BINARY_VECTOR: + case FLOAT16_VECTOR: + case BFLOAT16_VECTOR: + if (filedValues instanceof ByteBuffer) { + seatunnelField[fieldIndex] = filedValues; + break; + } else { + throw new MilvusConnectorException( + CommonErrorCode.UNSUPPORTED_DATA_TYPE, + "Unexpected vector value: " + filedValues); + } + case SPARSE_FLOAT_VECTOR: + if (filedValues instanceof Map) { + seatunnelField[fieldIndex] = filedValues; + break; + } else { + throw new MilvusConnectorException( + CommonErrorCode.UNSUPPORTED_DATA_TYPE, + "Unexpected vector value: " + filedValues); + } + default: + throw new MilvusConnectorException( + CommonErrorCode.UNSUPPORTED_DATA_TYPE, + "Unexpected value: " + seaTunnelDataType.getSqlType().name()); + } + } + + SeaTunnelRow seaTunnelRow = new SeaTunnelRow(seatunnelField); + seaTunnelRow.setTableId(tablePath.getFullName()); + seaTunnelRow.setRowKind(RowKind.INSERT); + return seaTunnelRow; + } + + public static PhysicalColumn convertColumn(FieldSchema fieldSchema) { + DataType dataType = fieldSchema.getDataType(); + PhysicalColumn.PhysicalColumnBuilder builder = PhysicalColumn.builder(); + builder.name(fieldSchema.getName()); + builder.sourceType(dataType.name()); + builder.comment(fieldSchema.getDescription()); + + switch (dataType) { + case Bool: + builder.dataType(BasicType.BOOLEAN_TYPE); + break; + case Int8: + builder.dataType(BasicType.BYTE_TYPE); + break; + case Int16: + builder.dataType(BasicType.SHORT_TYPE); + break; + case Int32: + builder.dataType(BasicType.INT_TYPE); + break; + case Int64: + builder.dataType(BasicType.LONG_TYPE); + break; + case Float: + builder.dataType(BasicType.FLOAT_TYPE); + break; + case Double: + builder.dataType(BasicType.DOUBLE_TYPE); + break; + case VarChar: + builder.dataType(BasicType.STRING_TYPE); + for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { + if (keyValuePair.getKey().equals("max_length")) { + builder.columnLength(Long.parseLong(keyValuePair.getValue()) * 4); + break; + } + } + break; + case String: + builder.dataType(BasicType.STRING_TYPE); + break; + case JSON: + builder.dataType(STRING_TYPE); + Map options = new HashMap<>(); + options.put(CommonOptions.JSON.getName(), true); + builder.options(options); + break; + case Array: + builder.dataType(ArrayType.STRING_ARRAY_TYPE); + break; + case FloatVector: + builder.dataType(VectorType.VECTOR_FLOAT_TYPE); + for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { + if (keyValuePair.getKey().equals("dim")) { + builder.scale(Integer.valueOf(keyValuePair.getValue())); + break; + } + } + break; + case BinaryVector: + builder.dataType(VectorType.VECTOR_BINARY_TYPE); + for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { + if (keyValuePair.getKey().equals("dim")) { + builder.scale(Integer.valueOf(keyValuePair.getValue())); + break; + } + } + break; + case SparseFloatVector: + builder.dataType(VectorType.VECTOR_SPARSE_FLOAT_TYPE); + break; + case Float16Vector: + builder.dataType(VectorType.VECTOR_FLOAT16_TYPE); + for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { + if (keyValuePair.getKey().equals("dim")) { + builder.scale(Integer.valueOf(keyValuePair.getValue())); + break; + } + } + break; + case BFloat16Vector: + builder.dataType(VectorType.VECTOR_BFLOAT16_TYPE); + for (KeyValuePair keyValuePair : fieldSchema.getTypeParamsList()) { + if (keyValuePair.getKey().equals("dim")) { + builder.scale(Integer.valueOf(keyValuePair.getValue())); + break; + } + } + break; + default: + throw new UnsupportedOperationException("Unsupported data type: " + dataType); + } + + return builder.build(); + } + + private JsonObject convertDynamicField(Map fieldValuesMap) { + JsonObject dynamicField = new JsonObject(); + for (Map.Entry entry : fieldValuesMap.entrySet()) { + if (!existField.contains(entry.getKey())) { + dynamicField.add(entry.getKey(), gson.toJsonTree(entry.getValue())); + } + } + return dynamicField; + } +} diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java index 14d89571b95..e33d89cc0ad 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java @@ -465,7 +465,9 @@ private boolean isIssueWeAlreadyKnow(String threadName) { // JDBC Hana driver || threadName.startsWith("Thread-") // JNA Cleaner - || threadName.startsWith("JNA Cleaner"); + || threadName.startsWith("JNA Cleaner") + // GRPC client + || threadName.startsWith("grpc"); } @Override diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransform.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransform.java index 037d4ba7424..c5c6fa4f9dc 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransform.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransform.java @@ -84,6 +84,7 @@ protected SeaTunnelRow transformRow(SeaTunnelRow inputRow) { SeaTunnelRow outputRow = new SeaTunnelRow(outputDataArray); outputRow.setRowKind(inputRow.getRowKind()); outputRow.setTableId(inputRow.getTableId()); + outputRow.setOptions(inputRow.getOptions()); return outputRow; } @@ -110,9 +111,13 @@ protected TableSchema transformTableSchema() { value, oldColumn.getDataType(), oldColumn.getColumnLength(), + oldColumn.getScale(), oldColumn.isNullable(), oldColumn.getDefaultValue(), - oldColumn.getComment()); + oldColumn.getComment(), + oldColumn.getSourceType(), + oldColumn.getOptions()); + outputColumns.add(outputColumn); outputFieldNames.add(outputColumn.getName()); needReaderColIndex.add(fieldIndex); From 25ae4923cc44baffb36246393c194fc258ba5a86 Mon Sep 17 00:00:00 2001 From: czs <56523920+czshh0628@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:44:57 +0800 Subject: [PATCH 34/72] [Fix][Doc] Correct hive-jdbc config `useKerberos` to `use_kerberos` (#7896) --- docs/en/connector-v2/source/HiveJdbc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/connector-v2/source/HiveJdbc.md b/docs/en/connector-v2/source/HiveJdbc.md index 19619d924c1..23227aa306f 100644 --- a/docs/en/connector-v2/source/HiveJdbc.md +++ b/docs/en/connector-v2/source/HiveJdbc.md @@ -72,7 +72,7 @@ Read external data source data through JDBC. | partition_num | Int | No | job parallelism | The number of partition count, only support positive integer. default value is job parallelism | | fetch_size | Int | No | 0 | For queries that return a large number of objects,you can configure
    the row fetch size used in the query toimprove performance by
    reducing the number database hits required to satisfy the selection criteria.
    Zero means use jdbc default value. | | common-options | | No | - | Source plugin common parameters, please refer to [Source Common Options](../source-common-options.md) for details | -| useKerberos | Boolean | No | no | Whether to enable Kerberos, default is false | +| use_kerberos | Boolean | No | no | Whether to enable Kerberos, default is false | | kerberos_principal | String | No | - | When use kerberos, we should set kerberos principal such as 'test_user@xxx'. | | kerberos_keytab_path | String | No | - | When use kerberos, we should set kerberos principal file path such as '/home/test/test_user.keytab' . | | krb5_path | String | No | /etc/krb5.conf | When use kerberos, we should set krb5 path file path such as '/seatunnel/krb5.conf' or use the default path '/etc/krb5.conf '. | From 10c37acb34e97c2fc56e485bfffe0a953ee03ff8 Mon Sep 17 00:00:00 2001 From: happyboy1024 <137260654+happyboy1024@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:31:01 +0800 Subject: [PATCH 35/72] [Feature][Doris] Support multi-table source read (#7895) Co-authored-by: happyboy1024 <296442618@qq.com> --- docs/en/connector-v2/source/Doris.md | 80 ++- docs/zh/connector-v2/source/Doris.md | 212 +++++++ .../doris/backend/BackendClient.java | 4 +- .../doris/catalog/DorisCatalog.java | 15 +- .../doris/catalog/DorisCatalogFactory.java | 11 +- .../connectors/doris/config/DorisConfig.java | 160 ------ .../connectors/doris/config/DorisOptions.java | 218 +------ .../doris/config/DorisSinkConfig.java | 123 ++++ .../doris/config/DorisSinkOptions.java | 170 ++++++ .../doris/config/DorisSourceConfig.java | 71 +++ .../doris/config/DorisSourceOptions.java | 103 ++++ .../doris/config/DorisTableConfig.java | 132 +++++ .../datatype/AbstractDorisTypeConverter.java | 13 +- .../doris/datatype/DorisTypeConverterV1.java | 5 +- .../doris/datatype/DorisTypeConverterV2.java | 7 +- .../connectors/doris/rest/RestService.java | 76 +-- .../connectors/doris/sink/DorisSink.java | 20 +- .../doris/sink/DorisSinkFactory.java | 11 +- .../doris/sink/committer/DorisCommitter.java | 22 +- .../doris/sink/writer/DorisSinkWriter.java | 44 +- .../doris/sink/writer/DorisStreamLoad.java | 16 +- .../connectors/doris/source/DorisSource.java | 29 +- .../doris/source/DorisSourceFactory.java | 99 ++-- .../doris/source/DorisSourceTable.java | 40 ++ .../source/reader/DorisSourceReader.java | 31 +- .../doris/source/reader/DorisValueReader.java | 21 +- .../split/DorisSourceSplitEnumerator.java | 35 +- .../doris/util/DorisCatalogUtil.java | 8 +- .../doris/catalog/DorisCreateTableTest.java | 6 +- .../connector-doris-e2e/pom.xml | 6 + .../e2e/connector/doris/DorisCatalogIT.java | 7 +- .../e2e/connector/doris/DorisMultiReadIT.java | 539 ++++++++++++++++++ .../doris_multi_source_to_assert.conf | 80 +++ .../resources/doris_multi_source_to_sink.conf | 62 ++ .../doris_multi_source_to_sink_2pc_false.conf | 62 ++ 35 files changed, 1947 insertions(+), 591 deletions(-) create mode 100644 docs/zh/connector-v2/source/Doris.md delete mode 100644 seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisConfig.java create mode 100644 seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java create mode 100644 seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkOptions.java create mode 100644 seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceConfig.java create mode 100644 seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceOptions.java create mode 100644 seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisTableConfig.java create mode 100644 seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceTable.java create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisMultiReadIT.java create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_assert.conf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink.conf create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink_2pc_false.conf diff --git a/docs/en/connector-v2/source/Doris.md b/docs/en/connector-v2/source/Doris.md index c67444b58c8..373b84f8fdd 100644 --- a/docs/en/connector-v2/source/Doris.md +++ b/docs/en/connector-v2/source/Doris.md @@ -13,15 +13,14 @@ - [x] [batch](../../concept/connector-v2-features.md) - [ ] [stream](../../concept/connector-v2-features.md) - [ ] [exactly-once](../../concept/connector-v2-features.md) -- [x] [schema projection](../../concept/connector-v2-features.md) +- [x] [column projection](../../concept/connector-v2-features.md) - [x] [parallelism](../../concept/connector-v2-features.md) - [x] [support user-defined split](../../concept/connector-v2-features.md) +- [x] [support multiple table read](../../concept/connector-v2-features.md) ## Description -Used to read data from Doris. -Doris Source will send a SQL to FE, FE will parse it into an execution plan, send it to BE, and BE will -directly return the data +Used to read data from Apache Doris. ## Supported DataSource Info @@ -29,11 +28,6 @@ directly return the data |------------|--------------------------------------|--------|-----|-------| | Doris | Only Doris2.0 or later is supported. | - | - | - | -## Database Dependency - -> Please download the support list corresponding to 'Maven' and copy it to the '$SEATNUNNEL_HOME/plugins/jdbc/lib/' -> working directory
    - ## Data Type Mapping | Doris Data type | SeaTunnel Data type | @@ -54,29 +48,40 @@ directly return the data ## Source Options +Base configuration: + | Name | Type | Required | Default | Description | |----------------------------------|--------|----------|------------|-----------------------------------------------------------------------------------------------------| | fenodes | string | yes | - | FE address, the format is `"fe_host:fe_http_port"` | | username | string | yes | - | User username | | password | string | yes | - | User password | +| doris.request.retries | int | no | 3 | Number of retries to send requests to Doris FE. | +| doris.request.read.timeout.ms | int | no | 30000 | | +| doris.request.connect.timeout.ms | int | no | 30000 | | +| query-port | string | no | 9030 | Doris QueryPort | +| doris.request.query.timeout.s | int | no | 3600 | Timeout period of Doris scan data, expressed in seconds. | +| table_list | string | 否 | - | table list | + +Table list configuration: + +| Name | Type | Required | Default | Description | +|----------------------------------|--------|----------|------------|-----------------------------------------------------------------------------------------------------| | database | string | yes | - | The name of Doris database | | table | string | yes | - | The name of Doris table | | doris.read.field | string | no | - | Use the 'doris.read.field' parameter to select the doris table columns to read | -| query-port | string | no | 9030 | Doris QueryPort | | doris.filter.query | string | no | - | Data filtering in doris. the format is "field = value",example : doris.filter.query = "F_ID > 2" | | doris.batch.size | int | no | 1024 | The maximum value that can be obtained by reading Doris BE once. | -| doris.request.query.timeout.s | int | no | 3600 | Timeout period of Doris scan data, expressed in seconds. | | doris.exec.mem.limit | long | no | 2147483648 | Maximum memory that can be used by a single be scan request. The default memory is 2G (2147483648). | -| doris.request.retries | int | no | 3 | Number of retries to send requests to Doris FE. | -| doris.request.read.timeout.ms | int | no | 30000 | | -| doris.request.connect.timeout.ms | int | no | 30000 | | + +Note: When this configuration corresponds to a single table, you can flatten the configuration items in table_list to the outer layer. ### Tips > It is not recommended to modify advanced parameters at will -## Task Example +## Example +### single table > This is an example of reading a Doris table and writing to Console. ``` @@ -159,4 +164,49 @@ sink { Console {} } ``` +### Multiple table +``` +env{ + parallelism = 1 + job.mode = "BATCH" +} +source{ + Doris { + fenodes = "xxxx:8030" + username = root + password = "" + table_list = [ + { + database = "st_source_0" + table = "doris_table_0" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT" + doris.filter.query = "F_ID >= 50" + }, + { + database = "st_source_1" + table = "doris_table_1" + } + ] + } +} + +transform {} + +sink{ + Doris { + fenodes = "xxxx:8030" + schema_save_mode = "RECREATE_SCHEMA" + username = root + password = "" + database = "st_sink" + table = "${table_name}" + sink.enable-2pc = "true" + sink.label-prefix = "test_json" + doris.config = { + format="json" + read_json_by_line="true" + } + } +} +``` diff --git a/docs/zh/connector-v2/source/Doris.md b/docs/zh/connector-v2/source/Doris.md new file mode 100644 index 00000000000..ba3549473a5 --- /dev/null +++ b/docs/zh/connector-v2/source/Doris.md @@ -0,0 +1,212 @@ +# Doris + +> Doris 源连接器 + +## 支持的引擎 + +> Spark
    +> Flink
    +> SeaTunnel Zeta
    + +## 主要功能 + +- [x] [批处理](../../concept/connector-v2-features.md) +- [ ] [流处理](../../concept/connector-v2-features.md) +- [ ] [精确一次](../../concept/connector-v2-features.md) +- [x] [列投影](../../concept/connector-v2-features.md) +- [x] [并行度](../../concept/connector-v2-features.md) +- [x] [支持用户自定义分片](../../concept/connector-v2-features.md) +- [x] [支持多表读](../../concept/connector-v2-features.md) + +## 描述 + +用于 Apache Doris 的源连接器。 + +## 支持的数据源信息 + +| 数据源 | 支持版本 | 驱动 | Url | Maven | +|------------|--------------------------------------|--------|-----|-------| +| Doris | 仅支持Doris2.0及以上版本. | - | - | - | + +## 数据类型映射 + +| Doris 数据类型 | SeaTunnel 数据类型 | +|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| INT | INT | +| TINYINT | TINYINT | +| SMALLINT | SMALLINT | +| BIGINT | BIGINT | +| LARGEINT | STRING | +| BOOLEAN | BOOLEAN | +| DECIMAL | DECIMAL((Get the designated column's specified column size)+1,
    (Gets the designated column's number of digits to right of the decimal point.))) | +| FLOAT | FLOAT | +| DOUBLE | DOUBLE | +| CHAR
    VARCHAR
    STRING
    TEXT | STRING | +| DATE | DATE | +| DATETIME
    DATETIME(p) | TIMESTAMP | +| ARRAY | ARRAY | + +## 源选项 + +基础配置: + +| 名称 | 类型 | 是否必须 | 默认值 | 描述 | +|----------------------------------|--------|----------|------------|-----------------------------------------------------------------------------------------------------| +| fenodes | string | yes | - | FE 地址, 格式:`"fe_host:fe_http_port"` | +| username | string | yes | - | 用户名 | +| password | string | yes | - | 密码 | +| doris.request.retries | int | no | 3 | 请求Doris FE的重试次数 | +| doris.request.read.timeout.ms | int | no | 30000 | | +| doris.request.connect.timeout.ms | int | no | 30000 | | +| query-port | string | no | 9030 | Doris查询端口 | +| doris.request.query.timeout.s | int | no | 3600 | Doris扫描数据的超时时间,单位秒 | +| table_list | string | 否 | - | 表清单 | + +表清单配置: + +| 名称 | 类型 | 是否必须 | 默认值 | 描述 | +|----------------------------------|--------|----------|------------|-----------------------------------------------------------------------------------------------------| +| database | string | yes | - | 数据库 | +| table | string | yes | - | 表名 | +| doris.read.field | string | no | - | 选择要读取的Doris表字段 | +| doris.filter.query | string | no | - | 数据过滤. 格式:"字段 = 值", 例如:doris.filter.query = "F_ID > 2" | +| doris.batch.size | int | no | 1024 | 每次能够从BE中读取到的最大行数 | +| doris.exec.mem.limit | long | no | 2147483648 | 单个be扫描请求可以使用的最大内存。默认内存为2G(2147483648) | + +注意: 当此配置对应于单个表时,您可以将table_list中的配置项展平到外层。 + +### 提示 + +> 不建议随意修改高级参数 + +## 例子 + +### 单表 +> 这是一个从doris读取数据后,输出到控制台的例子: + +``` +env { + parallelism = 2 + job.mode = "BATCH" +} +source{ + Doris { + fenodes = "doris_e2e:8030" + username = root + password = "" + database = "e2e_source" + table = "doris_e2e_table" + } +} + +transform { + # If you would like to get more information about how to configure seatunnel and see full list of transform plugins, + # please go to https://seatunnel.apache.org/docs/transform/sql +} + +sink { + Console {} +} +``` + +使用`doris.read.field`参数来选择需要读取的Doris表字段: + +``` +env { + parallelism = 2 + job.mode = "BATCH" +} +source{ + Doris { + fenodes = "doris_e2e:8030" + username = root + password = "" + database = "e2e_source" + table = "doris_e2e_table" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT,F_SMALLINT" + } +} + +transform { + # If you would like to get more information about how to configure seatunnel and see full list of transform plugins, + # please go to https://seatunnel.apache.org/docs/transform/sql +} + +sink { + Console {} +} +``` + +使用`doris.filter.query`来过滤数据,参数值将作为过滤条件直接传递到doris: + +``` +env { + parallelism = 2 + job.mode = "BATCH" +} +source{ + Doris { + fenodes = "doris_e2e:8030" + username = root + password = "" + database = "e2e_source" + table = "doris_e2e_table" + doris.filter.query = "F_ID > 2" + } +} + +transform { + # If you would like to get more information about how to configure seatunnel and see full list of transform plugins, + # please go to https://seatunnel.apache.org/docs/transform/sql +} + +sink { + Console {} +} +``` +### 多表 +``` +env{ + parallelism = 1 + job.mode = "BATCH" +} + +source{ + Doris { + fenodes = "xxxx:8030" + username = root + password = "" + table_list = [ + { + database = "st_source_0" + table = "doris_table_0" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT" + doris.filter.query = "F_ID >= 50" + }, + { + database = "st_source_1" + table = "doris_table_1" + } + ] + } +} + +transform {} + +sink{ + Doris { + fenodes = "xxxx:8030" + schema_save_mode = "RECREATE_SCHEMA" + username = root + password = "" + database = "st_sink" + table = "${table_name}" + sink.enable-2pc = "true" + sink.label-prefix = "test_json" + doris.config = { + format="json" + read_json_by_line="true" + } + } +} +``` diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/backend/BackendClient.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/backend/BackendClient.java index 31bdb2a78e7..04f96d2d607 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/backend/BackendClient.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/backend/BackendClient.java @@ -25,7 +25,7 @@ import org.apache.seatunnel.shade.org.apache.thrift.transport.TTransport; import org.apache.seatunnel.shade.org.apache.thrift.transport.TTransportException; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSourceConfig; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorErrorCode; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.source.serialization.Routing; @@ -55,7 +55,7 @@ public class BackendClient { private final int socketTimeout; private final int connectTimeout; - public BackendClient(Routing routing, DorisConfig readOptions) { + public BackendClient(Routing routing, DorisSourceConfig readOptions) { this.routing = routing; this.connectTimeout = readOptions.getRequestConnectTimeoutMs(); this.socketTimeout = readOptions.getRequestReadTimeoutMs(); diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java index a7f5eabf63d..324200e5e4d 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java @@ -37,7 +37,6 @@ import org.apache.seatunnel.common.exception.CommonError; import org.apache.seatunnel.common.exception.CommonErrorCode; import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; import org.apache.seatunnel.connectors.doris.config.DorisOptions; import org.apache.seatunnel.connectors.doris.datatype.DorisTypeConverterFactory; import org.apache.seatunnel.connectors.doris.datatype.DorisTypeConverterV2; @@ -85,7 +84,7 @@ public class DorisCatalog implements Catalog { private Connection conn; - private DorisConfig dorisConfig; + private String createTableTemplate; private String dorisVersion; @@ -110,9 +109,9 @@ public DorisCatalog( Integer queryPort, String username, String password, - DorisConfig config) { + String createTableTemplate) { this(catalogName, frontEndNodes, queryPort, username, password); - this.dorisConfig = config; + this.createTableTemplate = createTableTemplate; } public DorisCatalog( @@ -121,9 +120,9 @@ public DorisCatalog( Integer queryPort, String username, String password, - DorisConfig config, + String createTableTemplate, String defaultDatabase) { - this(catalogName, frontEndNodes, queryPort, username, password, config); + this(catalogName, frontEndNodes, queryPort, username, password, createTableTemplate); this.defaultDatabase = defaultDatabase; } @@ -414,7 +413,7 @@ public void createTable(TablePath tablePath, CatalogTable table, boolean ignoreI String stmt = DorisCatalogUtil.getCreateTableStatement( - dorisConfig.getCreateTableTemplate(), tablePath, table, typeConverter); + createTableTemplate, tablePath, table, typeConverter); try (Statement statement = conn.createStatement()) { statement.execute(stmt); } catch (SQLException e) { @@ -510,7 +509,7 @@ public PreviewResult previewAction( checkArgument(catalogTable.isPresent(), "CatalogTable cannot be null"); return new SQLPreviewResult( DorisCatalogUtil.getCreateTableStatement( - dorisConfig.getCreateTableTemplate(), + createTableTemplate, tablePath, catalogTable.get(), // used for test when typeConverter is null diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalogFactory.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalogFactory.java index 1071b52f05a..7fd1da603e2 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalogFactory.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalogFactory.java @@ -22,11 +22,14 @@ import org.apache.seatunnel.api.table.catalog.Catalog; import org.apache.seatunnel.api.table.factory.CatalogFactory; import org.apache.seatunnel.api.table.factory.Factory; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSinkOptions; import com.google.auto.service.AutoService; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.IDENTIFIER; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE; + @AutoService(Factory.class) public class DorisCatalogFactory implements CatalogFactory { @@ -38,13 +41,13 @@ public Catalog createCatalog(String catalogName, ReadonlyConfig options) { options.get(DorisOptions.QUERY_PORT), options.get(DorisOptions.USERNAME), options.get(DorisOptions.PASSWORD), - DorisConfig.of(options), - options.get(DorisOptions.DEFAULT_DATABASE)); + options.get(SAVE_MODE_CREATE_TEMPLATE), + options.get(DorisSinkOptions.DEFAULT_DATABASE)); } @Override public String factoryIdentifier() { - return DorisConfig.IDENTIFIER; + return IDENTIFIER; } @Override diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisConfig.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisConfig.java deleted file mode 100644 index f7155e8a647..00000000000 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisConfig.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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.seatunnel.connectors.doris.config; - -import org.apache.seatunnel.shade.com.typesafe.config.Config; - -import org.apache.seatunnel.api.configuration.ReadonlyConfig; - -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -import java.io.Serializable; -import java.util.Map; -import java.util.Properties; - -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DATABASE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_BATCH_SIZE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_DESERIALIZE_ARROW_ASYNC; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_DESERIALIZE_QUEUE_SIZE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_EXEC_MEM_LIMIT; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_FILTER_QUERY; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_READ_FIELD; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_REQUEST_CONNECT_TIMEOUT_MS; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_REQUEST_QUERY_TIMEOUT_S; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_REQUEST_READ_TIMEOUT_MS; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_REQUEST_RETRIES; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_SINK_CONFIG_PREFIX; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_TABLET_SIZE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.FENODES; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.NEEDS_UNSUPPORTED_TYPE_CASTING; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.PASSWORD; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.QUERY_PORT; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SAVE_MODE_CREATE_TEMPLATE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SINK_BUFFER_COUNT; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SINK_BUFFER_SIZE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SINK_CHECK_INTERVAL; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SINK_ENABLE_2PC; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SINK_ENABLE_DELETE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SINK_LABEL_PREFIX; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.SINK_MAX_RETRIES; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.USERNAME; - -@Setter -@Getter -@ToString -public class DorisConfig implements Serializable { - - public static final String IDENTIFIER = "Doris"; - - // common option - private String frontends; - private String database; - private String table; - private String username; - private String password; - private Integer queryPort; - private int batchSize; - - // source option - private String readField; - private String filterQuery; - private Integer tabletSize; - private Integer requestConnectTimeoutMs; - private Integer requestReadTimeoutMs; - private Integer requestQueryTimeoutS; - private Integer requestRetries; - private Boolean deserializeArrowAsync; - private int deserializeQueueSize; - private Long execMemLimit; - private boolean useOldApi; - - // sink option - private Boolean enable2PC; - private Boolean enableDelete; - private String labelPrefix; - private Integer checkInterval; - private Integer maxRetries; - private Integer bufferSize; - private Integer bufferCount; - private Properties streamLoadProps; - private boolean needsUnsupportedTypeCasting; - - // create table option - private String createTableTemplate; - - public static DorisConfig of(Config pluginConfig) { - return of(ReadonlyConfig.fromConfig(pluginConfig)); - } - - public static DorisConfig of(ReadonlyConfig config) { - - DorisConfig dorisConfig = new DorisConfig(); - - // common option - dorisConfig.setFrontends(config.get(FENODES)); - dorisConfig.setUsername(config.get(USERNAME)); - dorisConfig.setPassword(config.get(PASSWORD)); - dorisConfig.setQueryPort(config.get(QUERY_PORT)); - dorisConfig.setStreamLoadProps(parseStreamLoadProperties(config)); - dorisConfig.setDatabase(config.get(DATABASE)); - dorisConfig.setTable(config.get(TABLE)); - - // source option - dorisConfig.setReadField(config.get(DORIS_READ_FIELD)); - dorisConfig.setFilterQuery(config.get(DORIS_FILTER_QUERY)); - dorisConfig.setTabletSize(config.get(DORIS_TABLET_SIZE)); - dorisConfig.setRequestConnectTimeoutMs(config.get(DORIS_REQUEST_CONNECT_TIMEOUT_MS)); - dorisConfig.setRequestQueryTimeoutS(config.get(DORIS_REQUEST_QUERY_TIMEOUT_S)); - dorisConfig.setRequestReadTimeoutMs(config.get(DORIS_REQUEST_READ_TIMEOUT_MS)); - dorisConfig.setRequestRetries(config.get(DORIS_REQUEST_RETRIES)); - dorisConfig.setDeserializeArrowAsync(config.get(DORIS_DESERIALIZE_ARROW_ASYNC)); - dorisConfig.setDeserializeQueueSize(config.get(DORIS_DESERIALIZE_QUEUE_SIZE)); - dorisConfig.setBatchSize(config.get(DORIS_BATCH_SIZE)); - dorisConfig.setExecMemLimit(config.get(DORIS_EXEC_MEM_LIMIT)); - - // sink option - dorisConfig.setEnable2PC(config.get(SINK_ENABLE_2PC)); - dorisConfig.setLabelPrefix(config.get(SINK_LABEL_PREFIX)); - dorisConfig.setCheckInterval(config.get(SINK_CHECK_INTERVAL)); - dorisConfig.setMaxRetries(config.get(SINK_MAX_RETRIES)); - dorisConfig.setBufferSize(config.get(SINK_BUFFER_SIZE)); - dorisConfig.setBufferCount(config.get(SINK_BUFFER_COUNT)); - dorisConfig.setEnableDelete(config.get(SINK_ENABLE_DELETE)); - dorisConfig.setNeedsUnsupportedTypeCasting(config.get(NEEDS_UNSUPPORTED_TYPE_CASTING)); - - // create table option - dorisConfig.setCreateTableTemplate(config.get(SAVE_MODE_CREATE_TEMPLATE)); - - return dorisConfig; - } - - private static Properties parseStreamLoadProperties(ReadonlyConfig config) { - Properties streamLoadProps = new Properties(); - if (config.getOptional(DORIS_SINK_CONFIG_PREFIX).isPresent()) { - Map map = config.getOptional(DORIS_SINK_CONFIG_PREFIX).get(); - map.forEach( - (key, value) -> { - streamLoadProps.put(key.toLowerCase(), value); - }); - } - return streamLoadProps; - } -} diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisOptions.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisOptions.java index ddf1195b6ed..bcdf24c9d7b 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisOptions.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisOptions.java @@ -20,32 +20,12 @@ import org.apache.seatunnel.api.configuration.Option; import org.apache.seatunnel.api.configuration.Options; import org.apache.seatunnel.api.configuration.util.OptionRule; -import org.apache.seatunnel.api.sink.DataSaveMode; -import org.apache.seatunnel.api.sink.SaveModePlaceHolder; -import org.apache.seatunnel.api.sink.SchemaSaveMode; - -import java.util.Map; - -import static org.apache.seatunnel.api.sink.SinkCommonOptions.MULTI_TABLE_SINK_REPLICA; public interface DorisOptions { - int DORIS_TABLET_SIZE_MIN = 1; - int DORIS_TABLET_SIZE_DEFAULT = Integer.MAX_VALUE; - int DORIS_REQUEST_CONNECT_TIMEOUT_MS_DEFAULT = 30 * 1000; - int DORIS_REQUEST_READ_TIMEOUT_MS_DEFAULT = 30 * 1000; - int DORIS_REQUEST_QUERY_TIMEOUT_S_DEFAULT = 3600; - int DORIS_REQUEST_RETRIES_DEFAULT = 3; - Boolean DORIS_DESERIALIZE_ARROW_ASYNC_DEFAULT = false; - int DORIS_DESERIALIZE_QUEUE_SIZE_DEFAULT = 64; - int DORIS_BATCH_SIZE_DEFAULT = 1024; - long DORIS_EXEC_MEM_LIMIT_DEFAULT = 2147483648L; - int DEFAULT_SINK_CHECK_INTERVAL = 10000; - int DEFAULT_SINK_MAX_RETRIES = 3; - int DEFAULT_SINK_BUFFER_SIZE = 256 * 1024; - int DEFAULT_SINK_BUFFER_COUNT = 3; - + String IDENTIFIER = "Doris"; String DORIS_DEFAULT_CLUSTER = "default_cluster"; + int DORIS_BATCH_SIZE_DEFAULT = 1024; // common option Option FENODES = @@ -72,6 +52,7 @@ public interface DorisOptions { .stringType() .noDefaultValue() .withDescription("the doris user name."); + Option PASSWORD = Options.key("password") .stringType() @@ -79,202 +60,17 @@ public interface DorisOptions { .withDescription("the doris password."); Option TABLE = - Options.key("table") - .stringType() - .noDefaultValue() - .withDescription("the doris table name."); + Options.key("table").stringType().noDefaultValue().withDescription("table"); + Option DATABASE = - Options.key("database") - .stringType() - .noDefaultValue() - .withDescription("the doris database name."); + Options.key("database").stringType().noDefaultValue().withDescription("database"); + Option DORIS_BATCH_SIZE = Options.key("doris.batch.size") .intType() .defaultValue(DORIS_BATCH_SIZE_DEFAULT) .withDescription("the batch size of the doris read/write."); - // source config options - Option DORIS_READ_FIELD = - Options.key("doris.read.field") - .stringType() - .noDefaultValue() - .withDescription( - "List of column names in the Doris table, separated by commas"); - Option DORIS_FILTER_QUERY = - Options.key("doris.filter.query") - .stringType() - .noDefaultValue() - .withDescription( - "Filter expression of the query, which is transparently transmitted to Doris. Doris uses this expression to complete source-side data filtering"); - Option DORIS_TABLET_SIZE = - Options.key("doris.request.tablet.size") - .intType() - .defaultValue(DORIS_TABLET_SIZE_DEFAULT) - .withDescription(""); - Option DORIS_REQUEST_CONNECT_TIMEOUT_MS = - Options.key("doris.request.connect.timeout.ms") - .intType() - .defaultValue(DORIS_REQUEST_CONNECT_TIMEOUT_MS_DEFAULT) - .withDescription(""); - Option DORIS_REQUEST_READ_TIMEOUT_MS = - Options.key("doris.request.read.timeout.ms") - .intType() - .defaultValue(DORIS_REQUEST_READ_TIMEOUT_MS_DEFAULT) - .withDescription(""); - Option DORIS_REQUEST_QUERY_TIMEOUT_S = - Options.key("doris.request.query.timeout.s") - .intType() - .defaultValue(DORIS_REQUEST_QUERY_TIMEOUT_S_DEFAULT) - .withDescription(""); - Option DORIS_REQUEST_RETRIES = - Options.key("doris.request.retries") - .intType() - .defaultValue(DORIS_REQUEST_RETRIES_DEFAULT) - .withDescription(""); - Option DORIS_DESERIALIZE_ARROW_ASYNC = - Options.key("doris.deserialize.arrow.async") - .booleanType() - .defaultValue(DORIS_DESERIALIZE_ARROW_ASYNC_DEFAULT) - .withDescription(""); - Option DORIS_DESERIALIZE_QUEUE_SIZE = - Options.key("doris.request.retriesdoris.deserialize.queue.size") - .intType() - .defaultValue(DORIS_DESERIALIZE_QUEUE_SIZE_DEFAULT) - .withDescription(""); - - Option DORIS_EXEC_MEM_LIMIT = - Options.key("doris.exec.mem.limit") - .longType() - .defaultValue(DORIS_EXEC_MEM_LIMIT_DEFAULT) - .withDescription(""); - - // sink config options - Option SINK_ENABLE_2PC = - Options.key("sink.enable-2pc") - .booleanType() - .defaultValue(false) - .withDescription("enable 2PC while loading"); - - Option SINK_CHECK_INTERVAL = - Options.key("sink.check-interval") - .intType() - .defaultValue(DEFAULT_SINK_CHECK_INTERVAL) - .withDescription("check exception with the interval while loading"); - Option SINK_MAX_RETRIES = - Options.key("sink.max-retries") - .intType() - .defaultValue(DEFAULT_SINK_MAX_RETRIES) - .withDescription("the max retry times if writing records to database failed."); - Option SINK_BUFFER_SIZE = - Options.key("sink.buffer-size") - .intType() - .defaultValue(DEFAULT_SINK_BUFFER_SIZE) - .withDescription("the buffer size to cache data for stream load."); - Option SINK_BUFFER_COUNT = - Options.key("sink.buffer-count") - .intType() - .defaultValue(DEFAULT_SINK_BUFFER_COUNT) - .withDescription("the buffer count to cache data for stream load."); - Option SINK_LABEL_PREFIX = - Options.key("sink.label-prefix") - .stringType() - .defaultValue("") - .withDescription("the unique label prefix."); - Option SINK_ENABLE_DELETE = - Options.key("sink.enable-delete") - .booleanType() - .defaultValue(false) - .withDescription("whether to enable the delete function"); - - Option> DORIS_SINK_CONFIG_PREFIX = - Options.key("doris.config") - .mapType() - .noDefaultValue() - .withDescription( - "The parameter of the Stream Load data_desc. " - + "The way to specify the parameter is to add the prefix `doris.config` to the original load parameter name "); - - Option DEFAULT_DATABASE = - Options.key("default-database") - .stringType() - .defaultValue("information_schema") - .withDescription(""); - - Option SCHEMA_SAVE_MODE = - Options.key("schema_save_mode") - .enumType(SchemaSaveMode.class) - .defaultValue(SchemaSaveMode.CREATE_SCHEMA_WHEN_NOT_EXIST) - .withDescription("schema_save_mode"); - - Option DATA_SAVE_MODE = - Options.key("data_save_mode") - .enumType(DataSaveMode.class) - .defaultValue(DataSaveMode.APPEND_DATA) - .withDescription("data_save_mode"); - - Option CUSTOM_SQL = - Options.key("custom_sql").stringType().noDefaultValue().withDescription("custom_sql"); - - Option NEEDS_UNSUPPORTED_TYPE_CASTING = - Options.key("needs_unsupported_type_casting") - .booleanType() - .defaultValue(false) - .withDescription( - "Whether to enable the unsupported type casting, such as Decimal64 to Double"); - - // create table - Option SAVE_MODE_CREATE_TEMPLATE = - Options.key("save_mode_create_template") - .stringType() - .defaultValue( - "CREATE TABLE IF NOT EXISTS `" - + SaveModePlaceHolder.DATABASE.getPlaceHolder() - + "`.`" - + SaveModePlaceHolder.TABLE.getPlaceHolder() - + "` (\n" - + SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getPlaceHolder() - + ",\n" - + SaveModePlaceHolder.ROWTYPE_FIELDS.getPlaceHolder() - + "\n" - + ") ENGINE=OLAP\n" - + " UNIQUE KEY (" - + SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getPlaceHolder() - + ")\n" - + "DISTRIBUTED BY HASH (" - + SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getPlaceHolder() - + ")\n " - + "PROPERTIES (\n" - + "\"replication_allocation\" = \"tag.location.default: 1\",\n" - + "\"in_memory\" = \"false\",\n" - + "\"storage_format\" = \"V2\",\n" - + "\"disable_auto_compaction\" = \"false\"\n" - + ")") - .withDescription("Create table statement template, used to create Doris table"); - - OptionRule.Builder SINK_RULE = - OptionRule.builder() - .required( - FENODES, - USERNAME, - PASSWORD, - SINK_LABEL_PREFIX, - DORIS_SINK_CONFIG_PREFIX, - DATA_SAVE_MODE, - SCHEMA_SAVE_MODE) - .optional( - DATABASE, - TABLE, - TABLE_IDENTIFIER, - QUERY_PORT, - DORIS_BATCH_SIZE, - SINK_ENABLE_2PC, - SINK_ENABLE_DELETE, - MULTI_TABLE_SINK_REPLICA, - SAVE_MODE_CREATE_TEMPLATE, - NEEDS_UNSUPPORTED_TYPE_CASTING) - .conditional(DATA_SAVE_MODE, DataSaveMode.CUSTOM_PROCESSING, CUSTOM_SQL); - OptionRule.Builder CATALOG_RULE = OptionRule.builder().required(FENODES, QUERY_PORT, USERNAME, PASSWORD); } diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java new file mode 100644 index 00000000000..8f0d948042f --- /dev/null +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.seatunnel.connectors.doris.config; + +import org.apache.seatunnel.shade.com.typesafe.config.Config; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.Map; +import java.util.Properties; + +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DATABASE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_BATCH_SIZE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.FENODES; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.PASSWORD; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.QUERY_PORT; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.USERNAME; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.DORIS_SINK_CONFIG_PREFIX; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.NEEDS_UNSUPPORTED_TYPE_CASTING; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SINK_BUFFER_COUNT; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SINK_BUFFER_SIZE; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SINK_CHECK_INTERVAL; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SINK_ENABLE_2PC; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SINK_ENABLE_DELETE; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SINK_LABEL_PREFIX; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.SINK_MAX_RETRIES; + +@Setter +@Getter +@ToString +public class DorisSinkConfig implements Serializable { + + // common option + private String frontends; + private String database; + private String table; + private String username; + private String password; + private Integer queryPort; + private int batchSize; + + // sink option + private Boolean enable2PC; + private Boolean enableDelete; + private String labelPrefix; + private Integer checkInterval; + private Integer maxRetries; + private Integer bufferSize; + private Integer bufferCount; + private Properties streamLoadProps; + private boolean needsUnsupportedTypeCasting; + + // create table option + private String createTableTemplate; + + public static DorisSinkConfig of(Config pluginConfig) { + return of(ReadonlyConfig.fromConfig(pluginConfig)); + } + + public static DorisSinkConfig of(ReadonlyConfig config) { + + DorisSinkConfig dorisSinkConfig = new DorisSinkConfig(); + + // common option + dorisSinkConfig.setFrontends(config.get(FENODES)); + dorisSinkConfig.setUsername(config.get(USERNAME)); + dorisSinkConfig.setPassword(config.get(PASSWORD)); + dorisSinkConfig.setQueryPort(config.get(QUERY_PORT)); + dorisSinkConfig.setStreamLoadProps(parseStreamLoadProperties(config)); + dorisSinkConfig.setDatabase(config.get(DATABASE)); + dorisSinkConfig.setTable(config.get(TABLE)); + dorisSinkConfig.setBatchSize(config.get(DORIS_BATCH_SIZE)); + + // sink option + dorisSinkConfig.setEnable2PC(config.get(SINK_ENABLE_2PC)); + dorisSinkConfig.setLabelPrefix(config.get(SINK_LABEL_PREFIX)); + dorisSinkConfig.setCheckInterval(config.get(SINK_CHECK_INTERVAL)); + dorisSinkConfig.setMaxRetries(config.get(SINK_MAX_RETRIES)); + dorisSinkConfig.setBufferSize(config.get(SINK_BUFFER_SIZE)); + dorisSinkConfig.setBufferCount(config.get(SINK_BUFFER_COUNT)); + dorisSinkConfig.setEnableDelete(config.get(SINK_ENABLE_DELETE)); + dorisSinkConfig.setNeedsUnsupportedTypeCasting(config.get(NEEDS_UNSUPPORTED_TYPE_CASTING)); + + // create table option + dorisSinkConfig.setCreateTableTemplate(config.get(SAVE_MODE_CREATE_TEMPLATE)); + + return dorisSinkConfig; + } + + private static Properties parseStreamLoadProperties(ReadonlyConfig config) { + Properties streamLoadProps = new Properties(); + if (config.getOptional(DORIS_SINK_CONFIG_PREFIX).isPresent()) { + Map map = config.getOptional(DORIS_SINK_CONFIG_PREFIX).get(); + map.forEach( + (key, value) -> { + streamLoadProps.put(key.toLowerCase(), value); + }); + } + return streamLoadProps; + } +} diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkOptions.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkOptions.java new file mode 100644 index 00000000000..372418d12a4 --- /dev/null +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkOptions.java @@ -0,0 +1,170 @@ +/* + * 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.seatunnel.connectors.doris.config; + +import org.apache.seatunnel.api.configuration.Option; +import org.apache.seatunnel.api.configuration.Options; +import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.sink.DataSaveMode; +import org.apache.seatunnel.api.sink.SaveModePlaceHolder; +import org.apache.seatunnel.api.sink.SchemaSaveMode; + +import java.util.Map; + +import static org.apache.seatunnel.api.sink.SinkCommonOptions.MULTI_TABLE_SINK_REPLICA; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DATABASE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_BATCH_SIZE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.FENODES; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.PASSWORD; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.QUERY_PORT; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE_IDENTIFIER; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.USERNAME; + +public interface DorisSinkOptions { + + int DEFAULT_SINK_CHECK_INTERVAL = 10000; + int DEFAULT_SINK_MAX_RETRIES = 3; + int DEFAULT_SINK_BUFFER_SIZE = 256 * 1024; + int DEFAULT_SINK_BUFFER_COUNT = 3; + + Option SINK_ENABLE_2PC = + Options.key("sink.enable-2pc") + .booleanType() + .defaultValue(false) + .withDescription("enable 2PC while loading"); + + Option SINK_CHECK_INTERVAL = + Options.key("sink.check-interval") + .intType() + .defaultValue(DEFAULT_SINK_CHECK_INTERVAL) + .withDescription("check exception with the interval while loading"); + Option SINK_MAX_RETRIES = + Options.key("sink.max-retries") + .intType() + .defaultValue(DEFAULT_SINK_MAX_RETRIES) + .withDescription("the max retry times if writing records to database failed."); + Option SINK_BUFFER_SIZE = + Options.key("sink.buffer-size") + .intType() + .defaultValue(DEFAULT_SINK_BUFFER_SIZE) + .withDescription("the buffer size to cache data for stream load."); + Option SINK_BUFFER_COUNT = + Options.key("sink.buffer-count") + .intType() + .defaultValue(DEFAULT_SINK_BUFFER_COUNT) + .withDescription("the buffer count to cache data for stream load."); + Option SINK_LABEL_PREFIX = + Options.key("sink.label-prefix") + .stringType() + .defaultValue("") + .withDescription("the unique label prefix."); + Option SINK_ENABLE_DELETE = + Options.key("sink.enable-delete") + .booleanType() + .defaultValue(false) + .withDescription("whether to enable the delete function"); + + Option> DORIS_SINK_CONFIG_PREFIX = + Options.key("doris.config") + .mapType() + .noDefaultValue() + .withDescription( + "The parameter of the Stream Load data_desc. " + + "The way to specify the parameter is to add the prefix `doris.config` to the original load parameter name "); + + Option DEFAULT_DATABASE = + Options.key("default-database") + .stringType() + .defaultValue("information_schema") + .withDescription(""); + + Option SCHEMA_SAVE_MODE = + Options.key("schema_save_mode") + .enumType(SchemaSaveMode.class) + .defaultValue(SchemaSaveMode.CREATE_SCHEMA_WHEN_NOT_EXIST) + .withDescription("schema_save_mode"); + + Option DATA_SAVE_MODE = + Options.key("data_save_mode") + .enumType(DataSaveMode.class) + .defaultValue(DataSaveMode.APPEND_DATA) + .withDescription("data_save_mode"); + + Option CUSTOM_SQL = + Options.key("custom_sql").stringType().noDefaultValue().withDescription("custom_sql"); + + Option NEEDS_UNSUPPORTED_TYPE_CASTING = + Options.key("needs_unsupported_type_casting") + .booleanType() + .defaultValue(false) + .withDescription( + "Whether to enable the unsupported type casting, such as Decimal64 to Double"); + + // create table + Option SAVE_MODE_CREATE_TEMPLATE = + Options.key("save_mode_create_template") + .stringType() + .defaultValue( + "CREATE TABLE IF NOT EXISTS `" + + SaveModePlaceHolder.DATABASE.getPlaceHolder() + + "`.`" + + SaveModePlaceHolder.TABLE.getPlaceHolder() + + "` (\n" + + SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getPlaceHolder() + + ",\n" + + SaveModePlaceHolder.ROWTYPE_FIELDS.getPlaceHolder() + + "\n" + + ") ENGINE=OLAP\n" + + " UNIQUE KEY (" + + SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getPlaceHolder() + + ")\n" + + "DISTRIBUTED BY HASH (" + + SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getPlaceHolder() + + ")\n " + + "PROPERTIES (\n" + + "\"replication_allocation\" = \"tag.location.default: 1\",\n" + + "\"in_memory\" = \"false\",\n" + + "\"storage_format\" = \"V2\",\n" + + "\"disable_auto_compaction\" = \"false\"\n" + + ")") + .withDescription("Create table statement template, used to create Doris table"); + + OptionRule.Builder SINK_RULE = + OptionRule.builder() + .required( + FENODES, + USERNAME, + PASSWORD, + SINK_LABEL_PREFIX, + DORIS_SINK_CONFIG_PREFIX, + DATA_SAVE_MODE, + SCHEMA_SAVE_MODE) + .optional( + DATABASE, + TABLE, + TABLE_IDENTIFIER, + QUERY_PORT, + DORIS_BATCH_SIZE, + SINK_ENABLE_2PC, + SINK_ENABLE_DELETE, + MULTI_TABLE_SINK_REPLICA, + SAVE_MODE_CREATE_TEMPLATE, + NEEDS_UNSUPPORTED_TYPE_CASTING) + .conditional(DATA_SAVE_MODE, DataSaveMode.CUSTOM_PROCESSING, CUSTOM_SQL); +} diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceConfig.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceConfig.java new file mode 100644 index 00000000000..999f8fbfeaa --- /dev/null +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceConfig.java @@ -0,0 +1,71 @@ +/* + * 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.seatunnel.connectors.doris.config; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; + +import lombok.Data; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.util.List; + +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.FENODES; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.PASSWORD; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.QUERY_PORT; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.USERNAME; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_DESERIALIZE_ARROW_ASYNC; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_DESERIALIZE_QUEUE_SIZE; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_REQUEST_CONNECT_TIMEOUT_MS; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_REQUEST_QUERY_TIMEOUT_S; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_REQUEST_READ_TIMEOUT_MS; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_REQUEST_RETRIES; + +@Data +@SuperBuilder +public class DorisSourceConfig implements Serializable { + + private String frontends; + private Integer queryPort; + private String username; + private String password; + private Integer requestConnectTimeoutMs; + private Integer requestReadTimeoutMs; + private Integer requestQueryTimeoutS; + private Integer requestRetries; + private Boolean deserializeArrowAsync; + private int deserializeQueueSize; + private boolean useOldApi; + private List tableConfigList; + + public static DorisSourceConfig of(ReadonlyConfig config) { + DorisSourceConfigBuilder builder = DorisSourceConfig.builder(); + builder.tableConfigList(DorisTableConfig.of(config)); + builder.frontends(config.get(FENODES)); + builder.queryPort(config.get(QUERY_PORT)); + builder.username(config.get(USERNAME)); + builder.password(config.get(PASSWORD)); + builder.requestConnectTimeoutMs(config.get(DORIS_REQUEST_CONNECT_TIMEOUT_MS)); + builder.requestReadTimeoutMs(config.get(DORIS_REQUEST_READ_TIMEOUT_MS)); + builder.requestQueryTimeoutS(config.get(DORIS_REQUEST_QUERY_TIMEOUT_S)); + builder.requestRetries(config.get(DORIS_REQUEST_RETRIES)); + builder.deserializeArrowAsync(config.get(DORIS_DESERIALIZE_ARROW_ASYNC)); + builder.deserializeQueueSize(config.get(DORIS_DESERIALIZE_QUEUE_SIZE)); + return builder.build(); + } +} diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceOptions.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceOptions.java new file mode 100644 index 00000000000..2ee852ffccc --- /dev/null +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSourceOptions.java @@ -0,0 +1,103 @@ +/* + * 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.seatunnel.connectors.doris.config; + +import org.apache.seatunnel.api.configuration.Option; +import org.apache.seatunnel.api.configuration.Options; + +import java.util.List; + +public interface DorisSourceOptions { + + int DORIS_TABLET_SIZE_MIN = 1; + int DORIS_TABLET_SIZE_DEFAULT = Integer.MAX_VALUE; + int DORIS_REQUEST_CONNECT_TIMEOUT_MS_DEFAULT = 30 * 1000; + int DORIS_REQUEST_READ_TIMEOUT_MS_DEFAULT = 30 * 1000; + int DORIS_REQUEST_QUERY_TIMEOUT_S_DEFAULT = 3600; + int DORIS_REQUEST_RETRIES_DEFAULT = 3; + Boolean DORIS_DESERIALIZE_ARROW_ASYNC_DEFAULT = false; + int DORIS_DESERIALIZE_QUEUE_SIZE_DEFAULT = 64; + long DORIS_EXEC_MEM_LIMIT_DEFAULT = 2147483648L; + + Option> TABLE_LIST = + Options.key("table_list") + .listType(DorisTableConfig.class) + .noDefaultValue() + .withDescription("table list config."); + + Option DORIS_READ_FIELD = + Options.key("doris.read.field") + .stringType() + .noDefaultValue() + .withDescription( + "List of column names in the Doris table, separated by commas"); + Option DORIS_FILTER_QUERY = + Options.key("doris.filter.query") + .stringType() + .noDefaultValue() + .withDescription( + "Filter expression of the query, which is transparently transmitted to Doris. Doris uses this expression to complete source-side data filtering"); + + Option DORIS_TABLET_SIZE = + Options.key("doris.request.tablet.size") + .intType() + .defaultValue(DORIS_TABLET_SIZE_DEFAULT) + .withDescription(""); + + Option DORIS_REQUEST_CONNECT_TIMEOUT_MS = + Options.key("doris.request.connect.timeout.ms") + .intType() + .defaultValue(DORIS_REQUEST_CONNECT_TIMEOUT_MS_DEFAULT) + .withDescription(""); + + Option DORIS_REQUEST_READ_TIMEOUT_MS = + Options.key("doris.request.read.timeout.ms") + .intType() + .defaultValue(DORIS_REQUEST_READ_TIMEOUT_MS_DEFAULT) + .withDescription(""); + + Option DORIS_REQUEST_QUERY_TIMEOUT_S = + Options.key("doris.request.query.timeout.s") + .intType() + .defaultValue(DORIS_REQUEST_QUERY_TIMEOUT_S_DEFAULT) + .withDescription(""); + + Option DORIS_REQUEST_RETRIES = + Options.key("doris.request.retries") + .intType() + .defaultValue(DORIS_REQUEST_RETRIES_DEFAULT) + .withDescription(""); + + Option DORIS_DESERIALIZE_ARROW_ASYNC = + Options.key("doris.deserialize.arrow.async") + .booleanType() + .defaultValue(DORIS_DESERIALIZE_ARROW_ASYNC_DEFAULT) + .withDescription(""); + + Option DORIS_DESERIALIZE_QUEUE_SIZE = + Options.key("doris.request.retriesdoris.deserialize.queue.size") + .intType() + .defaultValue(DORIS_DESERIALIZE_QUEUE_SIZE_DEFAULT) + .withDescription(""); + + Option DORIS_EXEC_MEM_LIMIT = + Options.key("doris.exec.mem.limit") + .longType() + .defaultValue(DORIS_EXEC_MEM_LIMIT_DEFAULT) + .withDescription(""); +} diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisTableConfig.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisTableConfig.java new file mode 100644 index 00000000000..624d25636b2 --- /dev/null +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisTableConfig.java @@ -0,0 +1,132 @@ +/* + * 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.seatunnel.connectors.doris.config; + +import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; + +import org.apache.commons.lang3.StringUtils; + +import lombok.Builder; +import lombok.Data; +import lombok.experimental.Tolerate; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DATABASE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_BATCH_SIZE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_EXEC_MEM_LIMIT; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_FILTER_QUERY; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_READ_FIELD; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_TABLET_SIZE; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.TABLE_LIST; + +@Data +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class DorisTableConfig implements Serializable { + + @JsonProperty("table") + private String table; + + @JsonProperty("database") + private String database; + + @JsonProperty("doris.read.field") + private String readField; + + @JsonProperty("doris.filter.query") + private String filterQuery; + + @JsonProperty("doris.batch.size") + private int batchSize; + + @JsonProperty("doris.request.tablet.size") + private int tabletSize; + + @JsonProperty("doris.exec.mem.limit") + private long execMemLimit; + + @Tolerate + public DorisTableConfig() {} + + public static List of(ReadonlyConfig connectorConfig) { + List tableList; + if (connectorConfig.getOptional(TABLE_LIST).isPresent()) { + tableList = connectorConfig.get(TABLE_LIST); + } else { + DorisTableConfig tableProperty = + DorisTableConfig.builder() + .table(connectorConfig.get(TABLE)) + .database(connectorConfig.get(DATABASE)) + .readField(connectorConfig.get(DORIS_READ_FIELD)) + .filterQuery(connectorConfig.get(DORIS_FILTER_QUERY)) + .batchSize(connectorConfig.get(DORIS_BATCH_SIZE)) + .tabletSize(connectorConfig.get(DORIS_TABLET_SIZE)) + .execMemLimit(connectorConfig.get(DORIS_EXEC_MEM_LIMIT)) + .build(); + tableList = Collections.singletonList(tableProperty); + } + + if (tableList.size() > 1) { + List tableIds = + tableList.stream() + .map(DorisTableConfig::getTableIdentifier) + .collect(Collectors.toList()); + Set tableIdSet = new HashSet<>(tableIds); + if (tableIdSet.size() < tableList.size() - 1) { + throw new IllegalArgumentException( + "Please configure unique `database`.`table`, not allow null/duplicate: " + + tableIds); + } + } + + for (DorisTableConfig dorisTableConfig : tableList) { + if (StringUtils.isBlank(dorisTableConfig.getDatabase())) { + throw new IllegalArgumentException( + "Please configure `database`, not allow null database in config."); + } + if (StringUtils.isBlank(dorisTableConfig.getTable())) { + throw new IllegalArgumentException( + "Please configure `table`, not allow null table in config."); + } + if (dorisTableConfig.getBatchSize() <= 0) { + dorisTableConfig.setBatchSize(DORIS_BATCH_SIZE.defaultValue()); + } + if (dorisTableConfig.getExecMemLimit() <= 0) { + dorisTableConfig.setExecMemLimit(DORIS_EXEC_MEM_LIMIT.defaultValue()); + } + if (dorisTableConfig.getTabletSize() <= 0) { + dorisTableConfig.setTabletSize(DORIS_TABLET_SIZE.defaultValue()); + } + } + return tableList; + } + + public String getTableIdentifier() { + return String.format("%s.%s", database, table); + } +} diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/AbstractDorisTypeConverter.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/AbstractDorisTypeConverter.java index e6b9b95361d..67266b453f5 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/AbstractDorisTypeConverter.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/AbstractDorisTypeConverter.java @@ -26,12 +26,13 @@ import org.apache.seatunnel.api.table.type.DecimalType; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.common.exception.CommonError; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; import lombok.extern.slf4j.Slf4j; import java.util.Locale; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.IDENTIFIER; + @Slf4j public abstract class AbstractDorisTypeConverter implements TypeConverter { public static final String DORIS_NULL = "NULL"; @@ -186,7 +187,7 @@ public void sampleTypeConverter( break; default: throw CommonError.convertToSeaTunnelTypeError( - DorisConfig.IDENTIFIER, dorisColumnType, typeDefine.getName()); + IDENTIFIER, dorisColumnType, typeDefine.getName()); } } @@ -234,7 +235,7 @@ protected void sampleReconvertString( } throw CommonError.convertToConnectorTypeError( - DorisConfig.IDENTIFIER, column.getDataType().getSqlType().name(), column.getName()); + IDENTIFIER, column.getDataType().getSqlType().name(), column.getName()); } protected BasicTypeDefine sampleReconvert( @@ -366,9 +367,7 @@ protected BasicTypeDefine sampleReconvert( break; default: throw CommonError.convertToConnectorTypeError( - DorisConfig.IDENTIFIER, - column.getDataType().getSqlType().name(), - column.getName()); + IDENTIFIER, column.getDataType().getSqlType().name(), column.getName()); } return builder.build(); } @@ -430,7 +429,7 @@ private void reconvertBuildArrayInternal( break; default: throw CommonError.convertToConnectorTypeError( - DorisConfig.IDENTIFIER, elementType.getSqlType().name(), columnName); + IDENTIFIER, elementType.getSqlType().name(), columnName); } } diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV1.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV1.java index fb129249702..9b7e98368fb 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV1.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV1.java @@ -23,11 +23,12 @@ import org.apache.seatunnel.api.table.converter.TypeConverter; import org.apache.seatunnel.api.table.type.DecimalType; import org.apache.seatunnel.api.table.type.LocalTimeType; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; import com.google.auto.service.AutoService; import lombok.extern.slf4j.Slf4j; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.IDENTIFIER; + /** Doris type converter for version 1.2.x */ @Slf4j @AutoService(TypeConverter.class) @@ -42,7 +43,7 @@ public class DorisTypeConverterV1 extends AbstractDorisTypeConverter { @Override public String identifier() { - return DorisConfig.IDENTIFIER; + return IDENTIFIER; } @Override diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV2.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV2.java index 3b5ebde0f47..46ae79251e0 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV2.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConverterV2.java @@ -28,7 +28,6 @@ import org.apache.seatunnel.api.table.type.MapType; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.common.exception.CommonError; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; import com.google.auto.service.AutoService; import lombok.extern.slf4j.Slf4j; @@ -37,6 +36,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.IDENTIFIER; + /** Doris type converter for version 2.x */ @Slf4j @AutoService(TypeConverter.class) @@ -62,7 +63,7 @@ public class DorisTypeConverterV2 extends AbstractDorisTypeConverter { @Override public String identifier() { - return DorisConfig.IDENTIFIER; + return IDENTIFIER; } @Override @@ -166,7 +167,7 @@ private void convertArray( DecimalArrayType decimalArray = new DecimalArrayType(new DecimalType(20, 0)); builder.dataType(decimalArray); } else { - throw CommonError.convertToSeaTunnelTypeError(DorisConfig.IDENTIFIER, columnType, name); + throw CommonError.convertToSeaTunnelTypeError(IDENTIFIER, columnType, name); } } diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/rest/RestService.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/rest/RestService.java index b516157443a..97fd3ca78e9 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/rest/RestService.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/rest/RestService.java @@ -18,12 +18,13 @@ package org.apache.seatunnel.connectors.doris.rest; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; -import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSourceConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSourceOptions; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorErrorCode; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.rest.models.QueryPlan; import org.apache.seatunnel.connectors.doris.rest.models.Tablet; +import org.apache.seatunnel.connectors.doris.source.DorisSourceTable; import org.apache.seatunnel.connectors.doris.util.ErrorMessages; import org.apache.commons.io.IOUtils; @@ -69,11 +70,12 @@ public class RestService implements Serializable { private static final String QUERY_PLAN = "_query_plan"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static String send(DorisConfig dorisConfig, HttpRequestBase request, Logger logger) + private static String send( + DorisSourceConfig dorisSourceConfig, HttpRequestBase request, Logger logger) throws DorisConnectorException { - int connectTimeout = dorisConfig.getRequestConnectTimeoutMs(); - int socketTimeout = dorisConfig.getRequestReadTimeoutMs(); - int retries = dorisConfig.getRequestRetries(); + int connectTimeout = dorisSourceConfig.getRequestConnectTimeoutMs(); + int socketTimeout = dorisSourceConfig.getRequestReadTimeoutMs(); + int retries = dorisSourceConfig.getRequestRetries(); logger.trace( "connect timeout set to '{}'. socket timeout set to '{}'. retries set to '{}'.", connectTimeout, @@ -90,7 +92,7 @@ private static String send(DorisConfig dorisConfig, HttpRequestBase request, Log logger.info( "Send request to Doris FE '{}' with user '{}'.", request.getURI(), - dorisConfig.getUsername()); + dorisSourceConfig.getUsername()); IOException ex = null; int statusCode = -1; @@ -102,15 +104,15 @@ private static String send(DorisConfig dorisConfig, HttpRequestBase request, Log response = getConnectionGet( request.getURI().toString(), - dorisConfig.getUsername(), - dorisConfig.getPassword(), + dorisSourceConfig.getUsername(), + dorisSourceConfig.getPassword(), logger); } else { response = getConnectionPost( request, - dorisConfig.getUsername(), - dorisConfig.getPassword(), + dorisSourceConfig.getUsername(), + dorisSourceConfig.getPassword(), logger); } if (StringUtils.isEmpty(response)) { @@ -251,11 +253,16 @@ public static String randomEndpoint(String feNodes, Logger logger) } @VisibleForTesting - static String getUriStr(DorisConfig dorisConfig, Logger logger) throws DorisConnectorException { - String tableIdentifier = dorisConfig.getDatabase() + "." + dorisConfig.getTable(); + static String getUriStr( + DorisSourceConfig dorisSourceConfig, DorisSourceTable dorisSourceTable, Logger logger) + throws DorisConnectorException { + String tableIdentifier = + dorisSourceTable.getTablePath().getDatabaseName() + + "." + + dorisSourceTable.getTablePath().getTableName(); String[] identifier = parseIdentifier(tableIdentifier, logger); return "http://" - + randomEndpoint(dorisConfig.getFrontends(), logger) + + randomEndpoint(dorisSourceConfig.getFrontends(), logger) + API_PREFIX + "/" + identifier[0] @@ -265,9 +272,13 @@ static String getUriStr(DorisConfig dorisConfig, Logger logger) throws DorisConn } public static List findPartitions( - SeaTunnelRowType rowType, DorisConfig dorisConfig, Logger logger) + DorisSourceConfig dorisSourceConfig, DorisSourceTable dorisSourceTable, Logger logger) throws DorisConnectorException { - String tableIdentifier = dorisConfig.getDatabase() + "." + dorisConfig.getTable(); + String tableIdentifier = + dorisSourceTable.getTablePath().getDatabaseName() + + "." + + dorisSourceTable.getTablePath().getTableName(); + SeaTunnelRowType rowType = dorisSourceTable.getCatalogTable().getSeaTunnelRowType(); String[] tableIdentifiers = parseIdentifier(tableIdentifier, logger); String readFields = "*"; if (rowType.getFieldNames().length != 0) { @@ -281,12 +292,13 @@ public static List findPartitions( + "`.`" + tableIdentifiers[1] + "`"; - if (!StringUtils.isEmpty(dorisConfig.getFilterQuery())) { - sql += " where " + dorisConfig.getFilterQuery(); + if (!StringUtils.isEmpty(dorisSourceTable.getFilterQuery())) { + sql += " where " + dorisSourceTable.getFilterQuery(); } logger.debug("Query SQL Sending to Doris FE is: '{}'.", sql); - HttpPost httpPost = new HttpPost(getUriStr(dorisConfig, logger) + QUERY_PLAN); + HttpPost httpPost = + new HttpPost(getUriStr(dorisSourceConfig, dorisSourceTable, logger) + QUERY_PLAN); String entity = "{\"sql\": \"" + sql + "\"}"; logger.debug("Post body Sending to Doris FE is: '{}'.", entity); StringEntity stringEntity = new StringEntity(entity, StandardCharsets.UTF_8); @@ -294,12 +306,12 @@ public static List findPartitions( stringEntity.setContentType("application/json"); httpPost.setEntity(stringEntity); - String resStr = send(dorisConfig, httpPost, logger); + String resStr = send(dorisSourceConfig, httpPost, logger); logger.debug("Find partition response is '{}'.", resStr); QueryPlan queryPlan = getQueryPlan(resStr, logger); Map> be2Tablets = selectBeForTablet(queryPlan, logger); return tabletsMapToPartition( - dorisConfig, + dorisSourceTable, be2Tablets, queryPlan.getOpaqued_query_plan(), tableIdentifiers[0], @@ -397,18 +409,18 @@ static Map> selectBeForTablet(QueryPlan queryPlan, Logger log } @VisibleForTesting - static int tabletCountLimitForOnePartition(DorisConfig dorisConfig, Logger logger) { - int tabletsSize = DorisOptions.DORIS_TABLET_SIZE_DEFAULT; - if (dorisConfig.getTabletSize() != null) { - tabletsSize = dorisConfig.getTabletSize(); + static int tabletCountLimitForOnePartition(DorisSourceTable dorisSourceTable, Logger logger) { + int tabletsSize = DorisSourceOptions.DORIS_TABLET_SIZE_DEFAULT; + if (dorisSourceTable.getTabletSize() != null) { + tabletsSize = dorisSourceTable.getTabletSize(); } - if (tabletsSize < DorisOptions.DORIS_TABLET_SIZE_MIN) { + if (tabletsSize < DorisSourceOptions.DORIS_TABLET_SIZE_MIN) { logger.warn( "{} is less than {}, set to default value {}.", - DorisOptions.DORIS_TABLET_SIZE, - DorisOptions.DORIS_TABLET_SIZE_MIN, - DorisOptions.DORIS_TABLET_SIZE_MIN); - tabletsSize = DorisOptions.DORIS_TABLET_SIZE_MIN; + DorisSourceOptions.DORIS_TABLET_SIZE, + DorisSourceOptions.DORIS_TABLET_SIZE_MIN, + DorisSourceOptions.DORIS_TABLET_SIZE_MIN); + tabletsSize = DorisSourceOptions.DORIS_TABLET_SIZE_MIN; } logger.debug("Tablet size is set to {}.", tabletsSize); return tabletsSize; @@ -416,14 +428,14 @@ static int tabletCountLimitForOnePartition(DorisConfig dorisConfig, Logger logge @VisibleForTesting static List tabletsMapToPartition( - DorisConfig dorisConfig, + DorisSourceTable dorisSourceTable, Map> be2Tablets, String opaquedQueryPlan, String database, String table, Logger logger) throws DorisConnectorException { - int tabletsSize = tabletCountLimitForOnePartition(dorisConfig, logger); + int tabletsSize = tabletCountLimitForOnePartition(dorisSourceTable, logger); List partitions = new ArrayList<>(); for (Map.Entry> beInfo : be2Tablets.entrySet()) { logger.debug("Generate partition with beInfo: '{}'.", beInfo); diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java index c0a9a2a5a17..deb88a51b11 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java @@ -33,8 +33,8 @@ import org.apache.seatunnel.api.table.factory.CatalogFactory; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.common.constants.PluginType; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; -import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSinkConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSinkOptions; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.sink.committer.DorisCommitInfo; import org.apache.seatunnel.connectors.doris.sink.committer.DorisCommitInfoSerializer; @@ -55,7 +55,7 @@ public class DorisSink SupportSaveMode, SupportMultiTableSink { - private final DorisConfig dorisConfig; + private final DorisSinkConfig dorisSinkConfig; private final ReadonlyConfig config; private final CatalogTable catalogTable; private String jobId; @@ -63,7 +63,7 @@ public class DorisSink public DorisSink(ReadonlyConfig config, CatalogTable catalogTable) { this.config = config; this.catalogTable = catalogTable; - this.dorisConfig = DorisConfig.of(config); + this.dorisSinkConfig = DorisSinkConfig.of(config); } @Override @@ -79,13 +79,13 @@ public void setJobContext(JobContext jobContext) { @Override public DorisSinkWriter createWriter(SinkWriter.Context context) throws IOException { return new DorisSinkWriter( - context, Collections.emptyList(), catalogTable, dorisConfig, jobId); + context, Collections.emptyList(), catalogTable, dorisSinkConfig, jobId); } @Override public SinkWriter restoreWriter( SinkWriter.Context context, List states) throws IOException { - return new DorisSinkWriter(context, states, catalogTable, dorisConfig, jobId); + return new DorisSinkWriter(context, states, catalogTable, dorisSinkConfig, jobId); } @Override @@ -95,7 +95,7 @@ public Optional> getWriterStateSerializer() { @Override public Optional> createCommitter() throws IOException { - return Optional.of(new DorisCommitter(dorisConfig)); + return Optional.of(new DorisCommitter(dorisSinkConfig)); } @Override @@ -127,11 +127,11 @@ public Optional getSaveModeHandler() { Catalog catalog = catalogFactory.createCatalog(catalogFactory.factoryIdentifier(), config); return Optional.of( new DefaultSaveModeHandler( - config.get(DorisOptions.SCHEMA_SAVE_MODE), - config.get(DorisOptions.DATA_SAVE_MODE), + config.get(DorisSinkOptions.SCHEMA_SAVE_MODE), + config.get(DorisSinkOptions.DATA_SAVE_MODE), catalog, catalogTable, - config.get(DorisOptions.CUSTOM_SQL))); + config.get(DorisSinkOptions.CUSTOM_SQL))); } @Override diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSinkFactory.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSinkFactory.java index e1849c39341..9a2ce67be27 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSinkFactory.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSinkFactory.java @@ -26,7 +26,7 @@ import org.apache.seatunnel.api.table.factory.TableSinkFactory; import org.apache.seatunnel.api.table.factory.TableSinkFactoryContext; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSinkOptions; import org.apache.seatunnel.connectors.doris.sink.committer.DorisCommitInfo; import org.apache.seatunnel.connectors.doris.sink.writer.DorisSinkState; import org.apache.seatunnel.connectors.doris.util.UnsupportedTypeConverterUtils; @@ -39,15 +39,14 @@ import java.util.List; import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DATABASE; -import static org.apache.seatunnel.connectors.doris.config.DorisOptions.NEEDS_UNSUPPORTED_TYPE_CASTING; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.IDENTIFIER; import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE; import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE_IDENTIFIER; +import static org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.NEEDS_UNSUPPORTED_TYPE_CASTING; @AutoService(Factory.class) public class DorisSinkFactory implements TableSinkFactory { - public static final String IDENTIFIER = "Doris"; - @Override public String factoryIdentifier() { return IDENTIFIER; @@ -55,12 +54,12 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { - return DorisOptions.SINK_RULE.build(); + return DorisSinkOptions.SINK_RULE.build(); } @Override public List excludeTablePlaceholderReplaceKeys() { - return Arrays.asList(DorisOptions.SAVE_MODE_CREATE_TEMPLATE.key()); + return Arrays.asList(DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key()); } @Override diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/committer/DorisCommitter.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/committer/DorisCommitter.java index 5c6e81ba7e2..b92f2869bc9 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/committer/DorisCommitter.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/committer/DorisCommitter.java @@ -18,7 +18,7 @@ package org.apache.seatunnel.connectors.doris.sink.committer; import org.apache.seatunnel.api.sink.SinkCommitter; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSinkConfig; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorErrorCode; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.sink.HttpPutBuilder; @@ -46,15 +46,15 @@ public class DorisCommitter implements SinkCommitter { private static final String COMMIT_PATTERN = "http://%s/api/%s/_stream_load_2pc"; private static final int HTTP_TEMPORARY_REDIRECT = 200; private final CloseableHttpClient httpClient; - private final DorisConfig dorisConfig; + private final DorisSinkConfig dorisSinkConfig; int maxRetry; - public DorisCommitter(DorisConfig dorisConfig) { - this(dorisConfig, new HttpUtil().getHttpClient()); + public DorisCommitter(DorisSinkConfig dorisSinkConfig) { + this(dorisSinkConfig, new HttpUtil().getHttpClient()); } - public DorisCommitter(DorisConfig dorisConfig, CloseableHttpClient client) { - this.dorisConfig = dorisConfig; + public DorisCommitter(DorisSinkConfig dorisSinkConfig, CloseableHttpClient client) { + this.dorisSinkConfig = dorisSinkConfig; this.httpClient = client; } @@ -80,11 +80,11 @@ private void commitTransaction(DorisCommitInfo committable) int retry = 0; String hostPort = committable.getHostPort(); CloseableHttpResponse response = null; - while (retry++ <= dorisConfig.getMaxRetries()) { + while (retry++ <= dorisSinkConfig.getMaxRetries()) { HttpPutBuilder putBuilder = new HttpPutBuilder(); putBuilder .setUrl(String.format(COMMIT_PATTERN, hostPort, committable.getDb())) - .baseAuth(dorisConfig.getUsername(), dorisConfig.getPassword()) + .baseAuth(dorisSinkConfig.getUsername(), dorisSinkConfig.getPassword()) .addCommonHeader() .addTxnId(committable.getTxbID()) .setEmptyEntity() @@ -93,14 +93,14 @@ private void commitTransaction(DorisCommitInfo committable) response = httpClient.execute(putBuilder.build()); } catch (IOException e) { log.error("commit transaction failed: ", e); - hostPort = dorisConfig.getFrontends(); + hostPort = dorisSinkConfig.getFrontends(); continue; } statusCode = response.getStatusLine().getStatusCode(); reasonPhrase = response.getStatusLine().getReasonPhrase(); if (statusCode != HTTP_TEMPORARY_REDIRECT) { log.warn("commit failed with {}, reason {}", hostPort, reasonPhrase); - hostPort = dorisConfig.getFrontends(); + hostPort = dorisSinkConfig.getFrontends(); } else { break; } @@ -139,7 +139,7 @@ private void abortTransaction(DorisCommitInfo committable) while (retry++ <= maxRetry) { HttpPutBuilder builder = new HttpPutBuilder(); builder.setUrl(String.format(COMMIT_PATTERN, hostPort, committable.getDb())) - .baseAuth(dorisConfig.getUsername(), dorisConfig.getPassword()) + .baseAuth(dorisSinkConfig.getUsername(), dorisSinkConfig.getPassword()) .addCommonHeader() .addTxnId(committable.getTxbID()) .setEmptyEntity() diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisSinkWriter.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisSinkWriter.java index b5aa5274216..f6dfae55346 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisSinkWriter.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisSinkWriter.java @@ -22,7 +22,7 @@ import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSinkConfig; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorErrorCode; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.rest.RestService; @@ -59,7 +59,7 @@ public class DorisSinkWriter new ArrayList<>(Arrays.asList(LoadStatus.SUCCESS, LoadStatus.PUBLISH_TIMEOUT)); private long lastCheckpointId; private DorisStreamLoad dorisStreamLoad; - private final DorisConfig dorisConfig; + private final DorisSinkConfig dorisSinkConfig; private final String labelPrefix; private final LabelGenerator labelGenerator; private final int intervalTime; @@ -72,41 +72,41 @@ public DorisSinkWriter( SinkWriter.Context context, List state, CatalogTable catalogTable, - DorisConfig dorisConfig, + DorisSinkConfig dorisSinkConfig, String jobId) { - this.dorisConfig = dorisConfig; + this.dorisSinkConfig = dorisSinkConfig; this.catalogTable = catalogTable; this.lastCheckpointId = !state.isEmpty() ? state.get(0).getCheckpointId() : 0; log.info("restore checkpointId {}", lastCheckpointId); - log.info("labelPrefix " + dorisConfig.getLabelPrefix()); + log.info("labelPrefix " + dorisSinkConfig.getLabelPrefix()); this.labelPrefix = - dorisConfig.getLabelPrefix() + dorisSinkConfig.getLabelPrefix() + "_" + catalogTable.getTablePath().getFullName().replaceAll("\\.", "_") + "_" + jobId + "_" + context.getIndexOfSubtask(); - this.labelGenerator = new LabelGenerator(labelPrefix, dorisConfig.getEnable2PC()); + this.labelGenerator = new LabelGenerator(labelPrefix, dorisSinkConfig.getEnable2PC()); this.scheduledExecutorService = new ScheduledThreadPoolExecutor( 1, new ThreadFactoryBuilder().setNameFormat("stream-load-check").build()); - this.serializer = createSerializer(dorisConfig, catalogTable.getSeaTunnelRowType()); - this.intervalTime = dorisConfig.getCheckInterval(); + this.serializer = createSerializer(dorisSinkConfig, catalogTable.getSeaTunnelRowType()); + this.intervalTime = dorisSinkConfig.getCheckInterval(); this.initializeLoad(); } private void initializeLoad() { - String backend = RestService.randomEndpoint(dorisConfig.getFrontends(), log); + String backend = RestService.randomEndpoint(dorisSinkConfig.getFrontends(), log); try { this.dorisStreamLoad = new DorisStreamLoad( backend, catalogTable.getTablePath(), - dorisConfig, + dorisSinkConfig, labelGenerator, new HttpUtil().getHttpClient()); - if (dorisConfig.getEnable2PC()) { + if (dorisSinkConfig.getEnable2PC()) { dorisStreamLoad.abortPreCommit(labelPrefix, lastCheckpointId + 1); } } catch (Exception e) { @@ -124,15 +124,15 @@ public void write(SeaTunnelRow element) throws IOException { checkLoadException(); byte[] serialize = serializer.serialize( - dorisConfig.isNeedsUnsupportedTypeCasting() + dorisSinkConfig.isNeedsUnsupportedTypeCasting() ? UnsupportedTypeConverterUtils.convertRow(element) : element); if (Objects.isNull(serialize)) { return; } dorisStreamLoad.writeRecord(serialize); - if (!dorisConfig.getEnable2PC() - && dorisStreamLoad.getRecordCount() >= dorisConfig.getBatchSize()) { + if (!dorisSinkConfig.getEnable2PC() + && dorisStreamLoad.getRecordCount() >= dorisSinkConfig.getBatchSize()) { flush(); startLoad(labelGenerator.generateLabel(lastCheckpointId)); } @@ -141,7 +141,7 @@ public void write(SeaTunnelRow element) throws IOException { @Override public Optional prepareCommit() throws IOException { RespContent respContent = flush(); - if (!dorisConfig.getEnable2PC() || respContent == null) { + if (!dorisSinkConfig.getEnable2PC() || respContent == null) { return Optional.empty(); } long txnId = respContent.getTxnId(); @@ -178,7 +178,7 @@ private void startLoad(String label) { @Override public void abortPrepare() { - if (dorisConfig.getEnable2PC()) { + if (dorisSinkConfig.getEnable2PC()) { try { dorisStreamLoad.abortPreCommit(labelPrefix, lastCheckpointId + 1); } catch (Exception e) { @@ -208,7 +208,7 @@ private void checkLoadException() { @Override public void close() throws IOException { - if (!dorisConfig.getEnable2PC()) { + if (!dorisSinkConfig.getEnable2PC()) { flush(); } if (scheduledExecutorService != null) { @@ -220,14 +220,14 @@ public void close() throws IOException { } private DorisSerializer createSerializer( - DorisConfig dorisConfig, SeaTunnelRowType seaTunnelRowType) { + DorisSinkConfig dorisSinkConfig, SeaTunnelRowType seaTunnelRowType) { return new SeaTunnelRowSerializer( - dorisConfig + dorisSinkConfig .getStreamLoadProps() .getProperty(LoadConstants.FORMAT_KEY) .toLowerCase(), seaTunnelRowType, - dorisConfig.getStreamLoadProps().getProperty(LoadConstants.FIELD_DELIMITER_KEY), - dorisConfig.getEnableDelete()); + dorisSinkConfig.getStreamLoadProps().getProperty(LoadConstants.FIELD_DELIMITER_KEY), + dorisSinkConfig.getEnableDelete()); } } diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java index 8ec59e81ece..1e0ee7b9c2e 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java @@ -22,7 +22,7 @@ import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.common.utils.ExceptionUtils; import org.apache.seatunnel.common.utils.JsonUtils; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSinkConfig; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorErrorCode; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.rest.models.RespContent; @@ -89,20 +89,20 @@ public class DorisStreamLoad implements Serializable { public DorisStreamLoad( String hostPort, TablePath tablePath, - DorisConfig dorisConfig, + DorisSinkConfig dorisSinkConfig, LabelGenerator labelGenerator, CloseableHttpClient httpClient) { this.hostPort = hostPort; this.db = tablePath.getDatabaseName(); this.table = tablePath.getTableName(); - this.user = dorisConfig.getUsername(); - this.passwd = dorisConfig.getPassword(); + this.user = dorisSinkConfig.getUsername(); + this.passwd = dorisSinkConfig.getPassword(); this.labelGenerator = labelGenerator; this.loadUrlStr = String.format(LOAD_URL_PATTERN, hostPort, db, table); this.abortUrlStr = String.format(ABORT_URL_PATTERN, hostPort, db); - this.enable2PC = dorisConfig.getEnable2PC(); - this.streamLoadProp = dorisConfig.getStreamLoadProps(); - this.enableDelete = dorisConfig.getEnableDelete(); + this.enable2PC = dorisSinkConfig.getEnable2PC(); + this.streamLoadProp = dorisSinkConfig.getStreamLoadProps(); + this.enableDelete = dorisSinkConfig.getEnableDelete(); this.httpClient = httpClient; this.executorService = new ThreadPoolExecutor( @@ -113,7 +113,7 @@ public DorisStreamLoad( new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().setNameFormat("stream-load-upload").build()); this.recordStream = - new RecordStream(dorisConfig.getBufferSize(), dorisConfig.getBufferCount()); + new RecordStream(dorisSinkConfig.getBufferSize(), dorisSinkConfig.getBufferCount()); lineDelimiter = streamLoadProp.getProperty(LINE_DELIMITER_KEY, LINE_DELIMITER_DEFAULT).getBytes(); loadBatchFirstRecord = true; diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSource.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSource.java index c04f074021a..8b5f168a2d5 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSource.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSource.java @@ -17,34 +17,36 @@ package org.apache.seatunnel.connectors.doris.source; -import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.source.Boundedness; import org.apache.seatunnel.api.source.SeaTunnelSource; import org.apache.seatunnel.api.source.SourceReader; import org.apache.seatunnel.api.source.SourceSplitEnumerator; import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSourceConfig; import org.apache.seatunnel.connectors.doris.source.reader.DorisSourceReader; import org.apache.seatunnel.connectors.doris.source.split.DorisSourceSplit; import org.apache.seatunnel.connectors.doris.source.split.DorisSourceSplitEnumerator; import lombok.extern.slf4j.Slf4j; -import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Slf4j public class DorisSource implements SeaTunnelSource { private static final long serialVersionUID = 6139826339248788618L; - private final DorisConfig config; - private final CatalogTable catalogTable; + private final DorisSourceConfig config; + private final Map dorisSourceTables; - public DorisSource(ReadonlyConfig config, CatalogTable catalogTable) { - this.config = DorisConfig.of(config); - this.catalogTable = catalogTable; + public DorisSource( + DorisSourceConfig config, Map dorisSourceTables) { + this.config = config; + this.dorisSourceTables = dorisSourceTables; } @Override @@ -59,20 +61,21 @@ public Boundedness getBoundedness() { @Override public List getProducedCatalogTables() { - return Collections.singletonList(catalogTable); + return dorisSourceTables.values().stream() + .map(DorisSourceTable::getCatalogTable) + .collect(Collectors.toList()); } @Override public SourceReader createReader( SourceReader.Context readerContext) { - return new DorisSourceReader(readerContext, config, catalogTable.getSeaTunnelRowType()); + return new DorisSourceReader(readerContext, config, dorisSourceTables); } @Override public SourceSplitEnumerator createEnumerator( SourceSplitEnumerator.Context enumeratorContext) { - return new DorisSourceSplitEnumerator( - enumeratorContext, config, catalogTable.getSeaTunnelRowType()); + return new DorisSourceSplitEnumerator(enumeratorContext, config, dorisSourceTables); } @Override @@ -80,6 +83,6 @@ public SourceSplitEnumerator restoreEnumerat SourceSplitEnumerator.Context enumeratorContext, DorisSourceState checkpointState) { return new DorisSourceSplitEnumerator( - enumeratorContext, config, catalogTable.getSeaTunnelRowType(), checkpointState); + enumeratorContext, config, dorisSourceTables, checkpointState); } } diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceFactory.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceFactory.java index 75cc266edad..506a7c97dc8 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceFactory.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceFactory.java @@ -17,7 +17,6 @@ package org.apache.seatunnel.connectors.doris.source; -import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.source.SeaTunnelSource; import org.apache.seatunnel.api.source.SourceSplit; @@ -29,8 +28,8 @@ import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext; import org.apache.seatunnel.connectors.doris.catalog.DorisCatalog; import org.apache.seatunnel.connectors.doris.catalog.DorisCatalogFactory; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; -import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSourceConfig; +import org.apache.seatunnel.connectors.doris.config.DorisTableConfig; import org.apache.commons.lang3.StringUtils; @@ -39,62 +38,88 @@ import java.io.Serializable; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DATABASE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.DORIS_BATCH_SIZE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.FENODES; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.IDENTIFIER; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.PASSWORD; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.QUERY_PORT; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.TABLE; +import static org.apache.seatunnel.connectors.doris.config.DorisOptions.USERNAME; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_FILTER_QUERY; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.DORIS_READ_FIELD; +import static org.apache.seatunnel.connectors.doris.config.DorisSourceOptions.TABLE_LIST; + @Slf4j @AutoService(Factory.class) public class DorisSourceFactory implements TableSourceFactory { @Override public String factoryIdentifier() { - return DorisConfig.IDENTIFIER; + return IDENTIFIER; } @Override public OptionRule optionRule() { return OptionRule.builder() - .required( - DorisOptions.FENODES, - DorisOptions.USERNAME, - DorisOptions.PASSWORD, - DorisOptions.DATABASE, - DorisOptions.TABLE) - .optional(DorisOptions.DORIS_FILTER_QUERY) - .optional(DorisOptions.DORIS_READ_FIELD) - .optional(DorisOptions.QUERY_PORT) - .optional(DorisOptions.DORIS_BATCH_SIZE) + .required(FENODES, USERNAME, PASSWORD) + .optional(TABLE_LIST) + .optional(DATABASE) + .optional(TABLE) + .optional(DORIS_FILTER_QUERY) + .optional(DORIS_READ_FIELD) + .optional(QUERY_PORT) + .optional(DORIS_BATCH_SIZE) .build(); } @Override public TableSource createSource(TableSourceFactoryContext context) { - ReadonlyConfig options = context.getOptions(); - CatalogTable table; - DorisCatalogFactory dorisCatalogFactory = new DorisCatalogFactory(); - DorisCatalog catalog = (DorisCatalog) dorisCatalogFactory.createCatalog("doris", options); - catalog.open(); - String tableIdentifier = - options.get(DorisOptions.DATABASE) + "." + options.get(DorisOptions.TABLE); - TablePath tablePath = TablePath.of(tableIdentifier); + DorisSourceConfig dorisSourceConfig = DorisSourceConfig.of(context.getOptions()); + List dorisTableConfigList = dorisSourceConfig.getTableConfigList(); + Map dorisSourceTables = new HashMap<>(); + for (DorisTableConfig dorisTableConfig : dorisTableConfigList) { + CatalogTable table; + DorisCatalogFactory dorisCatalogFactory = new DorisCatalogFactory(); + DorisCatalog catalog = + (DorisCatalog) dorisCatalogFactory.createCatalog("doris", context.getOptions()); + catalog.open(); + TablePath tablePath = TablePath.of(dorisTableConfig.getTableIdentifier()); + String readFields = dorisTableConfig.getReadField(); + try { + List readFiledList = null; + if (StringUtils.isNotBlank(readFields)) { + readFiledList = + Arrays.stream(readFields.split(",")) + .map(String::trim) + .collect(Collectors.toList()); + } - try { - String read_fields = options.get(DorisOptions.DORIS_READ_FIELD); - List readFiledList = null; - if (StringUtils.isNotBlank(read_fields)) { - readFiledList = - Arrays.stream(read_fields.split(",")) - .map(String::trim) - .collect(Collectors.toList()); + table = catalog.getTable(tablePath, readFiledList); + } catch (Exception e) { + log.error("create source error"); + throw e; } - - table = catalog.getTable(tablePath, readFiledList); - } catch (Exception e) { - log.error("create source error"); - throw e; + dorisSourceTables.put( + tablePath, + DorisSourceTable.builder() + .catalogTable(table) + .tablePath(tablePath) + .readField(readFields) + .filterQuery(dorisTableConfig.getFilterQuery()) + .batchSize(dorisTableConfig.getBatchSize()) + .tabletSize(dorisTableConfig.getTabletSize()) + .execMemLimit(dorisTableConfig.getExecMemLimit()) + .build()); } - CatalogTable finalTable = table; - return () -> (SeaTunnelSource) new DorisSource(options, finalTable); + return () -> + (SeaTunnelSource) + new DorisSource(dorisSourceConfig, dorisSourceTables); } @Override diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceTable.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceTable.java new file mode 100644 index 00000000000..b09568db9ed --- /dev/null +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/DorisSourceTable.java @@ -0,0 +1,40 @@ +/* + * 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.seatunnel.connectors.doris.source; + +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.TablePath; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serializable; + +@Data +@Builder +public class DorisSourceTable implements Serializable { + private static final long serialVersionUID = 1L; + + private final TablePath tablePath; + private String readField; + private String filterQuery; + private int batchSize; + private Integer tabletSize; + private Long execMemLimit; + private final CatalogTable catalogTable; +} diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisSourceReader.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisSourceReader.java index 66c4e1f269f..ffe1d0e54a0 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisSourceReader.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisSourceReader.java @@ -20,10 +20,13 @@ import org.apache.seatunnel.api.source.Boundedness; import org.apache.seatunnel.api.source.Collector; import org.apache.seatunnel.api.source.SourceReader; +import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.type.SeaTunnelRow; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSourceConfig; +import org.apache.seatunnel.connectors.doris.exception.DorisConnectorErrorCode; +import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.rest.PartitionDefinition; +import org.apache.seatunnel.connectors.doris.source.DorisSourceTable; import org.apache.seatunnel.connectors.doris.source.split.DorisSourceSplit; import lombok.extern.slf4j.Slf4j; @@ -32,27 +35,30 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Queue; @Slf4j public class DorisSourceReader implements SourceReader { private final Context context; - private final DorisConfig dorisConfig; + private final DorisSourceConfig dorisSourceConfig; private final Queue splitsQueue; private volatile boolean noMoreSplits; private DorisValueReader valueReader; - private SeaTunnelRowType seaTunnelRowType; + private final Map tables; public DorisSourceReader( - Context context, DorisConfig dorisConfig, SeaTunnelRowType seaTunnelRowType) { + Context context, + DorisSourceConfig dorisSourceConfig, + Map tables) { this.splitsQueue = new ArrayDeque<>(); this.context = context; - this.dorisConfig = dorisConfig; - this.seaTunnelRowType = seaTunnelRowType; + this.dorisSourceConfig = dorisSourceConfig; + this.tables = tables; } @Override @@ -71,7 +77,16 @@ public void pollNext(Collector output) throws Exception { DorisSourceSplit nextSplit = splitsQueue.poll(); if (nextSplit != null) { PartitionDefinition partition = nextSplit.getPartitionDefinition(); - valueReader = new DorisValueReader(partition, dorisConfig, seaTunnelRowType); + DorisSourceTable dorisSourceTable = + tables.get(TablePath.of(partition.getDatabase(), partition.getTable())); + if (dorisSourceTable == null) { + throw new DorisConnectorException( + DorisConnectorErrorCode.SHOULD_NEVER_HAPPEN, + String.format( + "the table '%s.%s' cannot be found in table_list of job configuration.", + partition.getDatabase(), partition.getTable())); + } + valueReader = new DorisValueReader(partition, dorisSourceConfig, dorisSourceTable); while (valueReader.hasNext()) { SeaTunnelRow record = valueReader.next(); output.collect(record); diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisValueReader.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisValueReader.java index 18d3d004d94..68d2eecfb51 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisValueReader.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/reader/DorisValueReader.java @@ -20,11 +20,12 @@ import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; import org.apache.seatunnel.connectors.doris.backend.BackendClient; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSourceConfig; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorErrorCode; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.rest.PartitionDefinition; import org.apache.seatunnel.connectors.doris.rest.models.Schema; +import org.apache.seatunnel.connectors.doris.source.DorisSourceTable; import org.apache.seatunnel.connectors.doris.source.serialization.Routing; import org.apache.seatunnel.connectors.doris.source.serialization.RowBatch; import org.apache.seatunnel.connectors.doris.util.SchemaUtils; @@ -54,7 +55,8 @@ public class DorisValueReader { protected Lock clientLock = new ReentrantLock(); private PartitionDefinition partition; - private DorisConfig config; + private DorisSourceTable dorisSourceTable; + private DorisSourceConfig config; protected int offset = 0; protected AtomicBoolean eos = new AtomicBoolean(false); @@ -72,12 +74,15 @@ public class DorisValueReader { protected boolean asyncThreadStarted; public DorisValueReader( - PartitionDefinition partition, DorisConfig config, SeaTunnelRowType seaTunnelRowType) { + PartitionDefinition partition, + DorisSourceConfig config, + DorisSourceTable dorisSourceTable) { this.partition = partition; this.config = config; + this.dorisSourceTable = dorisSourceTable; this.client = backendClient(); this.deserializeArrowToRowBatchAsync = config.getDeserializeArrowAsync(); - this.seaTunnelRowType = seaTunnelRowType; + this.seaTunnelRowType = dorisSourceTable.getCatalogTable().getSeaTunnelRowType(); int blockingQueueSize = config.getDeserializeQueueSize(); if (this.deserializeArrowToRowBatchAsync) { this.rowBatchBlockingQueue = new ArrayBlockingQueue<>(blockingQueueSize); @@ -117,9 +122,9 @@ private TScanOpenParams openParams() { params.tablet_ids = Arrays.asList(partition.getTabletIds().toArray(new Long[] {})); params.opaqued_query_plan = partition.getQueryPlan(); // max row number of one read batch - Integer batchSize = config.getBatchSize(); + Integer batchSize = dorisSourceTable.getBatchSize(); Integer queryDorisTimeout = config.getRequestQueryTimeoutS(); - Long execMemLimit = config.getExecMemLimit(); + Long execMemLimit = dorisSourceTable.getExecMemLimit(); params.setBatchSize(batchSize); params.setQueryTimeout(queryDorisTimeout); params.setMemLimit(execMemLimit); @@ -250,7 +255,9 @@ public SeaTunnelRow next() { throw new DorisConnectorException( DorisConnectorErrorCode.SHOULD_NEVER_HAPPEN, "never happen error."); } - return rowBatch.next(); + SeaTunnelRow next = rowBatch.next(); + next.setTableId(dorisSourceTable.getTablePath().toString()); + return next; } public void close() { diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/split/DorisSourceSplitEnumerator.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/split/DorisSourceSplitEnumerator.java index d2d2e61d7e1..1aa10a88b54 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/split/DorisSourceSplitEnumerator.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/source/split/DorisSourceSplitEnumerator.java @@ -18,13 +18,14 @@ package org.apache.seatunnel.connectors.doris.source.split; import org.apache.seatunnel.api.source.SourceSplitEnumerator; -import org.apache.seatunnel.api.table.type.SeaTunnelRowType; +import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated; -import org.apache.seatunnel.connectors.doris.config.DorisConfig; +import org.apache.seatunnel.connectors.doris.config.DorisSourceConfig; import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException; import org.apache.seatunnel.connectors.doris.rest.PartitionDefinition; import org.apache.seatunnel.connectors.doris.rest.RestService; import org.apache.seatunnel.connectors.doris.source.DorisSourceState; +import org.apache.seatunnel.connectors.doris.source.DorisSourceTable; import lombok.extern.slf4j.Slf4j; @@ -41,31 +42,31 @@ public class DorisSourceSplitEnumerator implements SourceSplitEnumerator { - private Context context; - private DorisConfig dorisConfig; + private final Context context; + private final DorisSourceConfig dorisSourceConfig; private volatile boolean shouldEnumerate; private final Map> pendingSplit; - private SeaTunnelRowType seaTunnelRowType; + private final Map dorisSourceTables; private final Object stateLock = new Object(); public DorisSourceSplitEnumerator( Context context, - DorisConfig dorisConfig, - SeaTunnelRowType seaTunnelRowType) { - this(context, dorisConfig, seaTunnelRowType, null); + DorisSourceConfig dorisSourceConfig, + Map dorisSourceTables) { + this(context, dorisSourceConfig, dorisSourceTables, null); } public DorisSourceSplitEnumerator( Context context, - DorisConfig dorisConfig, - SeaTunnelRowType rowType, + DorisSourceConfig dorisSourceConfig, + Map dorisSourceTables, DorisSourceState dorisSourceState) { this.context = context; - this.dorisConfig = dorisConfig; - this.seaTunnelRowType = rowType; + this.dorisSourceConfig = dorisSourceConfig; + this.dorisSourceTables = dorisSourceTables; this.pendingSplit = new ConcurrentHashMap<>(); this.shouldEnumerate = (dorisSourceState == null); if (dorisSourceState != null) { @@ -149,10 +150,12 @@ public void notifyCheckpointComplete(long checkpointId) {} private List getDorisSourceSplit() { List splits = new ArrayList<>(); - List partitions = - RestService.findPartitions(seaTunnelRowType, dorisConfig, log); - for (PartitionDefinition partition : partitions) { - splits.add(new DorisSourceSplit(partition, String.valueOf(partition.hashCode()))); + for (DorisSourceTable dorisSourceTable : dorisSourceTables.values()) { + List partitions = + RestService.findPartitions(dorisSourceConfig, dorisSourceTable, log); + for (PartitionDefinition partition : partitions) { + splits.add(new DorisSourceSplit(partition, String.valueOf(partition.hashCode()))); + } } return splits; } diff --git a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java index e4f8804be02..53b38049f98 100644 --- a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java +++ b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java @@ -24,7 +24,7 @@ import org.apache.seatunnel.api.table.catalog.TableSchema; import org.apache.seatunnel.api.table.converter.BasicTypeDefine; import org.apache.seatunnel.api.table.converter.TypeConverter; -import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSinkOptions; import org.apache.seatunnel.connectors.seatunnel.common.sql.template.SqlTemplate; import org.apache.commons.lang3.StringUtils; @@ -155,7 +155,7 @@ public static String getCreateTableStatement( SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getPlaceHolder(), primaryKey, tablePath.getFullName(), - DorisOptions.SAVE_MODE_CREATE_TEMPLATE.key()); + DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key()); template = template.replaceAll( SaveModePlaceHolder.ROWTYPE_PRIMARY_KEY.getReplacePlaceHolder(), @@ -165,7 +165,7 @@ public static String getCreateTableStatement( SaveModePlaceHolder.ROWTYPE_UNIQUE_KEY.getPlaceHolder(), uniqueKey, tablePath.getFullName(), - DorisOptions.SAVE_MODE_CREATE_TEMPLATE.key()); + DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key()); template = template.replaceAll( SaveModePlaceHolder.ROWTYPE_UNIQUE_KEY.getReplacePlaceHolder(), uniqueKey); @@ -174,7 +174,7 @@ public static String getCreateTableStatement( SaveModePlaceHolder.ROWTYPE_DUPLICATE_KEY.getPlaceHolder(), dupKey, tablePath.getFullName(), - DorisOptions.SAVE_MODE_CREATE_TEMPLATE.key()); + DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key()); template = template.replaceAll( SaveModePlaceHolder.ROWTYPE_DUPLICATE_KEY.getReplacePlaceHolder(), dupKey); diff --git a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java index 02b3c5478f4..cdaa55487c6 100644 --- a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java +++ b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/catalog/DorisCreateTableTest.java @@ -31,7 +31,7 @@ import org.apache.seatunnel.api.table.type.LocalTimeType; import org.apache.seatunnel.common.exception.CommonError; import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException; -import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSinkOptions; import org.apache.seatunnel.connectors.doris.datatype.DorisTypeConverterV1; import org.apache.seatunnel.connectors.doris.util.DorisCatalogUtil; @@ -141,7 +141,7 @@ public void test() { + "\"disable_auto_compaction\" = \"false\"\n" + ")"); - String createTemplate = DorisOptions.SAVE_MODE_CREATE_TEMPLATE.defaultValue(); + String createTemplate = DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE.defaultValue(); CatalogTable catalogTable = CatalogTable.of( TableIdentifier.of("test", "test1", "test2"), @@ -171,7 +171,7 @@ public void test() { SaveModePlaceHolder.getDisplay(primaryKeyHolder), createTemplate, primaryKeyHolder, - DorisOptions.SAVE_MODE_CREATE_TEMPLATE.key()); + DorisSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key()); Assertions.assertEquals( exceptSeaTunnelRuntimeException.getMessage(), actualSeaTunnelRuntimeException.getMessage()); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/pom.xml b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/pom.xml index 7a3008adb3a..f1c9f159d5f 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/pom.xml +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/pom.xml @@ -56,6 +56,12 @@ test-jar test + + org.apache.seatunnel + connector-assert + ${project.version} + test + org.testcontainers diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java index 07fab009083..a2da0bd302b 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisCatalogIT.java @@ -35,6 +35,8 @@ import org.apache.seatunnel.connectors.doris.catalog.DorisCatalog; import org.apache.seatunnel.connectors.doris.catalog.DorisCatalogFactory; import org.apache.seatunnel.connectors.doris.config.DorisOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSinkOptions; +import org.apache.seatunnel.connectors.doris.config.DorisSourceOptions; import org.apache.seatunnel.connectors.doris.sink.DorisSinkFactory; import org.apache.seatunnel.connectors.doris.source.DorisSourceFactory; @@ -224,7 +226,7 @@ void testSaveMode() { put(DorisOptions.TABLE.key(), "test4"); put(DorisOptions.USERNAME.key(), USERNAME); put(DorisOptions.PASSWORD.key(), PASSWORD); - put(DorisOptions.NEEDS_UNSUPPORTED_TYPE_CASTING.key(), true); + put(DorisSinkOptions.NEEDS_UNSUPPORTED_TYPE_CASTING.key(), true); } }); upstreamTable @@ -282,7 +284,8 @@ public void testDorisSourceSelectFieldsNotLossKeysInformation() { put(DorisOptions.USERNAME.key(), USERNAME); put(DorisOptions.PASSWORD.key(), PASSWORD); put( - DorisOptions.DORIS_READ_FIELD.key(), + DorisSourceOptions.DORIS_READ_FIELD + .key(), "k1,k2"); put( DorisOptions.FENODES.key(), diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisMultiReadIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisMultiReadIT.java new file mode 100644 index 00000000000..dd604b57145 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisMultiReadIT.java @@ -0,0 +1,539 @@ +/* + * 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.seatunnel.e2e.connector.doris; + +import org.apache.seatunnel.api.table.type.SeaTunnelRow; +import org.apache.seatunnel.common.utils.ExceptionUtils; +import org.apache.seatunnel.common.utils.JsonUtils; +import org.apache.seatunnel.connectors.doris.util.DorisCatalogUtil; +import org.apache.seatunnel.e2e.common.container.ContainerExtendedFactory; +import org.apache.seatunnel.e2e.common.container.TestContainer; +import org.apache.seatunnel.e2e.common.junit.TestContainerExtension; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestTemplate; +import org.testcontainers.containers.Container; + +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Slf4j +public class DorisMultiReadIT extends AbstractDorisIT { + private static final String UNIQUE_TABLE_0 = "doris_e2e_unique_table_0"; + private static final String UNIQUE_TABLE_1 = "doris_e2e_unique_table_1"; + private static final String SOURCE_DB_0 = "e2e_source_0"; + private static final String SOURCE_DB_1 = "e2e_source_1"; + private static final String sinkDB = "e2e_sink"; + private Connection conn; + + private static final String INIT_UNIQUE_TABLE_DATA_SQL = + "insert into %s.%s" + + " (\n" + + " F_ID,\n" + + " F_INT,\n" + + " F_BIGINT,\n" + + " F_TINYINT,\n" + + " F_SMALLINT,\n" + + " F_DECIMAL,\n" + + " F_LARGEINT,\n" + + " F_BOOLEAN,\n" + + " F_DOUBLE,\n" + + " F_FLOAT,\n" + + " F_CHAR,\n" + + " F_VARCHAR_11,\n" + + " F_STRING,\n" + + " F_DATETIME_P,\n" + + " F_DATETIME,\n" + + " F_DATE,\n" + + " MAP_VARCHAR_BOOLEAN,\n" + + " MAP_CHAR_TINYINT,\n" + + " MAP_STRING_SMALLINT,\n" + + " MAP_INT_INT,\n" + + " MAP_TINYINT_BIGINT,\n" + + " MAP_SMALLINT_LARGEINT,\n" + + " MAP_BIGINT_FLOAT,\n" + + " MAP_LARGEINT_DOUBLE,\n" + + " MAP_STRING_DECIMAL,\n" + + " MAP_DECIMAL_DATE,\n" + + " MAP_DATE_DATETIME,\n" + + " MAP_DATETIME_CHAR,\n" + + " MAP_CHAR_VARCHAR,\n" + + " MAP_VARCHAR_STRING\n" + + ")values(\n" + + "\t?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?\n" + + ")"; + + private final String UNIQUE_TABLE_COLUMN_STRING = + "F_ID, F_INT, F_BIGINT, F_TINYINT, F_SMALLINT, F_DECIMAL, F_LARGEINT, F_BOOLEAN, F_DOUBLE, F_FLOAT, F_CHAR, F_VARCHAR_11, F_STRING, F_DATETIME_P, F_DATETIME, F_DATE, MAP_VARCHAR_BOOLEAN, MAP_CHAR_TINYINT, MAP_STRING_SMALLINT, MAP_INT_INT, MAP_TINYINT_BIGINT, MAP_SMALLINT_LARGEINT, MAP_BIGINT_FLOAT, MAP_LARGEINT_DOUBLE, MAP_STRING_DECIMAL, MAP_DECIMAL_DATE, MAP_DATE_DATETIME, MAP_DATETIME_CHAR, MAP_CHAR_VARCHAR, MAP_VARCHAR_STRING"; + + @TestContainerExtension + protected final ContainerExtendedFactory extendedFactory = + container -> { + Container.ExecResult extraCommands = + container.execInContainer( + "bash", + "-c", + "mkdir -p /tmp/seatunnel/plugins/jdbc/lib && cd /tmp/seatunnel/plugins/jdbc/lib && wget " + + DRIVER_JAR); + Assertions.assertEquals(0, extraCommands.getExitCode(), extraCommands.getStderr()); + }; + + @TestTemplate + public void testDorisMultiRead(TestContainer container) + throws IOException, InterruptedException { + initializeJdbcTable(); + // init table_0 + batchInsertUniqueTableData(SOURCE_DB_0, UNIQUE_TABLE_0); + // init table_1 + batchInsertUniqueTableData(SOURCE_DB_1, UNIQUE_TABLE_1); + // test assert row num + Container.ExecResult execResult = + container.executeJob("/doris_multi_source_to_assert.conf"); + Assertions.assertEquals(0, execResult.getExitCode()); + // execute multi read with 2pc enable + execResult = container.executeJob("/doris_multi_source_to_sink.conf"); + Assertions.assertEquals(0, execResult.getExitCode()); + checkSinkData(SOURCE_DB_0, UNIQUE_TABLE_0, "where F_ID >= 50"); + checkSinkData(SOURCE_DB_1, UNIQUE_TABLE_1, "where F_ID < 40"); + // clean sink database data + clearSinkUniqueTable(); + // execute multi read without 2pc enable + Container.ExecResult execResult2 = + container.executeJob("/doris_multi_source_to_sink_2pc_false.conf"); + Assertions.assertEquals(0, execResult2.getExitCode()); + checkSinkData(SOURCE_DB_0, UNIQUE_TABLE_0, "where F_ID >= 50"); + checkSinkData(SOURCE_DB_1, UNIQUE_TABLE_1, "where F_ID < 40"); + // clean all data + clearSourceUniqueTable(); + clearSinkUniqueTable(); + } + + protected void checkSinkData(String database, String tableName, String sqlCondition) { + try { + assertHasData(database, tableName); + assertHasData(sinkDB, tableName); + + PreparedStatement sourcePre = + conn.prepareStatement(DorisCatalogUtil.TABLE_SCHEMA_QUERY); + sourcePre.setString(1, database); + sourcePre.setString(2, tableName); + ResultSet sourceResultSet = sourcePre.executeQuery(); + + PreparedStatement sinkPre = conn.prepareStatement(DorisCatalogUtil.TABLE_SCHEMA_QUERY); + sinkPre.setString(1, sinkDB); + sinkPre.setString(2, tableName); + ResultSet sinkResultSet = sinkPre.executeQuery(); + + while (sourceResultSet.next()) { + if (sinkResultSet.next()) { + String sourceColumnType = sourceResultSet.getString("COLUMN_TYPE"); + String sinkColumnType = sinkResultSet.getString("COLUMN_TYPE"); + // because seatunnel type can not save the scale and length of the key type and + // value type in the MapType, + // so we use the longest scale on the doris sink to prevent data overflow. + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals("map", sinkColumnType); + continue; + } + + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals("map", sinkColumnType); + continue; + } + + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals( + "map", sinkColumnType); + continue; + } + + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals("map", sinkColumnType); + continue; + } + + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals("map", sinkColumnType); + continue; + } + + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals("map", sinkColumnType); + continue; + } + + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals("map", sinkColumnType); + continue; + } + + if (sourceColumnType.equalsIgnoreCase("map")) { + Assertions.assertEquals("map", sinkColumnType); + continue; + } + + Assertions.assertEquals( + sourceColumnType.toUpperCase(Locale.ROOT), + sinkColumnType.toUpperCase(Locale.ROOT)); + } + } + + String sourceSql = + String.format( + "select * from %s.%s %s order by F_ID ", + database, tableName, sqlCondition); + String sinkSql = String.format("select * from %s.%s order by F_ID", sinkDB, tableName); + checkSourceAndSinkTableDate(sourceSql, sinkSql, UNIQUE_TABLE_COLUMN_STRING); + } catch (Exception e) { + throw new RuntimeException("Doris connection error", e); + } + } + + private void checkSourceAndSinkTableDate(String sourceSql, String sinkSql, String columnsString) + throws Exception { + List columnList = + Arrays.stream(columnsString.split(",")) + .map(x -> x.trim()) + .collect(Collectors.toList()); + Statement sourceStatement = + conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + Statement sinkStatement = + conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + ResultSet sourceResultSet = sourceStatement.executeQuery(sourceSql); + ResultSet sinkResultSet = sinkStatement.executeQuery(sinkSql); + Assertions.assertEquals( + sourceResultSet.getMetaData().getColumnCount(), + sinkResultSet.getMetaData().getColumnCount()); + while (sourceResultSet.next()) { + if (sinkResultSet.next()) { + for (String column : columnList) { + Object source = sourceResultSet.getObject(column); + Object sink = sinkResultSet.getObject(column); + if (!Objects.deepEquals(source, sink)) { + // source read map will create map in doris + // sink, because seatunnel type can not save the scale in MapType + // so we use the longest scale on the doris sink to prevent data overflow. + String sinkStr = sink.toString().replaceAll(".000000", ""); + Assertions.assertEquals(source, sinkStr); + } + } + } + } + // Check the row numbers is equal + sourceResultSet.last(); + sinkResultSet.last(); + Assertions.assertEquals(sourceResultSet.getRow(), sinkResultSet.getRow()); + } + + private Integer tableCount(String db, String table) { + try (Statement statement = conn.createStatement()) { + String sql = String.format("select count(*) from %s.%s", db, table); + ResultSet source = statement.executeQuery(sql); + if (source.next()) { + int rowCount = source.getInt(1); + return rowCount; + } + } catch (Exception e) { + throw new RuntimeException("Failed to check data in Doris server", e); + } + return -1; + } + + private void assertHasData(String db, String table) { + try (Statement statement = conn.createStatement()) { + String sql = String.format("select * from %s.%s limit 1", db, table); + ResultSet source = statement.executeQuery(sql); + Assertions.assertTrue(source.next()); + } catch (Exception e) { + throw new RuntimeException("test doris server image error", e); + } + } + + private void clearSourceUniqueTable() { + try (Statement statement = conn.createStatement()) { + statement.execute(String.format("TRUNCATE TABLE %s.%s", SOURCE_DB_0, UNIQUE_TABLE_0)); + statement.execute(String.format("TRUNCATE TABLE %s.%s", SOURCE_DB_1, UNIQUE_TABLE_1)); + } catch (SQLException e) { + throw new RuntimeException("test doris server image error", e); + } + } + + private void clearSinkUniqueTable() { + try (Statement statement = conn.createStatement()) { + statement.execute(String.format("TRUNCATE TABLE %s.%s", sinkDB, UNIQUE_TABLE_0)); + statement.execute(String.format("TRUNCATE TABLE %s.%s", sinkDB, UNIQUE_TABLE_1)); + } catch (SQLException e) { + throw new RuntimeException("test doris server image error", e); + } + } + + protected void initializeJdbcTable() { + try { + URLClassLoader urlClassLoader = + new URLClassLoader( + new URL[] {new URL(DRIVER_JAR)}, + DorisMultiReadIT.class.getClassLoader()); + Thread.currentThread().setContextClassLoader(urlClassLoader); + Driver driver = (Driver) urlClassLoader.loadClass(DRIVER_CLASS).newInstance(); + Properties props = new Properties(); + props.put("user", USERNAME); + props.put("password", PASSWORD); + conn = driver.connect(String.format(URL, container.getHost()), props); + try (Statement statement = conn.createStatement()) { + // create test databases + statement.execute(createDatabase(SOURCE_DB_0)); + statement.execute(createDatabase(SOURCE_DB_1)); + statement.execute(createDatabase(sinkDB)); + log.info("create source and sink database succeed"); + // create source and sink table + statement.execute(createUniqueTableForTest(SOURCE_DB_0, UNIQUE_TABLE_0)); + statement.execute(createUniqueTableForTest(SOURCE_DB_1, UNIQUE_TABLE_1)); + statement.execute(createUniqueTableForTest(sinkDB, UNIQUE_TABLE_0)); + statement.execute(createUniqueTableForTest(sinkDB, UNIQUE_TABLE_1)); + } catch (SQLException e) { + throw new RuntimeException("Initializing table failed!", e); + } + } catch (Exception e) { + throw new RuntimeException("Initializing jdbc failed!", e); + } + } + + private String createDatabase(String db) { + return String.format("CREATE DATABASE IF NOT EXISTS %s ;", db); + } + + private String createUniqueTableForTest(String db, String table) { + String createTableSql = + "create table if not exists `%s`.`%s`(\n" + + "F_ID bigint null,\n" + + "F_INT int null,\n" + + "F_BIGINT bigint null,\n" + + "F_TINYINT tinyint null,\n" + + "F_SMALLINT smallint null,\n" + + "F_DECIMAL decimal(18,6) null,\n" + + "F_LARGEINT largeint null,\n" + + "F_BOOLEAN boolean null,\n" + + "F_DOUBLE double null,\n" + + "F_FLOAT float null,\n" + + "F_CHAR char null,\n" + + "F_VARCHAR_11 varchar(11) null,\n" + + "F_STRING string null,\n" + + "F_DATETIME_P datetime(6),\n" + + "F_DATETIME datetime,\n" + + "F_DATE date,\n" + + "MAP_VARCHAR_BOOLEAN map,\n" + + "MAP_CHAR_TINYINT MAP,\n" + + "MAP_STRING_SMALLINT MAP,\n" + + "MAP_INT_INT MAP,\n" + + "MAP_TINYINT_BIGINT MAP,\n" + + "MAP_SMALLINT_LARGEINT MAP,\n" + + "MAP_BIGINT_FLOAT MAP,\n" + + "MAP_LARGEINT_DOUBLE MAP,\n" + + "MAP_STRING_DECIMAL MAP,\n" + + "MAP_DECIMAL_DATE MAP,\n" + + "MAP_DATE_DATETIME MAP,\n" + + "MAP_DATETIME_CHAR MAP,\n" + + "MAP_CHAR_VARCHAR MAP,\n" + + "MAP_VARCHAR_STRING MAP\n" + + ")\n" + + "UNIQUE KEY(`F_ID`)\n" + + "DISTRIBUTED BY HASH(`F_ID`) BUCKETS 1\n" + + "properties(\n" + + "\"replication_allocation\" = \"tag.location.default: 1\"" + + ");"; + return String.format(createTableSql, db, table); + } + + protected void batchInsertUniqueTableData(String database, String tableName) { + List rows = genUniqueTableTestData(100L); + try { + conn.setAutoCommit(false); + try (PreparedStatement preparedStatement = + conn.prepareStatement( + String.format(INIT_UNIQUE_TABLE_DATA_SQL, database, tableName))) { + for (SeaTunnelRow row : rows) { + for (int index = 0; index < row.getFields().length; index++) { + preparedStatement.setObject(index + 1, row.getFields()[index]); + } + preparedStatement.addBatch(); + } + preparedStatement.executeBatch(); + } + conn.commit(); + } catch (Exception exception) { + log.error(ExceptionUtils.getMessage(exception)); + String message = ExceptionUtils.getMessage(exception); + getErrorUrl(message); + throw new RuntimeException("get connection error", exception); + } + log.info("insert data succeed"); + } + + private List genUniqueTableTestData(Long nums) { + List datas = new ArrayList<>(); + Map varcharBooleanMap = new HashMap<>(); + varcharBooleanMap.put("aa", true); + + Map charTinyintMap = new HashMap<>(); + charTinyintMap.put("a", (byte) 1); + + Map stringSmallintMap = new HashMap<>(); + stringSmallintMap.put("aa", Short.valueOf("1")); + + Map intIntMap = new HashMap<>(); + intIntMap.put(1, 1); + + Map tinyintBigintMap = new HashMap<>(); + tinyintBigintMap.put((byte) 1, 1L); + + Map smallintLargeintMap = new HashMap<>(); + smallintLargeintMap.put(Short.valueOf("1"), Long.valueOf("11")); + + Map bigintFloatMap = new HashMap<>(); + bigintFloatMap.put(Long.valueOf("1"), Float.valueOf("11.1")); + + Map largeintDoubtMap = new HashMap<>(); + largeintDoubtMap.put(11L, Double.valueOf("11.1")); + + String stringDecimalMap = "{\"11\":\"10.2\"}"; + + String decimalDateMap = "{\"10.02\":\"2020-02-01\"}"; + + String dateDatetimeMap = "{\"2020-02-01\":\"2020-02-01 12:00:00\"}"; + + String datetimeCharMap = "{\"2020-02-01 12:00:00\":\"1\"}"; + + String charVarcharMap = "{\"1\":\"11\"}"; + + String varcharStringMap = "{\"11\":\"11\"}"; + for (int i = 0; i < nums; i++) { + datas.add( + new SeaTunnelRow( + new Object[] { + Long.valueOf(i), + GenerateTestData.genInt(), + GenerateTestData.genBigint(), + GenerateTestData.genTinyint(), + GenerateTestData.genSmallint(), + GenerateTestData.genBigDecimal(18, 6), + GenerateTestData.genBigInteger(126), + GenerateTestData.genBoolean(), + GenerateTestData.genDouble(), + GenerateTestData.genFloat(0, 1000), + GenerateTestData.genString(1), + GenerateTestData.genString(11), + GenerateTestData.genString(12), + GenerateTestData.genDatetimeString(false), + GenerateTestData.genDatetimeString(true), + GenerateTestData.genDateString(), + JsonUtils.toJsonString(varcharBooleanMap), + JsonUtils.toJsonString(charTinyintMap), + JsonUtils.toJsonString(stringSmallintMap), + JsonUtils.toJsonString(intIntMap), + JsonUtils.toJsonString(tinyintBigintMap), + JsonUtils.toJsonString(smallintLargeintMap), + JsonUtils.toJsonString(bigintFloatMap), + JsonUtils.toJsonString(largeintDoubtMap), + stringDecimalMap, + decimalDateMap, + dateDatetimeMap, + datetimeCharMap, + charVarcharMap, + varcharStringMap + })); + } + log.info("generate test data succeed"); + return datas; + } + + @AfterAll + public void close() throws SQLException { + if (conn != null) { + conn.close(); + } + } + + public void getErrorUrl(String message) { + // 使用正则表达式匹配URL + Pattern pattern = Pattern.compile("http://[\\w./?=&-_]+"); + Matcher matcher = pattern.matcher(message); + String urlString = null; + if (matcher.find()) { + log.error("Found URL: " + matcher.group()); + urlString = matcher.group(); + } else { + log.error("No URL found."); + return; + } + + try { + URL url = new URL(urlString); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + // 设置请求方法 + connection.setRequestMethod("GET"); + + // 设置连接超时时间 + connection.setConnectTimeout(5000); + // 设置读取超时时间 + connection.setReadTimeout(5000); + + int responseCode = connection.getResponseCode(); + + if (responseCode == HttpURLConnection.HTTP_OK) { + BufferedReader in = + new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + StringBuilder response = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + } else { + log.error("GET request not worked"); + } + } catch (Exception e) { + log.error(ExceptionUtils.getMessage(e)); + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_assert.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_assert.conf new file mode 100644 index 00000000000..d067671c8ee --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_assert.conf @@ -0,0 +1,80 @@ +# +# 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. +# + +env{ + parallelism = 1 + job.mode = "BATCH" +} + +source{ + Doris { + fenodes = "doris_e2e:8030" + username = root + password = "" + table_list = [ + { + database = "e2e_source_0" + table = "doris_e2e_unique_table_0" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT,F_SMALLINT,F_DECIMAL,F_LARGEINT,F_BOOLEAN,F_DOUBLE,F_FLOAT,F_CHAR,F_VARCHAR_11,F_STRING,F_DATETIME_P,F_DATETIME,F_DATE,MAP_VARCHAR_BOOLEAN, MAP_CHAR_TINYINT, MAP_STRING_SMALLINT, MAP_INT_INT, MAP_TINYINT_BIGINT, MAP_SMALLINT_LARGEINT, MAP_BIGINT_FLOAT, MAP_LARGEINT_DOUBLE, MAP_STRING_DECIMAL, MAP_DECIMAL_DATE, MAP_DATE_DATETIME, MAP_DATETIME_CHAR, MAP_CHAR_VARCHAR, MAP_VARCHAR_STRING" + doris.filter.query = "F_ID >= 50" + }, + { + database = "e2e_source_1" + table = "doris_e2e_unique_table_1" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT,F_SMALLINT,F_DECIMAL,F_LARGEINT,F_BOOLEAN,F_DOUBLE,F_FLOAT,F_CHAR,F_VARCHAR_11,F_STRING,F_DATETIME_P,F_DATETIME,F_DATE,MAP_VARCHAR_BOOLEAN, MAP_CHAR_TINYINT, MAP_STRING_SMALLINT, MAP_INT_INT, MAP_TINYINT_BIGINT, MAP_SMALLINT_LARGEINT, MAP_BIGINT_FLOAT, MAP_LARGEINT_DOUBLE, MAP_STRING_DECIMAL, MAP_DECIMAL_DATE, MAP_DATE_DATETIME, MAP_DATETIME_CHAR, MAP_CHAR_VARCHAR, MAP_VARCHAR_STRING" + doris.filter.query = "F_ID < 40" + } + ] + } +} + +transform {} + +sink { + Assert { + rules = { + tables_configs = [ + { + table_path = "e2e_source_0.doris_e2e_unique_table_0" + row_rules = [ + { + rule_type = MAX_ROW + rule_value = 50 + }, + { + rule_type = MIN_ROW + rule_value = 50 + } + ] + }, + { + table_path = "e2e_source_1.doris_e2e_unique_table_1" + row_rules = [ + { + rule_type = MAX_ROW + rule_value = 40 + }, + { + rule_type = MIN_ROW + rule_value = 40 + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink.conf new file mode 100644 index 00000000000..df36da514ad --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink.conf @@ -0,0 +1,62 @@ +# +# 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. +# + +env{ + parallelism = 1 + job.mode = "BATCH" +} + +source{ + Doris { + fenodes = "doris_e2e:8030" + username = root + password = "" + table_list = [ + { + database = "e2e_source_0" + table = "doris_e2e_unique_table_0" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT,F_SMALLINT,F_DECIMAL,F_LARGEINT,F_BOOLEAN,F_DOUBLE,F_FLOAT,F_CHAR,F_VARCHAR_11,F_STRING,F_DATETIME_P,F_DATETIME,F_DATE,MAP_VARCHAR_BOOLEAN, MAP_CHAR_TINYINT, MAP_STRING_SMALLINT, MAP_INT_INT, MAP_TINYINT_BIGINT, MAP_SMALLINT_LARGEINT, MAP_BIGINT_FLOAT, MAP_LARGEINT_DOUBLE, MAP_STRING_DECIMAL, MAP_DECIMAL_DATE, MAP_DATE_DATETIME, MAP_DATETIME_CHAR, MAP_CHAR_VARCHAR, MAP_VARCHAR_STRING" + doris.filter.query = "F_ID >= 50" + }, + { + database = "e2e_source_1" + table = "doris_e2e_unique_table_1" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT,F_SMALLINT,F_DECIMAL,F_LARGEINT,F_BOOLEAN,F_DOUBLE,F_FLOAT,F_CHAR,F_VARCHAR_11,F_STRING,F_DATETIME_P,F_DATETIME,F_DATE,MAP_VARCHAR_BOOLEAN, MAP_CHAR_TINYINT, MAP_STRING_SMALLINT, MAP_INT_INT, MAP_TINYINT_BIGINT, MAP_SMALLINT_LARGEINT, MAP_BIGINT_FLOAT, MAP_LARGEINT_DOUBLE, MAP_STRING_DECIMAL, MAP_DECIMAL_DATE, MAP_DATE_DATETIME, MAP_DATETIME_CHAR, MAP_CHAR_VARCHAR, MAP_VARCHAR_STRING" + doris.filter.query = "F_ID < 40" + } + ] + } +} + +transform {} + +sink{ + Doris { + fenodes = "doris_e2e:8030" + schema_save_mode = "RECREATE_SCHEMA" + username = root + password = "" + database = "e2e_sink" + table = "${table_name}" + sink.enable-2pc = "true" + sink.label-prefix = "test_json" + doris.config = { + format="json" + read_json_by_line="true" + } + } +} \ No newline at end of file diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink_2pc_false.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink_2pc_false.conf new file mode 100644 index 00000000000..d17a90b0181 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/resources/doris_multi_source_to_sink_2pc_false.conf @@ -0,0 +1,62 @@ +# +# 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. +# + +env{ + parallelism = 1 + job.mode = "BATCH" +} + +source{ + Doris { + fenodes = "doris_e2e:8030" + username = root + password = "" + table_list = [ + { + database = "e2e_source_0" + table = "doris_e2e_unique_table_0" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT,F_SMALLINT,F_DECIMAL,F_LARGEINT,F_BOOLEAN,F_DOUBLE,F_FLOAT,F_CHAR,F_VARCHAR_11,F_STRING,F_DATETIME_P,F_DATETIME,F_DATE,MAP_VARCHAR_BOOLEAN, MAP_CHAR_TINYINT, MAP_STRING_SMALLINT, MAP_INT_INT, MAP_TINYINT_BIGINT, MAP_SMALLINT_LARGEINT, MAP_BIGINT_FLOAT, MAP_LARGEINT_DOUBLE, MAP_STRING_DECIMAL, MAP_DECIMAL_DATE, MAP_DATE_DATETIME, MAP_DATETIME_CHAR, MAP_CHAR_VARCHAR, MAP_VARCHAR_STRING" + doris.filter.query = "F_ID >= 50" + }, + { + database = "e2e_source_1" + table = "doris_e2e_unique_table_1" + doris.read.field = "F_ID,F_INT,F_BIGINT,F_TINYINT,F_SMALLINT,F_DECIMAL,F_LARGEINT,F_BOOLEAN,F_DOUBLE,F_FLOAT,F_CHAR,F_VARCHAR_11,F_STRING,F_DATETIME_P,F_DATETIME,F_DATE,MAP_VARCHAR_BOOLEAN, MAP_CHAR_TINYINT, MAP_STRING_SMALLINT, MAP_INT_INT, MAP_TINYINT_BIGINT, MAP_SMALLINT_LARGEINT, MAP_BIGINT_FLOAT, MAP_LARGEINT_DOUBLE, MAP_STRING_DECIMAL, MAP_DECIMAL_DATE, MAP_DATE_DATETIME, MAP_DATETIME_CHAR, MAP_CHAR_VARCHAR, MAP_VARCHAR_STRING" + doris.filter.query = "F_ID < 40" + } + ] + } +} + +transform {} + +sink{ + Doris { + fenodes = "doris_e2e:8030" + schema_save_mode = "RECREATE_SCHEMA" + username = root + password = "" + database = "e2e_sink" + table = "${table_name}" + sink.enable-2pc = "false" + sink.label-prefix = "test_json" + doris.config = { + format="json" + read_json_by_line="true" + } + } +} \ No newline at end of file From 93d38d5f46aa4da71a203a9a9b2ae5d4e6cdec90 Mon Sep 17 00:00:00 2001 From: Jast Date: Sat, 26 Oct 2024 02:46:27 +0800 Subject: [PATCH 36/72] [Improve][Core] Add protobuf transform test case (#7914) --- .../e2e/connector/kafka/KafkaIT.java | 108 +++++++++++++---- .../kafka_protobuf_transform_to_assert.conf | 109 ++++++++++++++++++ 2 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/protobuf/kafka_protobuf_transform_to_assert.conf diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java index 4a57cbdbd35..986e5f9f2e5 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java @@ -37,6 +37,7 @@ import org.apache.seatunnel.api.table.type.SeaTunnelDataType; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; +import org.apache.seatunnel.common.utils.JsonUtils; import org.apache.seatunnel.connectors.seatunnel.kafka.config.MessageFormat; import org.apache.seatunnel.connectors.seatunnel.kafka.serialize.DefaultSeaTunnelRowSerializer; import org.apache.seatunnel.e2e.common.TestResource; @@ -64,6 +65,7 @@ import org.apache.kafka.common.serialization.ByteArraySerializer; import org.apache.kafka.common.serialization.StringDeserializer; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -693,30 +695,11 @@ public void testKafkaProtobufToAssert(TestContainer container) ProtobufDeserializationSchema deserializationSchema = new ProtobufDeserializationSchema(catalogTable); - // Create serializer DefaultSeaTunnelRowSerializer serializer = - DefaultSeaTunnelRowSerializer.create( - "test_protobuf_topic_fake_source", - seaTunnelRowType, - MessageFormat.PROTOBUF, - DEFAULT_FIELD_DELIMITER, - readonlyConfig); - - // Produce records to Kafka - IntStream.range(0, 20) - .forEach( - i -> { - try { - SeaTunnelRow originalRow = buildSeaTunnelRow(); - ProducerRecord producerRecord = - serializer.serializeRow(originalRow); - producer.send(producerRecord).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Error sending Kafka message", e); - } - }); + getDefaultSeaTunnelRowSerializer( + "test_protobuf_topic_fake_source", seaTunnelRowType, readonlyConfig); - producer.flush(); + sendData(serializer); // Execute the job and validate Container.ExecResult execResult = container.executeJob(confFile); @@ -769,6 +752,87 @@ public void testKafkaProtobufToAssert(TestContainer container) }); } + private @NotNull DefaultSeaTunnelRowSerializer getDefaultSeaTunnelRowSerializer( + String topic, SeaTunnelRowType seaTunnelRowType, ReadonlyConfig readonlyConfig) { + // Create serializer + DefaultSeaTunnelRowSerializer serializer = + DefaultSeaTunnelRowSerializer.create( + topic, + seaTunnelRowType, + MessageFormat.PROTOBUF, + DEFAULT_FIELD_DELIMITER, + readonlyConfig); + return serializer; + } + + private void sendData(DefaultSeaTunnelRowSerializer serializer) { + // Produce records to Kafka + IntStream.range(0, 20) + .forEach( + i -> { + try { + SeaTunnelRow originalRow = buildSeaTunnelRow(); + ProducerRecord producerRecord = + serializer.serializeRow(originalRow); + producer.send(producerRecord).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Error sending Kafka message", e); + } + }); + + producer.flush(); + } + + @TestTemplate + public void testKafkaProtobufForTransformToAssert(TestContainer container) + throws IOException, InterruptedException, URISyntaxException { + + String confFile = "/protobuf/kafka_protobuf_transform_to_assert.conf"; + String path = getTestConfigFile(confFile); + Config config = ConfigFactory.parseFile(new File(path)); + Config sinkConfig = config.getConfigList("source").get(0); + ReadonlyConfig readonlyConfig = ReadonlyConfig.fromConfig(sinkConfig); + SeaTunnelRowType seaTunnelRowType = buildSeaTunnelRowType(); + + // Create serializer + DefaultSeaTunnelRowSerializer serializer = + getDefaultSeaTunnelRowSerializer( + "test_protobuf_topic_transform_fake_source", + seaTunnelRowType, + readonlyConfig); + + // Produce records to Kafka + sendData(serializer); + + // Execute the job and validate + Container.ExecResult execResult = container.executeJob(confFile); + Assertions.assertEquals(0, execResult.getExitCode(), execResult.getStderr()); + + try (KafkaConsumer consumer = + new KafkaConsumer<>(kafkaByteConsumerConfig())) { + consumer.subscribe(Arrays.asList("verify_protobuf_transform")); + Map offsets = + consumer.endOffsets( + Arrays.asList(new TopicPartition("verify_protobuf_transform", 0))); + Long endOffset = offsets.entrySet().iterator().next().getValue(); + Long lastProcessedOffset = -1L; + + do { + ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); + for (ConsumerRecord record : records) { + if (lastProcessedOffset < record.offset()) { + String data = new String(record.value(), "UTF-8"); + ObjectNode jsonNodes = JsonUtils.parseObject(data); + Assertions.assertEquals(jsonNodes.size(), 2); + Assertions.assertEquals(jsonNodes.get("city").asText(), "city_value"); + Assertions.assertEquals(jsonNodes.get("c_string").asText(), "test data"); + } + lastProcessedOffset = record.offset(); + } + } while (lastProcessedOffset < endOffset - 1); + } + } + public static String getTestConfigFile(String configFile) throws FileNotFoundException, URISyntaxException { URL resource = KafkaIT.class.getResource(configFile); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/protobuf/kafka_protobuf_transform_to_assert.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/protobuf/kafka_protobuf_transform_to_assert.conf new file mode 100644 index 00000000000..1a48db8c5fb --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/protobuf/kafka_protobuf_transform_to_assert.conf @@ -0,0 +1,109 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" + spark.app.name = "SeaTunnel" + spark.executor.instances = 1 + spark.executor.cores = 1 + spark.executor.memory = "1g" + spark.master = local +} + +source { + Kafka { + topic = "test_protobuf_topic_transform_fake_source" + format = protobuf + protobuf_message_name = Person + protobuf_schema = """ + syntax = "proto3"; + + package org.apache.seatunnel.format.protobuf; + + option java_outer_classname = "ProtobufE2E"; + + message Person { + int32 c_int32 = 1; + int64 c_int64 = 2; + float c_float = 3; + double c_double = 4; + bool c_bool = 5; + string c_string = 6; + bytes c_bytes = 7; + + message Address { + string street = 1; + string city = 2; + string state = 3; + string zip = 4; + } + + Address address = 8; + + map attributes = 9; + + repeated string phone_numbers = 10; + } + """ + schema = { + fields { + c_int32 = int + c_int64 = long + c_float = float + c_double = double + c_bool = boolean + c_string = string + c_bytes = bytes + + Address { + city = string + state = string + street = string + } + attributes = "map" + phone_numbers = "array" + } + } + bootstrap.servers = "kafkaCluster:9092" + start_mode = "earliest" + result_table_name = "kafka_table" + } +} + +transform { + Sql { + source_table_name = "kafka_table" + result_table_name = "kafka_table_transform" + query = "select Address.city,c_string from kafka_table" + } +} + +sink { + kafka { + topic = "verify_protobuf_transform" + source_table_name = "kafka_table_transform" + bootstrap.servers = "kafkaCluster:9092" + kafka.request.timeout.ms = 60000 + kafka.config = { + acks = "all" + request.timeout.ms = 60000 + buffer.memory = 33554432 + } + + } +} From 4ff3338e3f4fb92f2724ce86c3893134cf8f480b Mon Sep 17 00:00:00 2001 From: Jast Date: Sat, 26 Oct 2024 02:47:16 +0800 Subject: [PATCH 37/72] [Improve][Connector-V2][FTP] delete duplicated code (#7915) --- .../org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java index bc246e9007b..7b73c3ee706 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java @@ -94,7 +94,6 @@ public void startUp() throws Exception { int passiveEndPort = 30004; ftpContainer = new GenericContainer<>(FTP_IMAGE) - .withExposedPorts(FTP_PORT) .withNetwork(NETWORK) .withExposedPorts(FTP_PORT) .withExposedPorts( From ba7fa23008c1e3be5cd5991413ac95bc666c6829 Mon Sep 17 00:00:00 2001 From: QiaoJ-Chen Date: Sat, 26 Oct 2024 07:46:35 +0800 Subject: [PATCH 38/72] [Feature][Engine UI] partial pages of the engine (#7602) --- .github/workflows/backend.yml | 31 + .gitignore | 5 + docs/en/seatunnel-engine/web-ui.md | 48 + docs/images/ui/detail.png | Bin 0 -> 144877 bytes docs/images/ui/finished.png | Bin 0 -> 56303 bytes docs/images/ui/master.png | Bin 0 -> 74979 bytes docs/images/ui/overview.png | Bin 0 -> 195654 bytes docs/images/ui/running.png | Bin 0 -> 40669 bytes docs/images/ui/workers.png | Bin 0 -> 75261 bytes docs/sidebars.js | 3 +- docs/zh/seatunnel-engine/web-ui.md | 47 + seatunnel-engine/pom.xml | 1 + .../seatunnel-engine-server/pom.xml | 5 + .../seatunnel/engine/server/JettyService.java | 11 +- .../seatunnel-engine-ui/.env.development | 22 + .../seatunnel-engine-ui/.env.production | 19 + .../seatunnel-engine-ui/.eslintrc.cjs | 46 + .../seatunnel-engine-ui/.gitignore | 30 + .../seatunnel-engine-ui/.prettierrc.json | 8 + .../seatunnel-engine-ui/README.md | 60 + .../seatunnel-engine-ui/cypress.config.ts | 25 + .../cypress/e2e/example.cy.ts | 25 + .../cypress/e2e/tsconfig.json | 8 + .../cypress/fixtures/example.json | 5 + .../cypress/support/commands.ts | 56 + .../cypress/support/e2e.ts | 37 + seatunnel-engine/seatunnel-engine-ui/env.d.ts | 17 + .../seatunnel-engine-ui/index.html | 33 + .../seatunnel-engine-ui/package-lock.json | 10313 ++++++++++++++++ .../seatunnel-engine-ui/package.json | 64 + seatunnel-engine/seatunnel-engine-ui/pom.xml | 159 + .../seatunnel-engine-ui/postcss.config.js | 23 + .../seatunnel-engine-ui/public/favicon.ico | Bin 0 -> 211862 bytes .../seatunnel-engine-ui/src/App.tsx | 67 + .../seatunnel-engine-ui/src/assets/logo.png | Bin 0 -> 211862 bytes .../seatunnel-engine-ui/src/assets/main.scss | 22 + .../seatunnel-engine-ui/src/assets/style.scss | 20 + .../src/assets/tailwind.scss | 21 + .../src/components/configuration/index.tsx | 45 + .../directed-acyclic-graph/index.scss | 72 + .../directed-acyclic-graph/index.tsx | 363 + .../src/components/job-log/index.tsx | 45 + .../src/layouts/main/header/index.tsx | 39 + .../src/layouts/main/header/info/index.tsx | 41 + .../src/layouts/main/header/logo/index.tsx | 33 + .../src/layouts/main/index.tsx | 76 + .../layouts/main/sidebar/index.module.scss | 84 + .../src/layouts/main/sidebar/index.tsx | 140 + .../src/locales/en_US/common.ts | 23 + .../src/locales/en_US/detail.ts | 22 + .../src/locales/en_US/index.ts | 30 + .../src/locales/en_US/jobs.ts | 21 + .../src/locales/en_US/managers.ts | 20 + .../src/locales/en_US/menu.ts | 26 + .../seatunnel-engine-ui/src/locales/index.ts | 32 + .../src/locales/zh_CN/common.ts | 23 + .../src/locales/zh_CN/detail.ts | 22 + .../src/locales/zh_CN/index.ts | 30 + .../src/locales/zh_CN/jobs.ts | 21 + .../src/locales/zh_CN/managers.ts | 20 + .../src/locales/zh_CN/menu.ts | 23 + .../seatunnel-engine-ui/src/main.ts | 31 + .../seatunnel-engine-ui/src/router/index.ts | 32 + .../seatunnel-engine-ui/src/router/routes.ts | 61 + .../src/service/job-log/index.ts | 27 + .../src/service/job-log/types.ts | 22 + .../src/service/job/index.ts | 31 + .../src/service/job/types.ts | 80 + .../src/service/manager/index.ts | 24 + .../src/service/manager/types.ts | 67 + .../src/service/overview/index.ts | 24 + .../src/service/overview/types.ts | 28 + .../src/service/service.ts | 69 + .../seatunnel-engine-ui/src/service/types.ts | 32 + .../seatunnel-engine-ui/src/store/counter.ts | 29 + .../src/store/setting/index.ts | 65 + .../src/store/setting/types.ts | 27 + .../src/tests/jobs.spec.ts | 74 + .../src/tests/managers.spec.ts | 66 + .../src/tests/overview.spec.ts | 62 + .../src/tests/remain-time.spec.ts | 39 + .../src/tests/setting.spec.ts | 35 + .../src/utils/getTypeFromStatus.ts | 43 + .../seatunnel-engine-ui/src/utils/log.ts | 80 + .../seatunnel-engine-ui/src/utils/time.ts | 32 + .../src/views/jobs/detail.scss | 25 + .../src/views/jobs/detail.tsx | 267 + .../src/views/jobs/finished-jobs.tsx | 104 + .../src/views/jobs/index.tsx | 35 + .../src/views/jobs/running-jobs.tsx | 105 + .../src/views/managers/index.tsx | 96 + .../src/views/overview/baseInfo.tsx | 61 + .../src/views/overview/index.tsx | 36 + .../seatunnel-engine-ui/tailwind.config.js | 28 + .../seatunnel-engine-ui/tsconfig.app.json | 37 + .../seatunnel-engine-ui/tsconfig.json | 33 + .../seatunnel-engine-ui/tsconfig.node.json | 36 + .../seatunnel-engine-ui/tsconfig.vitest.json | 29 + .../seatunnel-engine-ui/vite.config.ts | 45 + .../seatunnel-engine-ui/vitest.config.ts | 31 + 100 files changed, 14528 insertions(+), 2 deletions(-) create mode 100644 docs/en/seatunnel-engine/web-ui.md create mode 100644 docs/images/ui/detail.png create mode 100644 docs/images/ui/finished.png create mode 100644 docs/images/ui/master.png create mode 100644 docs/images/ui/overview.png create mode 100644 docs/images/ui/running.png create mode 100644 docs/images/ui/workers.png create mode 100644 docs/zh/seatunnel-engine/web-ui.md create mode 100644 seatunnel-engine/seatunnel-engine-ui/.env.development create mode 100644 seatunnel-engine/seatunnel-engine-ui/.env.production create mode 100644 seatunnel-engine/seatunnel-engine-ui/.eslintrc.cjs create mode 100644 seatunnel-engine/seatunnel-engine-ui/.gitignore create mode 100644 seatunnel-engine/seatunnel-engine-ui/.prettierrc.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/README.md create mode 100644 seatunnel-engine/seatunnel-engine-ui/cypress.config.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/cypress/e2e/example.cy.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/cypress/e2e/tsconfig.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/cypress/fixtures/example.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/cypress/support/commands.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/cypress/support/e2e.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/env.d.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/index.html create mode 100644 seatunnel-engine/seatunnel-engine-ui/package-lock.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/package.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/pom.xml create mode 100644 seatunnel-engine/seatunnel-engine-ui/postcss.config.js create mode 100644 seatunnel-engine/seatunnel-engine-ui/public/favicon.ico create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/App.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/assets/logo.png create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/assets/main.scss create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/assets/style.scss create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/assets/tailwind.scss create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/components/configuration/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.scss create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/components/job-log/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/layouts/main/header/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/layouts/main/header/info/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/layouts/main/header/logo/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/layouts/main/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/layouts/main/sidebar/index.module.scss create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/layouts/main/sidebar/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/en_US/common.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/en_US/detail.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/en_US/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/en_US/jobs.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/en_US/managers.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/en_US/menu.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/zh_CN/common.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/zh_CN/detail.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/zh_CN/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/zh_CN/jobs.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/zh_CN/managers.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/locales/zh_CN/menu.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/main.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/router/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/router/routes.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/job-log/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/job-log/types.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/job/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/job/types.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/manager/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/manager/types.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/overview/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/overview/types.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/service.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/service/types.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/store/counter.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/store/setting/index.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/store/setting/types.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/tests/jobs.spec.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/tests/managers.spec.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/tests/overview.spec.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/tests/remain-time.spec.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/tests/setting.spec.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/utils/getTypeFromStatus.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/utils/log.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/utils/time.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.scss create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/jobs/finished-jobs.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/jobs/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/jobs/running-jobs.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/managers/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/overview/baseInfo.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/src/views/overview/index.tsx create mode 100644 seatunnel-engine/seatunnel-engine-ui/tailwind.config.js create mode 100644 seatunnel-engine/seatunnel-engine-ui/tsconfig.app.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/tsconfig.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/tsconfig.node.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/tsconfig.vitest.json create mode 100644 seatunnel-engine/seatunnel-engine-ui/vite.config.ts create mode 100644 seatunnel-engine/seatunnel-engine-ui/vitest.config.ts diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 34645086d42..2d3e05b4080 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -306,6 +306,37 @@ jobs: npm set strict-ssl false npm install npm run build + + seatunnel-ui: + if: needs.changes.outputs.api == 'true' + needs: [ changes, sanity-check ] + name: Build SeaTunnel UI + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout PR + uses: actions/checkout@v3 + + - uses: actions/setup-node@v2 + with: + node-version: 20.x + + - name: Install Dependencies and Check Code Style + run: | + cd seatunnel-engine/seatunnel-engine-ui/ + npm install + npm run lint + + - name: Run unit tests + run: | + cd seatunnel-engine/seatunnel-engine-ui/ + npm run test:unit + + - name: Build SeaTunnel UI + run: | + cd seatunnel-engine/seatunnel-engine-ui/ + npm run build + unit-test: needs: [ changes, sanity-check ] if: needs.changes.outputs.api == 'true' || (needs.changes.outputs.api == 'false' && needs.changes.outputs.ut-modules != '') diff --git a/.gitignore b/.gitignore index c8732a4acfd..204a966b8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,8 @@ seatunnel-examples /lib/* version.properties +node/ + +dist/ + +seatunnel-engine/seatunnel-engine-server/**/ui/* \ No newline at end of file diff --git a/docs/en/seatunnel-engine/web-ui.md b/docs/en/seatunnel-engine/web-ui.md new file mode 100644 index 00000000000..934e6abe7cb --- /dev/null +++ b/docs/en/seatunnel-engine/web-ui.md @@ -0,0 +1,48 @@ +# Apache SeaTunnel Web UI Documentation + +## Access + +Before accessing the web ui we need to enable the http rest api. first you need to configure it in the `seatunnel.yaml` configuration file + +``` +seatunnel: + engine: + http: + enable-http: true + port: 8080 + +``` + +Then visit `http://ip:8080/#/overview` + +## Overview + +The Web UI of Apache SeaTunnel offers a user-friendly interface for monitoring and managing SeaTunnel jobs. Through the Web UI, users can view real-time information on currently running jobs, finished jobs, and the status of worker and master nodes within the cluster. The main functional modules include Jobs, Workers, and Master, each providing detailed status information and operational options to help users efficiently manage and optimize their data processing workflows. +![overview.png](../../images/ui/overview.png) + +## Jobs + +### Running Jobs + +The "Running Jobs" section lists all SeaTunnel jobs that are currently in execution. Users can view basic information for each job, including Job ID, submission time, status, execution time, and more. By clicking on a specific job, users can access detailed information such as task distribution, resource utilization, and log outputs, allowing for real-time monitoring of job progress and timely handling of potential issues. +![running.png](../../images/ui/running.png) +![detail.png](../../images/ui/detail.png) + +### Finished Jobs + +The "Finished Jobs" section displays all SeaTunnel jobs that have either successfully completed or failed. This section provides execution results, completion times, durations, and failure reasons (if any) for each job. Users can review past job records through this module to analyze job performance, troubleshoot issues, or rerun specific jobs as needed. +![finished.png](../../images/ui/finished.png) + +## Workers + +### Workers Information + +The "Workers" section displays detailed information about all worker nodes in the cluster, including each worker's address, running status, CPU and memory usage, number of tasks being executed, and more. Through this module, users can monitor the health of each worker node, promptly identify and address resource bottlenecks or node failures, ensuring the stable operation of the SeaTunnel cluster. +![workers.png](../../images/ui/workers.png) + +## Master + +### Master Information + +The "Master" section provides the status and configuration information of the master node in the SeaTunnel cluster. Users can view the master's address, running status, job scheduling responsibilities, and overall resource allocation within the cluster. This module helps users gain a comprehensive understanding of the cluster's core management components, facilitating cluster configuration optimization and troubleshooting. +![master.png](../../images/ui/master.png) diff --git a/docs/images/ui/detail.png b/docs/images/ui/detail.png new file mode 100644 index 0000000000000000000000000000000000000000..a376b6e4880f71aac01c212548c8db241f7207ce GIT binary patch literal 144877 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 literal 0 HcmV?d00001 diff --git a/docs/images/ui/finished.png b/docs/images/ui/finished.png new file mode 100644 index 0000000000000000000000000000000000000000..fa800bd6029fa1f4f2bd23d2911a05c4e2ac3bdc GIT binary patch literal 56303 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 literal 0 HcmV?d00001 diff --git a/docs/images/ui/overview.png b/docs/images/ui/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..671235324994882274c54ea6d1a8759bc2c2073b GIT binary patch literal 195654 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 M0e$wFOqEJaki0EB+jqm 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 literal 0 HcmV?d00001 diff --git a/docs/images/ui/running.png b/docs/images/ui/running.png new file mode 100644 index 0000000000000000000000000000000000000000..889edb303b1c04baa845fb3220c7a42e896ebdb2 GIT binary patch literal 40669 zcmeFZ1yo$!vLM_MA|#OD!6LZ3LqdW#-Do#%!D%$O1qcZc+`Ahm5Ud+-90Gi}G!mq7 z32wpNf9|_?-v8cxGw+}I-r3soMM0uBu(NckQ|!yZ!}uswfAP1Khd= z0NlFy0j?(j(l>Yiwc~F{{%aU+ps)V|5Iq14K1jcFiw{e%7wab{qD zIOmU8{ZR^kH0Hk>e*P$hKT6?`Quw13{wRe%2KfKjeRv6Y_4Z%gvHPdy8D#z^X?qP&N-w^AQGDjN3b+RRE5I!w*XNeZ$)My*N&O-da?69O^o>hD z(;ElM{opF<_{P8To4*D$Tm$4!;y?XsEq^ZlyA*g5{?T5)?KwlE1lJf>uE=PsCe@-& zHxxmZ8KG=VEAy2b>o~sZI*v1g5>u1CjUC@^^VA*prRpp}WH#Em#%Re_fb}X{!j3+C z6funqf`S$NtV;W$)RWFd=R&=b%*GARIbqRU4Ms`XoG+fZ)pJ%)y6L54-H%%HmhuM? zW7U<7jvrpX6iO9P=$thvDbiVXH*@i`K-2l& z)WAcUzXsC?Rv1PzI2!`Fhe_HccD^-*B0iFcYv|`NzZhWr0L$@fLW4g}2*%E%Dg+#s zVOb}`XZN>2t8fwDiNvDEdg&GMAx`U)J3~gNh8OPz6vy~48lbheP9Iu)$UCV^)RK{X z$LpwS$=YBQ!qedat9&H8c+7MxxMNy?8f-(*A(jtIHNYH2B@~b;+*pxX(bO@XqbB_O zIb;|JLI*3MrWkbu>Bt@xPlNDGa`EpARJ{C@#9?EvA4O)9dqIU*6dx^?H@br1W4k50 zlF;#!X@<%9Fb24igED>4$zm4>=Cc7ZubgHrPjYVX+&6MwyZ7C+AD>hgc zSpa%Rd!du`8gL>AbREqAhfO6@I$Q(T&ElrAf_|(oe|`31@Q|;{Pzia7b9?)R{H{U+ zf$BAYd5ky<*X+~+S8OHWh|^j-s7BkSn0E!K<#keNj}dj3SZe1(Rd~&|e-X_aAxU#I zV{VIPfI<(gU-G|F$k-AP+L(PK89ge8o1KvTi&y|O$gbHJ8bId;PLyF8NG6w0URhyb zrI7yWb$Ea_sVbDIU3_q)ALdC{-px^W&O2w+_-iQjWe%?HTulXIpcOZ^GXCa{9=M-M zBQbff4%=ss>7&v~)#3E8G{6_TZ)TP9i^<3Y?TsH5KnD@?

    NyH$C-mA{IQ3%8YlE6tr%}#BGIhcN8Ggvd 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 literal 0 HcmV?d00001 diff --git a/docs/images/ui/workers.png b/docs/images/ui/workers.png new file mode 100644 index 0000000000000000000000000000000000000000..a2bf39ec218ab44a229386d187cf07f31bc0bb6a GIT binary patch literal 75261 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

    a=%n*0zR+wIFvot`901~gv!XZLh zltfu9;r~(elZyPDU+iCKeI;L0A^zF1MMZVy-%J~%qIG*gMJ0cl$a8j?CtlRNN1EYG z&8MM(WcGl&=3-LKFAW*6{4!1)nu-q}PK_n8ZnVF~jDy{*3T9npoHcV8pA+gLR>r4n z77)EPj7UZbMgg+;Cspq=d&8epJ#4v@LPX>A(6?@rV;K^MZsiv6yKKv|yLEfVx0Vmb z^FIKYUqG`(%%6@1a?aah09R1~HI>Utx5AyN!xpvrgFEgUkderk4M;rli0%}3Zg0F5 zi?tWQ3#Xl`g2J<93+uM)Twg74TK#^Cf)L16^+}zr{jFh`OM**WMZ$FPH4He5oOe`X z#gBmg$QI(CdoQ(6BrJ_%ZeJWUA?Mk#bC|&2|Ip&*jSGtm$nIqmffQMc8?sE-%W*zj6JG_v%d|;^K)b?& zx1X>)s8^q>J+*kE-o0;45@_J6uqmSf-GoZzl4_V4+t-4NCA~rUjb=b$N{Hy_ z$5N60LS6_f>j%wZZVEd?Edp9b$!(X14gZ3~n*~t#GzJbohiCUvSP<#SfH|{Jwu}NggjQ%Y257eAGb<6V3(?<$h=ReSd~fZ)o-)(~~vB-U(C|9*;MZAK#T& z%0H6v&2isdpim=ex>G>n`Sf28{-uu?FQ$`@Pj2hUzqy_#X7R)< zo|wfGvv^__|9el&fl4OBni60o)g=mscj?U~W#j-Qv9AoFUwYF}8L3;oQEb9r7UiFM zD50N^H4AtcJhY*{*1rv*Iz{zQlQRFEMVfz--uDXF)Svem58LYKIIDB=SuP zahz4DPn0I-Hv-F*V#$|I zk&_d(@~=wvL_uQ7C-&sTGM+dRCw>>j9X+v(Cl2??)ZyU&>P)NrIG$#{Dz#kcDJl=~ zy<8Kd?eLRopxgv@{6i;yq6~3p0$v$bYhW%MnphiJ#j9;4&`$(f#9Y=k_wh_g7|wMN zG?O`4&~6Um#UE`IR3!|RADi)gp`a!1f>wqO5XUB`XUAf{seatunnel-engine-core seatunnel-engine-storage seatunnel-engine-serializer + seatunnel-engine-ui diff --git a/seatunnel-engine/seatunnel-engine-server/pom.xml b/seatunnel-engine/seatunnel-engine-server/pom.xml index 26081dbb49b..9bd13dcc4aa 100644 --- a/seatunnel-engine/seatunnel-engine-server/pom.xml +++ b/seatunnel-engine/seatunnel-engine-server/pom.xml @@ -33,6 +33,11 @@ seatunnel-engine-core ${project.version} + + org.apache.seatunnel + seatunnel-engine-ui + ${project.version} + org.apache.seatunnel checkpoint-storage-hdfs diff --git a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java index f9fff817f77..ea14c729f22 100644 --- a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java +++ b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/JettyService.java @@ -47,6 +47,7 @@ import javax.servlet.DispatcherType; +import java.net.URL; import java.util.EnumSet; import static org.apache.seatunnel.engine.server.rest.RestConstant.ENCRYPT_CONFIG; @@ -88,7 +89,15 @@ public void createJettyServer() { FilterHolder filterHolder = new FilterHolder(new ExceptionHandlingFilter()); context.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST)); - context.addServlet(new ServletHolder("default", new DefaultServlet()), "/"); + ServletHolder defaultServlet = new ServletHolder("default", DefaultServlet.class); + URL uiResource = JettyService.class.getClassLoader().getResource("ui"); + if (uiResource != null) { + defaultServlet.setInitParameter("resourceBase", uiResource.toExternalForm()); + } else { + log.warn("UI resources not found in classpath"); + } + + context.addServlet(defaultServlet, "/"); ServletHolder overviewHolder = new ServletHolder(new OverviewServlet(nodeEngine)); ServletHolder runningJobsHolder = new ServletHolder(new RunningJobsServlet(nodeEngine)); diff --git a/seatunnel-engine/seatunnel-engine-ui/.env.development b/seatunnel-engine/seatunnel-engine-ui/.env.development new file mode 100644 index 00000000000..147df08f620 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/.env.development @@ -0,0 +1,22 @@ + # Licensed to the Apache Software Foundation (ASF) under one or more + # contributor license agreements. See the NOTICE file distributed with + # this work for additional information regarding copyright ownership. + # The ASF licenses this file to You under the Apache License, Version 2.0 + # (the "License"); you may not use this file except in compliance with + # the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + +NODE_ENV=development + +# if you want to use the online server, you need to set this to `http://124.221.211.72:8081` +# VITE_APP_API_SERVICE='http://localhost:5801' +VITE_APP_API_SERVICE='http://124.221.211.72:8081' +# the context path of api service +VITE_APP_API_BASE='/api' diff --git a/seatunnel-engine/seatunnel-engine-ui/.env.production b/seatunnel-engine/seatunnel-engine-ui/.env.production new file mode 100644 index 00000000000..d9ab5285ca7 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/.env.production @@ -0,0 +1,19 @@ + # 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. + +NODE_ENV=production + +# the context path of api service +VITE_APP_API_BASE='' diff --git a/seatunnel-engine/seatunnel-engine-ui/.eslintrc.cjs b/seatunnel-engine/seatunnel-engine-ui/.eslintrc.cjs new file mode 100644 index 00000000000..d519de59eb1 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/.eslintrc.cjs @@ -0,0 +1,46 @@ +/* + * 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. + */ + +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + 'extends': [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript', + '@vue/eslint-config-prettier/skip-formatting' + ], + overrides: [ + { + files: [ + 'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}', + 'cypress/support/**/*.{js,ts,jsx,tsx}' + ], + 'extends': [ + 'plugin:cypress/recommended' + ] + } + ], + parserOptions: { + ecmaVersion: 'latest' + }, + rules: { + "vue/multi-word-component-names": "off" + } +} diff --git a/seatunnel-engine/seatunnel-engine-ui/.gitignore b/seatunnel-engine/seatunnel-engine-ui/.gitignore new file mode 100644 index 00000000000..8ee54e8d343 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/seatunnel-engine/seatunnel-engine-ui/.prettierrc.json b/seatunnel-engine/seatunnel-engine-ui/.prettierrc.json new file mode 100644 index 00000000000..66e23359c3d --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "tabWidth": 2, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "none" +} \ No newline at end of file diff --git a/seatunnel-engine/seatunnel-engine-ui/README.md b/seatunnel-engine/seatunnel-engine-ui/README.md new file mode 100644 index 00000000000..1d80a8f4bf8 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/README.md @@ -0,0 +1,60 @@ +# seatunnel-engine-ui + +## Development Environment Dependencies + +- Node 18+/20+ required +- npm 7+ + +- modify `VITE_APP_API_SERVICE` and `VITE_APP_API_BASE` in `.env.development` +- quick start + +```sh +npm install +npm run dev +``` + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` + +### Run Unit Tests with [Vitest] + +```sh +npm run test:unit +``` + +### Run End-to-End Tests with [Cypress] + +```sh +npm run test:e2e:dev +``` + +This runs the end-to-end tests against the Vite development server. +It is much faster than the production build. + +But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): + +```sh +npm run build +npm run test:e2e +``` + +### Lint with [ESLint] + +```sh +npm run lint +``` diff --git a/seatunnel-engine/seatunnel-engine-ui/cypress.config.ts b/seatunnel-engine/seatunnel-engine-ui/cypress.config.ts new file mode 100644 index 00000000000..e400f4eedc4 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/cypress.config.ts @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', + baseUrl: 'http://localhost:4173' + } +}) diff --git a/seatunnel-engine/seatunnel-engine-ui/cypress/e2e/example.cy.ts b/seatunnel-engine/seatunnel-engine-ui/cypress/e2e/example.cy.ts new file mode 100644 index 00000000000..e27d711e87d --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/cypress/e2e/example.cy.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// https://on.cypress.io/api + +describe('My First Test', () => { + it('visits the app root url', () => { + cy.visit('/') + cy.contains('h1', 'You did it!') + }) +}) diff --git a/seatunnel-engine/seatunnel-engine-ui/cypress/e2e/tsconfig.json b/seatunnel-engine/seatunnel-engine-ui/cypress/e2e/tsconfig.json new file mode 100644 index 00000000000..c94f1d49b4f --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/cypress/e2e/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["./**/*", "../support/**/*"], + "compilerOptions": { + "isolatedModules": false, + "types": ["cypress"] + } +} diff --git a/seatunnel-engine/seatunnel-engine-ui/cypress/fixtures/example.json b/seatunnel-engine/seatunnel-engine-ui/cypress/fixtures/example.json new file mode 100644 index 00000000000..02e4254378e --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/seatunnel-engine/seatunnel-engine-ui/cypress/support/commands.ts b/seatunnel-engine/seatunnel-engine-ui/cypress/support/commands.ts new file mode 100644 index 00000000000..afa1ad38334 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/cypress/support/commands.ts @@ -0,0 +1,56 @@ +/// +/* + * 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 example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } + +export {} diff --git a/seatunnel-engine/seatunnel-engine-ui/cypress/support/e2e.ts b/seatunnel-engine/seatunnel-engine-ui/cypress/support/e2e.ts new file mode 100644 index 00000000000..8f37ad5c860 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/cypress/support/e2e.ts @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/seatunnel-engine/seatunnel-engine-ui/env.d.ts b/seatunnel-engine/seatunnel-engine-ui/env.d.ts new file mode 100644 index 00000000000..6630422c6b1 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/env.d.ts @@ -0,0 +1,17 @@ +/// +/* + * 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. + */ diff --git a/seatunnel-engine/seatunnel-engine-ui/index.html b/seatunnel-engine/seatunnel-engine-ui/index.html new file mode 100644 index 00000000000..a1758db29f2 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + Seatunnel Engine UI + + + +
    + + + + \ No newline at end of file diff --git a/seatunnel-engine/seatunnel-engine-ui/package-lock.json b/seatunnel-engine/seatunnel-engine-ui/package-lock.json new file mode 100644 index 00000000000..3118ae48ec3 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/package-lock.json @@ -0,0 +1,10313 @@ +{ + "name": "seatunnel-engine-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "seatunnel-engine-ui", + "version": "0.0.0", + "dependencies": { + "@antv/x6": "^2.18.1", + "@antv/x6-plugin-selection": "^2.2.2", + "@antv/x6-vue-shape": "^2.1.2", + "@vicons/ionicons5": "^0.12.0", + "autoprefixer": "^10.4.20", + "axios": "^1.7.7", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "naive-ui": "^2.39.0", + "nprogress": "^0.2.0", + "pinia": "^2.1.7", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "vue": "^3.4.29", + "vue-i18n": "^10.0.1", + "vue-router": "^4.3.3" + }, + "devDependencies": { + "@pinia/testing": "^0.1.5", + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node20": "^20.1.4", + "@types/jsdom": "^21.1.7", + "@types/node": "^20.14.5", + "@types/nprogress": "^0.2.3", + "@vitejs/plugin-vue": "^5.0.5", + "@vitejs/plugin-vue-jsx": "^4.0.0", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.5.1", + "cypress": "^13.12.0", + "eslint": "^8.57.0", + "eslint-plugin-cypress": "^3.3.0", + "eslint-plugin-vue": "^9.23.0", + "jsdom": "^24.1.0", + "npm-run-all2": "^6.2.0", + "prettier": "^3.2.5", + "sass-embedded": "^1.78.0", + "start-server-and-test": "^2.0.4", + "typescript": "~5.4.0", + "vite": "^5.3.1", + "vite-plugin-vue-devtools": "^7.3.1", + "vitest": "^1.5.3", + "vue-tsc": "^2.0.21" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antv/x6": { + "version": "2.18.1", + "resolved": "https://registry.npmmirror.com/@antv/x6/-/x6-2.18.1.tgz", + "integrity": "sha512-FkWdbLOpN9J7dfJ+kiBxzowSx2N6syBily13NMVdMs+wqC6Eo5sLXWCZjQHateTFWgFw7ZGi2y9o3Pmdov1sXw==", + "license": "MIT", + "dependencies": { + "@antv/x6-common": "^2.0.16", + "@antv/x6-geometry": "^2.0.5", + "utility-types": "^3.10.0" + } + }, + "node_modules/@antv/x6-common": { + "version": "2.0.17", + "resolved": "https://registry.npmmirror.com/@antv/x6-common/-/x6-common-2.0.17.tgz", + "integrity": "sha512-37g7vmRkNdYzZPdwjaMSZEGv/MMH0S4r70/Jwoab1mioycmuIBN73iyziX8m56BvJSDucZ3J/6DU07otWqzS6A==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.15", + "utility-types": "^3.10.0" + } + }, + "node_modules/@antv/x6-geometry": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@antv/x6-geometry/-/x6-geometry-2.0.5.tgz", + "integrity": "sha512-MId6riEQkxphBpVeTcL4ZNXL4lScyvDEPLyIafvWMcWNTGK0jgkK7N20XSzqt8ltJb0mGUso5s56mrk8ysHu2A==", + "license": "MIT" + }, + "node_modules/@antv/x6-plugin-selection": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-selection/-/x6-plugin-selection-2.2.2.tgz", + "integrity": "sha512-s2gtR9Onlhr7HOHqyqg0d+4sG76JCcQEbvrZZ64XmSChlvieIPlC3YtH4dg1KMNhYIuBmBmpSum6S0eVTEiPQw==", + "license": "MIT", + "peerDependencies": { + "@antv/x6": "^2.x" + } + }, + "node_modules/@antv/x6-vue-shape": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@antv/x6-vue-shape/-/x6-vue-shape-2.1.2.tgz", + "integrity": "sha512-lfLNJ2ztK8NP2JBAWTD6m5Wol0u6tOqj2KdOhWZoT8EtEw9rMmAdxsr8uTi9MRJO9pDMM0nbsR3cidnMh7VeDQ==", + "license": "MIT", + "dependencies": { + "vue-demi": "latest" + }, + "peerDependencies": { + "@antv/x6": "^2.x", + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^2.0.0 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@antv/x6-vue-shape/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.8", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.8", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.7.tgz", + "integrity": "sha512-q1mqqqH0e1lhmsEQHV5U8OmdueBC2y0RFr2oUzZoFRtN3MvPmt2fsFRcNQAoGLTSNdHBFUYGnlgcRFhkBbKjPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-decorators": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.7.tgz", + "integrity": "sha512-oXduHo642ZhstLVYTe2z2GSJIruU0c/W3/Ghr6A5yGMsVrvdnxO1z+3pbTcT7f3/Clnt+1z8D/w1r1f1SHaCHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.7.tgz", + "integrity": "sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-syntax-typescript": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@bufbuild/protobuf/-/protobuf-2.2.0.tgz", + "integrity": "sha512-+imAQkHf7U/Rwvu0wk1XWgsP3WnpCWmK7B48f0XqSNzgk64+grljTKC7pnO/xBiEMUziF7vKRfbBnOQhg126qQ==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@css-render/plugin-bem": { + "version": "0.15.14", + "resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz", + "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==", + "license": "MIT", + "peerDependencies": { + "css-render": "~0.15.14" + } + }, + "node_modules/@css-render/vue3-ssr": { + "version": "0.15.14", + "resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz", + "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@cypress/request/-/request-3.0.5.tgz", + "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.13.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@intlify/core-base": { + "version": "10.0.4", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-10.0.4.tgz", + "integrity": "sha512-GG428DkrrWCMhxRMRQZjuS7zmSUzarYcaHJqG9VB8dXAxw4iQDoKVQ7ChJRB6ZtsCsX3Jse1PEUlHrJiyQrOTg==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "10.0.4", + "@intlify/shared": "10.0.4" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "10.0.4", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-10.0.4.tgz", + "integrity": "sha512-AFbhEo10DP095/45EauinQJ5hJ3rJUmuuqltGguvc3WsvezZN+g8qNHLGWKu60FHQVizMrQY7VJ+zVlBXlQQkQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "10.0.4", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "10.0.4", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-10.0.4.tgz", + "integrity": "sha512-ukFn0I01HsSgr3VYhYcvkTCLS7rGa0gw4A4AMpcy/A9xx/zRJy7PS2BElMXLwUazVFMAr5zuiTk3MQeoeGXaJg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "license": "Apache-2.0" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pinia/testing": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/@pinia/testing/-/testing-0.1.6.tgz", + "integrity": "sha512-Q40s3kpjXpjmcnc61l84wyG83yVmkBi5rRdSoPpwQoRfSnNKKr52XjFFt6hP8iBxehYS9NR+D57T1uzgnEVPHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "pinia": ">=2.2.3" + } + }, + "node_modules/@pinia/testing/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.4", + "resolved": "https://registry.npmmirror.com/@tsconfig/node20/-/node20-20.1.4.tgz", + "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmmirror.com/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "20.16.12", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.16.12.tgz", + "integrity": "sha512-LfPFB0zOeCeCNQV3i+67rcoVvoN5n0NVuR2vLG0O5ySQMgchuZlC4lgz546ZOJyDtj5KIgOxy+lacOimfqZAIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/nprogress": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@types/nprogress/-/nprogress-0.2.3.tgz", + "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vicons/ionicons5": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.12.0.tgz", + "integrity": "sha512-Iy1EUVRpX0WWxeu1VIReR1zsZLMc4fqpt223czR+Rpnrwu7pt46nbnC2ycO7ItI/uqDLJxnbcMC7FujKs9IfFA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.0.1.tgz", + "integrity": "sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7", + "@vue/babel-plugin-jsx": "^1.2.2" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.0", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.6", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.6.tgz", + "integrity": "sha512-FxUfxaB8sCqvY46YjyAAV6c3mMIq/NWQMVvJ+uS4yxr1KzOvyg61gAuOnNvgCvO4TZ7HcLExBEsWcDu4+K4E8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.6" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.6", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.6.tgz", + "integrity": "sha512-Nsh7UW2ruK+uURIPzjJgF0YRGP5CX9nQHypA2OMqdM2FKy7rh+uv3XgPnWPw30JADbKvZ5HuBzG4gSbVDYVtiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.6", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.6.tgz", + "integrity": "sha512-NMIrA7y5OOqddL9VtngPWYmdQU03htNKFtAYidbYfWA0TOhyGVd9tfcP4TsLWQ+RBWDZCbBqsr8xzU0ZOxYTCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.6", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz", + "integrity": "sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.5.tgz", + "integrity": "sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.6", + "@babel/types": "^7.25.6", + "@vue/babel-helper-vue-transform-on": "1.2.5", + "@vue/babel-plugin-resolve-type": "1.2.5", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.5.tgz", + "integrity": "sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/parser": "^7.25.6", + "@vue/compiler-sfc": "^3.5.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/devtools-core": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-7.5.2.tgz", + "integrity": "sha512-J7vcCb2P7bH3TvikqSe3BquCZsgWC7PL0t9yO88c3LUK3cyhQdJoWcn0Tkgop55UztHWs40+7uQNDmTkcdNZAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.5.2", + "@vue/devtools-shared": "^7.5.2", + "mitt": "^3.0.1", + "nanoid": "^3.3.4", + "pathe": "^1.1.2", + "vite-hot-client": "^0.2.3" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.5.2.tgz", + "integrity": "sha512-0leUOE2HBfl8sHf9ePKzxqnCFskkU22tWWqd9OfeSlslAKE30/TViYvWcF4vgQmPlJnAAdHU0WfW5dYlCeOiuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.5.2", + "birpc": "^0.2.19", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.5.2.tgz", + "integrity": "sha512-+zmcixnD6TAo+zwm30YuwZckhL9iIi4u+gFwbq9C8zpm3SMndTlEYZtNhAHUhOXB+bCkzyunxw80KQ/T0trF4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", + "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "peerDependencies": { + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": ">=4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.4.1", + "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "vue": "3.5.12" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmmirror.com/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz", + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "dev": true, + "license": "MIT/X11" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001669", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmmirror.com/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmmirror.com/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-render": { + "version": "0.15.14", + "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz", + "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "~0.8.0", + "csstype": "~3.0.5" + } + }, + "node_modules/css-render/node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/cypress": { + "version": "13.15.0", + "resolved": "https://registry.npmmirror.com/cypress/-/cypress-13.15.0.tgz", + "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.4", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cypress/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cypress/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.40", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.40.tgz", + "integrity": "sha512-LYm78o6if4zTasnYclgQzxEcgMoIcybWOhkATWepN95uwVVWV0/IW10v+2sIeHE+bIYWipLneTftVyQm45UY7g==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-cypress": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-cypress/-/eslint-plugin-cypress-3.6.0.tgz", + "integrity": "sha512-7IAMcBbTVu5LpWeZRn5a9mQ30y4hKp3AfTz+6nSD/x/7YyLMoBI6X7XjDLYI6zFvuy4Q4QVGl563AGEXGW/aSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "globals": "^13.20.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-cypress/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-cypress/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.29.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.29.0.tgz", + "integrity": "sha512-hamyjrBhNH6Li6R1h1VF9KHfshJlKgKEg3ARbGTn72CMNDSMhWbgC7NdkRDEh25AFW+4SDATzyNM+3gWuZii8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmmirror.com/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.10.0", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmmirror.com/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "24.1.3", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/listr2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/naive-ui": { + "version": "2.40.1", + "resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.40.1.tgz", + "integrity": "sha512-3NkL+vLRQZKQxCHXa+7xiD6oM74OrQELaehDkGYRYpr6kjT+JJB+Z7h+5LC70gn8VkbgCAETv0+uRWF+6MLlgQ==", + "license": "MIT", + "dependencies": { + "@css-render/plugin-bem": "^0.15.14", + "@css-render/vue3-ssr": "^0.15.14", + "@types/katex": "^0.16.2", + "@types/lodash": "^4.14.198", + "@types/lodash-es": "^4.17.9", + "async-validator": "^4.2.5", + "css-render": "^0.15.14", + "csstype": "^3.1.3", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "evtd": "^0.2.4", + "highlight.js": "^11.8.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "seemly": "^0.3.8", + "treemate": "^0.3.11", + "vdirs": "^0.1.8", + "vooks": "^0.2.12", + "vueuc": "^0.4.63" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-all2": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/npm-run-all2/-/npm-run-all2-6.2.3.tgz", + "integrity": "sha512-5RsxC7jEc/RjxOYBVdEfrJf5FsJ0pHA7jr2/OxrThXknajETCTYjigOCG3iaGjdYIKEQlDuCG0ir0T1HTva8pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.3", + "memorystream": "^0.3.1", + "minimatch": "^9.0.0", + "pidtree": "^0.6.0", + "read-package-json-fast": "^3.0.2", + "shell-quote": "^1.7.3" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^14.18.0 || ^16.13.0 || >=18.0.0", + "npm": ">= 8" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmmirror.com/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.2.4.tgz", + "integrity": "sha512-K7ZhpMY9iJ9ShTC0cR2+PnxdQRuwVIsXDO/WIEV/RnMC/vmSoKDTKW/exNQYPI+4ij10UjXqdNiEHwn47McANQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass-embedded": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded/-/sass-embedded-1.80.1.tgz", + "integrity": "sha512-FQiaiA2Bc1a3/nXdl9cAz19cKKcW5uU+k1JUWx5Tt1UcSYmV0B+5V0GDHwtyF7UeCuoBMRl3B3LxQ6n317HLYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^4.0.0", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-android-arm": "1.80.1", + "sass-embedded-android-arm64": "1.80.1", + "sass-embedded-android-ia32": "1.80.1", + "sass-embedded-android-riscv64": "1.80.1", + "sass-embedded-android-x64": "1.80.1", + "sass-embedded-darwin-arm64": "1.80.1", + "sass-embedded-darwin-x64": "1.80.1", + "sass-embedded-linux-arm": "1.80.1", + "sass-embedded-linux-arm64": "1.80.1", + "sass-embedded-linux-ia32": "1.80.1", + "sass-embedded-linux-musl-arm": "1.80.1", + "sass-embedded-linux-musl-arm64": "1.80.1", + "sass-embedded-linux-musl-ia32": "1.80.1", + "sass-embedded-linux-musl-riscv64": "1.80.1", + "sass-embedded-linux-musl-x64": "1.80.1", + "sass-embedded-linux-riscv64": "1.80.1", + "sass-embedded-linux-x64": "1.80.1", + "sass-embedded-win32-arm64": "1.80.1", + "sass-embedded-win32-ia32": "1.80.1", + "sass-embedded-win32-x64": "1.80.1" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.80.1.tgz", + "integrity": "sha512-XvUHgUN98IZFu7tb1xc15Q/VdtaYXUnkse1n4xHQSohIOzm0TytJDmUpSkMAEFFd+iBQBxyWc40rj6IPXQH/RA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.80.1.tgz", + "integrity": "sha512-AjWjEtyvOLs9fIVvR2mHtRy2/OB/IzUGBYjOqGozU2BUC6acnuRmqLrKwprmZxTL7DDgQRoT/gtoo9wdtHHoEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-ia32": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.80.1.tgz", + "integrity": "sha512-gzrlN2juF8F09zS3JqTBnc7Fq7ydIkXHeR8z4XVVpy2PTDLqOM0+HyVTJGOdRgJFWmMkl7BzD5c+pl8pW/EI7Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.80.1.tgz", + "integrity": "sha512-pFivqYfibQlno2Uc6I/YVeCFDpr4ltvv1mcIHQ0IL0u4+PqxKJXONGBOcRMAPFEQKDEiIUDml40HB+i5q9PUhg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.80.1.tgz", + "integrity": "sha512-v4wHQ/bNV/s/ZWe3eYkkW8cEUao0woW5EBIzRZLtY0MOdIPnomum8uH+rRP3YGOQrawegjiDP8O9Fb+nhWYXVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.80.1.tgz", + "integrity": "sha512-CcY0FfzrnEkmWWevTEaEfZTRzWGHpz3aGFqNoRmkqrWrZYdiRqnr6PFHQJLugM76IoX1SfU2r8lXz25WKOzEBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.80.1.tgz", + "integrity": "sha512-kaoww70nI8/7dCGRY4JTMEZYGTxBvqGZni1CWhBOZBTE/w1GH+1z6XGw7klVzGyw0Ysxms+abAMd8fVdCzUTzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.80.1.tgz", + "integrity": "sha512-6WGgcO/3bw8s2yB0TP6V4S3tCTq94bhGiSM93Xw8mr0cVPxR/VoUBeIvGdAmRKBmJiGlcJcdAbNcLGtyr7eF6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.80.1.tgz", + "integrity": "sha512-jEn24I5MaVMgj6Tb3EzkIxIoKVvTE1lr0021N5Tv5zeG7cZCPrRO5+JplQnmwMm3i22PqYvJjhSCJ18URJPqrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-ia32": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.80.1.tgz", + "integrity": "sha512-EXjSErbGxlpRWfro1fjqTACA6y/AN39LeqyL9FdM7ImGozpHbTllMkciM7wj8mZ0Rk8XLDMnvB53kYfgnjtzRA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.80.1.tgz", + "integrity": "sha512-XR75p54m/NLKEcFMfUacqDTcbjr0gB1KaVwCoTsk3knbtcDfQDJNVnp5A48gu32yZXcDpGjNX+g8pUzQrKof3g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.80.1.tgz", + "integrity": "sha512-uONMRDG1VgZYoyezncPw8i8T93qUyIoNAGoKGO2ZnuAtXwjBGWtEAdN5Xf2LnmJTyTFlLHDUZs8OHLTA958oVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-ia32": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.80.1.tgz", + "integrity": "sha512-4/2UUcSXxmvlr/HlOwa9m/DcljNFs1pcuoIAlY53f1WCd1JYGrqPUqlqGYGHCV9wuRHs1Pi1FHSBXAezyg3++w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.80.1.tgz", + "integrity": "sha512-wrh98HrCQQIVI12P3NkeGFTRrK6SuIDUHsYrumK3yCR8GdYIjbt8/Ij3nvrkTTrh03/1dNJIAYi/2svAFenJoQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.80.1.tgz", + "integrity": "sha512-JOF/SjJYY5rEjyozS0sTRiIqIDZKFaZiBfJYP/z9yU7VHT9cd6XAxQ2iOkGSNO7cWInSFSit1kmSK50k3Ghm9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.80.1.tgz", + "integrity": "sha512-DSX54EubX7Jh00J/hQO8L3WXQ1ynTm0K06GE/iildyy7nxjvF2KUkWKZDLKv56pLwevcRZR1iLXkdJUG92GWyg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.80.1.tgz", + "integrity": "sha512-v/5UDLV7+g1x8EwfIMYAADfWL5aiuxjP+4hW/arMEU9DaXPofymvZOnpLhylYyVduIDAMsj4BgIn9g0vA5ErQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.80.1.tgz", + "integrity": "sha512-81f+X4sIsiQS8ynpYH0sARA0st1LzTuM88OU18Th19oNblSWcaXSQ8lxljwzNPvHcWx/FLPdgIoGW1Wzx3MeoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-ia32": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.80.1.tgz", + "integrity": "sha512-VYSQyySBj3excFUytRBXjAHww75810+dOYFcBkSRupx04nCQwQm0avYqcjQfYl+eNzS4oNeEAQvhTRk8HHNDGg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.80.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.80.1.tgz", + "integrity": "sha512-EtrqZckPgVSGH/rQxY7jdcX3wqltS1uzp2YEV7VAn27iwoDwThTmslQgpfWUFLVyaIjqgUjx58GJkHu020J19g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sass-embedded/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/seemly": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.9.tgz", + "integrity": "sha512-bMLcaEqhIViiPbaumjLN8t1y+JpD/N8SiyYOyp0i0W6RgdyLWboIsUWAbZojF//JyerxPZR5Tgda+x3Pdne75A==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/start-server-and-test": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/start-server-and-test/-/start-server-and-test-2.0.8.tgz", + "integrity": "sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "arg": "^5.0.2", + "bluebird": "3.7.2", + "check-more-types": "2.24.0", + "debug": "4.3.7", + "execa": "5.1.1", + "lazy-ass": "1.6.0", + "ps-tree": "1.2.0", + "wait-on": "8.0.1" + }, + "bin": { + "server-test": "src/bin/start.js", + "start-server-and-test": "src/bin/start.js", + "start-test": "src/bin/start.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/start-server-and-test/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/start-server-and-test/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/start-server-and-test/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmmirror.com/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/superjson": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.14", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.14.tgz", + "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/treemate": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz", + "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vdirs": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz", + "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "5.4.9", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.9.tgz", + "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-hot-client": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-0.2.3.tgz", + "integrity": "sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "node_modules/vite-node": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "0.8.7", + "resolved": "https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-0.8.7.tgz", + "integrity": "sha512-/XXou3MVc13A5O9/2Nd6xczjrUwt7ZyI9h8pTnUMkr5SshLcb0PJUOVq2V+XVkdeU4njsqAtmK87THZuO2coGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "debug": "^4.3.6", + "error-stack-parser-es": "^0.1.5", + "fs-extra": "^11.2.0", + "open": "^10.1.0", + "perfect-debounce": "^1.0.0", + "picocolors": "^1.0.1", + "sirv": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-inspect/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.5.2.tgz", + "integrity": "sha512-+lQOKW0kZAvLxy9KcsmtOk5Hsu0ibVAot9odFwCCASE4jukb0zaWGIyZwFLk4IsWNDT3iISvajIr704UYcZL6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^7.5.2", + "@vue/devtools-kit": "^7.5.2", + "@vue/devtools-shared": "^7.5.2", + "execa": "^8.0.1", + "sirv": "^2.0.4", + "vite-plugin-inspect": "^0.8.7", + "vite-plugin-vue-inspector": "^5.2.0" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.2.0.tgz", + "integrity": "sha512-wWxyb9XAtaIvV/Lr7cqB1HIzmHZFVUJsTNm3yAxkS87dgh/Ky4qr2wDEWNxF23fdhVa3jQ8MZREpr4XyiuaRqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0" + } + }, + "node_modules/vitest": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vitest/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vitest/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vooks": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz", + "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.12", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-2.1.6.tgz", + "integrity": "sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-i18n": { + "version": "10.0.4", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-10.0.4.tgz", + "integrity": "sha512-1xkzVxqBLk2ZFOmeI+B5r1J7aD/WtNJ4j9k2mcFcQo5BnOmHBmD7z4/oZohh96AAaRZ4Q7mNQvxc9h+aT+Md3w==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "10.0.4", + "@intlify/shared": "10.0.4", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.4.5", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.4.5.tgz", + "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.1.6.tgz", + "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.1", + "@vue/language-core": "2.1.6", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue-tsc/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vueuc": { + "version": "0.4.64", + "resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz", + "integrity": "sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA==", + "license": "MIT", + "dependencies": { + "@css-render/vue3-ssr": "^0.15.10", + "@juggle/resize-observer": "^3.3.1", + "css-render": "^0.15.10", + "evtd": "^0.2.4", + "seemly": "^0.3.6", + "vdirs": "^0.1.4", + "vooks": "^0.2.4" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/wait-on": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/wait-on/-/wait-on-8.0.1.tgz", + "integrity": "sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.7.7", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/seatunnel-engine/seatunnel-engine-ui/package.json b/seatunnel-engine/seatunnel-engine-ui/package.json new file mode 100644 index 00000000000..6d29bb53ce0 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/package.json @@ -0,0 +1,64 @@ +{ + "name": "seatunnel-engine-ui", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "test:unit": "vitest", + "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", + "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", + "build-only": "vite build", + "type-check": "vue-tsc --build --force", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/" + }, + "dependencies": { + "@antv/x6": "^2.18.1", + "@antv/x6-plugin-selection": "^2.2.2", + "@antv/x6-vue-shape": "^2.1.2", + "@vicons/ionicons5": "^0.12.0", + "autoprefixer": "^10.4.20", + "axios": "^1.7.7", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "naive-ui": "^2.39.0", + "nprogress": "^0.2.0", + "pinia": "^2.1.7", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "vue": "^3.4.29", + "vue-i18n": "^10.0.1", + "vue-router": "^4.3.3" + }, + "devDependencies": { + "@pinia/testing": "^0.1.5", + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node20": "^20.1.4", + "@types/jsdom": "^21.1.7", + "@types/node": "^20.14.5", + "@types/nprogress": "^0.2.3", + "@vitejs/plugin-vue": "^5.0.5", + "@vitejs/plugin-vue-jsx": "^4.0.0", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.5.1", + "cypress": "^13.12.0", + "eslint": "^8.57.0", + "eslint-plugin-cypress": "^3.3.0", + "eslint-plugin-vue": "^9.23.0", + "jsdom": "^24.1.0", + "npm-run-all2": "^6.2.0", + "prettier": "^3.2.5", + "sass-embedded": "^1.78.0", + "start-server-and-test": "^2.0.4", + "typescript": "~5.4.0", + "vite": "^5.3.1", + "vite-plugin-vue-devtools": "^7.3.1", + "vitest": "^1.5.3", + "vue-tsc": "^2.0.21" + } +} \ No newline at end of file diff --git a/seatunnel-engine/seatunnel-engine-ui/pom.xml b/seatunnel-engine/seatunnel-engine-ui/pom.xml new file mode 100644 index 00000000000..5244e73c5b2 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/pom.xml @@ -0,0 +1,159 @@ + + + + 4.0.0 + + org.apache.seatunnel + seatunnel-engine + ${revision} + + seatunnel-engine-ui + SeaTunnel : Engine : UI + + + v16.13.2 + 8.1.2 + node_modules + ../seatunnel-engine-server/src/main/resources/ui + .deployed + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + clean-rmdir + + exec + + clean + + ${executable.rmdir} + ${basedir} + ${args.rm.clean} ${dist.dir} ${nodemodules.dir} ${deployed.dir} + + 0 + 1 + 2 + + + + + + + com.github.eirslett + frontend-maven-plugin + 1.10.3 + + ${build.node.version} + ${build.npm.version} + + + + install node and npm + + install-node-and-npm + + generate-resources + + + npm install + + npm + + generate-resources + + install + + + + npm run build + + npm + + package + + run build + + + + + + + + + + windows + + + win + + + + win + \ + cmd + ${basedir}\gzip-content.cmd + /C brunch + node.exe + cmd + /C mkdir + cmd + /C npm + cmd + /C rmdir /S /Q + cmd + cmd + /C + + + + linux + + + unix + + + + linux + / + brunch + gzip + + node + mkdir + + npm + + rm + -rf + sh + sh + + + + + diff --git a/seatunnel-engine/seatunnel-engine-ui/postcss.config.js b/seatunnel-engine/seatunnel-engine-ui/postcss.config.js new file mode 100644 index 00000000000..af637b45455 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/postcss.config.js @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +} \ No newline at end of file diff --git a/seatunnel-engine/seatunnel-engine-ui/public/favicon.ico b/seatunnel-engine/seatunnel-engine-ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..aafed25d366c5319069145cc24803b877ba9f6bd GIT binary patch literal 211862 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%0A~aC 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 + + + + + + + ) + } +}) + +export default App diff --git a/seatunnel-engine/seatunnel-engine-ui/src/assets/logo.png b/seatunnel-engine/seatunnel-engine-ui/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aafed25d366c5319069145cc24803b877ba9f6bd GIT binary patch literal 211862 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%0A~aC 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, + default: () => ({}) + } + }, + setup(props) { + const format = (value: any) => { + value = JSON.stringify(value) + if (value) { + value = value.replace(/^"(.*)"$/, '$1') + } + return value || '' + } + return () => ( + + {props.data && + Object.entries(props.data).map(([key, value]) => ( + {format(value)} + ))} + + ) + } +}) diff --git a/seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.scss b/seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.scss new file mode 100644 index 00000000000..6cd68220039 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.scss @@ -0,0 +1,72 @@ +/* + * 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. + */ + +.node { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0 6px; + width: 100%; + height: 100%; + background-color: #fff; + border: 1px solid #c2c8d5; + border-left: 4px solid var(--node-color); + border-radius: 4px; + box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06); + padding: 6px 8px; + .label { + flex: 1; + color: #666; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + .status { + color: var(--node-color); + } +} +.x6-node-selected .node { + border-color: var(--node-color); + border-radius: 2px; + box-shadow: 0 0 0 4px #d4e8fe; +} +.x6-edge:hover path:nth-child(2){ + stroke: #1890ff; + stroke-width: 1px; +} + +.x6-edge-selected path:nth-child(2){ + stroke: #1890ff; + stroke-width: 1.5px !important; +} + +@keyframes running-line { + to { + stroke-dashoffset: -1000; + } +} +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.tsx b/seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.tsx new file mode 100644 index 00000000000..3f1111997d7 --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/src/components/directed-acyclic-graph/index.tsx @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Graph, Path, Cell } from '@antv/x6' +import { Selection } from '@antv/x6-plugin-selection' +import { register } from '@antv/x6-vue-shape' +import { defineComponent, onMounted, watch, type PropType } from 'vue' +import './index.scss' +import type { Job, JobStatus, Vertex } from '@/service/job/types' +import { getColorFromStatus } from '@/utils/getTypeFromStatus' + +interface NodeStatus { + id: number + status: JobStatus + label?: string +} + +const AlgoNode = (props: any) => { + const { node } = props + const data = node?.getData() as NodeStatus + const { label, status } = data + const style = `--node-color:${getColorFromStatus(status)?.textColor};` + return ( +
    + {label} +
    + ) +} + +const nodeWidth = 300 +register({ + shape: 'dag-node', + width: nodeWidth, + height: 48, + component: AlgoNode, + ports: { + groups: { + left: { + position: 'left', + attrs: { + circle: { + r: 4, + magnet: true, + stroke: '#C2C8D5', + strokeWidth: 1, + fill: '#fff' + } + } + }, + right: { + position: 'right', + attrs: { + circle: { + r: 4, + magnet: true, + stroke: '#C2C8D5', + strokeWidth: 1, + fill: '#fff' + } + } + } + } + } +}) + +Graph.registerEdge( + 'dag-edge', + { + inherit: 'edge', + attrs: { + line: { + stroke: '#C2C8D5', + strokeWidth: 1, + targetMarker: null + } + } + }, + true +) + +Graph.registerConnector( + 'algo-connector', + (s, e) => { + const offset = 4 + const delta = Math.abs(e.x - s.x) + const control = Math.floor((delta / 3) * 2) + + const v1 = { y: s.y, x: s.x + offset + control } + const v2 = { y: e.y, x: e.x - offset - control } + + return Path.normalize( + `M ${s.x} ${s.y} + L ${s.x + offset} ${s.y} + C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x - offset} ${e.y} + L ${e.x} ${e.y} + ` + ) + }, + true +) + +export default defineComponent({ + props: { + job: { + type: Object as PropType, + required: true + }, + focusedId: { + type: Number, + required: true + }, + onNodeClick: { + type: Function as PropType<(vertex?: Vertex) => void>, + required: true + } + }, + setup(props) { + let focusedId = 0 + let graph: Graph + watch( + () => props.focusedId, + () => { + if (!graph || focusedId === props.focusedId) return + if (props.focusedId) { + // const cell = graph.getCellById('node-' + props.focusedId) + // if (cell) { + // cell.trigger('click') + // } + graph.select('node-' + props.focusedId) + } else { + graph.select('node-0') + // graph.trigger('blank:click') + } + } + ) + onMounted(() => { + graph = new Graph({ + container: document.getElementById('container')!, + panning: { + enabled: true, + eventTypes: ['leftMouseDown', 'mouseWheel'] + }, + mousewheel: { + enabled: true, + modifiers: 'ctrl', + factor: 1.1, + maxScale: 1.5, + minScale: 0.5 + }, + highlighting: { + magnetAdsorbed: { + name: 'stroke', + args: { + attrs: { + fill: '#fff', + stroke: '#31d0c6', + strokeWidth: 4 + } + } + } + }, + connecting: { + snap: true, + allowBlank: false, + allowLoop: false, + highlight: true, + connector: 'algo-connector', + connectionPoint: 'anchor', + anchor: 'center', + validateMagnet({ magnet }) { + return magnet.getAttribute('port-group') !== 'left' + }, + createEdge() { + return graph.createEdge({ + shape: 'dag-edge', + attrs: { + line: { + strokeDasharray: '5 5' + } + }, + zIndex: -1 + }) + } + } + }) + graph.use( + new Selection({ + multiple: false, + rubberEdge: true, + rubberNode: true, + modifiers: 'shift', + rubberband: true + }) + ) + + graph.on('edge:connected', ({ edge }) => { + edge.attr({ + line: { + strokeDasharray: '' + } + }) + }) + + graph.on('node:change:data', ({ node }) => { + const edges = graph.getIncomingEdges(node) + const { status } = node.getData() as NodeStatus + edges?.forEach((edge) => { + if (status === 'RUNNING') { + edge.attr('line/strokeDasharray', 5) + edge.attr('line/style/animation', 'running-line 30s infinite linear') + } else { + edge.attr('line/strokeDasharray', '') + edge.attr('line/style/animation', '') + } + }) + }) + graph.on('node:click', ({ node }) => { + const { id } = node.getData() as NodeStatus + focusedId = id + const vertex = props?.job?.jobDag?.vertexInfoMap?.find((item) => item.vertexId === id) + props.onNodeClick(vertex) + }) + graph.on('blank:click', () => { + props.onNodeClick() + }) + + const init = () => { + const matrix = [] as Vertex[][] + const items: Cell.Metadata[] = [] + + const offsetY = 140 + const offsetX = nodeWidth + 200 + + const processed = [] as Vertex[] + const vertexs = props?.job?.jobDag?.vertexInfoMap || [] + const edgeMap = props?.job?.jobDag?.pipelineEdges || {} + let zIndex = 0 + for (const pipelineId of Object.keys(edgeMap)) { + const edges = edgeMap[pipelineId] + const row = [] as Vertex[] + matrix.push(row) + for (const edge of edges) { + items.push({ + id: `edge-${pipelineId}-${edge.inputVertexId}-${edge.targetVertexId}`, + shape: 'dag-edge', + source: { + cell: `node-${edge.inputVertexId}`, + port: `node-${edge.inputVertexId}-right` + }, + target: { + cell: `node-${edge.targetVertexId}`, + port: `node-${edge.targetVertexId}-left` + }, + zIndex: zIndex++ + }) + const input = vertexs.find((item) => item.vertexId === Number(edge.inputVertexId)) + if (input && !processed.includes(input)) { + row.push(input) + processed.push(input) + } + const target = vertexs.find((item) => item.vertexId === Number(edge.targetVertexId)) + if (target && !processed.includes(target)) { + row.push(target) + processed.push(target) + } + } + } + matrix.forEach((row) => { + row.sort((a, b) => { + if (a.type === 'source') { + return -1 + } else if (b.type === 'sink') { + return 1 + } else { + return 0 + } + }) + }) + type Port = { id: string; group: string } + matrix.forEach((row, rowNumber) => { + row.forEach((item, colNumber) => { + const data: NodeStatus = { + id: item.vertexId, + label: item.vertexName, + status: props?.job?.jobStatus + } + const id = 'node-' + item.vertexId + const ports = [] as Port[] + if (colNumber !== 0) { + ports.push({ + id: `${id}-left`, + group: 'left' + }) + } + if (colNumber !== row.length - 1) { + ports.push({ + id: `${id}-right`, + group: 'right' + }) + } + items.push({ + id, + shape: 'dag-node', + x: colNumber * offsetX, + y: rowNumber * offsetY, + data, + ports + }) + }) + }) + + const cells: Cell[] = [] + items.forEach((item) => { + if (item.shape === 'dag-node') { + cells.push(graph.createNode(item)) + } else { + cells.push(graph.createEdge(item)) + } + }) + graph.resetCells(cells) + } + + // 显示节点状态 + const showNodeStatus = async (statusList: NodeStatus[][]) => { + const status = statusList[Math.floor(Math.random() * statusList.length)] + status?.forEach((item) => { + const { id, status } = item + const node = graph.getCellById(`node-${id}`) + const data = node.getData() as NodeStatus + node.setData({ + ...data, + status + }) + }) + if (!status) return + setTimeout(() => { + showNodeStatus(statusList) + }, 5000) + } + + setTimeout(() => { + init() + graph.centerContent() + }, 500) + }) + + return () =>
    + } +}) diff --git a/seatunnel-engine/seatunnel-engine-ui/src/components/job-log/index.tsx b/seatunnel-engine/seatunnel-engine-ui/src/components/job-log/index.tsx new file mode 100644 index 00000000000..b5d2d0e90db --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/src/components/job-log/index.tsx @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getJobLogs } from '@/service/job-log' +import type { JobLog } from '@/service/job-log/types' +import { NCollapse, NCollapseItem, NSpace } from 'naive-ui' +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + props: { + jobId: { + type: String, + required: true + } + }, + setup(props) { + const logList = ref([] as JobLog[]) + getJobLogs(props.jobId).then((res) => (logList.value = res)) + return () => ( +
    + + {logList.value.map((log) => ( + +