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

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


The following commit(s) were added to refs/heads/master by this push:
     new df27bbc5001 Fix table delete routing to region leaders (#18779)
df27bbc5001 is described below

commit df27bbc5001ad61920228698dc33db3678d2232a
Author: Jiang Tian <[email protected]>
AuthorDate: Wed Sep 30 18:55:49 2026 +0800

    Fix table delete routing to region leaders (#18779)
    
    * Fix table delete routing to region leaders
    
    * spotless
---
 .../it/db/it/IoTDBDeletionTableClusterIT.java      | 152 +++++++++++++++++++++
 .../partition/GetRegionGroupsByTimeResp.java       |  25 ++++
 .../manager/partition/PartitionManager.java        |   3 +-
 3 files changed, 179 insertions(+), 1 deletion(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBDeletionTableClusterIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBDeletionTableClusterIT.java
new file mode 100644
index 00000000000..2ecd502a0a6
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBDeletionTableClusterIT.java
@@ -0,0 +1,152 @@
+/*
+ * 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.relational.it.db.it;
+
+import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+
+import org.awaitility.Awaitility;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.iotdb.itbase.env.BaseEnv.TABLE_SQL_DIALECT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(IoTDBTestRunner.class)
+@Category(TableClusterIT.class)
+public class IoTDBDeletionTableClusterIT {
+
+  private static final String DATABASE = "sc3";
+
+  @BeforeClass
+  public static void setUpClass() throws SQLException {
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setConfigNodeConsensusProtocolClass(ConsensusFactory.RATIS_CONSENSUS)
+        
.setSchemaRegionConsensusProtocolClass(ConsensusFactory.RATIS_CONSENSUS)
+        .setDataRegionConsensusProtocolClass(ConsensusFactory.IOT_CONSENSUS)
+        .setSchemaReplicationFactor(2)
+        .setDataReplicationFactor(2);
+    EnvFactory.getEnv().initClusterEnvironment(3, 3);
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE " + DATABASE);
+    }
+  }
+
+  @AfterClass
+  public static void tearDownClass() {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  /**
+   * deleting time=2 from three flushed devices must leave exactly (1,d1,1) 
and (3,d3,3), through
+   * every DataNode coordinator, both before and after another flush.
+   */
+  @Test
+  public void testDeleteByTimeAfterFlush() throws SQLException {
+    checkDeletionOnEveryDataNode("time_delete_", " WHERE time = 2", 1, 3);
+  }
+
+  /**
+   * an unconditional DELETE must remove all three flushed rows through every 
DataNode coordinator,
+   * and another flush must not make the deleted data visible again.
+   */
+  @Test
+  public void testDeleteAllAfterFlush() throws SQLException {
+    checkDeletionOnEveryDataNode("full_delete_", "");
+  }
+
+  private void checkDeletionOnEveryDataNode(
+      String tablePrefix, String predicate, int... remainingTimes) throws 
SQLException {
+    for (int i = 0; i < EnvFactory.getEnv().getDataNodeWrapperList().size(); 
i++) {
+      String table = DATABASE + "." + tablePrefix + i;
+      // Pin each DELETE to a different coordinator and use fresh data for 
every attempt.
+      try (Connection connection =
+              EnvFactory.getEnv()
+                  .getWriteOnlyConnectionWithSpecifiedDataNode(
+                      EnvFactory.getEnv().getDataNodeWrapper(i), 
TABLE_SQL_DIALECT);
+          Statement statement = connection.createStatement()) {
+        statement.execute("CREATE TABLE " + table + "(device_id STRING TAG, s1 
INT32 FIELD)");
+        statement.execute(
+            "INSERT INTO "
+                + table
+                + "(time, device_id, s1) VALUES 
(1,'d1',1),(2,'d2',2),(3,'d3',3)");
+        statement.execute("FLUSH");
+        assertRowsOnEveryDataNode(table, 1, 2, 3);
+
+        statement.execute("DELETE FROM " + table + predicate);
+        // Successful execution alone cannot detect the silent no-op reported 
in TDB-449.
+        // Allow replica propagation before concluding that the deletion did 
not take effect.
+        Awaitility.await()
+            .atMost(10, TimeUnit.SECONDS)
+            .untilAsserted(() -> assertRowsOnEveryDataNode(table, 
remainingTimes));
+        statement.execute("FLUSH");
+        assertRowsOnEveryDataNode(table, remainingTimes);
+      }
+    }
+  }
+
+  private void assertRowsOnEveryDataNode(String table, int... expectedTimes) 
throws SQLException {
+    for (DataNodeWrapper dataNode : 
EnvFactory.getEnv().getDataNodeWrapperList()) {
+      String context = table + " on " + dataNode.getIpAndPortString();
+      try (Connection connection =
+              EnvFactory.getEnv()
+                  .getConnection(
+                      dataNode,
+                      SessionConfig.DEFAULT_USER,
+                      SessionConfig.DEFAULT_PASSWORD,
+                      TABLE_SQL_DIALECT);
+          Statement statement = connection.createStatement()) {
+        try (ResultSet resultSet = statement.executeQuery("SELECT count(*) 
FROM " + table)) {
+          assertTrue(context, resultSet.next());
+          assertEquals(context, expectedTimes.length, resultSet.getLong(1));
+          assertFalse(context, resultSet.next());
+        }
+        try (ResultSet resultSet =
+            statement.executeQuery("SELECT time, device_id, s1 FROM " + table 
+ " ORDER BY time")) {
+          for (int time : expectedTimes) {
+            assertTrue(context, resultSet.next());
+            assertEquals(context, time, resultSet.getLong("time"));
+            assertEquals(context, "d" + time, 
resultSet.getString("device_id"));
+            assertEquals(context, time, resultSet.getInt("s1"));
+          }
+          assertFalse(context, resultSet.next());
+        }
+      }
+    }
+  }
+}
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/response/partition/GetRegionGroupsByTimeResp.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/response/partition/GetRegionGroupsByTimeResp.java
index 3166ec30df1..a51a9ef513c 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/response/partition/GetRegionGroupsByTimeResp.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/response/partition/GetRegionGroupsByTimeResp.java
@@ -19,12 +19,16 @@
 
 package org.apache.iotdb.confignode.consensus.response.partition;
 
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupId;
 import org.apache.iotdb.common.rpc.thrift.TRegionReplicaSet;
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.confignode.rpc.thrift.TGetRegionGroupsByTimeResp;
 import org.apache.iotdb.consensus.common.DataSet;
 import org.apache.iotdb.rpc.TSStatusCode;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 public class GetRegionGroupsByTimeResp implements DataSet {
@@ -43,6 +47,27 @@ public class GetRegionGroupsByTimeResp implements DataSet {
     return status;
   }
 
+  /** Return a response whose replica lists put the current leader first. */
+  public GetRegionGroupsByTimeResp reorderByLeader(
+      final Map<TConsensusGroupId, Integer> regionLeaderMap) {
+    if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      return this;
+    }
+    final Set<TRegionReplicaSet> reordered = new HashSet<>();
+    for (final TRegionReplicaSet replicaSet : regionReplicaSets) {
+      final TRegionReplicaSet copy = replicaSet.deepCopy();
+      final int leaderId = regionLeaderMap.getOrDefault(copy.getRegionId(), 
-1);
+      for (int i = 0; i < copy.getDataNodeLocationsSize(); i++) {
+        if (copy.getDataNodeLocations().get(i).getDataNodeId() == leaderId) {
+          Collections.swap(copy.getDataNodeLocations(), 0, i);
+          break;
+        }
+      }
+      reordered.add(copy);
+    }
+    return new GetRegionGroupsByTimeResp(status, reordered);
+  }
+
   public TGetRegionGroupsByTimeResp convertToRpcResp() {
     TGetRegionGroupsByTimeResp resp = new TGetRegionGroupsByTimeResp();
     resp.setStatus(status);
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/partition/PartitionManager.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/partition/PartitionManager.java
index 249a4f9bd0e..48ebe498a50 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/partition/PartitionManager.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/partition/PartitionManager.java
@@ -1329,7 +1329,8 @@ public class PartitionManager {
     final GetRegionGroupsByTimePlan plan =
         new GetRegionGroupsByTimePlan(req.getDatabase(), req.getStartTime(), 
req.getEndTime());
     try {
-      return (GetRegionGroupsByTimeResp) getConsensusManager().read(plan);
+      return ((GetRegionGroupsByTimeResp) getConsensusManager().read(plan))
+          .reorderByLeader(getLoadManager().getRegionLeaderMap());
     } catch (final ConsensusException e) {
       LOGGER.warn(CONSENSUS_READ_ERROR, e);
       final TSStatus res = new 
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode());

Reply via email to