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 a498d7d2a46 Fix schema mutation validation, accounting and deletion 
performance (#18861)
a498d7d2a46 is described below

commit a498d7d2a4608993c5c7d0c11f954e77b0554900
Author: Caideyipi <[email protected]>
AuthorDate: Wed Oct 14 14:34:34 2026 +0800

    Fix schema mutation validation, accounting and deletion performance (#18861)
    
    * Fix schema mutation validation, accounting and deletion performance
    
    Validate aligned batch aliases, keep blacklist statistics idempotent, avoid 
repeated child scans during deletion, and correct and reuse device attribute 
storage. Add regression coverage for retries, snapshots, nested devices and all 
schema engine modes.
    
    * Fix aligned memchunk tests for current API
---
 .../schema/source/DeviceBlackListConstructor.java  |  14 ++-
 .../attribute/DeviceAttributeStore.java            |  75 ++++++++----
 .../mtree/impl/mem/MTreeBelowSGMemoryImpl.java     |  23 +---
 .../impl/mem/mnode/impl/BasicInternalMNode.java    |  60 +++++++---
 .../mtree/impl/pbtree/MTreeBelowSGCachedImpl.java  |   1 +
 .../schemaregion/utils/MetaFormatUtils.java        |  23 ++++
 .../schemaRegion/SchemaRegionAliasAndTagTest.java  |  62 ++++++++++
 .../source/DeviceBlackListConstructorTest.java     | 120 +++++++++++++++++++
 .../attribute/DeviceAttributeStoreTest.java        | 129 +++++++++++++++++++++
 .../mtree/impl/mem/MTreeBelowSGMemoryImplTest.java | 129 +++++++++++++++++++++
 10 files changed, 575 insertions(+), 61 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/schema/source/DeviceBlackListConstructor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/schema/source/DeviceBlackListConstructor.java
index 3391f3e4080..b8f315b3a0c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/schema/source/DeviceBlackListConstructor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/schema/source/DeviceBlackListConstructor.java
@@ -64,9 +64,8 @@ public class DeviceBlackListConstructor extends DeviceUpdater 
{
 
   public void handleDeviceNode(final IDeviceMNode<IMemMNode> node) throws 
MetadataException {
     if (withoutFilter()) {
-      node.preDeactivateSelfOrTemplate();
+      preDeleteDevice(node);
       preDeletedNum++;
-      regionStatistics.decreaseTableDevice(tableName, 1);
       return;
     }
     nodes.add(node);
@@ -76,14 +75,21 @@ public class DeviceBlackListConstructor extends 
DeviceUpdater {
   @Override
   protected void update() {
     preDeletedNum += indexes.size();
-    regionStatistics.decreaseTableDevice(tableName, indexes.size());
     for (final Integer index : indexes) {
-      nodes.get(index).preDeactivateSelfOrTemplate();
+      preDeleteDevice(nodes.get(index));
     }
     nodes.clear();
     super.clear();
   }
 
+  private void preDeleteDevice(final IDeviceMNode<IMemMNode> node) {
+    // Retried requests still count matches, but statistics only change on the 
first transition.
+    if (!node.isPreDeactivateSelfOrTemplate()) {
+      node.preDeactivateSelfOrTemplate();
+      regionStatistics.decreaseTableDevice(tableName, 1);
+    }
+  }
+
   public long getPreDeletedNum() {
     return preDeletedNum;
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStore.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStore.java
index 2c71005fab2..8b9e65b5e2d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStore.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStore.java
@@ -43,6 +43,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -57,6 +58,9 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
   private static final long MAP_SIZE = 
RamUsageEstimator.shallowSizeOfInstance(HashMap.class);
 
   private final List<Map<String, Binary>> deviceAttributeList = new 
ArrayList<>();
+  // Reuse holes without moving live pointers or changing the snapshot format.
+  private BitSet freeAttributePointers = new BitSet(0);
+  private int nextFreeAttributePointer;
 
   private final MemSchemaRegionStatistics regionStatistics;
 
@@ -67,8 +71,10 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
   // It's OK not to calculate the region statistics here. The caller shall 
ensure that the
   // statistics is cleared at the outside.
   @Override
-  public void clear() {
+  public synchronized void clear() {
     deviceAttributeList.clear();
+    freeAttributePointers = new BitSet(0);
+    nextFreeAttributePointer = 0;
   }
 
   @Override
@@ -113,7 +119,7 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
   }
 
   @Override
-  public void loadFromSnapshot(final File snapshotDir) throws IOException {
+  public synchronized void loadFromSnapshot(final File snapshotDir) throws 
IOException {
     final File snapshot =
         SystemFileFactory.INSTANCE.getFile(snapshotDir, 
SchemaConstant.DEVICE_ATTRIBUTE_SNAPSHOT);
     if (!snapshot.exists()) {
@@ -133,7 +139,7 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
   public synchronized int createAttribute(
       final List<String> nameList, final Object[] valueList, final String 
tableName) {
     // todo implement storage for device of diverse data types
-    long memUsage = MAP_SIZE + RamUsageEstimator.NUM_BYTES_OBJECT_REF;
+    long memUsage = MAP_SIZE;
     final Map<String, Binary> attributeMap = new HashMap<>();
     for (int i = 0; i < nameList.size(); i++) {
       if (valueList.length <= i) {
@@ -147,19 +153,29 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
       memUsage += MemUsageUtil.computeKVMemUsageInMap(nameList.get(i), value);
       addTableAttributeMemory(tableName, value.ramBytesUsed());
     }
-    deviceAttributeList.add(attributeMap);
+    int pointer = freeAttributePointers.nextSetBit(nextFreeAttributePointer);
+    if (pointer < 0) {
+      pointer = deviceAttributeList.size();
+      deviceAttributeList.add(attributeMap);
+      memUsage += RamUsageEstimator.NUM_BYTES_OBJECT_REF;
+    } else {
+      deviceAttributeList.set(pointer, attributeMap);
+      freeAttributePointers.clear(pointer);
+      nextFreeAttributePointer = pointer + 1;
+    }
     requestMemory(memUsage);
-    return deviceAttributeList.size() - 1;
+    return pointer;
   }
 
   @Override
-  public Map<String, Binary> alterAttribute(
+  public synchronized Map<String, Binary> alterAttribute(
       final int pointer,
       final List<String> nameList,
       final Object[] valueList,
       final String tableName) {
     // todo implement storage for device of diverse data types
     long memUsageDelta = 0L;
+    long valueMemUsageDelta = 0L;
     long originMemUsage;
     long updatedMemUsage;
     final Map<String, Binary> updateMap = new HashMap<>();
@@ -173,11 +189,12 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
         continue;
       }
       final Binary value = (Binary) valueList[i];
+      final Binary oldValue = attributeMap.get(key);
+      valueMemUsageDelta +=
+          (value == null ? 0 : value.ramBytesUsed())
+              - (oldValue == null ? 0 : oldValue.ramBytesUsed());
 
-      originMemUsage =
-          attributeMap.containsKey(key)
-              ? MemUsageUtil.computeKVMemUsageInMap(key, attributeMap.get(key))
-              : 0;
+      originMemUsage = oldValue == null ? 0 : 
MemUsageUtil.computeKVMemUsageInMap(key, oldValue);
       if (value != null) {
         if (!Objects.equals(value, attributeMap.put(key, value))) {
           updateMap.put(key, value);
@@ -193,28 +210,33 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
     }
     if (memUsageDelta > 0) {
       requestMemory(memUsageDelta);
-      addTableAttributeMemory(tableName, memUsageDelta);
     } else if (memUsageDelta < 0) {
       releaseMemory(-memUsageDelta);
-      decreaseTableAttributeMemory(tableName, -memUsageDelta);
+    }
+    if (valueMemUsageDelta > 0) {
+      addTableAttributeMemory(tableName, valueMemUsageDelta);
+    } else if (valueMemUsageDelta < 0) {
+      decreaseTableAttributeMemory(tableName, -valueMemUsageDelta);
     }
     return updateMap;
   }
 
   @Override
-  public void removeAttribute(final int pointer, final String tableName) {
-    releaseMemory(
-        MAP_SIZE + 
UpdateDetailContainer.sizeOfMapEntries(deviceAttributeList.get(pointer)));
+  public synchronized void removeAttribute(final int pointer, final String 
tableName) {
+    final Map<String, Binary> attributeMap = deviceAttributeList.get(pointer);
+    if (attributeMap == null) {
+      return;
+    }
+    releaseMemory(MAP_SIZE + 
UpdateDetailContainer.sizeOfMapEntries(attributeMap));
     decreaseTableAttributeMemory(
         tableName,
-        deviceAttributeList.get(pointer).values().stream()
-            .map(UpdateDetailContainer::sizeOf)
-            .reduce(0L, Long::sum));
+        
attributeMap.values().stream().map(UpdateDetailContainer::sizeOf).reduce(0L, 
Long::sum));
     deviceAttributeList.set(pointer, null);
+    markFreeAttributePointer(pointer);
   }
 
   @Override
-  public void removeAttribute(
+  public synchronized void removeAttribute(
       final int pointer, final String attributeName, final String tableName) {
     final Map<String, Binary> attributeMap = deviceAttributeList.get(pointer);
     if (Objects.isNull(attributeMap)) {
@@ -228,12 +250,12 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
   }
 
   @Override
-  public Map<String, Binary> getAttributes(final int pointer) {
+  public synchronized Map<String, Binary> getAttributes(final int pointer) {
     return deviceAttributeList.get(pointer);
   }
 
   @Override
-  public Binary getAttributes(final int pointer, final String name) {
+  public synchronized Binary getAttributes(final int pointer, final String 
name) {
     return deviceAttributeList.get(pointer).get(name);
   }
 
@@ -271,6 +293,9 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
     for (int i = 0; i < size; i++) {
       final Map<String, Binary> attributeMap = readMap(inputStream, false);
       deviceAttributeList.add(attributeMap);
+      if (attributeMap == null) {
+        markFreeAttributePointer(i);
+      }
       requestMemory(
           RamUsageEstimator.NUM_BYTES_OBJECT_REF
               + (Objects.nonNull(attributeMap)
@@ -279,6 +304,14 @@ public class DeviceAttributeStore implements 
IDeviceAttributeStore {
     }
   }
 
+  private void markFreeAttributePointer(final int pointer) {
+    final int oldCapacity = freeAttributePointers.size();
+    freeAttributePointers.set(pointer);
+    // Account for bitmap storage as well as the retained references in 
deviceAttributeList.
+    requestMemory((freeAttributePointers.size() - oldCapacity) / Byte.SIZE);
+    nextFreeAttributePointer = Math.min(nextFreeAttributePointer, pointer);
+  }
+
   public static Map<String, Binary> readMap(final InputStream inputStream, 
final boolean concurrent)
       throws IOException {
     final int length = ReadWriteIOUtils.readInt(inputStream);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
index f920c6d0efe..a5007d8e998 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
@@ -54,6 +54,7 @@ import 
org.apache.iotdb.db.queryengine.plan.statement.component.Ordering;
 import org.apache.iotdb.db.schemaengine.metric.SchemaRegionMemMetric;
 import org.apache.iotdb.db.schemaengine.rescon.MemSchemaRegionStatistics;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.IMemMNode;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.impl.BasicInternalMNode;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.info.TableDeviceInfo;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.loader.MNodeFactoryLoader;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.traverser.collector.EntityCollector;
@@ -403,6 +404,7 @@ public class MTreeBelowSGMemoryImpl {
       throws MetadataException {
     final List<IMeasurementMNode<IMemMNode>> measurementMNodeList = new 
ArrayList<>();
     MetaFormatUtils.checkSchemaMeasurementNames(measurements);
+    MetaFormatUtils.checkSchemaMeasurementAliases(devicePath, measurements, 
aliasList);
     final IMemMNode deviceParent = checkAndAutoCreateInternalPath(devicePath);
 
     // synchronize check and add, we need addChild operation be atomic.
@@ -706,28 +708,13 @@ public class MTreeBelowSGMemoryImpl {
   public void deleteEmptyInternalMNode(final IDeviceMNode<IMemMNode> 
entityMNode) {
     IMemMNode curNode = entityMNode.getAsMNode();
     if (!entityMNode.isUseTemplate()) {
-      boolean hasMeasurement = false;
-      boolean hasNonViewMeasurement = false;
-      IMemMNode child;
-      try (final IMNodeIterator<IMemMNode> iterator = 
store.getChildrenIterator(curNode)) {
-        while (iterator.hasNext()) {
-          child = iterator.next();
-          if (child.isMeasurement()) {
-            hasMeasurement = true;
-            if (!child.getAsMeasurementMNode().isLogicalView()) {
-              hasNonViewMeasurement = true;
-              break;
-            }
-          }
-        }
-      }
-
-      if (!hasMeasurement) {
+      final BasicInternalMNode internalNode = (BasicInternalMNode) 
curNode.getBasicMNode();
+      if (internalNode.getMeasurementChildCount() == 0) {
         synchronized (this) {
           curNode = store.setToInternal(entityMNode);
         }
         refreshAncestorsHavingDeviceDescendant(curNode.getParent());
-      } else if (!hasNonViewMeasurement) {
+      } else if (internalNode.getNonViewMeasurementChildCount() == 0) {
         // has some measurement but they are all logical view
         entityMNode.setAligned(null);
       }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/BasicInternalMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/BasicInternalMNode.java
index 8eb37b8bff7..a37b4234d70 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/BasicInternalMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/BasicInternalMNode.java
@@ -47,6 +47,10 @@ public class BasicInternalMNode extends BasicMNode 
implements IInternalMNode<IMe
   @SuppressWarnings("squid:S3077")
   private volatile IDeviceInfo<IMemMNode> deviceInfo = null;
 
+  // Direct children only. Snapshot loading rebuilds these counters through 
addChild.
+  private int measurementChildCount;
+  private int nonViewMeasurementChildCount;
+
   /** Constructor of MNode. */
   public BasicInternalMNode(IMemMNode parent, String name) {
     super(parent, name);
@@ -98,6 +102,9 @@ public class BasicInternalMNode extends BasicMNode 
implements IInternalMNode<IMe
     }
     child.setParent(this);
     IMemMNode existingChild = children.putIfAbsent(name, child);
+    if (existingChild == null) {
+      updateMeasurementChildCount(child, 1);
+    }
     return existingChild == null ? child : existingChild;
   }
 
@@ -114,21 +121,7 @@ public class BasicInternalMNode extends BasicMNode 
implements IInternalMNode<IMe
    */
   @Override
   public IMemMNode addChild(IMemMNode child) {
-    /* use cpu time to exchange memory
-     * measurementNode's children should be null to save memory
-     * add child method will only be called when writing MTree, which is not a 
frequent operation
-     */
-    if (children == null) {
-      // double check, children is volatile
-      synchronized (this) {
-        if (children == null) {
-          children = new MemMNodeContainer();
-        }
-      }
-    }
-
-    child.setParent(this);
-    children.putIfAbsent(child.getName(), child);
+    addChild(child.getName(), child);
     return child;
   }
 
@@ -136,7 +129,11 @@ public class BasicInternalMNode extends BasicMNode 
implements IInternalMNode<IMe
   @Override
   public IMemMNode deleteChild(String name) {
     if (children != null) {
-      return children.remove(name);
+      final IMemMNode child = children.remove(name);
+      if (child != null) {
+        updateMeasurementChildCount(child, -1);
+      }
+      return child;
     }
     return null;
   }
@@ -152,12 +149,39 @@ public class BasicInternalMNode extends BasicMNode 
implements IInternalMNode<IMe
   @Override
   public void setChildren(IMNodeContainer<IMemMNode> children) {
     this.children = children;
+    measurementChildCount = 0;
+    nonViewMeasurementChildCount = 0;
+    if (children != null) {
+      children.values().forEach(child -> updateMeasurementChildCount(child, 
1));
+    }
+  }
+
+  private void updateMeasurementChildCount(final IMemMNode child, final int 
delta) {
+    if (child.isMeasurement()) {
+      measurementChildCount += delta;
+      if (!child.getAsMeasurementMNode().isLogicalView()) {
+        nonViewMeasurementChildCount += delta;
+      }
+    }
+  }
+
+  public int getMeasurementChildCount() {
+    return measurementChildCount;
+  }
+
+  public int getNonViewMeasurementChildCount() {
+    return nonViewMeasurementChildCount;
   }
 
-  /** MNodeContainer reference and basic occupation, 8 + 80B. DeviceInfo 
reference and size. */
+  /** MNodeContainer reference and basic occupation, 8 + 80B. Counters, 8B. 
DeviceInfo and size. */
   @Override
   public int estimateSize() {
-    return 8 + 80 + super.estimateSize() + 8 + (deviceInfo == null ? 0 : 
deviceInfo.estimateSize());
+    return 8
+        + 80
+        + 8
+        + super.estimateSize()
+        + 8
+        + (deviceInfo == null ? 0 : deviceInfo.estimateSize());
   }
 
   @Override
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
index f181766731d..b3c531a97d6 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
@@ -471,6 +471,7 @@ public class MTreeBelowSGCachedImpl {
       throws MetadataException {
     final List<IMeasurementMNode<ICachedMNode>> measurementMNodeList = new 
ArrayList<>();
     MetaFormatUtils.checkSchemaMeasurementNames(measurements);
+    MetaFormatUtils.checkSchemaMeasurementAliases(devicePath, measurements, 
aliasList);
     final ICachedMNode deviceParent = 
checkAndAutoCreateInternalPath(devicePath);
 
     try {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/MetaFormatUtils.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/MetaFormatUtils.java
index 57ba2fbb366..c28a918e451 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/MetaFormatUtils.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/MetaFormatUtils.java
@@ -22,15 +22,18 @@ import 
org.apache.iotdb.commons.exception.IllegalPathException;
 import org.apache.iotdb.commons.exception.MetadataException;
 import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.exception.metadata.AliasAlreadyExistException;
 import org.apache.iotdb.db.exception.metadata.IllegalParameterOfPathException;
 import org.apache.iotdb.db.i18n.DataNodeSchemaMessages;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 import static org.apache.iotdb.commons.conf.IoTDBConstant.LOSS;
 import static org.apache.iotdb.commons.conf.IoTDBConstant.SDT;
@@ -102,6 +105,26 @@ public class MetaFormatUtils {
     }
   }
 
+  /** Validate the shared measurement/alias namespace before creating any 
member of a batch. */
+  public static void checkSchemaMeasurementAliases(
+      final PartialPath devicePath, final List<String> measurements, final 
List<String> aliases)
+      throws MetadataException {
+    if (aliases == null) {
+      return;
+    }
+    final Set<String> measurementNames = new HashSet<>(measurements);
+    final Set<String> aliasNames = new HashSet<>();
+    for (int i = 0; i < measurements.size(); i++) {
+      final String alias = aliases.get(i);
+      if (alias != null
+          && (!aliasNames.add(alias)
+              || (measurementNames.contains(alias) && 
!alias.equals(measurements.get(i))))) {
+        throw new AliasAlreadyExistException(
+            
devicePath.concatAsMeasurementPath(measurements.get(i)).getFullPath(), alias);
+      }
+    }
+  }
+
   /** check whether the database name uses illegal characters */
   public static void checkDatabase(final String database) throws 
IllegalPathException {
     if (!IoTDBConfig.DATABASE_PATTERN.matcher(database).matches()) {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionAliasAndTagTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionAliasAndTagTest.java
index 616a041750a..073fbc72a03 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionAliasAndTagTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionAliasAndTagTest.java
@@ -22,8 +22,10 @@ package org.apache.iotdb.db.metadata.schemaRegion;
 import org.apache.iotdb.commons.exception.MetadataException;
 import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.path.PathPatternTree;
+import org.apache.iotdb.db.exception.metadata.AliasAlreadyExistException;
 import org.apache.iotdb.db.schemaengine.schemaregion.ISchemaRegion;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.read.resp.info.ITimeSeriesSchemaInfo;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.write.req.impl.CreateAlignedTimeSeriesPlanImpl;
 
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.file.metadata.enums.CompressionType;
@@ -34,6 +36,7 @@ import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -58,6 +61,65 @@ public class SchemaRegionAliasAndTagTest extends 
AbstractSchemaRegionTest {
     schemaRegion = getSchemaRegion("root.sg", 0);
   }
 
+  @Test
+  public void testAlignedBatchRejectsConflictingAliasesBeforeCreation() throws 
Exception {
+    for (final List<String> aliases :
+        Arrays.asList(
+            Arrays.asList("dup", "dup"), Arrays.asList("s2", null), 
Arrays.asList(null, "s1"))) {
+      final CreateAlignedTimeSeriesPlanImpl plan = alignedAliasPlan(aliases);
+      try {
+        schemaRegion.createAlignedTimeSeries(plan);
+        Assert.fail("Expected alias collision");
+      } catch (final AliasAlreadyExistException expected) {
+        Assert.assertTrue(
+            SchemaRegionTestUtil.showTimeseries(schemaRegion, new 
PartialPath("root.sg.batch.*"))
+                .isEmpty());
+      }
+    }
+    schemaRegion.createAlignedTimeSeries(alignedAliasPlan(Arrays.asList("a1", 
"a2")));
+    final CreateAlignedTimeSeriesPlanImpl merge = 
alignedAliasPlan(Arrays.asList("dup", "dup"));
+    merge.setWithMerge(true);
+    try {
+      schemaRegion.createAlignedTimeSeries(merge);
+      Assert.fail("Expected alias collision during merge");
+    } catch (final AliasAlreadyExistException expected) {
+      checkAliasAndTagsAndAttributes(
+          "root.sg.batch.s1", "a1", Collections.emptyMap(), 
Collections.emptyMap());
+      checkAliasAndTagsAndAttributes(
+          "root.sg.batch.s2", "a2", Collections.emptyMap(), 
Collections.emptyMap());
+    }
+  }
+
+  @Test
+  public void testAlignedSelfAliasAndRemainingAliasAfterDeletion() throws 
Exception {
+    schemaRegion.createAlignedTimeSeries(alignedAliasPlan(Arrays.asList("s1", 
"a2")));
+    final PathPatternTree delete = new PathPatternTree();
+    delete.appendPathPattern(new PartialPath("root.sg.batch.s2"));
+    delete.constructTree();
+    schemaRegion.constructSchemaBlackList(delete);
+    schemaRegion.deleteTimeseriesInBlackList(delete);
+    checkAliasAndTagsAndAttributes(
+        "root.sg.batch.s1", "s1", Collections.emptyMap(), 
Collections.emptyMap());
+    Assert.assertEquals(
+        "root.sg.batch.s1",
+        SchemaRegionTestUtil.showTimeseries(schemaRegion, new 
PartialPath("root.sg.batch.s1"))
+            .get(0)
+            .getFullPath());
+  }
+
+  private CreateAlignedTimeSeriesPlanImpl alignedAliasPlan(final List<String> 
aliases)
+      throws MetadataException {
+    return new CreateAlignedTimeSeriesPlanImpl(
+        new PartialPath("root.sg.batch"),
+        new ArrayList<>(Arrays.asList("s1", "s2")),
+        new ArrayList<>(Collections.nCopies(2, TSDataType.INT32)),
+        new ArrayList<>(Collections.nCopies(2, TSEncoding.PLAIN)),
+        new ArrayList<>(Collections.nCopies(2, CompressionType.SNAPPY)),
+        new ArrayList<>(aliases),
+        null,
+        null);
+  }
+
   /**
    * Prepare timeseries
    *
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/schema/source/DeviceBlackListConstructorTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/schema/source/DeviceBlackListConstructorTest.java
new file mode 100644
index 00000000000..c9649b1da4c
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/schema/source/DeviceBlackListConstructorTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.db.queryengine.execution.operator.schema.source;
+
+import 
org.apache.iotdb.calc.transformation.dag.column.leaf.ConstantColumnTransformer;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.db.schemaengine.metric.SchemaRegionMemMetric;
+import org.apache.iotdb.db.schemaengine.rescon.MemSchemaEngineStatistics;
+import org.apache.iotdb.db.schemaengine.rescon.MemSchemaRegionStatistics;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.MTreeBelowSGMemoryImpl;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.common.block.column.BooleanColumn;
+import org.apache.tsfile.read.common.type.TypeFactory;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+
+public class DeviceBlackListConstructorTest {
+
+  @Test
+  public void testUnfilteredRetryAndRollback() throws Exception {
+    checkRetryAndRollback(false);
+  }
+
+  @Test
+  public void testFilteredRetryAndRollback() throws Exception {
+    checkRetryAndRollback(true);
+  }
+
+  private void checkRetryAndRollback(final boolean filtered) throws Exception {
+    final MemSchemaRegionStatistics statistics =
+        new MemSchemaRegionStatistics(0, new MemSchemaEngineStatistics());
+    final MTreeBelowSGMemoryImpl tree =
+        new MTreeBelowSGMemoryImpl(
+            PartialPath.getQualifiedDatabasePartialPath("root.db"),
+            node -> Collections.emptyMap(),
+            node -> Collections.emptyMap(),
+            statistics,
+            new SchemaRegionMemMetric(statistics, "root.db"));
+    final AtomicInteger pointer = new AtomicInteger();
+    tree.createOrUpdateTableDevice(
+        "t", new String[] {"d1"}, pointer::getAndIncrement, ignored -> {});
+    tree.createOrUpdateTableDevice(
+        "t", new String[] {"d2"}, pointer::getAndIncrement, ignored -> {});
+    assertEquals(2, statistics.getTableDevicesNumber("t"));
+
+    final PartialPath allDevices = new PartialPath("root.db.t.*");
+    // Both an overlapping pattern in one request and a new request after a 
retry visit d1 again.
+    for (int retry = 0; retry < 2; retry++) {
+      final DeviceBlackListConstructor constructor = 
newConstructor(statistics, filtered, true);
+      try (final DeviceBlackListConstructor ignored = constructor) {
+        tree.constructTableDeviceBlackList(new PartialPath("root.db.t.d1"), 
constructor);
+        tree.constructTableDeviceBlackList(allDevices, constructor);
+      }
+      assertEquals(3, constructor.getPreDeletedNum());
+      assertEquals(0, statistics.getTableDevicesNumber("t"));
+    }
+    tree.rollbackTableDeviceBlackList(allDevices);
+    tree.rollbackTableDeviceBlackList(allDevices);
+    assertEquals(2, statistics.getTableDevicesNumber("t"));
+
+    final DeviceBlackListConstructor noMatch = newConstructor(statistics, 
true, false);
+    try (final DeviceBlackListConstructor ignored = noMatch) {
+      tree.constructTableDeviceBlackList(allDevices, noMatch);
+    }
+    assertEquals(0, noMatch.getPreDeletedNum());
+    assertEquals(2, statistics.getTableDevicesNumber("t"));
+
+    final DeviceBlackListConstructor delete = newConstructor(statistics, 
filtered, true);
+    try (final DeviceBlackListConstructor ignored = delete) {
+      tree.constructTableDeviceBlackList(allDevices, delete);
+    }
+    final AtomicInteger deleted = new AtomicInteger();
+    tree.deleteTableDevicesInBlackList(
+        allDevices, ignored -> deleted.incrementAndGet(), ignored -> {});
+    assertEquals(2, deleted.get());
+    assertEquals(0, statistics.getTableDevicesNumber("t"));
+  }
+
+  private DeviceBlackListConstructor newConstructor(
+      final MemSchemaRegionStatistics statistics, final boolean filtered, 
final boolean matches) {
+    final ConstantColumnTransformer filter =
+        new ConstantColumnTransformer(
+            TypeFactory.getType(TSDataType.BOOLEAN),
+            new BooleanColumn(1, Optional.empty(), new boolean[] {matches}));
+    filter.addReferenceCount();
+    return new DeviceBlackListConstructor(
+        filtered ? Collections.singletonList(filter) : Collections.emptyList(),
+        filtered ? filter : null,
+        "t",
+        Collections.emptyList(),
+        (pointer, name) -> null,
+        statistics,
+        "db",
+        new TsTable("t"));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStoreTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStoreTest.java
index 880f5678418..0730d30d455 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStoreTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/attribute/DeviceAttributeStoreTest.java
@@ -19,20 +19,149 @@
 
 package org.apache.iotdb.db.schemaengine.schemaregion.attribute;
 
+import org.apache.iotdb.commons.schema.MemUsageUtil;
+import org.apache.iotdb.commons.schema.SchemaConstant;
+import org.apache.iotdb.db.schemaengine.rescon.MemSchemaEngineStatistics;
+import org.apache.iotdb.db.schemaengine.rescon.MemSchemaRegionStatistics;
+
 import org.apache.tsfile.utils.Binary;
 import org.apache.tsfile.utils.Constants;
+import org.apache.tsfile.utils.RamUsageEstimator;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
+import java.io.File;
+import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 public class DeviceAttributeStoreTest {
 
+  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  @Test
+  public void testAttributeMemoryUsesValueSizesAcrossUpdates() {
+    final MemSchemaRegionStatistics statistics = newStatistics();
+    final DeviceAttributeStore store = new DeviceAttributeStore(statistics);
+    final Binary first = new Binary("value", StandardCharsets.UTF_8);
+    final Binary second = new Binary("a longer attribute value", 
StandardCharsets.UTF_8);
+    final int pointer =
+        store.createAttribute(Collections.singletonList("a"), new Object[] 
{first}, "t");
+    final long emptySlotMemory =
+        statistics.getRegionMemoryUsage() - 
MemUsageUtil.computeKVMemUsageInMap("a", first);
+    assertEquals(first.ramBytesUsed(), 
statistics.getTableAttributeMemory("t"));
+
+    store.alterAttribute(pointer, Collections.singletonList("a"), new Object[] 
{second}, "t");
+    assertEquals(second.ramBytesUsed(), 
statistics.getTableAttributeMemory("t"));
+    assertEquals(
+        emptySlotMemory + MemUsageUtil.computeKVMemUsageInMap("a", second),
+        statistics.getRegionMemoryUsage());
+
+    store.alterAttribute(pointer, Arrays.asList("a", "b"), new Object[] {null, 
first}, "t");
+    assertEquals(first.ramBytesUsed(), 
statistics.getTableAttributeMemory("t"));
+    assertEquals(
+        emptySlotMemory + MemUsageUtil.computeKVMemUsageInMap("b", first),
+        statistics.getRegionMemoryUsage());
+
+    store.alterAttribute(
+        pointer, Collections.singletonList("b"), new Object[] 
{Constants.NONE}, "t");
+    assertEquals(first.ramBytesUsed(), 
statistics.getTableAttributeMemory("t"));
+    store.alterAttribute(pointer, Collections.singletonList("b"), new Object[] 
{null}, "t");
+    assertEquals(0, statistics.getTableAttributeMemory("t"));
+    assertEquals(emptySlotMemory, statistics.getRegionMemoryUsage());
+
+    store.alterAttribute(pointer, Collections.singletonList("c"), new Object[] 
{second}, "t");
+    store.removeAttribute(pointer, "c", "t");
+    assertEquals(0, statistics.getTableAttributeMemory("t"));
+    assertEquals(emptySlotMemory, statistics.getRegionMemoryUsage());
+
+    store.alterAttribute(pointer, Collections.singletonList("c"), new Object[] 
{first}, "t");
+    store.removeAttribute(pointer, "t");
+    assertEquals(0, statistics.getTableAttributeMemory("t"));
+    assertNull(store.getAttributes(pointer));
+    final long releasedMemory = statistics.getRegionMemoryUsage();
+    store.removeAttribute(pointer, "t");
+    assertEquals(releasedMemory, statistics.getRegionMemoryUsage());
+  }
+
+  @Test
+  public void testRepeatedCreateDeleteReusesSlotsAndBoundsSnapshot() throws 
Exception {
+    final MemSchemaRegionStatistics statistics = newStatistics();
+    final DeviceAttributeStore store = new DeviceAttributeStore(statistics);
+    for (int i = 0; i < 10000; i++) {
+      final int pointer = store.createAttribute(Collections.emptyList(), new 
Object[0], "t");
+      assertEquals(0, pointer);
+      store.removeAttribute(pointer, "t");
+    }
+    // One retained list reference and one bitmap word, independent of the 
number of deletions.
+    assertEquals(
+        RamUsageEstimator.NUM_BYTES_OBJECT_REF + Long.BYTES, 
statistics.getRegionMemoryUsage());
+    final File snapshotDir = temporaryFolder.newFolder();
+    assertTrue(store.createSnapshot(snapshotDir));
+    assertEquals(2L * Integer.BYTES, snapshotFile(snapshotDir).length());
+
+    final MemSchemaRegionStatistics recoveredStatistics = newStatistics();
+    final DeviceAttributeStore recovered = new 
DeviceAttributeStore(recoveredStatistics);
+    recovered.loadFromSnapshot(snapshotDir);
+    assertEquals(statistics.getRegionMemoryUsage(), 
recoveredStatistics.getRegionMemoryUsage());
+    assertEquals(0, recovered.createAttribute(Collections.emptyList(), new 
Object[0], "other"));
+    assertEquals(0, recoveredStatistics.getTableAttributeMemory("other"));
+  }
+
+  @Test
+  public void testSnapshotRebuildsFreeSlotsAndPreservesLivePointers() throws 
Exception {
+    final DeviceAttributeStore store = new DeviceAttributeStore(null);
+    final Binary value = new Binary("live", StandardCharsets.UTF_8);
+    for (int i = 0; i < 130; i++) {
+      assertEquals(
+          i, store.createAttribute(Collections.singletonList("a"), new 
Object[] {value}, "t"));
+    }
+    store.removeAttribute(1, "t");
+    store.removeAttribute(64, "t");
+    store.removeAttribute(129, "t");
+    final File snapshotDir = temporaryFolder.newFolder();
+    assertTrue(store.createSnapshot(snapshotDir));
+    try (final InputStream input = 
Files.newInputStream(snapshotFile(snapshotDir).toPath())) {
+      assertEquals(130, ReadWriteIOUtils.readInt(input));
+    }
+
+    final DeviceAttributeStore recovered = new DeviceAttributeStore(null);
+    recovered.loadFromSnapshot(snapshotDir);
+    for (int i = 0; i < 130; i++) {
+      if (i == 1 || i == 64 || i == 129) {
+        assertNull(recovered.getAttributes(i));
+      } else {
+        assertEquals(value, recovered.getAttributes(i, "a"));
+      }
+    }
+    assertEquals(1, recovered.createAttribute(Collections.emptyList(), new 
Object[0], "t"));
+    assertEquals(64, recovered.createAttribute(Collections.emptyList(), new 
Object[0], "t"));
+    recovered.removeAttribute(1, "t");
+    assertEquals(1, recovered.createAttribute(Collections.emptyList(), new 
Object[0], "t"));
+    assertEquals(129, recovered.createAttribute(Collections.emptyList(), new 
Object[0], "t"));
+    assertEquals(130, recovered.createAttribute(Collections.emptyList(), new 
Object[0], "t"));
+    recovered.clear();
+    assertEquals(0, recovered.createAttribute(Collections.emptyList(), new 
Object[0], "t"));
+  }
+
+  private static File snapshotFile(final File snapshotDir) {
+    return new File(snapshotDir, SchemaConstant.DEVICE_ATTRIBUTE_SNAPSHOT);
+  }
+
+  private static MemSchemaRegionStatistics newStatistics() {
+    return new MemSchemaRegionStatistics(0, new MemSchemaEngineStatistics());
+  }
+
   @Test
   public void testCreateAttributeIgnoresNamesWithoutValues() {
     final DeviceAttributeStore store = new DeviceAttributeStore(null);
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
index 2b811f1e945..c498f507f9d 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
@@ -32,6 +32,8 @@ import 
org.apache.iotdb.db.schemaengine.metric.SchemaRegionMemMetric;
 import org.apache.iotdb.db.schemaengine.rescon.MemSchemaEngineStatistics;
 import org.apache.iotdb.db.schemaengine.rescon.MemSchemaRegionStatistics;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.IMemMNode;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.container.MemMNodeContainer;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.impl.BasicInternalMNode;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.loader.MNodeFactoryLoader;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.traverser.collector.MeasurementCollector;
 
@@ -40,17 +42,144 @@ import 
org.apache.tsfile.file.metadata.enums.CompressionType;
 import org.apache.tsfile.file.metadata.enums.TSEncoding;
 import org.apache.tsfile.write.schema.MeasurementSchema;
 import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 public class MTreeBelowSGMemoryImplTest {
 
+  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  @Test
+  public void testWideDeviceDeletionDoesNotScanChildrenPerMeasurement() throws 
Exception {
+    final MTreeBelowSGMemoryImpl mtree = newMTree();
+    final int measurementCount = 1024;
+    final List<String> measurements = new ArrayList<>();
+    for (int i = 0; i < measurementCount; i++) {
+      measurements.add("s" + i);
+    }
+    mtree.createAlignedTimeSeries(
+        new PartialPath("root.sg.d"),
+        measurements,
+        Collections.nCopies(measurementCount, TSDataType.INT32),
+        Collections.nCopies(measurementCount, TSEncoding.PLAIN),
+        Collections.nCopies(measurementCount, CompressionType.SNAPPY),
+        null,
+        false,
+        Collections.emptySet(),
+        new AtomicBoolean(true));
+    final IMemMNode device = mtree.getNodeByPath(new PartialPath("root.sg.d"));
+    final CountingChildren children = new CountingChildren();
+    children.putAll(device.getChildren());
+    device.setChildren(children);
+    final PartialPath pattern = new PartialPath("root.sg.d.*");
+    mtree.constructSchemaBlackList(pattern, new AtomicBoolean());
+    final List<PartialPath> paths = mtree.getPreDeletedTimeSeries(pattern);
+    Assert.assertEquals(measurementCount, paths.size());
+    children.scans = 0;
+    for (final PartialPath path : paths) {
+      mtree.deleteTimeSeries(path);
+    }
+    Assert.assertEquals(0, children.scans);
+    assertPathNotExist(mtree, new PartialPath("root.sg.d"));
+    Assert.assertEquals(
+        0, mtree.getNodeByPath(new 
PartialPath("root.sg")).getSubtreeMeasurementCount());
+  }
+
+  @Test
+  public void testDirectChildCountsAfterSnapshotWithNestedDevice() throws 
Exception {
+    final MTreeBelowSGMemoryImpl mtree = newMTree();
+    for (final String path : Arrays.asList("root.sg.d.s1", 
"root.sg.d.child.s2")) {
+      mtree.createTimeSeries(
+          new MeasurementPath(path),
+          TSDataType.INT32,
+          TSEncoding.PLAIN,
+          CompressionType.SNAPPY,
+          null,
+          null,
+          false,
+          null);
+    }
+    final File snapshotDir = temporaryFolder.newFolder();
+    Assert.assertTrue(mtree.createSnapshot(snapshotDir));
+    final MemSchemaRegionStatistics statistics = newRegionStatistics();
+    final MTreeBelowSGMemoryImpl recovered =
+        MTreeBelowSGMemoryImpl.loadFromSnapshot(
+            snapshotDir,
+            "root.sg",
+            statistics,
+            new SchemaRegionMemMetric(statistics, "root.sg"),
+            node -> {},
+            node -> {},
+            (node, table) -> {},
+            node -> Collections.emptyMap(),
+            node -> Collections.emptyMap());
+    final IMemMNode device = recovered.getNodeByPath(new 
PartialPath("root.sg.d"));
+    Assert.assertEquals(
+        1, ((BasicInternalMNode) 
device.getBasicMNode()).getMeasurementChildCount());
+    Assert.assertEquals(2, device.getSubtreeMeasurementCount());
+    recovered.deleteTimeSeries(new PartialPath("root.sg.d.s1"));
+    Assert.assertFalse(recovered.getNodeByPath(new 
PartialPath("root.sg.d")).isDevice());
+    Assert.assertTrue(recovered.getNodeByPath(new 
PartialPath("root.sg.d.child")).isDevice());
+    recovered.deleteTimeSeries(new PartialPath("root.sg.d.child.s2"));
+    assertPathNotExist(recovered, new PartialPath("root.sg.d"));
+  }
+
+  @Test
+  public void testAlignmentResetsWhenOnlyLogicalViewsRemain() throws Exception 
{
+    final MTreeBelowSGMemoryImpl mtree = newMTree();
+    mtree.createTimeSeries(
+        new MeasurementPath("root.sg.d.s"),
+        TSDataType.INT32,
+        TSEncoding.PLAIN,
+        CompressionType.SNAPPY,
+        null,
+        null,
+        false,
+        null);
+    final IMemMNode device = mtree.getNodeByPath(new PartialPath("root.sg.d"));
+    // The OSS factory does not create logical views. Model a view child to 
verify cleanup
+    // semantics.
+    final IMemMNode view = mock(IMemMNode.class);
+    final IMeasurementMNode<IMemMNode> viewMeasurement = 
mock(IMeasurementMNode.class);
+    when(view.isMeasurement()).thenReturn(true);
+    when(view.getAsMeasurementMNode()).thenReturn(viewMeasurement);
+    when(viewMeasurement.isLogicalView()).thenReturn(true);
+    device.addChild("view", view);
+    device.addChild("view", view);
+    Assert.assertEquals(
+        2, ((BasicInternalMNode) 
device.getBasicMNode()).getMeasurementChildCount());
+    mtree.deleteTimeSeries(new PartialPath("root.sg.d.s"));
+    Assert.assertTrue(device.isDevice());
+    Assert.assertNull(device.getAsDeviceMNode().isAlignedNullable());
+    device.deleteChild("view");
+    mtree.deleteEmptyInternalMNode(device.getAsDeviceMNode());
+    assertPathNotExist(mtree, new PartialPath("root.sg.d"));
+  }
+
+  private static class CountingChildren extends MemMNodeContainer {
+    private int scans;
+
+    @Override
+    public Collection<IMemMNode> values() {
+      scans++;
+      return super.values();
+    }
+  }
+
   @Test
   public void testPathAndTableDeviceChecksAfterClear() throws Exception {
     final MTreeBelowSGMemoryImpl mtree = newMTree();

Reply via email to