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 c816d5f8d81 [To dev/1.3] Preserve LOAD sources after unrecoverable 
query fallback (#19090)
c816d5f8d81 is described below

commit c816d5f8d8180386f643840471b8394076a1e6c5
Author: Caideyipi <[email protected]>
AuthorDate: Tue Nov 3 09:30:42 2026 +0800

    [To dev/1.3] Preserve LOAD sources after unrecoverable query fallback 
(#19090)
    
    * Preserve LOAD sources after unrecoverable query fallback (#19073)
    
    * Fix LOAD fallback error reporting and scan-boundary resumption
    
    * Fix LOAD fallback checkpoints across chunk groups
    
    * Fix LOAD fallback metadata error propagation
    
    * Fix LOAD fallback metadata batches
    
    (cherry picked from commit 9463d011e857ca060c8d1c799f803a54fba9c115)
    
    * Adapt LOAD fallback tests to 1.3 scheduling
    
    * Check all DataNode logs in LOAD fallback IT
    
    * Exercise LOAD query fallback in single-node tests
---
 .../org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java  | 279 +++++++++++++++++
 .../converter/LoadTreeTsFileTabletIterator.java    | 230 +++++---------
 ...atementDataTypeConvertExecutionVisitorTest.java | 338 ++++++++++++++++++++-
 3 files changed, 693 insertions(+), 154 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java 
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java
index c2fbbb0e30b..61fe99e9ea1 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java
@@ -20,11 +20,13 @@
 package org.apache.iotdb.db.it;
 
 import org.apache.iotdb.commons.auth.entity.PrivilegeType;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
 import org.apache.iotdb.commons.path.MeasurementPath;
 import org.apache.iotdb.db.it.utils.TestUtils;
 import org.apache.iotdb.db.queryengine.common.header.ColumnHeaderConstant;
 import org.apache.iotdb.db.storageengine.dataregion.modification.Deletion;
 import 
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
 import org.apache.iotdb.it.env.EnvFactory;
 import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
 import org.apache.iotdb.it.framework.IoTDBTestRunner;
@@ -32,11 +34,20 @@ import org.apache.iotdb.it.utils.TsFileGenerator;
 import org.apache.iotdb.itbase.category.ClusterIT;
 import org.apache.iotdb.itbase.category.LocalStandaloneIT;
 import org.apache.iotdb.jdbc.IoTDBSQLException;
+import org.apache.iotdb.rpc.TSStatusCode;
 
+import org.apache.commons.io.FileUtils;
+import org.apache.tsfile.common.conf.TSFileConfig;
 import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.file.metadata.PlainDeviceID;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
 import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.read.TsFileSequenceReader;
 import org.apache.tsfile.read.common.Path;
 import org.apache.tsfile.utils.Pair;
+import org.apache.tsfile.write.TsFileWriter;
+import org.apache.tsfile.write.record.Tablet;
 import org.apache.tsfile.write.schema.MeasurementSchema;
 import org.junit.After;
 import org.junit.Assert;
@@ -49,6 +60,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.file.Files;
 import java.sql.Connection;
 import java.sql.ResultSet;
@@ -62,6 +75,10 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
@@ -782,6 +799,268 @@ public class IoTDBLoadTsFileIT {
     }
   }
 
+  @Test
+  public void testDirectoryQueryFallbackRetainsOnlyFailedSources() throws 
Exception {
+    for (final boolean delete : new boolean[] {false, true}) {
+      final File directory = new File(tmpDir, delete ? "batch-delete" : 
"batch-none");
+      Assert.assertTrue(directory.mkdir());
+      final String devicePrefix =
+          SchemaConfig.STORAGE_GROUP_0 + (delete ? ".batch_delete" : 
".batch_none");
+      final File normal = new File(directory, "401-0-0-0.tsfile");
+      final File recoverable = new File(directory, "402-0-0-0.tsfile");
+      final File failed = new File(directory, "403-0-0-0.tsfile");
+      final File[] sources = {normal, recoverable, failed};
+      try {
+        long recoverableMarkerOffset = -1;
+        long failedMarkerOffset = -1;
+        for (int i = 0; i < sources.length; i++) {
+          final long groupMarkerOffset =
+              writeFallbackSource(sources[i], devicePrefix + ".d" + i, i == 1, 
i > 0);
+          if (i == 1) {
+            recoverableMarkerOffset = groupMarkerOffset;
+          } else if (i == 2) {
+            failedMarkerOffset = groupMarkerOffset;
+          }
+        }
+        // All footer metadata stays valid, so LOAD must reach scheduling and 
query fallback.
+        replaceByte(recoverable, recoverableMarkerOffset, 0x7f);
+        final long compressionOffset = firstChunkCompressionOffset(failed);
+        final int originalCompression = replaceByte(failed, compressionOffset, 
0x7f);
+        final int originalFailedMarker = replaceByte(failed, 
failedMarkerOffset, 0x7f);
+
+        try (final Connection connection =
+                EnvFactory.getEnv()
+                    
.getConnectionWithSpecifiedDataNode(EnvFactory.getEnv().getDataNodeWrapper(0));
+            final Statement statement = connection.createStatement()) {
+          final SQLException failure =
+              Assert.assertThrows(
+                  SQLException.class, () -> 
statement.execute(fallbackLoadSql(directory, delete)));
+          assertFallbackLoadFailure(failure, failed);
+          Assert.assertFalse(failure.getMessage(), 
failure.getMessage().contains(normal.getName()));
+          Assert.assertFalse(
+              failure.getMessage(), 
failure.getMessage().contains(recoverable.getName()));
+          assertFallbackRows(statement, devicePrefix + ".d0");
+          assertFallbackRows(statement, devicePrefix + ".d1", 
fallbackPartitionTimestamps());
+          boolean foundConversionLog = false;
+          for (final DataNodeWrapper dataNodeWrapper :
+              EnvFactory.getEnv().getDataNodeWrapperList()) {
+            if (dataNodeWrapper.logContains(
+                "Load: Successfully converted TsFile "
+                    + recoverable.getAbsolutePath()
+                    + " into tablets and inserted.")) {
+              foundConversionLog = true;
+              break;
+            }
+          }
+          Assert.assertTrue(foundConversionLog);
+          assertFallbackSourceExists(normal, !delete);
+          assertFallbackSourceExists(recoverable, !delete);
+          assertFallbackSourceExists(failed, true);
+
+          replaceByte(failed, compressionOffset, originalCompression);
+          replaceByte(failed, failedMarkerOffset, originalFailedMarker);
+          statement.execute(fallbackLoadSql(failed, delete));
+          assertFallbackRows(statement, devicePrefix + ".d2", 
fallbackPartitionTimestamps());
+          assertFallbackRows(statement, devicePrefix + ".d0");
+          assertFallbackRows(statement, devicePrefix + ".d1", 
fallbackPartitionTimestamps());
+          assertFallbackSourceExists(failed, !delete);
+        }
+      } finally {
+        FileUtils.deleteDirectory(directory);
+      }
+    }
+  }
+
+  @Test
+  public void testConcurrentQueryFallbackFailureRetainsSourceForRetry() throws 
Exception {
+    final File good = new File(tmpDir, "301-0-0-0.tsfile");
+    final File bad = new File(tmpDir, "302-0-0-0.tsfile");
+    final String device = SchemaConfig.STORAGE_GROUP_0 + ".concurrent";
+    writeFallbackSource(good, device + ".good", false, false);
+    writeFallbackSource(bad, device + ".bad", false, true);
+    final long compressionOffset = firstChunkCompressionOffset(bad);
+    final int originalCompression = replaceByte(bad, compressionOffset, 0x7f);
+    final ExecutorService loaders = Executors.newFixedThreadPool(2);
+    final CountDownLatch ready = new CountDownLatch(2);
+    final CountDownLatch start = new CountDownLatch(1);
+    try {
+      final List<Future<?>> loads = new ArrayList<>();
+      for (final File source : Arrays.asList(good, bad)) {
+        loads.add(
+            loaders.submit(
+                () -> {
+                  try (final Connection connection = 
EnvFactory.getEnv().getConnection();
+                      final Statement statement = 
connection.createStatement()) {
+                    ready.countDown();
+                    Assert.assertTrue(start.await(30, TimeUnit.SECONDS));
+                    if (source.equals(bad)) {
+                      final SQLException failure =
+                          Assert.assertThrows(
+                              SQLException.class,
+                              () -> statement.execute(fallbackLoadSql(source, 
true)));
+                      assertFallbackLoadFailure(failure, bad);
+                    } else {
+                      statement.execute(fallbackLoadSql(source, true));
+                    }
+                  }
+                  return null;
+                }));
+      }
+      Assert.assertTrue(ready.await(30, TimeUnit.SECONDS));
+      start.countDown();
+      for (final Future<?> load : loads) {
+        load.get(90, TimeUnit.SECONDS);
+      }
+      assertFallbackSourceExists(good, false);
+      assertFallbackSourceExists(bad, true);
+      try (final Connection connection = EnvFactory.getEnv().getConnection();
+          final Statement statement = connection.createStatement()) {
+        assertFallbackRows(statement, device + ".good");
+        replaceByte(bad, compressionOffset, originalCompression);
+        statement.execute(fallbackLoadSql(bad, true));
+        assertFallbackRows(statement, device + ".bad", 
fallbackPartitionTimestamps());
+        assertFallbackRows(statement, device + ".good");
+      }
+      assertFallbackSourceExists(bad, false);
+    } finally {
+      start.countDown();
+      loaders.shutdownNow();
+      Assert.assertTrue(loaders.awaitTermination(30, TimeUnit.SECONDS));
+    }
+  }
+
+  private static String fallbackLoadSql(final File source, final boolean 
delete) {
+    return String.format(
+        "load '%s' with ('database-level'='2','on-success'='%s',"
+            + "'tablet-conversion-threshold'='-1')",
+        source.getAbsolutePath(), delete ? "delete" : "none");
+  }
+
+  private static void assertFallbackLoadFailure(final SQLException failure, 
final File source) {
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
failure.getErrorCode());
+    Assert.assertTrue(failure.getMessage(), 
failure.getMessage().contains(source.getName()));
+  }
+
+  private static long writeFallbackSource(
+      final File file,
+      final String device,
+      final boolean includeAnchor,
+      final boolean spanTimePartitions)
+      throws Exception {
+    final List<MeasurementSchema> schemas =
+        Arrays.asList(
+            new MeasurementSchema(
+                "s1", TSDataType.INT64, TSEncoding.PLAIN, 
CompressionType.UNCOMPRESSED),
+            new MeasurementSchema(
+                "s2", TSDataType.INT64, TSEncoding.PLAIN, 
CompressionType.UNCOMPRESSED));
+    final String anchor = SchemaConfig.STORAGE_GROUP_0 + ".a_fallback_anchor";
+    final long groupMarkerOffset;
+    try (final TsFileWriter writer = new TsFileWriter(file)) {
+      if (includeAnchor) {
+        // Preserve a valid first group for the encryption-header check, and 
keep physical device
+        // order consistent with the metadata index so query fallback can 
resume after this group.
+        writer.registerAlignedTimeseries(new Path(anchor), schemas);
+        final Tablet anchorTablet = new Tablet(anchor, schemas);
+        anchorTablet.addTimestamp(0, 0);
+        anchorTablet.addValue("s1", 0, 0L);
+        anchorTablet.addValue("s2", 0, 0L);
+        anchorTablet.rowSize = 1;
+        writer.writeAligned(anchorTablet);
+        writer.flushAllChunkGroups();
+      }
+      groupMarkerOffset = writer.getIOWriter().getPos();
+      writer.registerAlignedTimeseries(new Path(device), schemas);
+      final Tablet tablet = new Tablet(device, schemas);
+      for (int i = 0; i < 4; i++) {
+        final long timestamp =
+            spanTimePartitions && i >= 2
+                ? 
CommonDescriptor.getInstance().getConfig().getTimePartitionInterval() + i - 1
+                : i + 1;
+        tablet.addTimestamp(i, timestamp);
+        tablet.addValue("s1", i, (long) i);
+        tablet.addValue("s2", i, (long) i * 10);
+      }
+      tablet.rowSize = 4;
+      writer.writeAligned(tablet);
+    }
+    final TsFileResource resource = new TsFileResource(file);
+    final IDeviceID deviceId = new PlainDeviceID(device);
+    resource.updateStartTime(deviceId, 1);
+    resource.updateEndTime(
+        deviceId,
+        spanTimePartitions
+            ? 
CommonDescriptor.getInstance().getConfig().getTimePartitionInterval() + 2
+            : 4);
+    if (includeAnchor) {
+      final IDeviceID anchorId = new PlainDeviceID(anchor);
+      resource.updateStartTime(anchorId, 0);
+      resource.updateEndTime(anchorId, 0);
+    }
+    resource.serialize();
+    try (final ModificationFile mods = resource.getModFile()) {
+      mods.write(new Deletion(new MeasurementPath(device + ".s1"), 
Long.MAX_VALUE, 2, 2));
+    }
+    return groupMarkerOffset;
+  }
+
+  private static long firstChunkCompressionOffset(final File file) throws 
Exception {
+    try (final TsFileSequenceReader reader = new 
TsFileSequenceReader(file.getAbsolutePath())) {
+      reader.position(TSFileConfig.MAGIC_STRING.length() + 1L);
+      reader.readMarker();
+      reader.readChunkGroupHeader();
+      reader.readChunkHeader(reader.readMarker());
+      // The final two header bytes are compression and encoding; corrupt only 
compression.
+      return reader.position() - 2;
+    }
+  }
+
+  private static int replaceByte(final File file, final long offset, final int 
value)
+      throws IOException {
+    try (final RandomAccessFile bytes = new RandomAccessFile(file, "rw")) {
+      bytes.seek(offset);
+      final int original = bytes.readUnsignedByte();
+      bytes.seek(offset);
+      bytes.writeByte(value);
+      return original;
+    }
+  }
+
+  private static void assertFallbackSourceExists(final File file, final 
boolean expected) {
+    Assert.assertEquals(file.toString(), expected, file.exists());
+    Assert.assertEquals(
+        file.toString(), expected, new File(file + 
TsFileResource.RESOURCE_SUFFIX).exists());
+    Assert.assertEquals(
+        file.toString(), expected, new File(file + 
ModificationFile.FILE_SUFFIX).exists());
+  }
+
+  private static void assertFallbackRows(final Statement statement, final 
String device)
+      throws SQLException {
+    assertFallbackRows(statement, device, new long[] {1, 2, 3, 4});
+  }
+
+  private static long[] fallbackPartitionTimestamps() {
+    final long timePartitionInterval =
+        CommonDescriptor.getInstance().getConfig().getTimePartitionInterval();
+    return new long[] {1, 2, timePartitionInterval + 1, timePartitionInterval 
+ 2};
+  }
+
+  private static void assertFallbackRows(
+      final Statement statement, final String device, final long[] timestamps) 
throws SQLException {
+    try (final ResultSet rows = statement.executeQuery("select s1,s2 from " + 
device)) {
+      for (int i = 0; i < 4; i++) {
+        Assert.assertTrue(device, rows.next());
+        Assert.assertEquals(timestamps[i], rows.getLong(1));
+        if (i == 1) {
+          Assert.assertNull(rows.getObject(2));
+        } else {
+          Assert.assertEquals(i, rows.getLong(2));
+        }
+        Assert.assertEquals(i * 10, rows.getLong(3));
+      }
+      Assert.assertFalse(device, rows.next());
+    }
+  }
+
   @Test
   public void testLoadWithLastCache() throws Exception {
     registerSchema();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeTsFileTabletIterator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeTsFileTabletIterator.java
index 8c7e260aa68..3f0d9fe175a 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeTsFileTabletIterator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeTsFileTabletIterator.java
@@ -36,6 +36,7 @@ import org.apache.tsfile.file.metadata.IDeviceID;
 import org.apache.tsfile.file.metadata.PlainDeviceID;
 import org.apache.tsfile.file.metadata.TimeseriesMetadata;
 import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.read.TsFileSequenceReaderTimeseriesMetadataIterator;
 import org.apache.tsfile.utils.Pair;
 import org.apache.tsfile.write.record.Tablet;
 import org.apache.tsfile.write.schema.MeasurementSchema;
@@ -49,19 +50,18 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import static 
org.apache.iotdb.db.storageengine.load.LoadTsFilePathUtils.getValidatedDevicePath;
 
 /**
  * Load uses scan parsing first for throughput. If scan parsing hits 
corruption, fall back to query
- * parsing for the remaining measurements and devices so later data can still 
be loaded.
+ * parsing for the remaining measurements and devices so later data can still 
be loaded. Report any
+ * unrecoverable query failure after exhausting the readable data so a partial 
load cannot succeed.
  */
 class LoadTreeTsFileTabletIterator
     implements Iterable<Pair<Tablet, Boolean>>, Iterator<Pair<Tablet, 
Boolean>>, AutoCloseable {
@@ -91,14 +91,9 @@ class LoadTreeTsFileTabletIterator
   private Iterator<Pair<Tablet, Boolean>> activeIterator;
   private boolean scanInitialized;
   private boolean fallbackTriggered;
+  private RuntimeException unrecoverableQueryFailure;
 
-  private IDeviceID lastEmittedDevice;
-  private List<String> lastEmittedMeasurements = Collections.emptyList();
-  private long lastEmittedTimestamp = Long.MIN_VALUE;
-
-  private IDeviceID lastScanTabletDevice;
-  private List<String> lastScanTabletMeasurements = Collections.emptyList();
-  private final Map<IDeviceID, Set<String>> fullyEmittedMeasurementsByDevice =
+  private final Map<IDeviceID, Map<String, Long>> 
lastEmittedTimestampsByDevice =
       new LinkedHashMap<>();
 
   LoadTreeTsFileTabletIterator(final File file, final boolean isWithMod) {
@@ -117,8 +112,7 @@ class LoadTreeTsFileTabletIterator
       try {
         ensureActiveIterator();
         if (Objects.isNull(activeIterator)) {
-          close();
-          return false;
+          return finishIteration();
         }
 
         if (activeIterator.hasNext()) {
@@ -126,8 +120,7 @@ class LoadTreeTsFileTabletIterator
         }
 
         if (!switchToNextIterator()) {
-          close();
-          return false;
+          return finishIteration();
         }
       } catch (final Exception e) {
         if (recoverFromIteratorFailure(e)) {
@@ -222,7 +215,7 @@ class LoadTreeTsFileTabletIterator
           activeQueryTask.measurements,
           file.getAbsolutePath(),
           e);
-      splitOrSkipActiveQueryTask();
+      splitOrSkipActiveQueryTask(e);
       return true;
     }
 
@@ -237,15 +230,10 @@ class LoadTreeTsFileTabletIterator
     fallbackTriggered = true;
     final IDeviceID currentDevice =
         Objects.nonNull(scanParser) ? scanParser.getCurrentDevice() : null;
-    final List<String> currentMeasurements =
-        Objects.nonNull(scanParser) ? scanParser.getCurrentMeasurements() : 
Collections.emptyList();
-
-    markLastScanMeasurementsAsCompletedIfNeeded(currentDevice, 
currentMeasurements);
-
     closeScanParser();
 
     try {
-      pendingQueryTasks.addAll(buildQueryTasks(currentDevice, 
currentMeasurements));
+      pendingQueryTasks.addAll(buildQueryTasks());
     } catch (final Exception queryInitException) {
       LOGGER.warn(
           "Load: Failed to initialize query fallback for TsFile {} after scan 
parser failure.",
@@ -263,8 +251,7 @@ class LoadTreeTsFileTabletIterator
     return true;
   }
 
-  private ArrayDeque<QueryTask> buildQueryTasks(
-      final IDeviceID currentDevice, final List<String> currentMeasurements) 
throws IOException {
+  private ArrayDeque<QueryTask> buildQueryTasks() throws IOException {
     final LinkedHashMap<IDeviceID, List<String>> deviceMeasurementsMap =
         readDeviceMeasurementsInOrder();
     if (deviceMeasurementsMap.isEmpty()) {
@@ -272,23 +259,10 @@ class LoadTreeTsFileTabletIterator
     }
 
     final ArrayDeque<QueryTask> tasks = new ArrayDeque<>();
-    boolean includeCurrentAndFollowingDevices =
-        Objects.isNull(currentDevice) || 
!deviceMeasurementsMap.containsKey(currentDevice);
-
+    // Devices and measurements can recur in later chunk groups. The metadata 
index order does
+    // not describe the scan position, so resume every series from its own 
emitted prefix.
     for (final Map.Entry<IDeviceID, List<String>> entry : 
deviceMeasurementsMap.entrySet()) {
-      final IDeviceID device = entry.getKey();
-      if (!includeCurrentAndFollowingDevices && device.equals(currentDevice)) {
-        includeCurrentAndFollowingDevices = true;
-      }
-      if (!includeCurrentAndFollowingDevices) {
-        continue;
-      }
-
-      if (device.equals(currentDevice)) {
-        addCurrentDeviceQueryTasks(tasks, device, entry.getValue(), 
currentMeasurements);
-      } else {
-        addQueryTaskIfNecessary(tasks, device, entry.getValue(), 
Long.MIN_VALUE, Long.MAX_VALUE);
-      }
+      addQueryTasks(tasks, entry.getKey(), entry.getValue(), Long.MIN_VALUE, 
Long.MAX_VALUE);
     }
 
     return tasks;
@@ -298,49 +272,65 @@ class LoadTreeTsFileTabletIterator
       throws IOException {
     final LinkedHashMap<IDeviceID, List<String>> deviceMeasurementsMap = new 
LinkedHashMap<>();
     try (final TsFileSequenceReader reader = new 
TsFileSequenceReader(file.getAbsolutePath())) {
-      final Iterator<Pair<IDeviceID, List<TimeseriesMetadata>>> 
metadataIterator =
-          reader.iterAllTimeseriesMetadata(false, false);
+      // iterAllTimeseriesMetadata treats an IOException as the end of 
iteration. Use the
+      // throwing batch iterator so an incomplete metadata index cannot turn a 
partial load into a
+      // success without rereading every device's metadata separately.
+      final TsFileSequenceReaderTimeseriesMetadataIterator metadataIterator =
+          new TsFileSequenceReaderTimeseriesMetadataIterator(reader, false);
       while (metadataIterator.hasNext()) {
-        final Pair<IDeviceID, List<TimeseriesMetadata>> deviceMetadata = 
metadataIterator.next();
-        deviceMeasurementsMap.put(
-            deviceMetadata.getLeft(),
-            deviceMetadata.getRight().stream()
-                .map(TimeseriesMetadata::getMeasurementId)
-                .collect(Collectors.toList()));
+        for (final Map.Entry<IDeviceID, List<TimeseriesMetadata>> 
deviceMetadata :
+            metadataIterator.next().entrySet()) {
+          final Map<String, Long> emittedTimestamps =
+              lastEmittedTimestampsByDevice.getOrDefault(
+                  deviceMetadata.getKey(), Collections.emptyMap());
+          deviceMeasurementsMap
+              .computeIfAbsent(deviceMetadata.getKey(), key -> new 
ArrayList<>())
+              .addAll(
+                  deviceMetadata.getValue().stream()
+                      .filter(
+                          metadata -> {
+                            final Long emittedTimestamp =
+                                
emittedTimestamps.get(metadata.getMeasurementId());
+                            return !metadata.getMeasurementId().isEmpty()
+                                && (emittedTimestamp == null
+                                    || emittedTimestamp < 
metadata.getStatistics().getEndTime());
+                          })
+                      .map(TimeseriesMetadata::getMeasurementId)
+                      .collect(Collectors.toList()));
+        }
       }
     }
     return deviceMeasurementsMap;
   }
 
-  private void addCurrentDeviceQueryTasks(
+  private void addQueryTasks(
       final ArrayDeque<QueryTask> tasks,
       final IDeviceID device,
-      final List<String> allMeasurements,
-      final List<String> currentMeasurements) {
-    final Set<String> completedMeasurements =
-        fullyEmittedMeasurementsByDevice.getOrDefault(device, 
Collections.emptySet());
-    final Set<String> currentMeasurementSet = new 
LinkedHashSet<>(currentMeasurements);
-
-    final List<String> currentMeasurementsToResume = new ArrayList<>();
-    final List<String> remainingMeasurements = new ArrayList<>();
-    for (final String measurement : allMeasurements) {
-      if (completedMeasurements.contains(measurement)) {
+      final List<String> measurements,
+      final long startTime,
+      final long endTime) {
+    final Map<String, Long> emittedTimestamps =
+        lastEmittedTimestampsByDevice.getOrDefault(device, 
Collections.emptyMap());
+    final Map<Long, List<String>> measurementsByStartTime = new 
LinkedHashMap<>();
+    for (final String measurement : measurements) {
+      final Long emittedTimestamp = emittedTimestamps.get(measurement);
+      if (emittedTimestamp != null && emittedTimestamp >= endTime) {
         continue;
       }
-      if (currentMeasurementSet.contains(measurement)) {
-        currentMeasurementsToResume.add(measurement);
-      } else {
-        remainingMeasurements.add(measurement);
+      // A missing checkpoint differs from a checkpoint at Long.MIN_VALUE. A 
start time of
+      // Long.MAX_VALUE is also valid when the last unconsumed point has that 
timestamp.
+      final long resumeStartTime =
+          emittedTimestamp == null ? startTime : Math.max(startTime, 
emittedTimestamp + 1);
+      if (resumeStartTime <= endTime) {
+        measurementsByStartTime
+            .computeIfAbsent(resumeStartTime, key -> new ArrayList<>())
+            .add(measurement);
       }
     }
 
-    addQueryTaskIfNecessary(
-        tasks,
-        device,
-        currentMeasurementsToResume,
-        determineTaskResumeStartTime(device, currentMeasurementsToResume, 
Long.MIN_VALUE),
-        Long.MAX_VALUE);
-    addQueryTaskIfNecessary(tasks, device, remainingMeasurements, 
Long.MIN_VALUE, Long.MAX_VALUE);
+    measurementsByStartTime.forEach(
+        (resumeStartTime, taskMeasurements) ->
+            tasks.addLast(new QueryTask(device, taskMeasurements, 
resumeStartTime, endTime)));
   }
 
   private boolean activateNextQueryParser() {
@@ -396,7 +386,7 @@ class LoadTreeTsFileTabletIterator
             activeQueryTask.measurements,
             file.getAbsolutePath(),
             e);
-        splitOrSkipActiveQueryTask();
+        splitOrSkipActiveQueryTask(e);
       }
     }
 
@@ -411,15 +401,12 @@ class LoadTreeTsFileTabletIterator
     }
 
     final IDeviceID device = new PlainDeviceID(tablet.deviceId);
-    final List<String> measurements = extractMeasurementNames(tablet);
-
-    if (Objects.isNull(activeQueryParser)) {
-      recordScanProgress(device, measurements);
+    final Map<String, Long> emittedTimestamps =
+        lastEmittedTimestampsByDevice.computeIfAbsent(device, key -> new 
LinkedHashMap<>());
+    final long timestamp = tablet.timestamps[tablet.rowSize - 1];
+    for (final String measurement : extractMeasurementNames(tablet)) {
+      emittedTimestamps.merge(measurement, timestamp, Math::max);
     }
-
-    lastEmittedDevice = device;
-    lastEmittedMeasurements = measurements;
-    lastEmittedTimestamp = tablet.timestamps[tablet.rowSize - 1];
   }
 
   private boolean shouldRethrow(final Exception e) {
@@ -478,67 +465,15 @@ class LoadTreeTsFileTabletIterator
     pendingQueryTasks.clear();
   }
 
-  private void recordScanProgress(final IDeviceID device, final List<String> 
measurements) {
-    if (Objects.nonNull(lastScanTabletDevice)
-        && (!lastScanTabletDevice.equals(device)
-            || !measurementsEqual(lastScanTabletMeasurements, measurements))) {
-      markMeasurementsFullyEmitted(lastScanTabletDevice, 
lastScanTabletMeasurements);
-    }
-
-    lastScanTabletDevice = device;
-    lastScanTabletMeasurements = measurements;
-  }
-
-  private void markLastScanMeasurementsAsCompletedIfNeeded(
-      final IDeviceID currentDevice, final List<String> currentMeasurements) {
-    if (Objects.isNull(lastScanTabletDevice) || 
lastScanTabletMeasurements.isEmpty()) {
-      return;
-    }
-
-    if (!lastScanTabletDevice.equals(currentDevice)
-        || !currentMeasurements.isEmpty()
-            && !measurementsEqual(lastScanTabletMeasurements, 
currentMeasurements)) {
-      markMeasurementsFullyEmitted(lastScanTabletDevice, 
lastScanTabletMeasurements);
+  private boolean finishIteration() {
+    close();
+    if (Objects.nonNull(unrecoverableQueryFailure)) {
+      throw unrecoverableQueryFailure;
     }
+    return false;
   }
 
-  private void markMeasurementsFullyEmitted(
-      final IDeviceID device, final List<String> measurements) {
-    if (Objects.isNull(device) || measurements.isEmpty()) {
-      return;
-    }
-
-    fullyEmittedMeasurementsByDevice
-        .computeIfAbsent(device, key -> new LinkedHashSet<>())
-        .addAll(measurements);
-  }
-
-  private long determineTaskResumeStartTime(
-      final IDeviceID device, final List<String> measurements, final long 
defaultStartTime) {
-    if (measurements.isEmpty()
-        || !device.equals(lastEmittedDevice)
-        || lastEmittedTimestamp == Long.MIN_VALUE
-        || !measurementsEqual(measurements, lastEmittedMeasurements)) {
-      return defaultStartTime;
-    }
-
-    return lastEmittedTimestamp == Long.MAX_VALUE ? Long.MAX_VALUE : 
lastEmittedTimestamp + 1;
-  }
-
-  private void addQueryTaskIfNecessary(
-      final ArrayDeque<QueryTask> tasks,
-      final IDeviceID device,
-      final List<String> measurements,
-      final long startTime,
-      final long endTime) {
-    if (measurements.isEmpty() || startTime == Long.MAX_VALUE) {
-      return;
-    }
-
-    tasks.addLast(new QueryTask(device, measurements, startTime, endTime));
-  }
-
-  private void splitOrSkipActiveQueryTask() {
+  private void splitOrSkipActiveQueryTask(final Exception failure) {
     final QueryTask failedTask = activeQueryTask;
     closeActiveQueryParser();
     if (Objects.isNull(failedTask)) {
@@ -546,15 +481,20 @@ class LoadTreeTsFileTabletIterator
     }
 
     if (failedTask.measurements.size() <= 1) {
+      // Keep salvaging later tasks, but retain the first failure so the 
caller preserves the
+      // source.
+      if (Objects.isNull(unrecoverableQueryFailure)) {
+        unrecoverableQueryFailure = toRuntimeException(failure);
+      }
       return;
     }
 
-    final long resumeStartTime =
-        determineTaskResumeStartTime(
-            failedTask.device, failedTask.measurements, failedTask.startTime);
-    final List<QueryTask> splitTasks = failedTask.split(resumeStartTime);
-    for (int i = splitTasks.size() - 1; i >= 0; --i) {
-      pendingQueryTasks.addFirst(splitTasks.get(i));
+    final ArrayDeque<QueryTask> splitTasks = new ArrayDeque<>();
+    for (final QueryTask task : failedTask.split(failedTask.startTime)) {
+      addQueryTasks(splitTasks, task.device, task.measurements, 
task.startTime, task.endTime);
+    }
+    while (!splitTasks.isEmpty()) {
+      pendingQueryTasks.addFirst(splitTasks.removeLast());
     }
   }
 
@@ -564,12 +504,6 @@ class LoadTreeTsFileTabletIterator
         .collect(Collectors.toList());
   }
 
-  private boolean measurementsEqual(
-      final List<String> leftMeasurements, final List<String> 
rightMeasurements) {
-    return leftMeasurements.size() == rightMeasurements.size()
-        && new LinkedHashSet<>(leftMeasurements).equals(new 
LinkedHashSet<>(rightMeasurements));
-  }
-
   private static class QueryTask {
     private final IDeviceID device;
     private final List<String> measurements;
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeStatementDataTypeConvertExecutionVisitorTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeStatementDataTypeConvertExecutionVisitorTest.java
index 0cfbbc364f4..dc5ab073bcd 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeStatementDataTypeConvertExecutionVisitorTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/converter/LoadTreeStatementDataTypeConvertExecutionVisitorTest.java
@@ -30,10 +30,14 @@ import 
org.apache.iotdb.db.queryengine.plan.statement.crud.LoadTsFileStatement;
 import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.IMetadataIndexEntry;
+import org.apache.tsfile.file.MetaMarker;
 import org.apache.tsfile.file.metadata.AlignedChunkMetadata;
 import org.apache.tsfile.file.metadata.IChunkMetadata;
 import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.file.metadata.MetadataIndexNode;
 import org.apache.tsfile.file.metadata.PlainDeviceID;
+import org.apache.tsfile.file.metadata.enums.MetadataIndexNodeType;
 import org.apache.tsfile.file.metadata.enums.TSEncoding;
 import org.apache.tsfile.read.TsFileSequenceReader;
 import org.apache.tsfile.read.common.Path;
@@ -48,6 +52,7 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.io.File;
+import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -101,16 +106,299 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
             });
 
     final Optional<TSStatus> status =
-        
visitor.visitLoadFile(LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()),
 null);
+        visitor.visitLoadFile(
+            
LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()).setDeleteAfterLoad(true),
+            null);
 
     Assert.assertTrue(status.isPresent());
-    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
status.get().getCode());
+    Assert.assertTrue("A partial load must retain its source", 
tsFile.exists());
     final int loadedPointCountBeforeCorruption = 
pointCountByDevice.getOrDefault(DEVICE_0, 0);
     final int loadedPointCountAfterFallback = 
pointCountByDevice.getOrDefault(DEVICE_2, 0);
     Assert.assertTrue(loadedPointCountBeforeCorruption > 0);
     Assert.assertEquals(loadedPointCountBeforeCorruption, 
loadedPointCountAfterFallback);
   }
 
+  @Test
+  public void testFullyRecoveredQueryFallbackCanDeleteSource() throws 
Exception {
+    tsFile = new File("load-tree-query-fallback-recoverable-marker.tsfile");
+    final long groupMarkerOffset = writeTsFile(tsFile);
+    // Keep the first marker valid for the reader's encryption-header check. 
Indexed queries can
+    // recover every point without reading the second device's chunk-group 
marker.
+    try (final TsFileSequenceReader reader = new 
TsFileSequenceReader(tsFile.getAbsolutePath())) {
+      reader.position(groupMarkerOffset);
+      Assert.assertEquals(MetaMarker.CHUNK_GROUP_HEADER, reader.readMarker());
+    }
+    try (final RandomAccessFile file = new RandomAccessFile(tsFile, "rw")) {
+      file.seek(groupMarkerOffset);
+      file.writeByte(0x7f);
+    }
+    Assert.assertTrue("Expected scan parser to fail after corruption.", 
scanParserFails(tsFile));
+
+    final Map<String, Integer> pointCountByTimeseries = new HashMap<>();
+    final LoadTreeStatementDataTypeConvertExecutionVisitor visitor =
+        new LoadTreeStatementDataTypeConvertExecutionVisitor(
+            statement -> {
+              collectLoadedPointsByTimeseries(statement, 
pointCountByTimeseries);
+              return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+            });
+    final Optional<TSStatus> status =
+        visitor.visitLoadFile(
+            
LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()).setDeleteAfterLoad(true),
+            null);
+
+    Assert.assertTrue(status.isPresent());
+    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    for (final String device : Arrays.asList(DEVICE_0, DEVICE_1, DEVICE_2)) {
+      assertMeasurementLoadedCompletely(pointCountByTimeseries, device, 0);
+      assertMeasurementLoadedCompletely(pointCountByTimeseries, device, 1);
+    }
+    Assert.assertFalse("A fully recovered load can delete its source", 
tsFile.exists());
+  }
+
+  @Test
+  public void testFallbackRetainsSourceWhenFirstDeviceMetadataIsUnreadable() 
throws Exception {
+    assertFallbackRetainsSourceWhenMetadataIsUnreadable(DEVICE_0);
+  }
+
+  @Test
+  public void testFallbackRetainsSourceWhenLaterDeviceMetadataIsUnreadable() 
throws Exception {
+    assertFallbackRetainsSourceWhenMetadataIsUnreadable(DEVICE_1);
+  }
+
+  private void assertFallbackRetainsSourceWhenMetadataIsUnreadable(final 
String corruptedDevice)
+      throws Exception {
+    tsFile = File.createTempFile("load-tree-unreadable-metadata-", ".tsfile");
+    final long groupMarkerOffset = writeTsFile(tsFile);
+    final IDeviceID deviceId = new PlainDeviceID(corruptedDevice);
+    final long measurementIndexEndOffsetPosition;
+    try (final TsFileSequenceReader reader = new 
TsFileSequenceReader(tsFile.getAbsolutePath())) {
+      final MetadataIndexNode deviceIndex = 
reader.readFileMetadata().getMetadataIndex();
+      Assert.assertEquals(MetadataIndexNodeType.LEAF_DEVICE, 
deviceIndex.getNodeType());
+      final Pair<IMetadataIndexEntry, Long> deviceEntry =
+          deviceIndex.getChildIndexEntry(deviceId, true);
+      Assert.assertNotNull(deviceEntry);
+      final MetadataIndexNode measurementIndex =
+          reader.readMetadataIndexNode(deviceEntry.left.getOffset(), 
deviceEntry.right, false);
+      Assert.assertEquals(MetadataIndexNodeType.LEAF_MEASUREMENT, 
measurementIndex.getNodeType());
+      measurementIndexEndOffsetPosition = deviceEntry.right - Long.BYTES - 
Byte.BYTES;
+    }
+    // Preserve the footer and device index, but make the measurement metadata 
read reach EOF.
+    try (final RandomAccessFile bytes = new RandomAccessFile(tsFile, "rw")) {
+      bytes.seek(measurementIndexEndOffsetPosition);
+      bytes.writeLong(bytes.length() + 1);
+    }
+    try (final TsFileSequenceReader reader = new 
TsFileSequenceReader(tsFile.getAbsolutePath())) {
+      Assert.assertThrows(IOException.class, () -> 
reader.readDeviceMetadata(deviceId));
+    }
+    corruptGroupMarker(tsFile, groupMarkerOffset);
+    Assert.assertTrue("Expected scan parser to fail after corruption.", 
scanParserFails(tsFile));
+
+    final Map<String, Integer> points = new HashMap<>();
+    final LoadTreeStatementDataTypeConvertExecutionVisitor visitor =
+        new LoadTreeStatementDataTypeConvertExecutionVisitor(
+            statement -> {
+              collectLoadedPointsByTimeseries(statement, points);
+              return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+            });
+    final Optional<TSStatus> status =
+        visitor.visitLoadFile(
+            
LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()).setDeleteAfterLoad(true),
+            null);
+
+    Assert.assertTrue(status.isPresent());
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
status.get().getCode());
+    Assert.assertTrue("Unreadable metadata must retain the source", 
tsFile.exists());
+    assertMeasurementLoadedCompletely(points, DEVICE_0, 0);
+    assertMeasurementLoadedCompletely(points, DEVICE_0, 1);
+  }
+
+  @Test
+  public void testFallbackResumesRepeatedNonAlignedDevicesAcrossChunkGroups() 
throws Exception {
+    assertFallbackResumesRepeatedDevices(false);
+  }
+
+  @Test
+  public void testFallbackResumesRepeatedAlignedDevicesAcrossChunkGroups() 
throws Exception {
+    assertFallbackResumesRepeatedDevices(true);
+  }
+
+  @Test
+  public void testFallbackRetainsMeasurementsSplitAcrossMetadataBatches() 
throws Exception {
+    tsFile = File.createTempFile("load-tree-metadata-batches-", ".tsfile");
+    final int measurementCount = 4001;
+    final List<MeasurementSchema> schemas = new java.util.ArrayList<>();
+    for (int measurement = 0; measurement < measurementCount; measurement++) {
+      schemas.add(new MeasurementSchema("s" + measurement, TSDataType.INT64, 
TSEncoding.PLAIN));
+    }
+
+    final long secondGroupMarkerOffset;
+    try (final TsFileWriter writer = new TsFileWriter(tsFile)) {
+      writer.registerAlignedTimeseries(new Path(DEVICE_0), schemas);
+      writeWideAlignedRow(writer, schemas, 0);
+      writer.flushAllChunkGroups();
+      secondGroupMarkerOffset = writer.getIOWriter().getPos();
+      writeWideAlignedRow(writer, schemas, 1);
+    }
+    corruptGroupMarker(tsFile, secondGroupMarkerOffset);
+    Assert.assertTrue("Expected scan parser to fail after corruption.", 
scanParserFails(tsFile));
+
+    final Map<String, Map<Long, Long>> points = new HashMap<>();
+    final LoadTreeStatementDataTypeConvertExecutionVisitor visitor =
+        new LoadTreeStatementDataTypeConvertExecutionVisitor(
+            statement -> {
+              collectLoadedLongPoints(statement, points);
+              return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+            });
+    final Optional<TSStatus> status =
+        visitor.visitLoadFile(
+            
LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()).setDeleteAfterLoad(true),
+            null);
+
+    Assert.assertTrue(status.isPresent());
+    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    Assert.assertEquals(measurementCount, points.size());
+    for (int measurement = 0; measurement < measurementCount; measurement++) {
+      final String path = DEVICE_0 + ".s" + measurement;
+      final Map<Long, Long> values = points.get(path);
+      Assert.assertNotNull(path, values);
+      Assert.assertEquals(path, 2, values.size());
+      Assert.assertEquals(Long.valueOf(measurement * 10_000L), values.get(0L));
+      Assert.assertEquals(Long.valueOf(measurement * 10_000L + 1), 
values.get(1L));
+    }
+    Assert.assertFalse("A fully recovered load can delete its source", 
tsFile.exists());
+  }
+
+  @Test
+  public void testFallbackDoesNotReplayMinimumTimestamp() throws Exception {
+    assertFallbackResumesExtremeTimestamps(Long.MIN_VALUE);
+  }
+
+  @Test
+  public void testFallbackResumesAtMaximumTimestamp() throws Exception {
+    assertFallbackResumesExtremeTimestamps(Long.MAX_VALUE - 1);
+  }
+
+  private void assertFallbackResumesExtremeTimestamps(final long 
firstTimestamp) throws Exception {
+    tsFile = File.createTempFile("load-tree-extreme-timestamps-", ".tsfile");
+    final List<MeasurementSchema> schemas =
+        Arrays.asList(new MeasurementSchema("s0", TSDataType.INT64, 
TSEncoding.PLAIN));
+    final long secondFlushOffset;
+    try (final TsFileWriter writer = new TsFileWriter(tsFile)) {
+      writer.registerTimeseries(new Path(DEVICE_0), schemas);
+      writeRows(writer, schemas, DEVICE_0, firstTimestamp, 1, false);
+      writer.flushAllChunkGroups();
+      secondFlushOffset = writer.getIOWriter().getPos();
+      writeRows(writer, schemas, DEVICE_0, firstTimestamp + 1, 1, false);
+    }
+    corruptGroupMarker(tsFile, secondFlushOffset);
+
+    final Map<String, Map<Long, Long>> points = new HashMap<>();
+    final LoadTreeStatementDataTypeConvertExecutionVisitor visitor =
+        new LoadTreeStatementDataTypeConvertExecutionVisitor(
+            statement -> {
+              collectLoadedLongPoints(statement, points);
+              return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+            });
+    final Optional<TSStatus> status =
+        visitor.visitLoadFile(
+            
LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()).setDeleteAfterLoad(true),
+            null);
+    Assert.assertTrue(status.isPresent());
+    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    final Map<Long, Long> values = points.get(DEVICE_0 + ".s0");
+    Assert.assertNotNull(values);
+    Assert.assertEquals(2, values.size());
+    Assert.assertEquals(Long.valueOf(firstTimestamp), 
values.get(firstTimestamp));
+    Assert.assertEquals(Long.valueOf(firstTimestamp + 1), 
values.get(firstTimestamp + 1));
+    Assert.assertFalse("A fully recovered load can delete its source", 
tsFile.exists());
+  }
+
+  private void assertFallbackResumesRepeatedDevices(final boolean aligned) 
throws Exception {
+    tsFile = File.createTempFile("load-tree-repeated-devices-", ".tsfile");
+    final List<MeasurementSchema> schemas =
+        Arrays.asList(
+            new MeasurementSchema("s0", TSDataType.INT64, TSEncoding.PLAIN),
+            new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN));
+    final long secondFlushOffset;
+    try (final TsFileWriter writer = new TsFileWriter(tsFile)) {
+      for (final String device : Arrays.asList(DEVICE_0, DEVICE_1)) {
+        if (aligned) {
+          writer.registerAlignedTimeseries(new Path(device), schemas);
+        } else {
+          writer.registerTimeseries(new Path(device), schemas);
+        }
+        writeRows(writer, schemas, device, 0, 4, aligned);
+      }
+      writer.flushAllChunkGroups();
+      secondFlushOffset = writer.getIOWriter().getPos();
+      for (final String device : Arrays.asList(DEVICE_0, DEVICE_1)) {
+        writeRows(writer, schemas, device, 4, 4, aligned);
+      }
+    }
+    corruptGroupMarker(tsFile, secondFlushOffset);
+
+    final Map<String, Map<Long, Long>> points = new HashMap<>();
+    final LoadTreeStatementDataTypeConvertExecutionVisitor visitor =
+        new LoadTreeStatementDataTypeConvertExecutionVisitor(
+            statement -> {
+              collectLoadedLongPoints(statement, points);
+              return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+            });
+    final Optional<TSStatus> status =
+        visitor.visitLoadFile(
+            
LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()).setDeleteAfterLoad(true),
+            null);
+    Assert.assertTrue(status.isPresent());
+    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    for (final String device : Arrays.asList(DEVICE_0, DEVICE_1)) {
+      for (int measurement = 0; measurement < 2; measurement++) {
+        final String path = device + ".s" + measurement;
+        final Map<Long, Long> values = points.get(path);
+        Assert.assertNotNull(path, values);
+        Assert.assertEquals(path, 8, values.size());
+        for (long timestamp = 0; timestamp < 8; timestamp++) {
+          Assert.assertEquals(
+              path, Long.valueOf(timestamp + measurement * 100), 
values.get(timestamp));
+        }
+      }
+    }
+    Assert.assertFalse("A fully recovered load can delete its source", 
tsFile.exists());
+  }
+
+  private static void corruptGroupMarker(final File tsFile, final long offset) 
throws Exception {
+    try (final RandomAccessFile file = new RandomAccessFile(tsFile, "rw")) {
+      file.seek(offset);
+      Assert.assertEquals(MetaMarker.CHUNK_GROUP_HEADER, file.readByte());
+      file.seek(offset);
+      file.writeByte(0x7f);
+    }
+  }
+
+  private static void writeRows(
+      final TsFileWriter writer,
+      final List<MeasurementSchema> schemas,
+      final String device,
+      final long firstTimestamp,
+      final int rowCount,
+      final boolean aligned)
+      throws Exception {
+    final Tablet tablet = new Tablet(device, schemas, rowCount);
+    for (int row = 0; row < rowCount; row++) {
+      tablet.addTimestamp(row, firstTimestamp + row);
+      for (int column = 0; column < schemas.size(); column++) {
+        tablet.addValue(
+            schemas.get(column).getMeasurementId(), row, firstTimestamp + row 
+ column * 100);
+      }
+    }
+    tablet.rowSize = rowCount;
+    if (aligned) {
+      writer.writeAligned(tablet);
+    } else {
+      writer.write(tablet);
+    }
+  }
+
   @Test
   public void testFlushesPendingTabletsWhenIteratorFails() throws Exception {
     tsFile = File.createTempFile("load-tree-pending-tablet", ".tsfile");
@@ -158,7 +446,7 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
         
visitor.visitLoadFile(LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()),
 null);
 
     Assert.assertTrue(status.isPresent());
-    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
status.get().getCode());
 
     Assert.assertTrue(
         pointCountByTimeseries.getOrDefault(DEVICE_0 + ".s0", 0) < 
ROW_COUNT_PER_DEVICE);
@@ -190,7 +478,7 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
         
visitor.visitLoadFile(LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()),
 null);
 
     Assert.assertTrue(status.isPresent());
-    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
status.get().getCode());
 
     assertMeasurementLoadedCompletely(pointCountByTimeseries, DEVICE_1, 0);
     Assert.assertTrue(
@@ -222,7 +510,7 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
         
visitor.visitLoadFile(LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath()),
 null);
 
     Assert.assertTrue(status.isPresent());
-    Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.get().getCode());
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
status.get().getCode());
 
     for (int measurementIndex = 0; measurementIndex < 8; ++measurementIndex) {
       assertMeasurementLoadedCompletely(pointCountByTimeseries, 
ALIGNED_DEVICE, measurementIndex);
@@ -237,7 +525,7 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
         pointCountByTimeseries.getOrDefault(ALIGNED_DEVICE + ".s12", 0) < 
ROW_COUNT_PER_DEVICE);
   }
 
-  private void writeTsFile(final File file) throws Exception {
+  private long writeTsFile(final File file) throws Exception {
     if (file.exists()) {
       Assert.assertTrue(file.delete());
     }
@@ -247,11 +535,15 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
             new MeasurementSchema("s0", TSDataType.INT64, TSEncoding.PLAIN),
             new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN));
 
+    final long secondGroupMarkerOffset;
     try (final TsFileWriter writer = new TsFileWriter(file)) {
       writeDevice(writer, schemaList, DEVICE_0, 0);
+      writer.flushAllChunkGroups();
+      secondGroupMarkerOffset = writer.getIOWriter().getPos();
       writeDevice(writer, schemaList, DEVICE_1, 10_000);
       writeDevice(writer, schemaList, DEVICE_2, 20_000);
     }
+    return secondGroupMarkerOffset;
   }
 
   private void writeWideAlignedTsFile(
@@ -281,6 +573,19 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
     }
   }
 
+  private void writeWideAlignedRow(
+      final TsFileWriter writer, final List<MeasurementSchema> schemas, final 
long timestamp)
+      throws Exception {
+    final Tablet tablet = new Tablet(DEVICE_0, schemas, 1);
+    tablet.addTimestamp(0, timestamp);
+    for (int measurement = 0; measurement < schemas.size(); measurement++) {
+      tablet.addValue(
+          schemas.get(measurement).getMeasurementId(), 0, measurement * 
10_000L + timestamp);
+    }
+    tablet.rowSize = 1;
+    writer.writeAligned(tablet);
+  }
+
   private void writeDevice(
       final TsFileWriter writer,
       final List<MeasurementSchema> schemaList,
@@ -353,6 +658,27 @@ public class 
LoadTreeStatementDataTypeConvertExecutionVisitorTest {
     }
   }
 
+  private void collectLoadedLongPoints(
+      final Statement statement, final Map<String, Map<Long, Long>> points) {
+    Assert.assertTrue(statement instanceof InsertMultiTabletsStatement);
+    for (final InsertTabletStatement tablet :
+        ((InsertMultiTabletsStatement) 
statement).getInsertTabletStatementList()) {
+      for (int column = 0; column < tablet.getMeasurements().length; column++) 
{
+        final String path =
+            tablet.getDevicePath().getFullPath() + "." + 
tablet.getMeasurements()[column];
+        final Map<Long, Long> values = points.computeIfAbsent(path, key -> new 
HashMap<>());
+        for (int row = 0; row < tablet.getRowCount(); row++) {
+          if (!isNull(tablet, row, column)) {
+            final long timestamp = tablet.getTimes()[row];
+            Assert.assertNull(
+                "Duplicate point: " + path + " at " + timestamp,
+                values.put(timestamp, ((long[]) 
tablet.getColumns()[column])[row]));
+          }
+        }
+      }
+    }
+  }
+
   private void collectLoadedPointsByTimeseries(
       final Statement statement, final Map<String, Integer> 
pointCountByTimeseries) {
     Assert.assertTrue(statement instanceof InsertMultiTabletsStatement);

Reply via email to