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

jt2594838 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/dev/1.3 by this push:
     new 1794e0c128e [To dev/1.3] Support target database in write-back sink 
(#18086)
1794e0c128e is described below

commit 1794e0c128e5fbdbbc96cb51c8153e11b6209d71
Author: Caideyipi <[email protected]>
AuthorDate: Fri Jul 3 09:36:28 2026 +0800

    [To dev/1.3] Support target database in write-back sink (#18086)
    
    * Support target database in write-back sink (#18058)
    
    * Support target database in write back sink
    
    * Validate write-back sink target database
    
    * Cover write-back sink database validation semantics
    
    * Update WriteBackSink.java
    
    * Address write-back sink review comments
    
    * Clarify write-back sink target database normalization
    
    * spotless
---
 .../pipe/it/manual/IoTDBPipeWriteBackSinkIT.java   |  96 ++++++++++
 .../sink/protocol/writeback/WriteBackSink.java     | 196 ++++++++++++++++++++-
 2 files changed, 290 insertions(+), 2 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/manual/IoTDBPipeWriteBackSinkIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/manual/IoTDBPipeWriteBackSinkIT.java
new file mode 100644
index 00000000000..dda55ad4871
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/manual/IoTDBPipeWriteBackSinkIT.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.iotdb.pipe.it.manual;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.client.sync.SyncConfigNodeIServiceClient;
+import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq;
+import org.apache.iotdb.db.it.utils.TestUtils;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.MultiClusterIT2ManualCreateSchema;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({MultiClusterIT2ManualCreateSchema.class})
+public class IoTDBPipeWriteBackSinkIT extends AbstractPipeDualManualIT {
+
+  @Test
+  public void testWriteBackSinkWithTargetDatabaseForTreeModel() throws 
Exception {
+    TestUtils.executeNonQueries(
+        senderEnv,
+        Arrays.asList(
+            "create database root.source",
+            "create timeseries root.source.d1.s1 with 
datatype=INT32,encoding=PLAIN",
+            "create database root.target.db",
+            "create timeseries root.target.db.d1.s1 with 
datatype=INT32,encoding=PLAIN"),
+        null);
+
+    try (final SyncConfigNodeIServiceClient client =
+        (SyncConfigNodeIServiceClient) 
senderEnv.getLeaderConfigNodeConnection()) {
+      final Map<String, String> sourceAttributes = new HashMap<>();
+      final Map<String, String> processorAttributes = new HashMap<>();
+      final Map<String, String> sinkAttributes = new HashMap<>();
+
+      sourceAttributes.put("extractor.inclusion", "data.insert");
+      sourceAttributes.put("extractor.forwarding-pipe-requests", "false");
+      sourceAttributes.put("extractor.path", "root.source.**");
+      sourceAttributes.put("user", "root");
+
+      sinkAttributes.put("sink", "write-back-sink");
+      sinkAttributes.put("sink.database", "root.target.db");
+      sinkAttributes.put("user", "root");
+
+      final TSStatus status =
+          client.createPipe(
+              new TCreatePipeReq("testPipe", sinkAttributes)
+                  .setExtractorAttributes(sourceAttributes)
+                  .setProcessorAttributes(processorAttributes));
+      Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.getCode());
+
+      Assert.assertEquals(
+          TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
client.startPipe("testPipe").getCode());
+    }
+
+    TestUtils.executeNonQueries(
+        senderEnv,
+        Arrays.asList(
+            "insert into root.source.d1(time, s1) values (1, 1)",
+            "insert into root.source.d1(time, s1) values (2, 2)",
+            "flush"),
+        null);
+
+    TestUtils.assertDataEventuallyOnEnv(
+        senderEnv,
+        "select * from root.target.db.**",
+        "Time,root.target.db.d1.s1,",
+        Collections.unmodifiableSet(new HashSet<>(Arrays.asList("1,1,", 
"2,2,"))));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/writeback/WriteBackSink.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/writeback/WriteBackSink.java
index 269d0059c66..f5a7ad2ff31 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/writeback/WriteBackSink.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/writeback/WriteBackSink.java
@@ -20,8 +20,13 @@
 package org.apache.iotdb.db.pipe.sink.protocol.writeback;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.conf.IoTDBConstant;
+import org.apache.iotdb.commons.exception.IllegalPathException;
 import 
org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkNonReportTimeConfigurableException;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.utils.PathUtils;
 import org.apache.iotdb.db.auth.AuthorityChecker;
+import org.apache.iotdb.db.conf.IoTDBConfig;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.pipe.event.common.heartbeat.PipeHeartbeatEvent;
 import 
org.apache.iotdb.db.pipe.event.common.tablet.PipeInsertNodeTabletInsertionEvent;
@@ -36,7 +41,11 @@ import 
org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
 import 
org.apache.iotdb.db.queryengine.plan.analyze.schema.ClusterSchemaFetcher;
 import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode;
 import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement;
+import 
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertMultiTabletsStatement;
+import 
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsOfOneDeviceStatement;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement;
 import 
org.apache.iotdb.db.queryengine.plan.statement.pipe.PipeEnrichedStatement;
+import org.apache.iotdb.db.schemaengine.schemaregion.utils.MetaFormatUtils;
 import 
org.apache.iotdb.db.storageengine.dataregion.wal.exception.WALPipeException;
 import org.apache.iotdb.pipe.api.PipeConnector;
 import 
org.apache.iotdb.pipe.api.customizer.configuration.PipeConnectorRuntimeConfiguration;
@@ -52,21 +61,93 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Objects;
+
+import static 
org.apache.iotdb.commons.conf.IoTDBConstant.MAX_DATABASE_NAME_LENGTH;
 
 public class WriteBackSink implements PipeConnector {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(WriteBackSink.class);
+  private static final String CONNECTOR_IOTDB_DATABASE_KEY = 
"connector.database";
+  private static final String SINK_IOTDB_DATABASE_KEY = "sink.database";
+
+  private String targetTreeModelDatabaseName;
 
   @Override
   public void validate(final PipeParameterValidator validator) throws 
Exception {
-    // Do nothing
+    validator.validateSynonymAttributes(
+        Collections.singletonList(CONNECTOR_IOTDB_DATABASE_KEY),
+        Collections.singletonList(SINK_IOTDB_DATABASE_KEY),
+        false);
+
+    final String targetDatabase =
+        validator
+            .getParameters()
+            .getStringByKeys(CONNECTOR_IOTDB_DATABASE_KEY, 
SINK_IOTDB_DATABASE_KEY);
+    if (Objects.nonNull(targetDatabase)) {
+      validateTargetDatabase(targetDatabase);
+    }
   }
 
   @Override
   public void customize(
       final PipeParameters parameters, final PipeConnectorRuntimeConfiguration 
configuration)
       throws Exception {
-    // Do nothing
+    final String targetDatabase =
+        parameters.getStringByKeys(CONNECTOR_IOTDB_DATABASE_KEY, 
SINK_IOTDB_DATABASE_KEY);
+    if (Objects.nonNull(targetDatabase)) {
+      targetTreeModelDatabaseName = validateTargetDatabase(targetDatabase);
+    }
+  }
+
+  private static String validateTargetDatabase(final String targetDatabase) {
+    final String trimmedTargetDatabase = targetDatabase.trim();
+    if (trimmedTargetDatabase.startsWith(IoTDBConstant.PATH_ROOT + ".")) {
+      return validateAndNormalizeTreeModelDatabaseName(trimmedTargetDatabase);
+    }
+
+    try {
+      PathUtils.checkAndReturnSingleMeasurement(trimmedTargetDatabase);
+      return validateAndNormalizeTreeModelDatabaseName(
+          IoTDBConstant.PATH_ROOT
+              + IoTDBConstant.PATH_SEPARATOR
+              + trimmedTargetDatabase.toLowerCase(Locale.ENGLISH));
+    } catch (final Exception e) {
+      throw new PipeException(
+          String.format("The target database %s is invalid.", targetDatabase), 
e);
+    }
+  }
+
+  private static String validateAndNormalizeTreeModelDatabaseName(final String 
databaseName) {
+    try {
+      final PartialPath databasePath = new PartialPath(databaseName);
+      final String[] nodes = databasePath.getNodes();
+      if (nodes.length <= 1 || !IoTDBConstant.PATH_ROOT.equals(nodes[0])) {
+        throw new IllegalPathException(
+            databaseName, "the database name in tree model must start with 
'root.'.");
+      }
+
+      final String normalizedDatabaseName = databasePath.getFullPath();
+      MetaFormatUtils.checkDatabase(normalizedDatabaseName);
+
+      if (normalizedDatabaseName.length() > MAX_DATABASE_NAME_LENGTH) {
+        throw new IllegalPathException(
+            normalizedDatabaseName,
+            "the length of database name shall not exceed " + 
MAX_DATABASE_NAME_LENGTH);
+      }
+      return normalizedDatabaseName;
+    } catch (final Exception e) {
+      throw new PipeException(
+          String.format(
+              "The tree model database %s is invalid. The database name should 
match %s "
+                  + "and the length should not exceed %s.",
+              databaseName, IoTDBConfig.STORAGE_GROUP_PATTERN, 
MAX_DATABASE_NAME_LENGTH),
+          e);
+    }
   }
 
   @Override
@@ -129,6 +210,7 @@ public class WriteBackSink implements PipeConnector {
     final InsertNode insertNode = 
pipeInsertNodeTabletInsertionEvent.getInsertNode();
     final InsertBaseStatement statement =
         
PipeTransferTabletInsertNodeReq.toTPipeTransferRawReq(insertNode).constructStatement();
+    rewriteTreeModelDatabaseNameIfNecessary(statement, null);
     status = statement.isEmpty() ? RpcUtils.SUCCESS_STATUS : 
executeStatement(statement);
 
     if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
@@ -160,6 +242,8 @@ public class WriteBackSink implements PipeConnector {
                 pipeRawTabletInsertionEvent.convertToTablet(),
                 pipeRawTabletInsertionEvent.isAligned())
             .constructStatement();
+    rewriteTreeModelDatabaseNameIfNecessary(
+        statement, pipeRawTabletInsertionEvent.getTreeModelDatabaseName());
     final TSStatus status =
         statement.isEmpty() ? RpcUtils.SUCCESS_STATUS : 
executeStatement(statement);
 
@@ -181,6 +265,114 @@ public class WriteBackSink implements PipeConnector {
     throw new PipeException(exceptionMessage);
   }
 
+  private InsertBaseStatement rewriteTreeModelDatabaseNameIfNecessary(
+      final InsertBaseStatement statement, final String 
sourceTreeModelDatabaseName) {
+    if (Objects.isNull(targetTreeModelDatabaseName)) {
+      return statement;
+    }
+
+    rewriteTreeModelDatabaseName(statement, sourceTreeModelDatabaseName);
+    return statement;
+  }
+
+  private void rewriteTreeModelDatabaseName(
+      final InsertBaseStatement statement, final String 
sourceTreeModelDatabaseName) {
+    if (statement instanceof InsertRowsStatement) {
+      ((InsertRowsStatement) statement)
+          .getInsertRowStatementList()
+          .forEach(
+              rowStatement ->
+                  rewriteTreeModelDatabaseName(rowStatement, 
sourceTreeModelDatabaseName));
+      return;
+    }
+
+    if (statement instanceof InsertRowsOfOneDeviceStatement) {
+      final InsertRowsOfOneDeviceStatement insertRowsOfOneDeviceStatement =
+          (InsertRowsOfOneDeviceStatement) statement;
+      insertRowsOfOneDeviceStatement
+          .getInsertRowStatementList()
+          .forEach(
+              rowStatement ->
+                  rewriteTreeModelDatabaseName(rowStatement, 
sourceTreeModelDatabaseName));
+      insertRowsOfOneDeviceStatement.setDevicePath(
+          rewriteTreeModelDevicePath(
+              insertRowsOfOneDeviceStatement.getDevicePath(), 
sourceTreeModelDatabaseName));
+      return;
+    }
+
+    if (statement instanceof InsertMultiTabletsStatement) {
+      ((InsertMultiTabletsStatement) statement)
+          .getInsertTabletStatementList()
+          .forEach(
+              tabletStatement ->
+                  rewriteTreeModelDatabaseName(tabletStatement, 
sourceTreeModelDatabaseName));
+      return;
+    }
+
+    statement.setDevicePath(
+        rewriteTreeModelDevicePath(statement.getDevicePath(), 
sourceTreeModelDatabaseName));
+  }
+
+  private PartialPath rewriteTreeModelDevicePath(
+      final PartialPath devicePath, final String sourceTreeModelDatabaseName) {
+    if (Objects.isNull(devicePath)) {
+      return devicePath;
+    }
+
+    final String normalizedSourceTreeModelDatabaseName =
+        Objects.nonNull(sourceTreeModelDatabaseName)
+            ? 
validateAndNormalizeTreeModelDatabaseName(sourceTreeModelDatabaseName)
+            : inferTreeModelDatabaseName(devicePath);
+    if (Objects.isNull(normalizedSourceTreeModelDatabaseName)) {
+      return devicePath;
+    }
+
+    try {
+      final String[] sourceDatabaseNodes =
+          new PartialPath(normalizedSourceTreeModelDatabaseName).getNodes();
+      final String[] targetDatabaseNodes = new 
PartialPath(targetTreeModelDatabaseName).getNodes();
+      final String[] deviceNodes = devicePath.getNodes();
+      if (!startsWith(deviceNodes, sourceDatabaseNodes)) {
+        return devicePath;
+      }
+
+      final ArrayList<String> rebasedNodes =
+          new ArrayList<>(
+              targetDatabaseNodes.length + deviceNodes.length - 
sourceDatabaseNodes.length);
+      rebasedNodes.addAll(Arrays.asList(targetDatabaseNodes));
+      rebasedNodes.addAll(
+          Arrays.asList(deviceNodes).subList(sourceDatabaseNodes.length, 
deviceNodes.length));
+      return new PartialPath(rebasedNodes.toArray(new String[0]));
+    } catch (final Exception e) {
+      throw new PipeException(
+          String.format(
+              "Failed to rewrite tree model database from %s to %s for device 
path %s.",
+              normalizedSourceTreeModelDatabaseName, 
targetTreeModelDatabaseName, devicePath),
+          e);
+    }
+  }
+
+  private String inferTreeModelDatabaseName(final PartialPath devicePath) {
+    final String[] deviceNodes = devicePath.getNodes();
+    if (deviceNodes.length < 2 || 
!IoTDBConstant.PATH_ROOT.equals(deviceNodes[0])) {
+      return null;
+    }
+
+    return IoTDBConstant.PATH_ROOT + IoTDBConstant.PATH_SEPARATOR + 
deviceNodes[1];
+  }
+
+  private static boolean startsWith(final String[] nodes, final String[] 
prefixNodes) {
+    if (nodes.length < prefixNodes.length) {
+      return false;
+    }
+    for (int i = 0; i < prefixNodes.length; ++i) {
+      if (!Objects.equals(nodes[i], prefixNodes[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   private TSStatus executeStatement(final InsertBaseStatement statement) {
     return Coordinator.getInstance()
         .executeForTreeModel(

Reply via email to