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

JackieTien97 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 bf1ea20e235 Prevent ancestor devices from matching descendant template 
mounts (#19028)
bf1ea20e235 is described below

commit bf1ea20e2356734231a626ffa1c0a76373b59056
Author: alpass163 <[email protected]>
AuthorDate: Wed Oct 28 10:35:59 2026 +0800

    Prevent ancestor devices from matching descendant template mounts (#19028)
---
 .../template/ClusterTemplateManager.java           |  2 +-
 .../BatchActivateTemplateStatementTest.java        |  6 ++-
 .../template/ClusterTemplateManagerTest.java       | 62 ++++++++++++++++++++++
 .../org/apache/iotdb/commons/path/PartialPath.java | 10 ++++
 .../apache/iotdb/commons/path/PartialPathTest.java | 19 +++++++
 5 files changed, 97 insertions(+), 2 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManager.java
index 816effce329..25f699e2770 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManager.java
@@ -292,7 +292,7 @@ public class ClusterTemplateManager implements 
ITemplateManager {
     readWriteLock.readLock().lock();
     try {
       for (PartialPath templateSetPath : pathSetTemplateMap.keySet()) {
-        if (devicePath.startsWithOrPrefixOf(templateSetPath.getNodes())) {
+        if (devicePath.startsWithOrEqual(templateSetPath.getNodes())) {
           return new Pair<>(
               templateIdMap.get(pathSetTemplateMap.get(templateSetPath)), 
templateSetPath);
         }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/template/BatchActivateTemplateStatementTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/template/BatchActivateTemplateStatementTest.java
index 8ffc5f8691f..02ae8c1e7e9 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/template/BatchActivateTemplateStatementTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/template/BatchActivateTemplateStatementTest.java
@@ -74,7 +74,9 @@ public class BatchActivateTemplateStatementTest {
                 new PartialPath("root.sg.v1.d1"),
                 new PartialPath("root.sg.v2.d1"),
                 // no template is set on this path, so it contributes no path
-                new PartialPath("root.sg.v3.d1")));
+                new PartialPath("root.sg.v3.d1"),
+                // A template mounted below this ancestor must not be selected.
+                new PartialPath("root.sg")));
 
     final List<PartialPath> paths = statement.getPaths();
 
@@ -96,6 +98,8 @@ public class BatchActivateTemplateStatementTest {
         new PartialPath("root.sg.v1.d1"), new Pair<>(template1, new 
PartialPath("root.sg.v1")));
     deviceMap.put(
         new PartialPath("root.sg.v2.d1"), new Pair<>(template2, new 
PartialPath("root.sg.v2")));
+    // A template mounted below this ancestor must not be selected.
+    deviceMap.put(new PartialPath("root.sg"), new Pair<>(template1, new 
PartialPath("root.sg.v1")));
 
     final InternalBatchActivateTemplateStatement statement =
         new InternalBatchActivateTemplateStatement(deviceMap);
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManagerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManagerTest.java
index bc6cd1629b0..909ce7f544e 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManagerTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/template/ClusterTemplateManagerTest.java
@@ -22,6 +22,8 @@ package org.apache.iotdb.db.schemaengine.template;
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.client.IClientManager;
 import org.apache.iotdb.commons.consensus.ConfigRegionId;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.schema.template.Template;
 import org.apache.iotdb.db.protocol.client.ConfigNodeClient;
 import org.apache.iotdb.db.protocol.client.ConfigNodeClientManager;
 import org.apache.iotdb.db.protocol.client.ConfigNodeInfo;
@@ -34,6 +36,7 @@ import org.apache.thrift.TException;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.file.metadata.enums.CompressionType;
 import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.utils.Pair;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,6 +49,7 @@ import org.powermock.modules.junit4.PowerMockRunner;
 import java.util.Collections;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -118,6 +122,64 @@ public class ClusterTemplateManagerTest {
     assertTrue(status.getMessage().contains("test_template"));
   }
 
+  @Test
+  public void testCheckTemplateSetInfo() throws Exception {
+    ClusterTemplateManager manager = new ClusterTemplateManager();
+    Template template =
+        new Template(
+            "test_template",
+            Collections.singletonList("s1"),
+            Collections.singletonList(TSDataType.INT32),
+            Collections.singletonList(TSEncoding.PLAIN),
+            Collections.singletonList(CompressionType.UNCOMPRESSED));
+    template.setId(1);
+    PartialPath setPath = new PartialPath("root.db1.sg01.plant01");
+    manager.addTemplateSetInfo(
+        TemplateInternalRPCUtil.generateAddTemplateSetInfoBytes(template, 
setPath.getFullPath()));
+
+    Pair<Template, PartialPath> onMount = 
manager.checkTemplateSetInfo(setPath);
+    assertEquals(template.getId(), onMount.left.getId());
+    assertEquals(setPath, onMount.right);
+    Pair<Template, PartialPath> onDescendant =
+        manager.checkTemplateSetInfo(new 
PartialPath("root.db1.sg01.plant01.d1"));
+    assertEquals(template.getId(), onDescendant.left.getId());
+    assertEquals(setPath, onDescendant.right);
+
+    assertNull(manager.checkTemplateSetInfo(new PartialPath("root.db1.sg01")));
+    assertNull(manager.checkTemplateSetInfo(new PartialPath("root.db1")));
+    assertNull(manager.checkTemplateSetInfo(new PartialPath("root")));
+    assertNull(manager.checkTemplateSetInfo(new 
PartialPath("root.db1.sg01.plant02.d1")));
+    assertNull(manager.checkTemplateSetInfo(new 
PartialPath("root.db1.sg01.plant01_extra.d1")));
+    assertNull(manager.checkTemplateSetInfo(new 
PartialPath("root.db2.sg01.plant01.d1")));
+
+    // An ancestor is still a conflict when creating a measurement, not a 
template device.
+    assertEquals(
+        setPath,
+        manager.checkTemplateSetAndPreSetInfo(new 
PartialPath("root.db1.sg01"), null).right);
+
+    Template otherTemplate =
+        new Template(
+            "other_template",
+            Collections.singletonList("s2"),
+            Collections.singletonList(TSDataType.INT32),
+            Collections.singletonList(TSEncoding.PLAIN),
+            Collections.singletonList(CompressionType.UNCOMPRESSED));
+    otherTemplate.setId(2);
+    PartialPath otherSetPath = new PartialPath("root.db1.sg01.plant02");
+    manager.addTemplateSetInfo(
+        TemplateInternalRPCUtil.generateAddTemplateSetInfoBytes(
+            otherTemplate, otherSetPath.getFullPath()));
+
+    // A common ancestor must not pick either descendant's template.
+    assertNull(manager.checkTemplateSetInfo(new PartialPath("root.db1.sg01")));
+    assertEquals(
+        otherTemplate.getId(),
+        manager.checkTemplateSetInfo(new 
PartialPath("root.db1.sg01.plant02.d1")).left.getId());
+    assertEquals(
+        template.getId(),
+        manager.checkTemplateSetInfo(new 
PartialPath("root.db1.sg01.plant01.d1")).left.getId());
+  }
+
   private static TSStatus assertRpcFailure(
       RuntimeException exception, TException rpcFailure, TSStatusCode 
statusCode) {
     TSStatus status =
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/PartialPath.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/PartialPath.java
index 9041402be46..5fe2ce72e3b 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/PartialPath.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/PartialPath.java
@@ -941,6 +941,16 @@ public class PartialPath extends Path implements 
Comparable<Path>, Cloneable {
     return true;
   }
 
+  /** Returns whether the given nodes are a prefix of this path, including 
equality. */
+  public boolean startsWithOrEqual(String[] otherNodes) {
+    for (int i = 0; i < otherNodes.length; i++) {
+      if ((i >= nodes.length) || !nodes[i].equals(otherNodes[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   public boolean startsWith(String prefix) {
     return getFullPath().startsWith(prefix);
   }
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/path/PartialPathTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/path/PartialPathTest.java
index 777f06d2d39..b1102387b76 100644
--- 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/path/PartialPathTest.java
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/path/PartialPathTest.java
@@ -408,6 +408,25 @@ public class PartialPathTest {
     Assert.assertEquals("root", b.getFirstNode());
   }
 
+  @Test
+  public void testStartsWithOrEqual() throws IllegalPathException {
+    PartialPath setPath = new PartialPath("root.db1.sg01.plant01");
+    String[] setNodes = setPath.getNodes();
+
+    Assert.assertTrue(setPath.startsWithOrEqual(setNodes));
+    Assert.assertTrue(new 
PartialPath("root.db1.sg01.plant01.d1").startsWithOrEqual(setNodes));
+    Assert.assertFalse(new 
PartialPath("root.db1.sg01").startsWithOrEqual(setNodes));
+    Assert.assertFalse(new PartialPath("root").startsWithOrEqual(setNodes));
+    Assert.assertFalse(new 
PartialPath("root.db1.sg01.plant02").startsWithOrEqual(setNodes));
+    Assert.assertFalse(new 
PartialPath("root.db1.sg01.plant01_extra").startsWithOrEqual(setNodes));
+    Assert.assertFalse(new 
PartialPath("root.db2.sg01.plant01.d1").startsWithOrEqual(setNodes));
+    Assert.assertFalse(new 
PartialPath("root.db1.sg01.*").startsWithOrEqual(setNodes));
+    Assert.assertTrue(setPath.startsWithOrEqual(new String[0]));
+
+    // The bidirectional check is still needed for path-conflict detection.
+    Assert.assertTrue(new 
PartialPath("root.db1.sg01").startsWithOrPrefixOf(setNodes));
+  }
+
   @Test
   public void testAlterPrefixPath() throws IllegalPathException {
     // Plain path.

Reply via email to